lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }