lib

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

rshr_202_step_305_gate.rs (15883B)


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