rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

rshr_202_step_301_gate.rs (18079B)


      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 = 301;
     10 const GATE_DIGEST: &str = "bdbadb41ebcd7247fd4038db75956966e5daff9d824bcf8e91b50922c7ceb37f";
     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         "9d2ee2d488b002b5f0ca7795f1c75600a5a684d1f492a0fc8d7352ef23c261ae",
     20     ),
     21     (
     22         "Cargo.toml",
     23         "f3d01ba93661569cd7ba38b61c098a0425f24511a46ff0e14bd5e7adb9de74ea",
     24     ),
     25     (
     26         "contracts/release/rhi-artifact-contract.v3.json",
     27         "2548c52da4d9b721b9f87dc4a1a9ba3d3dc0eb30fc74c5f580f38fa94c2b59bd",
     28     ),
     29     (
     30         "flake.lock",
     31         "5d5b11622c341292f429a5f93e55f38a3506431b139720ff62f983b35bf7d55f",
     32     ),
     33     (
     34         "flake.nix",
     35         "d8da2c1ca51d82674a8c36f66f179b26d6abb3871c2a55da01a215b9af5190f2",
     36     ),
     37     (
     38         "radroots.service.source-lock.v3.toml",
     39         "cd8f293046ec8be9f74b1fbc562332e87bddab5e3c94b900ea5321aff33bd091",
     40     ),
     41 ];
     42 
     43 pub(crate) struct Arguments {
     44     pub(crate) step: u16,
     45     pub(crate) check_id: String,
     46     pub(crate) source_revision: String,
     47     pub(crate) source_tree: String,
     48     pub(crate) candidate_digest: String,
     49     pub(crate) platform: String,
     50     pub(crate) execution_request_sha256: String,
     51 }
     52 
     53 fn root() -> PathBuf {
     54     Path::new(env!("CARGO_MANIFEST_DIR"))
     55         .parent()
     56         .and_then(Path::parent)
     57         .expect("xtask must remain under tools/xtask")
     58         .to_path_buf()
     59 }
     60 
     61 fn sha256(bytes: &[u8]) -> String {
     62     hex::encode(Sha256::digest(bytes))
     63 }
     64 
     65 fn canonical(value: &Value) -> Result<Vec<u8>, String> {
     66     serde_json::to_vec(value).map_err(|_| "Step 301 JSON encoding failed".to_owned())
     67 }
     68 
     69 fn execute(command: &mut Command, label: &str) -> Result<Output, String> {
     70     let output = command
     71         .current_dir(root())
     72         .env("CARGO_NET_OFFLINE", "true")
     73         .env("CARGO_TERM_COLOR", "never")
     74         .output()
     75         .map_err(|_| format!("{label} could not start"))?;
     76     if output.stdout.len() > MAX_OUTPUT_BYTES || output.stderr.len() > MAX_OUTPUT_BYTES {
     77         return Err(format!("{label} exceeded its output bound"));
     78     }
     79     Ok(output)
     80 }
     81 
     82 fn bounded(command: &mut Command, label: &str) -> Result<Output, String> {
     83     let output = execute(command, label)?;
     84     if !output.status.success() {
     85         return Err(format!("{label} failed"));
     86     }
     87     Ok(output)
     88 }
     89 
     90 fn rejected(command: &mut Command, label: &str) -> Result<(), String> {
     91     if execute(command, label)?.status.success() {
     92         return Err(format!("{label} unexpectedly succeeded"));
     93     }
     94     Ok(())
     95 }
     96 
     97 fn resolve_nix() -> Result<PathBuf, String> {
     98     if let Some(explicit) = env::var_os("RSHR_NIX_EXECUTABLE") {
     99         return fs::canonicalize(explicit)
    100             .map_err(|_| "Step 301 Nix client is unavailable".to_owned());
    101     }
    102     let path = env::var_os("PATH").ok_or_else(|| "Step 301 PATH is absent".to_owned())?;
    103     env::split_paths(&path)
    104         .map(|directory| directory.join("nix"))
    105         .find(|candidate| candidate.is_file())
    106         .and_then(|candidate| fs::canonicalize(candidate).ok())
    107         .ok_or_else(|| "Step 301 Nix client is unavailable".to_owned())
    108 }
    109 
    110 fn object_keys(value: &Value, label: &str) -> Result<Vec<String>, String> {
    111     let mut keys = value
    112         .as_object()
    113         .ok_or_else(|| format!("Step 301 {label} is not an object"))?
    114         .keys()
    115         .cloned()
    116         .collect::<Vec<_>>();
    117     keys.sort_unstable();
    118     Ok(keys)
    119 }
    120 
    121 fn require_source_lock() -> Result<(), String> {
    122     let root = root();
    123     let lock_bytes = fs::read(root.join("radroots.service.source-lock.v3.toml"))
    124         .map_err(|_| "Step 301 source lock is unreadable".to_owned())?;
    125     let lock: toml::Value = toml::from_str(
    126         std::str::from_utf8(&lock_bytes)
    127             .map_err(|_| "Step 301 source lock is not UTF-8".to_owned())?,
    128     )
    129     .map_err(|_| "Step 301 source lock is invalid".to_owned())?;
    130     let cargo_sha = sha256(
    131         &fs::read(root.join("Cargo.lock"))
    132             .map_err(|_| "Step 301 Cargo lock is unreadable".to_owned())?,
    133     );
    134     let flake_sha = sha256(
    135         &fs::read(root.join("flake.lock"))
    136             .map_err(|_| "Step 301 flake lock is unreadable".to_owned())?,
    137     );
    138     let artifact_sha = sha256(
    139         &fs::read(root.join("contracts/release/rhi-artifact-contract.v3.json"))
    140             .map_err(|_| "Step 301 artifact contract is unreadable".to_owned())?,
    141     );
    142     if lock.get("schema").and_then(toml::Value::as_str) != Some("radroots.service.source-lock.v3")
    143         || lock
    144             .get("contract_version")
    145             .and_then(toml::Value::as_integer)
    146             != Some(3)
    147         || lock.get("revision").and_then(toml::Value::as_str) != Some(LIB_REVISION)
    148         || lock.get("cargo_lock_sha256").and_then(toml::Value::as_str) != Some(&cargo_sha)
    149         || lock["nix"]["material"].as_str() != Some("qualified")
    150         || lock["nix"]["lib_revision"].as_str() != Some(LIB_REVISION)
    151         || lock["nix"]["public_input_lock"]["sha256"].as_str() != Some(&flake_sha)
    152         || lock["artifact_contract"]["sha256"].as_str() != Some(&artifact_sha)
    153         || lock["sqlite"]["high_level_authority"].as_str() != Some("sqlx_only")
    154         || lock["sqlite"]["native_linkage_count"].as_integer() != Some(1)
    155     {
    156         return Err("Step 301 source lock differs".to_owned());
    157     }
    158 
    159     let flake_lock: Value = serde_json::from_slice(
    160         &fs::read(root.join("flake.lock"))
    161             .map_err(|_| "Step 301 flake lock is unreadable".to_owned())?,
    162     )
    163     .map_err(|_| "Step 301 flake lock is invalid".to_owned())?;
    164     if flake_lock.pointer("/nodes/root/inputs/lib") != Some(&json!("lib"))
    165         || flake_lock.pointer("/nodes/lib/locked/rev") != Some(&json!(LIB_REVISION))
    166         || flake_lock.pointer("/nodes/lib/original/rev") != Some(&json!(LIB_REVISION))
    167     {
    168         return Err("Step 301 exact Lib input differs".to_owned());
    169     }
    170     Ok(())
    171 }
    172 
    173 fn require_single_sqlite() -> Result<(), String> {
    174     let metadata = bounded(
    175         Command::new("cargo").args([
    176             "+1.97.1",
    177             "metadata",
    178             "--offline",
    179             "--locked",
    180             "--format-version",
    181             "1",
    182         ]),
    183         "Step 301 Cargo metadata",
    184     )?;
    185     let value: Value = serde_json::from_slice(&metadata.stdout)
    186         .map_err(|_| "Step 301 Cargo metadata is invalid".to_owned())?;
    187     let packages = value["packages"]
    188         .as_array()
    189         .ok_or_else(|| "Step 301 Cargo package inventory is absent".to_owned())?;
    190     let sqlite_ids = packages
    191         .iter()
    192         .filter(|package| package["name"] == "libsqlite3-sys")
    193         .filter_map(|package| package["id"].as_str())
    194         .collect::<Vec<_>>();
    195     if sqlite_ids.len() != 1 || packages.iter().any(|package| package["name"] == "rusqlite") {
    196         return Err("Step 301 native SQLite package inventory differs".to_owned());
    197     }
    198     let nodes = value["resolve"]["nodes"]
    199         .as_array()
    200         .ok_or_else(|| "Step 301 Cargo resolve inventory is absent".to_owned())?;
    201     let sqlite_node = nodes
    202         .iter()
    203         .find(|node| node["id"] == sqlite_ids[0])
    204         .ok_or_else(|| "Step 301 SQLite resolve node is absent".to_owned())?;
    205     let features = sqlite_node["features"]
    206         .as_array()
    207         .ok_or_else(|| "Step 301 SQLite features are absent".to_owned())?;
    208     if !features.iter().any(|feature| feature == "bundled") {
    209         return Err("Step 301 bundled SQLite feature is absent".to_owned());
    210     }
    211     Ok(())
    212 }
    213 
    214 fn require_outputs(nix: &Path) -> Result<(), String> {
    215     let show = bounded(
    216         Command::new(nix).args([
    217             "--offline",
    218             "flake",
    219             "show",
    220             "--json",
    221             "--all-systems",
    222             "--no-write-lock-file",
    223         ]),
    224         "Step 301 Nix output inventory",
    225     )?;
    226     let inventory: Value = serde_json::from_slice(&show.stdout)
    227         .map_err(|_| "Step 301 Nix output inventory is invalid".to_owned())?;
    228     let systems = ["aarch64-darwin", "x86_64-linux"];
    229     for family in ["apps", "checks", "devShells", "packages"] {
    230         if object_keys(&inventory[family], family)? != systems {
    231             return Err(format!("Step 301 {family} systems differ"));
    232         }
    233     }
    234     if object_keys(&inventory["nixosModules"], "nixosModules")? != ["default"]
    235         || inventory.pointer("/nixosModules/default/type") != Some(&json!("nixos-module"))
    236     {
    237         return Err("Step 301 NixOS module inventory differs".to_owned());
    238     }
    239     let expected_checks = [
    240         "check",
    241         "clippy",
    242         "config",
    243         "docs",
    244         "fmt",
    245         "integration",
    246         "package",
    247         "source-lock",
    248         "sqlx",
    249         "test",
    250     ];
    251     for system in systems {
    252         if object_keys(&inventory["apps"][system], "apps")? != ["default", "release-acceptance"]
    253             || object_keys(&inventory["checks"][system], "checks")? != expected_checks
    254             || object_keys(&inventory["devShells"][system], "devShells")? != ["default"]
    255             || inventory["packages"][system]["default"]["name"] != "rhi-0.1.0"
    256             || inventory["apps"][system]["default"]["description"] != "Run the built rhi service"
    257         {
    258             return Err("Step 301 service output inventory differs".to_owned());
    259         }
    260     }
    261     if object_keys(&inventory["packages"]["aarch64-darwin"], "Darwin packages")? != ["default"]
    262         || object_keys(&inventory["packages"]["x86_64-linux"], "Linux packages")?
    263             != ["default", "oci"]
    264         || inventory["packages"]["x86_64-linux"]["oci"]["name"] != "rhi.tar.gz"
    265     {
    266         return Err("Step 301 platform-specific package inventory differs".to_owned());
    267     }
    268     for system in ["x86_64-darwin", "aarch64-linux", "x86_64-windows"] {
    269         rejected(
    270             Command::new(nix).args([
    271                 "--offline",
    272                 "eval",
    273                 "--raw",
    274                 &format!(".#packages.{system}.default.name"),
    275             ]),
    276             "Step 301 excluded-system evaluation",
    277         )?;
    278     }
    279     rejected(
    280         Command::new(nix).args([
    281             "--offline",
    282             "eval",
    283             "--raw",
    284             ".#packages.aarch64-darwin.oci.name",
    285         ]),
    286         "Step 301 Darwin OCI evaluation",
    287     )?;
    288     Ok(())
    289 }
    290 
    291 fn require_nix() -> Result<(), String> {
    292     let executable = resolve_nix()?;
    293     if sha256(&fs::read(&executable).map_err(|_| "Step 301 Nix client is unreadable")?)
    294         != NIX_SHA256
    295     {
    296         return Err("Step 301 Nix client identity differs".to_owned());
    297     }
    298     let version = bounded(
    299         Command::new(&executable).arg("--version"),
    300         "Step 301 Nix version",
    301     )?;
    302     if sha256(&version.stdout) != NIX_VERSION_SHA256 {
    303         return Err("Step 301 Nix version differs".to_owned());
    304     }
    305     bounded(
    306         Command::new(&executable).args([
    307             "--offline",
    308             "flake",
    309             "check",
    310             "--all-systems",
    311             "--no-build",
    312             "--no-write-lock-file",
    313         ]),
    314         "Step 301 Nix evaluation",
    315     )?;
    316     require_outputs(&executable)
    317 }
    318 
    319 fn expected_contract(verifier_sha256: &str) -> Value {
    320     json!({
    321         "argv_template": [
    322             "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked",
    323             "-q", "-p", "rhi_xtask", "--", "rshr-step-301-gate", "--step={step}",
    324             "--check-id={check_id}", "--source-revision={source_revision}",
    325             "--source-tree={source_tree}", "--candidate-digest={candidate_digest}",
    326             "--platform=macos_aarch64",
    327             "--execution-request-sha256={execution_request_sha256}"
    328         ],
    329         "assertion_id": [format!("step_301_gate_01_{GATE_DIGEST}")],
    330         "check_id": format!("gate-01-{GATE_DIGEST}"),
    331         "environment_authority": {
    332             "cache_policy_id": "rshr-200-step-287-cache-policy.v1",
    333             "cache_policy_sha256": "3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa",
    334             "cadence_policy_id": "rshr-200-step-287-cadence-policy.v1",
    335             "cadence_policy_sha256": "d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1",
    336             "isolation": "extbuild_host_constrained",
    337             "network": "disabled",
    338             "network_policy_id": "none",
    339             "network_policy_sha256": "none",
    340             "resource_policy_id": "rshr-200-step-287-resource-policy.v1",
    341             "resource_policy_sha256": "05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e"
    342         },
    343         "environment_names": [
    344             "EXT_BUILD_CONFIG", "EXT_BUILD_MACHINE_CONFIG", "EXT_BUILD_ROOT", "HOME", "PATH",
    345             "RUSTUP_TOOLCHAIN", "TMPDIR"
    346         ],
    347         "gate_definition_sha256": GATE_DIGEST,
    348         "required_platforms": ["macos_aarch64"],
    349         "required_tools": ["rustc"],
    350         "result_schema": "radroots.services-hardening.rshr-200-step-check-result.v1",
    351         "schema": "radroots.services-hardening.rshr-200-step-check-command.v1",
    352         "step": STEP,
    353         "verifier_path": "tools/xtask/src/rshr_202_step_301_gate.rs",
    354         "verifier_sha256": verifier_sha256
    355     })
    356 }
    357 
    358 pub(crate) fn run(arguments: Arguments) -> Result<(), String> {
    359     let check_id = format!("gate-01-{GATE_DIGEST}");
    360     if arguments.step != STEP
    361         || arguments.check_id != check_id
    362         || arguments.candidate_digest != "none"
    363         || arguments.platform != "macos_aarch64"
    364         || arguments.source_revision.len() != 40
    365         || arguments.source_tree.len() != 40
    366         || arguments.execution_request_sha256.len() != 64
    367         || !arguments
    368             .source_revision
    369             .bytes()
    370             .chain(arguments.source_tree.bytes())
    371             .chain(arguments.execution_request_sha256.bytes())
    372             .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
    373     {
    374         return Err("Step 301 gate arguments differ".to_owned());
    375     }
    376     let root = root();
    377     if root.join(".github").exists() || root.join("radroots.service.source-lock.v2.toml").exists() {
    378         return Err("Step 301 forbidden legacy surface is present".to_owned());
    379     }
    380     for (relative, expected) in EXACT_SOURCES {
    381         let bytes = fs::read(root.join(relative))
    382             .map_err(|_| "Step 301 governed source is unreadable".to_owned())?;
    383         if sha256(&bytes) != *expected {
    384             return Err("Step 301 governed source bytes differ".to_owned());
    385         }
    386     }
    387     let flake_source = fs::read_to_string(root.join("flake.nix"))
    388         .map_err(|_| "Step 301 flake source is unreadable".to_owned())?;
    389     for forbidden in [
    390         "pkgs.clang",
    391         "libclang",
    392         "libsodium",
    393         "pkgs.openssl",
    394         "pkgs.pkg-config",
    395         "pkgs.sqlite",
    396     ] {
    397         if flake_source.contains(forbidden) {
    398             return Err("Step 301 forbidden native dependency is present".to_owned());
    399         }
    400     }
    401 
    402     let verifier_path = root.join("tools/xtask/src/rshr_202_step_301_gate.rs");
    403     let verifier_sha256 =
    404         sha256(&fs::read(verifier_path).map_err(|_| "Step 301 verifier is unreadable")?);
    405     let authority_path = root.join("contracts/rshr-202-step-301-gates.v1.json");
    406     let authority_bytes =
    407         fs::read(authority_path).map_err(|_| "Step 301 gate authority is unreadable")?;
    408     let authority: Value = serde_json::from_slice(&authority_bytes)
    409         .map_err(|_| "Step 301 gate authority is invalid".to_owned())?;
    410     let mut canonical_authority = canonical(&authority)?;
    411     canonical_authority.push(b'\n');
    412     let contracts = authority
    413         .get("gate_command_contract")
    414         .and_then(Value::as_array)
    415         .ok_or_else(|| "Step 301 gate contract is absent".to_owned())?;
    416     if authority_bytes != canonical_authority
    417         || authority.get("schema")
    418             != Some(&Value::String(
    419                 "radroots.rhi.rshr-202-step-301-gates.v1".to_owned(),
    420             ))
    421         || authority.get("step") != Some(&json!([STEP]))
    422         || contracts.as_slice() != [expected_contract(&verifier_sha256)]
    423     {
    424         return Err("Step 301 gate authority differs".to_owned());
    425     }
    426 
    427     require_source_lock()?;
    428     bounded(
    429         Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]),
    430         "Step 301 formatting",
    431     )?;
    432     bounded(
    433         Command::new("cargo").args([
    434             "+1.97.1",
    435             "check",
    436             "--offline",
    437             "--locked",
    438             "--workspace",
    439             "--all-targets",
    440         ]),
    441         "Step 301 Cargo check",
    442     )?;
    443     require_single_sqlite()?;
    444     require_nix()?;
    445 
    446     let contract = &contracts[0];
    447     let assertion = json!([{
    448         "id": format!("step_301_gate_01_{GATE_DIGEST}"),
    449         "result": "pass"
    450     }]);
    451     let result = json!({
    452         "schema": "radroots.services-hardening.rshr-200-step-check-result.v1",
    453         "step": STEP,
    454         "check_id": check_id,
    455         "gate_definition_sha256": GATE_DIGEST,
    456         "source_revision": arguments.source_revision,
    457         "source_tree": arguments.source_tree,
    458         "candidate_generation": 0,
    459         "candidate_digest": "none",
    460         "command_contract_sha256": sha256(&canonical(contract)?),
    461         "verifier_sha256": verifier_sha256,
    462         "execution_request": [{
    463             "platform": arguments.platform,
    464             "sha256": arguments.execution_request_sha256
    465         }],
    466         "assertion_inventory_sha256": sha256(&canonical(&assertion)?),
    467         "assertion": assertion,
    468         "result": "pass"
    469     });
    470     let mut bytes = canonical(&result)?;
    471     bytes.push(b'\n');
    472     std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes)
    473         .map_err(|_| "Step 301 result write failed".to_owned())
    474 }