lib

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

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 }