rshr_202_step_298_gate.rs (18691B)
1 use std::env; 2 use std::fs; 3 use std::path::{Path, PathBuf}; 4 use std::process::{Command, Output}; 5 6 use serde_json::{Value, json}; 7 use sha2::{Digest, Sha256}; 8 9 const STEP: u16 = 298; 10 const GATE_DIGEST: &str = "14c62391f40dcf5a2e166bac481c9eb8f50052ee65403ddbd63df5cc82ec6843"; 11 const NIX_SHA256: &str = "a59ab70f97f6d571642d13c7506aafec0a4275520d53daee2d8451be7c495cd1"; 12 const NIX_VERSION_SHA256: &str = "6db806391ffaea4cdb08ade0031feac399c0cd08474b3bfde8cb33f88a36c8e1"; 13 const MAX_OUTPUT_BYTES: usize = 32 * 1024 * 1024; 14 15 const EXACT_SOURCES: &[(&str, &str)] = &[ 16 ( 17 "contracts/architecture/decisions/services_hardening_source_lock.v3.json", 18 "3bc32c8ca2cecb06c8f8239ab1fe1fcfba93fe3ef0d60e9b078390347d08f817", 19 ), 20 ( 21 "contracts/release/lib-artifact-contract.v3.json", 22 "11d8fa7bc96191919a3d20189061aadcee105eefe546290032c2f2bab91b2f77", 23 ), 24 ( 25 "contracts/architecture/decisions/services_hardening_build_qualification.v3.json", 26 "4f1bf59e6411c28c9b202c81ed9455c3446525fc39c8e96276a48e4223de1394", 27 ), 28 ( 29 "build/nix/service/systems.nix", 30 "d16e21827022a2315234f4c5e4b485017a36ecd90a5559b01d23331cdd505e46", 31 ), 32 ( 33 "build/nix/service/fixture.nix", 34 "d9f4ec24762b2cadb4aed45518f81e53308d4e1bf7ff2708aec74a1485269402", 35 ), 36 ( 37 "build/nix/service/oci.nix", 38 "9111ce51465bbe45944b718e5e2477b43a08f8f7d9e29f43940a67e86e75daf1", 39 ), 40 ( 41 "contracts/releases/target_matrix.toml", 42 "28583b0a163e51468d9688b463902ec2cd22b59c99061630596839baf9396527", 43 ), 44 ( 45 "tools/xtask/src/service_build_qualification.rs", 46 "4a440a42c4387421fcf9d6c2d1fa86563822753fc1911f9552f098f38a1cc166", 47 ), 48 ( 49 "tools/xtask/src/target_qualification.rs", 50 "0e5b9506c70f5175edeae7cf9b7fb0f55a0cb6abf465a3f708d4234b2069c585", 51 ), 52 ]; 53 54 const BUILD_TESTS: &[&str] = &[ 55 "service_build_qualification::tests::checked_in_contract_and_fixture_are_exact", 56 "service_build_qualification::tests::contract_inventory_is_literal_and_complete", 57 "service_build_qualification::tests::contract_rejects_every_independent_governed_field_drift", 58 "service_build_qualification::tests::errors_are_fixed_and_source_free", 59 "service_build_qualification::tests::fixture_rejects_every_identity_and_lockfile_drift", 60 "service_build_qualification::tests::fixture_rejects_every_independent_metadata_drift", 61 ]; 62 63 const TARGET_TESTS: &[&str] = &[ 64 "target_qualification::tests::current_contract_selects_exact_toolchains_targets_and_packages", 65 "target_qualification::tests::unsupported_production_targets_are_rejected", 66 ]; 67 68 pub(crate) struct Arguments { 69 pub(crate) step: u16, 70 pub(crate) check_id: String, 71 pub(crate) source_revision: String, 72 pub(crate) source_tree: String, 73 pub(crate) candidate_digest: String, 74 pub(crate) platform: String, 75 pub(crate) execution_request_sha256: String, 76 } 77 78 fn root() -> PathBuf { 79 Path::new(env!("CARGO_MANIFEST_DIR")) 80 .parent() 81 .and_then(Path::parent) 82 .expect("xtask must remain under tools/xtask") 83 .to_path_buf() 84 } 85 86 fn sha256(bytes: &[u8]) -> String { 87 hex::encode(Sha256::digest(bytes)) 88 } 89 90 fn canonical(value: &Value) -> Result<Vec<u8>, String> { 91 serde_json::to_vec(value).map_err(|_| "Step 298 JSON encoding failed".to_owned()) 92 } 93 94 fn bounded(command: &mut Command, label: &str) -> Result<Output, String> { 95 let output = command 96 .current_dir(root()) 97 .env("CARGO_NET_OFFLINE", "true") 98 .env("CARGO_TERM_COLOR", "never") 99 .output() 100 .map_err(|_| format!("{label} could not start"))?; 101 if output.stdout.len() > MAX_OUTPUT_BYTES || output.stderr.len() > MAX_OUTPUT_BYTES { 102 return Err(format!("{label} exceeded its output bound")); 103 } 104 if !output.status.success() { 105 return Err(format!("{label} failed")); 106 } 107 Ok(output) 108 } 109 110 fn cargo(arguments: &[&str], label: &str) -> Result<Output, String> { 111 bounded(Command::new("cargo").args(arguments), label) 112 } 113 114 fn require_test_lane(filter: &str, expected: &[&str]) -> Result<(), String> { 115 let listed = cargo( 116 &[ 117 "+1.97.1", 118 "test", 119 "--offline", 120 "--locked", 121 "-p", 122 "xtask", 123 filter, 124 "--", 125 "--list", 126 "--format=terse", 127 ], 128 "Step 298 test inventory", 129 )?; 130 let text = std::str::from_utf8(&listed.stdout) 131 .map_err(|_| "Step 298 test inventory is not UTF-8".to_owned())?; 132 let mut observed = text 133 .lines() 134 .filter_map(|line| line.strip_suffix(": test")) 135 .collect::<Vec<_>>(); 136 observed.sort_unstable(); 137 let mut required = expected.to_vec(); 138 required.sort_unstable(); 139 if observed != required { 140 return Err("Step 298 test inventory differs".to_owned()); 141 } 142 cargo( 143 &[ 144 "+1.97.1", 145 "test", 146 "--offline", 147 "--locked", 148 "-p", 149 "xtask", 150 filter, 151 "--", 152 "--test-threads=1", 153 ], 154 "Step 298 mutation lane", 155 )?; 156 Ok(()) 157 } 158 159 fn resolve_nix() -> Result<PathBuf, String> { 160 if let Some(explicit) = env::var_os("RSHR_NIX_EXECUTABLE") { 161 return fs::canonicalize(explicit) 162 .map_err(|_| "Step 298 Nix client is unavailable".to_owned()); 163 } 164 let path = env::var_os("PATH").ok_or_else(|| "Step 298 PATH is absent".to_owned())?; 165 env::split_paths(&path) 166 .map(|directory| directory.join("nix")) 167 .find(|candidate| candidate.is_file()) 168 .and_then(|candidate| fs::canonicalize(candidate).ok()) 169 .ok_or_else(|| "Step 298 Nix client is unavailable".to_owned()) 170 } 171 172 fn require_nix() -> Result<(), String> { 173 let executable = resolve_nix()?; 174 let bytes = fs::read(&executable).map_err(|_| "Step 298 Nix client is unreadable")?; 175 if sha256(&bytes) != NIX_SHA256 { 176 return Err("Step 298 Nix client identity differs".to_owned()); 177 } 178 let version = bounded( 179 Command::new(&executable).arg("--version"), 180 "Step 298 Nix version", 181 )?; 182 if sha256(&version.stdout) != NIX_VERSION_SHA256 { 183 return Err("Step 298 Nix version differs".to_owned()); 184 } 185 let systems = bounded( 186 Command::new(&executable).args([ 187 "--offline", 188 "eval", 189 "--json", 190 "--file", 191 "build/nix/service/systems.nix", 192 ]), 193 "Step 298 Nix systems", 194 )?; 195 if systems.stdout != b"[\"aarch64-darwin\",\"x86_64-linux\"]\n" { 196 return Err("Step 298 Nix systems differ".to_owned()); 197 } 198 bounded( 199 Command::new(&executable).args([ 200 "--offline", 201 "flake", 202 "check", 203 "--no-build", 204 "--no-write-lock-file", 205 ]), 206 "Step 298 Nix flake evaluation", 207 )?; 208 Ok(()) 209 } 210 211 fn expected_contract(verifier_sha256: &str) -> Value { 212 json!({ 213 "argv_template": [ 214 "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked", 215 "-q", "-p", "xtask", "--", "rshr-step-298-gate", "--step={step}", 216 "--check-id={check_id}", "--source-revision={source_revision}", 217 "--source-tree={source_tree}", "--candidate-digest={candidate_digest}", 218 "--platform=macos_aarch64", 219 "--execution-request-sha256={execution_request_sha256}" 220 ], 221 "assertion_id": [format!("step_298_gate_01_{GATE_DIGEST}")], 222 "check_id": format!("gate-01-{GATE_DIGEST}"), 223 "environment_authority": { 224 "cache_policy_id": "rshr-200-step-287-cache-policy.v1", 225 "cache_policy_sha256": "3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa", 226 "cadence_policy_id": "rshr-200-step-287-cadence-policy.v1", 227 "cadence_policy_sha256": "d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1", 228 "isolation": "extbuild_host_constrained", 229 "network": "disabled", 230 "network_policy_id": "none", 231 "network_policy_sha256": "none", 232 "resource_policy_id": "rshr-200-step-287-resource-policy.v1", 233 "resource_policy_sha256": "05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e" 234 }, 235 "environment_names": [ 236 "EXT_BUILD_CONFIG", "EXT_BUILD_MACHINE_CONFIG", "EXT_BUILD_ROOT", "HOME", "PATH", 237 "RUSTUP_TOOLCHAIN", "TMPDIR" 238 ], 239 "gate_definition_sha256": GATE_DIGEST, 240 "required_platforms": ["macos_aarch64"], 241 "required_tools": ["rustc"], 242 "result_schema": "radroots.services-hardening.rshr-200-step-check-result.v1", 243 "schema": "radroots.services-hardening.rshr-200-step-check-command.v1", 244 "step": STEP, 245 "verifier_path": "tools/xtask/src/rshr_202_step_298_gate.rs", 246 "verifier_sha256": verifier_sha256 247 }) 248 } 249 250 fn validate_arguments(arguments: &Arguments) -> Result<String, String> { 251 let check_id = format!("gate-01-{GATE_DIGEST}"); 252 if arguments.step != STEP 253 || arguments.check_id != check_id 254 || arguments.candidate_digest != "none" 255 || arguments.platform != "macos_aarch64" 256 || arguments.source_revision.len() != 40 257 || arguments.source_tree.len() != 40 258 || arguments.execution_request_sha256.len() != 64 259 || !arguments 260 .source_revision 261 .bytes() 262 .chain(arguments.source_tree.bytes()) 263 .chain(arguments.execution_request_sha256.bytes()) 264 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) 265 { 266 return Err("Step 298 gate arguments differ".to_owned()); 267 } 268 Ok(check_id) 269 } 270 271 pub(crate) fn run(arguments: Arguments) -> Result<(), String> { 272 let check_id = validate_arguments(&arguments)?; 273 let root = root(); 274 if root.join(".github").exists() { 275 return Err("forbidden .github surface is present".to_owned()); 276 } 277 for (relative, expected) in EXACT_SOURCES { 278 let bytes = fs::read(root.join(relative)) 279 .map_err(|_| "Step 298 governed source is unreadable".to_owned())?; 280 if sha256(&bytes) != *expected { 281 return Err("Step 298 governed source bytes differ".to_owned()); 282 } 283 } 284 285 let verifier_path = root.join("tools/xtask/src/rshr_202_step_298_gate.rs"); 286 let verifier_sha256 = 287 sha256(&fs::read(verifier_path).map_err(|_| "Step 298 verifier is unreadable".to_owned())?); 288 let authority_path = root.join("contracts/rshr-202-step-298-gates.v1.json"); 289 let authority_bytes = 290 fs::read(authority_path).map_err(|_| "Step 298 gate authority is unreadable".to_owned())?; 291 let contract = validate_authority(&authority_bytes, &verifier_sha256)?; 292 293 cargo( 294 &["+1.97.1", "fmt", "--all", "--", "--check"], 295 "Step 298 formatting", 296 )?; 297 require_test_lane("service_build_qualification::tests", BUILD_TESTS)?; 298 require_test_lane("target_qualification::tests", TARGET_TESTS)?; 299 cargo( 300 &[ 301 "+1.97.1", 302 "run", 303 "--offline", 304 "--locked", 305 "-q", 306 "-p", 307 "xtask", 308 "--", 309 "contract", 310 "validate", 311 ], 312 "Step 298 contract validation", 313 )?; 314 require_nix()?; 315 316 let bytes = result_bytes(&arguments, &check_id, &verifier_sha256, &contract)?; 317 std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes) 318 .map_err(|_| "Step 298 result write failed".to_owned()) 319 } 320 321 fn validate_authority(authority_bytes: &[u8], verifier_sha256: &str) -> Result<Value, String> { 322 let authority: Value = serde_json::from_slice(authority_bytes) 323 .map_err(|_| "Step 298 gate authority is invalid".to_owned())?; 324 let mut canonical_authority = canonical(&authority)?; 325 canonical_authority.push(b'\n'); 326 let contracts = authority 327 .get("gate_command_contract") 328 .and_then(Value::as_array) 329 .ok_or_else(|| "Step 298 gate contract is absent".to_owned())?; 330 if authority_bytes != canonical_authority 331 || authority.get("schema") 332 != Some(&Value::String( 333 "radroots.lib.rshr-202-step-298-gates.v1".to_owned(), 334 )) 335 || authority.get("step") != Some(&json!([STEP])) 336 || contracts.as_slice() != [expected_contract(verifier_sha256)] 337 { 338 return Err("Step 298 gate authority differs".to_owned()); 339 } 340 Ok(contracts[0].clone()) 341 } 342 343 fn result_bytes( 344 arguments: &Arguments, 345 check_id: &str, 346 verifier_sha256: &str, 347 contract: &Value, 348 ) -> Result<Vec<u8>, String> { 349 let assertion = json!([{ 350 "id": format!("step_298_gate_01_{GATE_DIGEST}"), 351 "result": "pass" 352 }]); 353 let result = json!({ 354 "schema": "radroots.services-hardening.rshr-200-step-check-result.v1", 355 "step": STEP, 356 "check_id": check_id, 357 "gate_definition_sha256": GATE_DIGEST, 358 "source_revision": arguments.source_revision, 359 "source_tree": arguments.source_tree, 360 "candidate_generation": 0, 361 "candidate_digest": "none", 362 "command_contract_sha256": sha256(&canonical(contract)?), 363 "verifier_sha256": verifier_sha256, 364 "execution_request": [{ 365 "platform": arguments.platform, 366 "sha256": arguments.execution_request_sha256 367 }], 368 "assertion_inventory_sha256": sha256(&canonical(&assertion)?), 369 "assertion": assertion, 370 "result": "pass" 371 }); 372 let mut bytes = canonical(&result)?; 373 bytes.push(b'\n'); 374 Ok(bytes) 375 } 376 377 #[cfg(test)] 378 mod tests { 379 use super::*; 380 381 #[test] 382 fn command_capture_preserves_status_streams_and_enforces_both_output_limits() { 383 let output = bounded( 384 Command::new("/bin/sh").args(["-c", "printf output; printf diagnostic >&2"]), 385 "fixture", 386 ) 387 .unwrap(); 388 assert_eq!(output.stdout, b"output"); 389 assert_eq!(output.stderr, b"diagnostic"); 390 assert_eq!( 391 bounded(Command::new("/bin/sh").args(["-c", "exit 7"]), "fixture").unwrap_err(), 392 "fixture failed" 393 ); 394 let missing = tempfile::TempDir::new().unwrap(); 395 assert_eq!( 396 bounded(&mut Command::new(missing.path().join("absent")), "fixture").unwrap_err(), 397 "fixture could not start" 398 ); 399 for script in ["head -c \"$1\" /dev/zero", "head -c \"$1\" /dev/zero >&2"] { 400 let maximum = MAX_OUTPUT_BYTES.to_string(); 401 let output = bounded( 402 Command::new("/bin/sh").args(["-c", script, "fixture", &maximum]), 403 "fixture", 404 ) 405 .unwrap(); 406 assert_eq!(output.stdout.len() + output.stderr.len(), MAX_OUTPUT_BYTES); 407 let oversized = (MAX_OUTPUT_BYTES + 1).to_string(); 408 assert_eq!( 409 bounded( 410 Command::new("/bin/sh").args(["-c", script, "fixture", &oversized]), 411 "fixture" 412 ) 413 .unwrap_err(), 414 "fixture exceeded its output bound" 415 ); 416 } 417 } 418 419 fn arguments() -> Arguments { 420 Arguments { 421 step: STEP, 422 check_id: format!("gate-01-{GATE_DIGEST}"), 423 source_revision: "a".repeat(40), 424 source_tree: "0".repeat(40), 425 candidate_digest: "none".into(), 426 platform: "macos_aarch64".into(), 427 execution_request_sha256: "1".repeat(64), 428 } 429 } 430 431 #[test] 432 fn invalid_gate_arguments_are_rejected_before_external_work() { 433 assert_eq!( 434 validate_arguments(&arguments()).unwrap(), 435 format!("gate-01-{GATE_DIGEST}") 436 ); 437 for field in 0..8 { 438 let mut invalid = arguments(); 439 match field { 440 0 => invalid.step = 0, 441 1 => invalid.check_id.clear(), 442 2 => invalid.candidate_digest = "unbound".into(), 443 3 => invalid.platform = "linux".into(), 444 4 => invalid.source_revision.clear(), 445 5 => invalid.source_tree.clear(), 446 6 => invalid.execution_request_sha256.clear(), 447 _ => invalid.source_revision = "A".repeat(40), 448 } 449 assert_eq!(run(invalid).unwrap_err(), "Step 298 gate arguments differ"); 450 } 451 let mut invalid = arguments(); 452 invalid.source_tree = "g".repeat(40); 453 assert!(validate_arguments(&invalid).is_err()); 454 } 455 456 #[test] 457 fn authority_requires_canonical_bytes_and_exact_retained_bindings() { 458 let raw = include_bytes!("../../../contracts/rshr-202-step-298-gates.v1.json"); 459 let authority: Value = serde_json::from_slice(raw).unwrap(); 460 let verifier = authority["gate_command_contract"][0]["verifier_sha256"] 461 .as_str() 462 .unwrap(); 463 let contract = validate_authority(raw, verifier).unwrap(); 464 assert!(validate_authority(raw, &"f".repeat(64)).is_err()); 465 assert!(validate_authority(b"invalid", verifier).is_err()); 466 assert!(validate_authority(b"{}\n", verifier).is_err()); 467 assert!( 468 validate_authority(&serde_json::to_vec_pretty(&authority).unwrap(), verifier).is_err() 469 ); 470 for (pointer, value) in [ 471 ("/schema", json!("other")), 472 ("/step", json!([0])), 473 ("/gate_command_contract", json!([])), 474 ] { 475 let mut changed = authority.clone(); 476 *changed.pointer_mut(pointer).unwrap() = value; 477 let mut bytes = canonical(&changed).unwrap(); 478 bytes.push(b'\n'); 479 assert!(validate_authority(&bytes, verifier).is_err()); 480 } 481 // Encoding fixtures is not a historical gate execution or qualification. 482 let args = arguments(); 483 let bytes = result_bytes(&args, &args.check_id, verifier, &contract).unwrap(); 484 let result: Value = serde_json::from_slice(&bytes).unwrap(); 485 assert!(bytes.ends_with(b"\n")); 486 assert_eq!(result["source_revision"], args.source_revision); 487 assert_eq!(result["source_tree"], args.source_tree); 488 assert_eq!( 489 result["execution_request"][0]["sha256"], 490 args.execution_request_sha256 491 ); 492 assert_eq!( 493 result["command_contract_sha256"], 494 sha256(&canonical(&contract).unwrap()) 495 ); 496 assert_eq!( 497 result["assertion_inventory_sha256"], 498 sha256(&canonical(&result["assertion"]).unwrap()) 499 ); 500 } 501 }