rshr_202_step_305_gate.rs (15883B)
1 use std::{ 2 env, fs, 3 path::{Path, PathBuf}, 4 process::{Command, Output}, 5 }; 6 7 use serde_json::{Value, json}; 8 use sha2::{Digest as _, Sha256}; 9 10 const STEP: u16 = 305; 11 const GATE_DIGEST: &str = "29633a173a2b7ee52d97d7a51feca377c1c182879fb73b9f3cf7524046b35148"; 12 const MAX_OUTPUT_BYTES: usize = 32 * 1024 * 1024; 13 const EXACT_SOURCES: &[(&str, &str)] = &[ 14 ( 15 "Cargo.lock", 16 "a8edafae2d2b26465b2b99038ce55d80fa603481ebb5ebebbd810708ba4a894a", 17 ), 18 ( 19 "Cargo.toml", 20 "322b975e60004df42de5b1b9460bf84255d2210e97f4f1cf1ede84a068b7519e", 21 ), 22 ( 23 "contracts/architecture/decisions/services_hardening_release_artifacts.v4.json", 24 "6ed5bb06cf26565ac94d04f0b18a810e0a56372388e83e05c909c627da7ba7b4", 25 ), 26 ( 27 "tools/xtask/Cargo.toml", 28 "bf5442895085225a571bf8aa45b2316e732a5c9925911de26147a924466f8ba0", 29 ), 30 ( 31 "tools/xtask/src/artifact_admission.rs", 32 "87b538d5ca80dfbc3de232ded40320cc1b0ec555fa48056af79eb365cc74ea44", 33 ), 34 ( 35 "tools/xtask/src/exact_tree_archive.rs", 36 "e176233167f99b783af4e041422eb619740a1d896138c77b51731400efb68ab2", 37 ), 38 ( 39 "tools/xtask/src/main.rs", 40 "a4d543a4c690234203a88689ec7570aac0fcec53834ce6a7ef2970d83f34d216", 41 ), 42 ( 43 "tools/xtask/src/safe_artifact_io.rs", 44 "3b361b22036ff8db5f5468e33341a7b2990d6f80aa6bdde4f791bd1320587e4d", 45 ), 46 ( 47 "tools/xtask/src/service_release_artifacts.rs", 48 "52b5a014ce9ac58a0db849dfc105e7759732be5446b9cc16df4c80ceb303ec26", 49 ), 50 ( 51 "tools/xtask/src/service_source_lock_v3.rs", 52 "8594897380149d864554a1cc0c463308a228091def7704215b8ca693fc61371c", 53 ), 54 ]; 55 56 pub(crate) struct Arguments { 57 pub(crate) step: u16, 58 pub(crate) check_id: String, 59 pub(crate) source_revision: String, 60 pub(crate) source_tree: String, 61 pub(crate) candidate_digest: String, 62 pub(crate) platform: String, 63 pub(crate) execution_request_sha256: String, 64 } 65 66 fn root() -> PathBuf { 67 Path::new(env!("CARGO_MANIFEST_DIR")) 68 .parent() 69 .and_then(Path::parent) 70 .expect("xtask must remain under tools/xtask") 71 .to_path_buf() 72 } 73 74 fn sha256(bytes: &[u8]) -> String { 75 hex::encode(Sha256::digest(bytes)) 76 } 77 78 fn canonical(value: &Value) -> Result<Vec<u8>, String> { 79 serde_json::to_vec(value).map_err(|_| "Step 305 JSON encoding failed".to_owned()) 80 } 81 82 fn bounded(command: &mut Command, label: &str) -> Result<Output, String> { 83 let output = command 84 .current_dir(root()) 85 .env("CARGO_NET_OFFLINE", "true") 86 .env("CARGO_TERM_COLOR", "never") 87 .output() 88 .map_err(|_| format!("{label} could not start"))?; 89 if output.stdout.len() > MAX_OUTPUT_BYTES || output.stderr.len() > MAX_OUTPUT_BYTES { 90 return Err(format!("{label} exceeded its output bound")); 91 } 92 if output.status.success() { 93 Ok(output) 94 } else { 95 Err(format!("{label} failed")) 96 } 97 } 98 99 fn expected_contract(verifier_sha256: &str) -> Value { 100 json!({ 101 "argv_template": [ 102 "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked", 103 "-q", "-p", "xtask", "--", "rshr-step-305-gate", "--step={step}", 104 "--check-id={check_id}", "--source-revision={source_revision}", 105 "--source-tree={source_tree}", "--candidate-digest={candidate_digest}", 106 "--platform=macos_aarch64", "--execution-request-sha256={execution_request_sha256}" 107 ], 108 "assertion_id": [format!("step_305_gate_01_{GATE_DIGEST}")], 109 "check_id": format!("gate-01-{GATE_DIGEST}"), 110 "environment_authority": { 111 "cache_policy_id": "rshr-200-step-287-cache-policy.v1", 112 "cache_policy_sha256": "3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa", 113 "cadence_policy_id": "rshr-200-step-287-cadence-policy.v1", 114 "cadence_policy_sha256": "d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1", 115 "isolation": "extbuild_host_constrained", 116 "network": "disabled", 117 "network_policy_id": "none", 118 "network_policy_sha256": "none", 119 "resource_policy_id": "rshr-200-step-287-resource-policy.v1", 120 "resource_policy_sha256": "05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e" 121 }, 122 "environment_names": ["EXT_BUILD_CONFIG", "EXT_BUILD_MACHINE_CONFIG", "EXT_BUILD_ROOT", "HOME", "PATH", "RUSTUP_TOOLCHAIN", "TMPDIR"], 123 "gate_definition_sha256": GATE_DIGEST, 124 "required_platforms": ["macos_aarch64"], 125 "required_tools": ["git", "rustc"], 126 "result_schema": "radroots.services-hardening.rshr-200-step-check-result.v1", 127 "schema": "radroots.services-hardening.rshr-200-step-check-command.v1", 128 "step": STEP, 129 "verifier_path": "tools/xtask/src/rshr_202_step_305_gate.rs", 130 "verifier_sha256": verifier_sha256 131 }) 132 } 133 134 fn validate_arguments(arguments: &Arguments) -> Result<String, String> { 135 let check_id = format!("gate-01-{GATE_DIGEST}"); 136 if arguments.step != STEP 137 || arguments.check_id != check_id 138 || arguments.candidate_digest != "none" 139 || arguments.platform != "macos_aarch64" 140 || !valid_hex(&arguments.source_revision, 40) 141 || !valid_hex(&arguments.source_tree, 40) 142 || !valid_hex(&arguments.execution_request_sha256, 64) 143 { 144 return Err("Step 305 gate arguments differ".to_owned()); 145 } 146 Ok(check_id) 147 } 148 149 pub(crate) fn run(arguments: Arguments) -> Result<(), String> { 150 let check_id = validate_arguments(&arguments)?; 151 let root = root(); 152 if root.join(".github").exists() { 153 return Err("forbidden .github surface is present".to_owned()); 154 } 155 for (relative, expected) in EXACT_SOURCES { 156 let observed = sha256( 157 &fs::read(root.join(relative)) 158 .map_err(|_| "Step 305 governed source is unreadable".to_owned())?, 159 ); 160 if observed != *expected { 161 return Err(format!("Step 305 governed source bytes differ: {relative}")); 162 } 163 } 164 let verifier_path = root.join("tools/xtask/src/rshr_202_step_305_gate.rs"); 165 let verifier_sha256 = 166 sha256(&fs::read(verifier_path).map_err(|_| "Step 305 verifier is unreadable".to_owned())?); 167 let authority_bytes = fs::read(root.join("contracts/rshr-202-step-305-gates.v1.json")) 168 .map_err(|_| "Step 305 gate authority is unreadable".to_owned())?; 169 let contract = validate_authority(&authority_bytes, &verifier_sha256)?; 170 171 for (arguments, label) in [ 172 ( 173 vec!["+1.97.1", "fmt", "--all", "--", "--check"], 174 "Step 305 formatting", 175 ), 176 ( 177 vec!["+1.97.1", "check", "--offline", "--locked", "-p", "xtask"], 178 "Step 305 verifier check", 179 ), 180 ( 181 vec![ 182 "+1.97.1", 183 "test", 184 "--offline", 185 "--locked", 186 "-p", 187 "xtask", 188 "exact_tree_archive::tests", 189 ], 190 "Step 305 exact-tree archive tests", 191 ), 192 ( 193 vec![ 194 "+1.97.1", 195 "test", 196 "--offline", 197 "--locked", 198 "-p", 199 "xtask", 200 "service_source_lock_v3::tests", 201 ], 202 "Step 305 source-lock tests", 203 ), 204 ( 205 vec![ 206 "+1.97.1", 207 "test", 208 "--offline", 209 "--locked", 210 "-p", 211 "xtask", 212 "service_release_artifacts::tests", 213 ], 214 "Step 305 release-evidence and negative-vector tests", 215 ), 216 ] { 217 bounded(Command::new("cargo").args(arguments), label)?; 218 } 219 bounded( 220 Command::new("cargo").args([ 221 "+1.97.1", 222 "clippy", 223 "--offline", 224 "--locked", 225 "-p", 226 "xtask", 227 "--all-targets", 228 "--", 229 "-D", 230 "warnings", 231 ]), 232 "Step 305 clippy", 233 )?; 234 bounded( 235 Command::new("cargo").args([ 236 "+1.97.1", 237 "run", 238 "--offline", 239 "--locked", 240 "-q", 241 "-p", 242 "xtask", 243 "--", 244 "contract", 245 "validate", 246 ]), 247 "Step 305 contracts", 248 )?; 249 250 let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?; 251 std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes) 252 .map_err(|_| "Step 305 result write failed".to_owned()) 253 } 254 255 fn valid_hex(value: &str, length: usize) -> bool { 256 value.len() == length 257 && value 258 .bytes() 259 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) 260 } 261 262 fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> { 263 let authority: Value = serde_json::from_slice(authority_bytes) 264 .map_err(|_| "Step 305 gate authority is invalid".to_owned())?; 265 let mut canonical_authority = canonical(&authority)?; 266 canonical_authority.push(b'\n'); 267 let contracts = authority 268 .get("gate_command_contract") 269 .and_then(Value::as_array) 270 .ok_or_else(|| "Step 305 gate contract is absent".to_owned())?; 271 if authority_bytes != canonical_authority 272 || authority.get("schema") != Some(&json!("radroots.lib.rshr-202-step-305-gates.v1")) 273 || authority.get("step") != Some(&json!([STEP])) 274 || contracts.as_slice() != [expected_contract(verifier_sha256)] 275 { 276 return Err("Step 305 gate authority differs".to_owned()); 277 } 278 Ok(contracts[0].clone()) 279 } 280 281 fn result_bytes( 282 arguments: &Arguments, 283 check_id: &str, 284 verifier_sha256: &str, 285 contract: &Value, 286 ) -> Result<Vec<u8>, String> { 287 let assertion = json!([{ "id": format!("step_305_gate_01_{GATE_DIGEST}"), "result": "pass" }]); 288 let result = json!({ 289 "schema": "radroots.services-hardening.rshr-200-step-check-result.v1", 290 "step": STEP, 291 "check_id": check_id, 292 "gate_definition_sha256": GATE_DIGEST, 293 "source_revision": arguments.source_revision, 294 "source_tree": arguments.source_tree, 295 "candidate_generation": 0, 296 "candidate_digest": "none", 297 "command_contract_sha256": sha256(&canonical(contract)?), 298 "verifier_sha256": verifier_sha256, 299 "execution_request": [{"platform": arguments.platform, "sha256": arguments.execution_request_sha256}], 300 "assertion_inventory_sha256": sha256(&canonical(&assertion)?), 301 "assertion": assertion, 302 "result": "pass" 303 }); 304 let mut bytes = canonical(&result)?; 305 bytes.push(b'\n'); 306 Ok(bytes) 307 } 308 309 #[cfg(test)] 310 mod tests { 311 use super::*; 312 313 #[test] 314 fn command_capture_preserves_status_streams_and_enforces_both_output_limits() { 315 let output = bounded( 316 Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]), 317 "fixture", 318 ) 319 .unwrap(); 320 assert_eq!(output.stdout, b"output"); 321 assert_eq!(output.stderr, b"diagnostic"); 322 assert_eq!( 323 bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(), 324 "fixture failed" 325 ); 326 let missing = tempfile::TempDir::new().unwrap(); 327 assert_eq!( 328 bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(), 329 "fixture could not start" 330 ); 331 for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] { 332 let maximum = MAX_OUTPUT_BYTES.to_string(); 333 let output = bounded( 334 Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]), 335 "fixture", 336 ) 337 .unwrap(); 338 assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES); 339 let oversized = (MAX_OUTPUT_BYTES + 1).to_string(); 340 assert_eq!( 341 bounded( 342 Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]), 343 "fixture" 344 ) 345 .unwrap_err(), 346 "fixture exceeded its output bound" 347 ); 348 } 349 } 350 351 fn arguments() -> Arguments { 352 Arguments { 353 step: STEP, 354 check_id: format!("gate-01-{GATE_DIGEST}"), 355 source_revision: "a".repeat(40), 356 source_tree: "0".repeat(40), 357 candidate_digest: "none".into(), 358 platform: "macos_aarch64".into(), 359 execution_request_sha256: "1".repeat(64), 360 } 361 } 362 363 #[test] 364 fn invalid_gate_arguments_are_rejected_before_external_work() { 365 assert_eq!( 366 validate_arguments(&arguments()).unwrap(), 367 format!("gate-01-{GATE_DIGEST}") 368 ); 369 for field in 0..8 { 370 let mut invalid = arguments(); 371 match field { 372 0 => invalid.step = 0, 373 1 => invalid.check_id.clear(), 374 2 => invalid.candidate_digest = "unbound".into(), 375 3 => invalid.platform = "linux".into(), 376 4 => invalid.source_revision.clear(), 377 5 => invalid.source_tree.clear(), 378 6 => invalid.execution_request_sha256.clear(), 379 _ => invalid.source_revision = "A".repeat(40), 380 } 381 assert_eq!(run(invalid).unwrap_err(), "Step 305 gate arguments differ"); 382 } 383 let mut invalid = arguments(); 384 invalid.source_tree = "g".repeat(40); 385 assert!(validate_arguments(&invalid).is_err()); 386 } 387 388 #[test] 389 fn authority_requires_canonical_bytes_and_exact_retained_bindings() { 390 let raw = include_bytes!("../../../contracts/rshr-202-step-305-gates.v1.json"); 391 let authority: Value = serde_json::from_slice(raw).unwrap(); 392 let verifier = authority["gate_command_contract"][0]["verifier_sha256"] 393 .as_str() 394 .unwrap(); 395 let contract = validate_authority(raw, verifier).unwrap(); 396 assert!(validate_authority(raw, &"f".repeat(64)).is_err()); 397 assert!(validate_authority(b"invalid", verifier).is_err()); 398 assert!(validate_authority(b"{}\n", verifier).is_err()); 399 assert!( 400 validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err() 401 ); 402 for (pointer, value) in [ 403 ("/schema", json!("other")), 404 ("/step", json!([0])), 405 ("/gate_command_contract", json!([])), 406 ] { 407 let mut changed = authority.clone(); 408 *changed.pointer_mut(pointer).unwrap() = value; 409 let mut bytes = canonical(&changed).unwrap(); 410 bytes.push(b'\n'); 411 assert!(validate_authority(&bytes, verifier).is_err()); 412 } 413 // Encoding fixtures is not a historical gate execution or qualification. 414 let args = arguments(); 415 let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap(); 416 let result: Value = serde_json::from_slice(&bytes).unwrap(); 417 assert!(bytes.ends_with(b"\n")); 418 assert_eq!(result["source_revision"], args.source_revision); 419 assert_eq!(result["source_tree"], args.source_tree); 420 assert_eq!( 421 result["execution_request"][0]["sha256"], 422 args.execution_request_sha256 423 ); 424 assert_eq!( 425 result["command_contract_sha256"], 426 sha256(&canonical(&contract).unwrap()) 427 ); 428 assert_eq!( 429 result["assertion_inventory_sha256"], 430 sha256(&canonical(&result["assertion"]).unwrap()) 431 ); 432 } 433 }