radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

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 }