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