rshr_202_step_303_gate.rs (13856B)
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 = 303; 10 const GATE_DIGEST: &str = "79dc2cfbe14c07aeefba9779d61823291c7101d93fece3935909fc13f02261d0"; 11 const LIB_REVISION: &str = "055096853fca95e15d0f813d33a14aca13be3881"; 12 const NIX_SHA256: &str = "a59ab70f97f6d571642d13c7506aafec0a4275520d53daee2d8451be7c495cd1"; 13 const NIX_VERSION_SHA256: &str = "6db806391ffaea4cdb08ade0031feac399c0cd08474b3bfde8cb33f88a36c8e1"; 14 const MAX_OUTPUT_BYTES: usize = 32 * 1024 * 1024; 15 16 const EXACT_SOURCES: &[(&str, &str)] = &[ 17 ( 18 "Cargo.lock", 19 "b9278c33ad53b93e02b68017e5f8b62616feac9c6779747f1d446fe69c642052", 20 ), 21 ( 22 "Cargo.toml", 23 "a29ad1fa4fd60ac96b0b52698f3cd9fadb1324a890aa4f8fd1bc9a86935a8e8f", 24 ), 25 ( 26 "flake.lock", 27 "5d5b11622c341292f429a5f93e55f38a3506431b139720ff62f983b35bf7d55f", 28 ), 29 ( 30 "flake.nix", 31 "96d777e49c572087c4411b27e47f1b15e32983317cb24b9f9058485b4d090bab", 32 ), 33 ]; 34 35 pub(crate) struct Arguments { 36 pub(crate) step: u16, 37 pub(crate) check_id: String, 38 pub(crate) source_revision: String, 39 pub(crate) source_tree: String, 40 pub(crate) candidate_digest: String, 41 pub(crate) platform: String, 42 pub(crate) execution_request_sha256: String, 43 } 44 45 fn root() -> PathBuf { 46 Path::new(env!("CARGO_MANIFEST_DIR")) 47 .parent() 48 .and_then(Path::parent) 49 .expect("xtask must remain under tools/xtask") 50 .to_path_buf() 51 } 52 53 fn sha256(bytes: &[u8]) -> String { 54 hex::encode(Sha256::digest(bytes)) 55 } 56 57 fn canonical(value: &Value) -> Result<Vec<u8>, String> { 58 serde_json::to_vec(value).map_err(|_| "Step 303 JSON encoding failed".to_owned()) 59 } 60 61 fn execute(command: &mut Command, label: &str) -> Result<Output, String> { 62 let output = command 63 .current_dir(root()) 64 .env("CARGO_NET_OFFLINE", "true") 65 .env("CARGO_TERM_COLOR", "never") 66 .output() 67 .map_err(|_| format!("{label} could not start"))?; 68 if output.stdout.len() > MAX_OUTPUT_BYTES || output.stderr.len() > MAX_OUTPUT_BYTES { 69 return Err(format!("{label} exceeded its output bound")); 70 } 71 Ok(output) 72 } 73 74 fn bounded(command: &mut Command, label: &str) -> Result<Output, String> { 75 let output = execute(command, label)?; 76 if !output.status.success() { 77 return Err(format!("{label} failed")); 78 } 79 Ok(output) 80 } 81 82 fn rejected(command: &mut Command, label: &str) -> Result<(), String> { 83 if execute(command, label)?.status.success() { 84 return Err(format!("{label} unexpectedly succeeded")); 85 } 86 Ok(()) 87 } 88 89 fn resolve_nix() -> Result<PathBuf, String> { 90 if let Some(explicit) = env::var_os("RSHR_NIX_EXECUTABLE") { 91 return fs::canonicalize(explicit) 92 .map_err(|_| "Step 303 Nix client is unavailable".to_owned()); 93 } 94 let path = env::var_os("PATH").ok_or_else(|| "Step 303 PATH is absent".to_owned())?; 95 env::split_paths(&path) 96 .map(|directory| directory.join("nix")) 97 .find(|candidate| candidate.is_file()) 98 .and_then(|candidate| fs::canonicalize(candidate).ok()) 99 .ok_or_else(|| "Step 303 Nix client is unavailable".to_owned()) 100 } 101 102 fn object_keys(value: &Value, label: &str) -> Result<Vec<String>, String> { 103 let mut keys = value 104 .as_object() 105 .ok_or_else(|| format!("Step 303 {label} is not an object"))? 106 .keys() 107 .cloned() 108 .collect::<Vec<_>>(); 109 keys.sort_unstable(); 110 Ok(keys) 111 } 112 113 fn require_lock() -> Result<(), String> { 114 let lock: Value = serde_json::from_slice( 115 &fs::read(root().join("flake.lock")) 116 .map_err(|_| "Step 303 flake lock is unreadable".to_owned())?, 117 ) 118 .map_err(|_| "Step 303 flake lock is invalid".to_owned())?; 119 if lock.pointer("/nodes/root/inputs/lib") != Some(&json!("lib")) 120 || lock.pointer("/nodes/lib/locked/rev") != Some(&json!(LIB_REVISION)) 121 || lock.pointer("/nodes/lib/original/rev") != Some(&json!(LIB_REVISION)) 122 { 123 return Err("Step 303 exact Nix tooling input differs".to_owned()); 124 } 125 Ok(()) 126 } 127 128 fn require_outputs(nix: &Path) -> Result<(), String> { 129 let show = bounded( 130 Command::new(nix).args([ 131 "--offline", 132 "flake", 133 "show", 134 "--json", 135 "--all-systems", 136 "--no-write-lock-file", 137 ]), 138 "Step 303 Nix output inventory", 139 )?; 140 let inventory: Value = serde_json::from_slice(&show.stdout) 141 .map_err(|_| "Step 303 Nix output inventory is invalid".to_owned())?; 142 if object_keys(&inventory, "root output inventory")? != ["apps", "checks", "packages"] { 143 return Err("Step 303 root output inventory differs".to_owned()); 144 } 145 let systems = ["aarch64-darwin", "x86_64-linux"]; 146 for family in ["apps", "checks", "packages"] { 147 if object_keys(&inventory[family], family)? != systems { 148 return Err(format!("Step 303 {family} systems differ")); 149 } 150 } 151 for system in systems { 152 if object_keys(&inventory["apps"][system], "apps")? != ["default"] 153 || object_keys(&inventory["checks"][system], "checks")? != ["default"] 154 || object_keys(&inventory["packages"][system], "packages")? != ["default"] 155 || inventory["apps"][system]["default"]["description"] 156 != "Run the built radrootsd daemon" 157 || inventory["checks"][system]["default"]["name"] != "radrootsd-check-1" 158 || inventory["packages"][system]["default"]["name"] != "radrootsd-0.1.0" 159 { 160 return Err("Step 303 radrootsd output inventory differs".to_owned()); 161 } 162 } 163 for system in ["x86_64-darwin", "aarch64-linux", "x86_64-windows"] { 164 rejected( 165 Command::new(nix).args([ 166 "--offline", 167 "eval", 168 "--raw", 169 &format!(".#packages.{system}.default.name"), 170 ]), 171 "Step 303 excluded-system evaluation", 172 )?; 173 } 174 for attribute in [ 175 ".#devShells.aarch64-darwin.default.name", 176 ".#nixosModules.default", 177 ".#packages.x86_64-linux.oci.name", 178 ] { 179 rejected( 180 Command::new(nix).args(["--offline", "eval", "--raw", attribute]), 181 "Step 303 unowned output evaluation", 182 )?; 183 } 184 Ok(()) 185 } 186 187 fn require_nix() -> Result<(), String> { 188 let executable = resolve_nix()?; 189 if sha256(&fs::read(&executable).map_err(|_| "Step 303 Nix client is unreadable")?) 190 != NIX_SHA256 191 { 192 return Err("Step 303 Nix client identity differs".to_owned()); 193 } 194 let version = bounded( 195 Command::new(&executable).arg("--version"), 196 "Step 303 Nix version", 197 )?; 198 if sha256(&version.stdout) != NIX_VERSION_SHA256 { 199 return Err("Step 303 Nix version differs".to_owned()); 200 } 201 bounded( 202 Command::new(&executable).args([ 203 "--offline", 204 "flake", 205 "check", 206 "--all-systems", 207 "--no-build", 208 "--no-write-lock-file", 209 ]), 210 "Step 303 Nix evaluation", 211 )?; 212 require_outputs(&executable) 213 } 214 215 fn expected_contract(verifier_sha256: &str) -> Value { 216 json!({ 217 "argv_template": [ 218 "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked", 219 "-q", "-p", "radrootsd_xtask", "--", "rshr-step-303-gate", "--step={step}", 220 "--check-id={check_id}", "--source-revision={source_revision}", 221 "--source-tree={source_tree}", "--candidate-digest={candidate_digest}", 222 "--platform=macos_aarch64", 223 "--execution-request-sha256={execution_request_sha256}" 224 ], 225 "assertion_id": [format!("step_303_gate_01_{GATE_DIGEST}")], 226 "check_id": format!("gate-01-{GATE_DIGEST}"), 227 "environment_authority": { 228 "cache_policy_id": "rshr-200-step-287-cache-policy.v1", 229 "cache_policy_sha256": "3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa", 230 "cadence_policy_id": "rshr-200-step-287-cadence-policy.v1", 231 "cadence_policy_sha256": "d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1", 232 "isolation": "extbuild_host_constrained", 233 "network": "disabled", 234 "network_policy_id": "none", 235 "network_policy_sha256": "none", 236 "resource_policy_id": "rshr-200-step-287-resource-policy.v1", 237 "resource_policy_sha256": "05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e" 238 }, 239 "environment_names": [ 240 "EXT_BUILD_CONFIG", "EXT_BUILD_MACHINE_CONFIG", "EXT_BUILD_ROOT", "HOME", "PATH", 241 "RUSTUP_TOOLCHAIN", "TMPDIR" 242 ], 243 "gate_definition_sha256": GATE_DIGEST, 244 "required_platforms": ["macos_aarch64"], 245 "required_tools": ["rustc"], 246 "result_schema": "radroots.services-hardening.rshr-200-step-check-result.v1", 247 "schema": "radroots.services-hardening.rshr-200-step-check-command.v1", 248 "step": STEP, 249 "verifier_path": "tools/xtask/src/rshr_202_step_303_gate.rs", 250 "verifier_sha256": verifier_sha256 251 }) 252 } 253 254 pub(crate) fn run(arguments: Arguments) -> Result<(), String> { 255 let check_id = format!("gate-01-{GATE_DIGEST}"); 256 if arguments.step != STEP 257 || arguments.check_id != check_id 258 || arguments.candidate_digest != "none" 259 || arguments.platform != "macos_aarch64" 260 || arguments.source_revision.len() != 40 261 || arguments.source_tree.len() != 40 262 || arguments.execution_request_sha256.len() != 64 263 || !arguments 264 .source_revision 265 .bytes() 266 .chain(arguments.source_tree.bytes()) 267 .chain(arguments.execution_request_sha256.bytes()) 268 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) 269 { 270 return Err("Step 303 gate arguments differ".to_owned()); 271 } 272 let root = root(); 273 if root.join(".github").exists() || root.join(".act").exists() || root.join("docs").exists() { 274 return Err("Step 303 forbidden repository root is present".to_owned()); 275 } 276 for (relative, expected) in EXACT_SOURCES { 277 let bytes = fs::read(root.join(relative)) 278 .map_err(|_| "Step 303 governed source is unreadable".to_owned())?; 279 if sha256(&bytes) != *expected { 280 return Err("Step 303 governed source bytes differ".to_owned()); 281 } 282 } 283 let flake_source = fs::read_to_string(root.join("flake.nix")) 284 .map_err(|_| "Step 303 flake source is unreadable".to_owned())?; 285 for forbidden in [ 286 "writeShellApplication", 287 "git rev-parse", 288 "repo_root", 289 "devShells", 290 "nixosModules", 291 "aarch64-linux", 292 "x86_64-darwin", 293 ] { 294 if flake_source.contains(forbidden) { 295 return Err("Step 303 checkout wrapper or unowned output is present".to_owned()); 296 } 297 } 298 299 let verifier_path = root.join("tools/xtask/src/rshr_202_step_303_gate.rs"); 300 let verifier_sha256 = 301 sha256(&fs::read(verifier_path).map_err(|_| "Step 303 verifier is unreadable")?); 302 let authority_path = root.join("contracts/rshr-202-step-303-gates.v1.json"); 303 let authority_bytes = 304 fs::read(authority_path).map_err(|_| "Step 303 gate authority is unreadable")?; 305 let authority: Value = serde_json::from_slice(&authority_bytes) 306 .map_err(|_| "Step 303 gate authority is invalid".to_owned())?; 307 let mut canonical_authority = canonical(&authority)?; 308 canonical_authority.push(b'\n'); 309 let contracts = authority 310 .get("gate_command_contract") 311 .and_then(Value::as_array) 312 .ok_or_else(|| "Step 303 gate contract is absent".to_owned())?; 313 if authority_bytes != canonical_authority 314 || authority.get("schema") 315 != Some(&Value::String( 316 "radroots.radrootsd.rshr-202-step-303-gates.v1".to_owned(), 317 )) 318 || authority.get("step") != Some(&json!([STEP])) 319 || contracts.as_slice() != [expected_contract(&verifier_sha256)] 320 { 321 return Err("Step 303 gate authority differs".to_owned()); 322 } 323 324 require_lock()?; 325 bounded( 326 Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]), 327 "Step 303 formatting", 328 )?; 329 bounded( 330 Command::new("cargo").args([ 331 "+1.97.1", 332 "check", 333 "--offline", 334 "--locked", 335 "--workspace", 336 "--all-targets", 337 ]), 338 "Step 303 Cargo check", 339 )?; 340 require_nix()?; 341 342 let contract = &contracts[0]; 343 let assertion = json!([{ 344 "id": format!("step_303_gate_01_{GATE_DIGEST}"), 345 "result": "pass" 346 }]); 347 let result = json!({ 348 "schema": "radroots.services-hardening.rshr-200-step-check-result.v1", 349 "step": STEP, 350 "check_id": check_id, 351 "gate_definition_sha256": GATE_DIGEST, 352 "source_revision": arguments.source_revision, 353 "source_tree": arguments.source_tree, 354 "candidate_generation": 0, 355 "candidate_digest": "none", 356 "command_contract_sha256": sha256(&canonical(contract)?), 357 "verifier_sha256": verifier_sha256, 358 "execution_request": [{ 359 "platform": arguments.platform, 360 "sha256": arguments.execution_request_sha256 361 }], 362 "assertion_inventory_sha256": sha256(&canonical(&assertion)?), 363 "assertion": assertion, 364 "result": "pass" 365 }); 366 let mut bytes = canonical(&result)?; 367 bytes.push(b'\n'); 368 std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes) 369 .map_err(|_| "Step 303 result write failed".to_owned()) 370 }