lib

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

rshr_202_step_304_gate.rs (17672B)


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