rhi

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

commit c92a43a67140526a1efbeab4ee575990f044edf2
parent 3393914c7e1b337dd645aabbf19ab94923d6ac12
Author: triesap <tyson@radroots.org>
Date:   Mon,  7 Sep 2026 01:45:04 +0000

test(nix): bind the Step 301 RHI gate

- Bind the exact RHI source, Lib pin, flake lock, and artifact contract bytes.
- Verify Cargo, Nix output inventory, excluded systems, and bundled SQLite.
- Record the frozen Nix client and macOS platform evidence authority.
- Emit the governed Step 301 result without publication or activation.

Diffstat:
Acontracts/rshr-202-step-301-gates.v1.json | 1+
Mtools/xtask/src/main.rs | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/xtask/src/rshr_202_step_301_gate.rs | 474+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/xtask/src/rshr_202_step_301_platform.rs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 635 insertions(+), 0 deletions(-)

diff --git a/contracts/rshr-202-step-301-gates.v1.json b/contracts/rshr-202-step-301-gates.v1.json @@ -0,0 +1 @@ +{"gate_command_contract":[{"argv_template":["cargo","extbuild","run","--","cargo","run","--offline","--locked","-q","-p","rhi_xtask","--","rshr-step-301-gate","--step={step}","--check-id={check_id}","--source-revision={source_revision}","--source-tree={source_tree}","--candidate-digest={candidate_digest}","--platform=macos_aarch64","--execution-request-sha256={execution_request_sha256}"],"assertion_id":["step_301_gate_01_bdbadb41ebcd7247fd4038db75956966e5daff9d824bcf8e91b50922c7ceb37f"],"check_id":"gate-01-bdbadb41ebcd7247fd4038db75956966e5daff9d824bcf8e91b50922c7ceb37f","environment_authority":{"cache_policy_id":"rshr-200-step-287-cache-policy.v1","cache_policy_sha256":"3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa","cadence_policy_id":"rshr-200-step-287-cadence-policy.v1","cadence_policy_sha256":"d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1","isolation":"extbuild_host_constrained","network":"disabled","network_policy_id":"none","network_policy_sha256":"none","resource_policy_id":"rshr-200-step-287-resource-policy.v1","resource_policy_sha256":"05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e"},"environment_names":["EXT_BUILD_CONFIG","EXT_BUILD_MACHINE_CONFIG","EXT_BUILD_ROOT","HOME","PATH","RUSTUP_TOOLCHAIN","TMPDIR"],"gate_definition_sha256":"bdbadb41ebcd7247fd4038db75956966e5daff9d824bcf8e91b50922c7ceb37f","required_platforms":["macos_aarch64"],"required_tools":["rustc"],"result_schema":"radroots.services-hardening.rshr-200-step-check-result.v1","schema":"radroots.services-hardening.rshr-200-step-check-command.v1","step":301,"verifier_path":"tools/xtask/src/rshr_202_step_301_gate.rs","verifier_sha256":"a549813cc4905b31025379cadd3333ad8d0a9ecdc1dbe7f8c2644c15286b011a"}],"schema":"radroots.rhi.rshr-202-step-301-gates.v1","step":[301]} diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -1,5 +1,8 @@ #![forbid(unsafe_code)] +mod rshr_202_step_301_gate; +mod rshr_202_step_301_platform; + use std::{ collections::{BTreeMap, BTreeSet}, env, fmt, fs, @@ -371,10 +374,64 @@ fn run_main() -> Result<(), ReleaseError> { let args = parse_native_release_args(arguments.collect())?; native_release(&workspace_root(), &args) } + Some("rshr-step-301-gate") => { + let args = parse_rshr_step_301_gate_args(arguments.collect())?; + rshr_202_step_301_gate::run(args).map_err(|_| ReleaseError::Generation) + } + Some("rshr-step-301-platform-probe") if arguments.next().is_none() => { + rshr_202_step_301_platform::run().map_err(|_| ReleaseError::Generation) + } _ => Err(ReleaseError::InvalidArguments), } } +fn parse_rshr_step_301_gate_args( + values: Vec<String>, +) -> Result<rshr_202_step_301_gate::Arguments, ReleaseError> { + let mut step = None; + let mut check_id = None; + let mut source_revision = None; + let mut source_tree = None; + let mut candidate_digest = None; + let mut platform = None; + let mut execution_request_sha256 = None; + for value in values { + let (name, value) = value + .split_once('=') + .ok_or(ReleaseError::InvalidArguments)?; + let slot = match name { + "--check-id" => &mut check_id, + "--source-revision" => &mut source_revision, + "--source-tree" => &mut source_tree, + "--candidate-digest" => &mut candidate_digest, + "--platform" => &mut platform, + "--execution-request-sha256" => &mut execution_request_sha256, + "--step" => { + let parsed = value + .parse::<u16>() + .map_err(|_| ReleaseError::InvalidArguments)?; + if step.replace(parsed).is_some() { + return Err(ReleaseError::InvalidArguments); + } + continue; + } + _ => return Err(ReleaseError::InvalidArguments), + }; + if value.is_empty() || slot.replace(value.to_owned()).is_some() { + return Err(ReleaseError::InvalidArguments); + } + } + Ok(rshr_202_step_301_gate::Arguments { + step: step.ok_or(ReleaseError::InvalidArguments)?, + check_id: check_id.ok_or(ReleaseError::InvalidArguments)?, + source_revision: source_revision.ok_or(ReleaseError::InvalidArguments)?, + source_tree: source_tree.ok_or(ReleaseError::InvalidArguments)?, + candidate_digest: candidate_digest.ok_or(ReleaseError::InvalidArguments)?, + platform: platform.ok_or(ReleaseError::InvalidArguments)?, + execution_request_sha256: execution_request_sha256.ok_or(ReleaseError::InvalidArguments)?, + }) +} + fn workspace_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() diff --git a/tools/xtask/src/rshr_202_step_301_gate.rs b/tools/xtask/src/rshr_202_step_301_gate.rs @@ -0,0 +1,474 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; + +const STEP: u16 = 301; +const GATE_DIGEST: &str = "bdbadb41ebcd7247fd4038db75956966e5daff9d824bcf8e91b50922c7ceb37f"; +const LIB_REVISION: &str = "055096853fca95e15d0f813d33a14aca13be3881"; +const NIX_SHA256: &str = "a59ab70f97f6d571642d13c7506aafec0a4275520d53daee2d8451be7c495cd1"; +const NIX_VERSION_SHA256: &str = "6db806391ffaea4cdb08ade0031feac399c0cd08474b3bfde8cb33f88a36c8e1"; +const MAX_OUTPUT_BYTES: usize = 32 * 1024 * 1024; + +const EXACT_SOURCES: &[(&str, &str)] = &[ + ( + "Cargo.lock", + "9d2ee2d488b002b5f0ca7795f1c75600a5a684d1f492a0fc8d7352ef23c261ae", + ), + ( + "Cargo.toml", + "f3d01ba93661569cd7ba38b61c098a0425f24511a46ff0e14bd5e7adb9de74ea", + ), + ( + "contracts/release/rhi-artifact-contract.v3.json", + "d2fedbe4e8bee8f2e6c4396e2cb361bd20b303782cc729b8ee790cfd72bd4682", + ), + ( + "flake.lock", + "5d5b11622c341292f429a5f93e55f38a3506431b139720ff62f983b35bf7d55f", + ), + ( + "flake.nix", + "d8da2c1ca51d82674a8c36f66f179b26d6abb3871c2a55da01a215b9af5190f2", + ), + ( + "radroots.service.source-lock.v3.toml", + "e8593c3203bf27cef1c9bb412124ad5b73276a43a344eab5bd02406342710e30", + ), +]; + +pub(crate) struct Arguments { + pub(crate) step: u16, + pub(crate) check_id: String, + pub(crate) source_revision: String, + pub(crate) source_tree: String, + pub(crate) candidate_digest: String, + pub(crate) platform: String, + pub(crate) execution_request_sha256: String, +} + +fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("xtask must remain under tools/xtask") + .to_path_buf() +} + +fn sha256(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +fn canonical(value: &Value) -> Result<Vec<u8>, String> { + serde_json::to_vec(value).map_err(|_| "Step 301 JSON encoding failed".to_owned()) +} + +fn execute(command: &mut Command, label: &str) -> Result<Output, String> { + let output = command + .current_dir(root()) + .env("CARGO_NET_OFFLINE", "true") + .env("CARGO_TERM_COLOR", "never") + .output() + .map_err(|_| format!("{label} could not start"))?; + if output.stdout.len() > MAX_OUTPUT_BYTES || output.stderr.len() > MAX_OUTPUT_BYTES { + return Err(format!("{label} exceeded its output bound")); + } + Ok(output) +} + +fn bounded(command: &mut Command, label: &str) -> Result<Output, String> { + let output = execute(command, label)?; + if !output.status.success() { + return Err(format!("{label} failed")); + } + Ok(output) +} + +fn rejected(command: &mut Command, label: &str) -> Result<(), String> { + if execute(command, label)?.status.success() { + return Err(format!("{label} unexpectedly succeeded")); + } + Ok(()) +} + +fn resolve_nix() -> Result<PathBuf, String> { + if let Some(explicit) = env::var_os("RSHR_NIX_EXECUTABLE") { + return fs::canonicalize(explicit) + .map_err(|_| "Step 301 Nix client is unavailable".to_owned()); + } + let path = env::var_os("PATH").ok_or_else(|| "Step 301 PATH is absent".to_owned())?; + env::split_paths(&path) + .map(|directory| directory.join("nix")) + .find(|candidate| candidate.is_file()) + .and_then(|candidate| fs::canonicalize(candidate).ok()) + .ok_or_else(|| "Step 301 Nix client is unavailable".to_owned()) +} + +fn object_keys(value: &Value, label: &str) -> Result<Vec<String>, String> { + let mut keys = value + .as_object() + .ok_or_else(|| format!("Step 301 {label} is not an object"))? + .keys() + .cloned() + .collect::<Vec<_>>(); + keys.sort_unstable(); + Ok(keys) +} + +fn require_source_lock() -> Result<(), String> { + let root = root(); + let lock_bytes = fs::read(root.join("radroots.service.source-lock.v3.toml")) + .map_err(|_| "Step 301 source lock is unreadable".to_owned())?; + let lock: toml::Value = toml::from_str( + std::str::from_utf8(&lock_bytes) + .map_err(|_| "Step 301 source lock is not UTF-8".to_owned())?, + ) + .map_err(|_| "Step 301 source lock is invalid".to_owned())?; + let cargo_sha = sha256( + &fs::read(root.join("Cargo.lock")) + .map_err(|_| "Step 301 Cargo lock is unreadable".to_owned())?, + ); + let flake_sha = sha256( + &fs::read(root.join("flake.lock")) + .map_err(|_| "Step 301 flake lock is unreadable".to_owned())?, + ); + let artifact_sha = sha256( + &fs::read(root.join("contracts/release/rhi-artifact-contract.v3.json")) + .map_err(|_| "Step 301 artifact contract is unreadable".to_owned())?, + ); + if lock.get("schema").and_then(toml::Value::as_str) != Some("radroots.service.source-lock.v3") + || lock + .get("contract_version") + .and_then(toml::Value::as_integer) + != Some(3) + || lock.get("revision").and_then(toml::Value::as_str) != Some(LIB_REVISION) + || lock.get("cargo_lock_sha256").and_then(toml::Value::as_str) != Some(&cargo_sha) + || lock["nix"]["material"].as_str() != Some("qualified") + || lock["nix"]["lib_revision"].as_str() != Some(LIB_REVISION) + || lock["nix"]["public_input_lock"]["sha256"].as_str() != Some(&flake_sha) + || lock["artifact_contract"]["sha256"].as_str() != Some(&artifact_sha) + || lock["sqlite"]["high_level_authority"].as_str() != Some("sqlx_only") + || lock["sqlite"]["native_linkage_count"].as_integer() != Some(1) + { + return Err("Step 301 source lock differs".to_owned()); + } + + let flake_lock: Value = serde_json::from_slice( + &fs::read(root.join("flake.lock")) + .map_err(|_| "Step 301 flake lock is unreadable".to_owned())?, + ) + .map_err(|_| "Step 301 flake lock is invalid".to_owned())?; + if flake_lock.pointer("/nodes/root/inputs/lib") != Some(&json!("lib")) + || flake_lock.pointer("/nodes/lib/locked/rev") != Some(&json!(LIB_REVISION)) + || flake_lock.pointer("/nodes/lib/original/rev") != Some(&json!(LIB_REVISION)) + { + return Err("Step 301 exact Lib input differs".to_owned()); + } + Ok(()) +} + +fn require_single_sqlite() -> Result<(), String> { + let metadata = bounded( + Command::new("cargo").args([ + "+1.97.1", + "metadata", + "--offline", + "--locked", + "--format-version", + "1", + ]), + "Step 301 Cargo metadata", + )?; + let value: Value = serde_json::from_slice(&metadata.stdout) + .map_err(|_| "Step 301 Cargo metadata is invalid".to_owned())?; + let packages = value["packages"] + .as_array() + .ok_or_else(|| "Step 301 Cargo package inventory is absent".to_owned())?; + let sqlite_ids = packages + .iter() + .filter(|package| package["name"] == "libsqlite3-sys") + .filter_map(|package| package["id"].as_str()) + .collect::<Vec<_>>(); + if sqlite_ids.len() != 1 || packages.iter().any(|package| package["name"] == "rusqlite") { + return Err("Step 301 native SQLite package inventory differs".to_owned()); + } + let nodes = value["resolve"]["nodes"] + .as_array() + .ok_or_else(|| "Step 301 Cargo resolve inventory is absent".to_owned())?; + let sqlite_node = nodes + .iter() + .find(|node| node["id"] == sqlite_ids[0]) + .ok_or_else(|| "Step 301 SQLite resolve node is absent".to_owned())?; + let features = sqlite_node["features"] + .as_array() + .ok_or_else(|| "Step 301 SQLite features are absent".to_owned())?; + if !features.iter().any(|feature| feature == "bundled") { + return Err("Step 301 bundled SQLite feature is absent".to_owned()); + } + Ok(()) +} + +fn require_outputs(nix: &Path) -> Result<(), String> { + let show = bounded( + Command::new(nix).args([ + "--offline", + "flake", + "show", + "--json", + "--all-systems", + "--no-write-lock-file", + ]), + "Step 301 Nix output inventory", + )?; + let inventory: Value = serde_json::from_slice(&show.stdout) + .map_err(|_| "Step 301 Nix output inventory is invalid".to_owned())?; + let systems = ["aarch64-darwin", "x86_64-linux"]; + for family in ["apps", "checks", "devShells", "packages"] { + if object_keys(&inventory[family], family)? != systems { + return Err(format!("Step 301 {family} systems differ")); + } + } + if object_keys(&inventory["nixosModules"], "nixosModules")? != ["default"] + || inventory.pointer("/nixosModules/default/type") != Some(&json!("nixos-module")) + { + return Err("Step 301 NixOS module inventory differs".to_owned()); + } + let expected_checks = [ + "check", + "clippy", + "config", + "docs", + "fmt", + "integration", + "package", + "source-lock", + "sqlx", + "test", + ]; + for system in systems { + if object_keys(&inventory["apps"][system], "apps")? != ["default", "release-acceptance"] + || object_keys(&inventory["checks"][system], "checks")? != expected_checks + || object_keys(&inventory["devShells"][system], "devShells")? != ["default"] + || inventory["packages"][system]["default"]["name"] != "rhi-0.1.0" + || inventory["apps"][system]["default"]["description"] != "Run the built rhi service" + { + return Err("Step 301 service output inventory differs".to_owned()); + } + } + if object_keys(&inventory["packages"]["aarch64-darwin"], "Darwin packages")? != ["default"] + || object_keys(&inventory["packages"]["x86_64-linux"], "Linux packages")? + != ["default", "oci"] + || inventory["packages"]["x86_64-linux"]["oci"]["name"] != "rhi.tar.gz" + { + return Err("Step 301 platform-specific package inventory differs".to_owned()); + } + for system in ["x86_64-darwin", "aarch64-linux", "x86_64-windows"] { + rejected( + Command::new(nix).args([ + "--offline", + "eval", + "--raw", + &format!(".#packages.{system}.default.name"), + ]), + "Step 301 excluded-system evaluation", + )?; + } + rejected( + Command::new(nix).args([ + "--offline", + "eval", + "--raw", + ".#packages.aarch64-darwin.oci.name", + ]), + "Step 301 Darwin OCI evaluation", + )?; + Ok(()) +} + +fn require_nix() -> Result<(), String> { + let executable = resolve_nix()?; + if sha256(&fs::read(&executable).map_err(|_| "Step 301 Nix client is unreadable")?) + != NIX_SHA256 + { + return Err("Step 301 Nix client identity differs".to_owned()); + } + let version = bounded( + Command::new(&executable).arg("--version"), + "Step 301 Nix version", + )?; + if sha256(&version.stdout) != NIX_VERSION_SHA256 { + return Err("Step 301 Nix version differs".to_owned()); + } + bounded( + Command::new(&executable).args([ + "--offline", + "flake", + "check", + "--all-systems", + "--no-build", + "--no-write-lock-file", + ]), + "Step 301 Nix evaluation", + )?; + require_outputs(&executable) +} + +fn expected_contract(verifier_sha256: &str) -> Value { + json!({ + "argv_template": [ + "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked", + "-q", "-p", "rhi_xtask", "--", "rshr-step-301-gate", "--step={step}", + "--check-id={check_id}", "--source-revision={source_revision}", + "--source-tree={source_tree}", "--candidate-digest={candidate_digest}", + "--platform=macos_aarch64", + "--execution-request-sha256={execution_request_sha256}" + ], + "assertion_id": [format!("step_301_gate_01_{GATE_DIGEST}")], + "check_id": format!("gate-01-{GATE_DIGEST}"), + "environment_authority": { + "cache_policy_id": "rshr-200-step-287-cache-policy.v1", + "cache_policy_sha256": "3e81d178bce97b6c349dfbb00c68fd6f620ac00b1a1c8d37b12e9998f3c9eaaa", + "cadence_policy_id": "rshr-200-step-287-cadence-policy.v1", + "cadence_policy_sha256": "d24903df8659ee3772297c84994911efe7d21cb8b988320ddc6ddce0431892a1", + "isolation": "extbuild_host_constrained", + "network": "disabled", + "network_policy_id": "none", + "network_policy_sha256": "none", + "resource_policy_id": "rshr-200-step-287-resource-policy.v1", + "resource_policy_sha256": "05d3c7a89185d3c55678d97955193fce2ed92b1eee5af99083d77ea64c98d14e" + }, + "environment_names": [ + "EXT_BUILD_CONFIG", "EXT_BUILD_MACHINE_CONFIG", "EXT_BUILD_ROOT", "HOME", "PATH", + "RUSTUP_TOOLCHAIN", "TMPDIR" + ], + "gate_definition_sha256": GATE_DIGEST, + "required_platforms": ["macos_aarch64"], + "required_tools": ["rustc"], + "result_schema": "radroots.services-hardening.rshr-200-step-check-result.v1", + "schema": "radroots.services-hardening.rshr-200-step-check-command.v1", + "step": STEP, + "verifier_path": "tools/xtask/src/rshr_202_step_301_gate.rs", + "verifier_sha256": verifier_sha256 + }) +} + +pub(crate) fn run(arguments: Arguments) -> Result<(), String> { + let check_id = format!("gate-01-{GATE_DIGEST}"); + if arguments.step != STEP + || arguments.check_id != check_id + || arguments.candidate_digest != "none" + || arguments.platform != "macos_aarch64" + || arguments.source_revision.len() != 40 + || arguments.source_tree.len() != 40 + || arguments.execution_request_sha256.len() != 64 + || !arguments + .source_revision + .bytes() + .chain(arguments.source_tree.bytes()) + .chain(arguments.execution_request_sha256.bytes()) + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err("Step 301 gate arguments differ".to_owned()); + } + let root = root(); + if root.join(".github").exists() || root.join("radroots.service.source-lock.v2.toml").exists() { + return Err("Step 301 forbidden legacy surface is present".to_owned()); + } + for (relative, expected) in EXACT_SOURCES { + let bytes = fs::read(root.join(relative)) + .map_err(|_| "Step 301 governed source is unreadable".to_owned())?; + if sha256(&bytes) != *expected { + return Err("Step 301 governed source bytes differ".to_owned()); + } + } + let flake_source = fs::read_to_string(root.join("flake.nix")) + .map_err(|_| "Step 301 flake source is unreadable".to_owned())?; + for forbidden in [ + "pkgs.clang", + "libclang", + "libsodium", + "pkgs.openssl", + "pkgs.pkg-config", + "pkgs.sqlite", + ] { + if flake_source.contains(forbidden) { + return Err("Step 301 forbidden native dependency is present".to_owned()); + } + } + + let verifier_path = root.join("tools/xtask/src/rshr_202_step_301_gate.rs"); + let verifier_sha256 = + sha256(&fs::read(verifier_path).map_err(|_| "Step 301 verifier is unreadable")?); + let authority_path = root.join("contracts/rshr-202-step-301-gates.v1.json"); + let authority_bytes = + fs::read(authority_path).map_err(|_| "Step 301 gate authority is unreadable")?; + let authority: Value = serde_json::from_slice(&authority_bytes) + .map_err(|_| "Step 301 gate authority is invalid".to_owned())?; + let mut canonical_authority = canonical(&authority)?; + canonical_authority.push(b'\n'); + let contracts = authority + .get("gate_command_contract") + .and_then(Value::as_array) + .ok_or_else(|| "Step 301 gate contract is absent".to_owned())?; + if authority_bytes != canonical_authority + || authority.get("schema") + != Some(&Value::String( + "radroots.rhi.rshr-202-step-301-gates.v1".to_owned(), + )) + || authority.get("step") != Some(&json!([STEP])) + || contracts.as_slice() != [expected_contract(&verifier_sha256)] + { + return Err("Step 301 gate authority differs".to_owned()); + } + + require_source_lock()?; + bounded( + Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]), + "Step 301 formatting", + )?; + bounded( + Command::new("cargo").args([ + "+1.97.1", + "check", + "--offline", + "--locked", + "--workspace", + "--all-targets", + ]), + "Step 301 Cargo check", + )?; + require_single_sqlite()?; + require_nix()?; + + let contract = &contracts[0]; + let assertion = json!([{ + "id": format!("step_301_gate_01_{GATE_DIGEST}"), + "result": "pass" + }]); + let result = json!({ + "schema": "radroots.services-hardening.rshr-200-step-check-result.v1", + "step": STEP, + "check_id": check_id, + "gate_definition_sha256": GATE_DIGEST, + "source_revision": arguments.source_revision, + "source_tree": arguments.source_tree, + "candidate_generation": 0, + "candidate_digest": "none", + "command_contract_sha256": sha256(&canonical(contract)?), + "verifier_sha256": verifier_sha256, + "execution_request": [{ + "platform": arguments.platform, + "sha256": arguments.execution_request_sha256 + }], + "assertion_inventory_sha256": sha256(&canonical(&assertion)?), + "assertion": assertion, + "result": "pass" + }); + let mut bytes = canonical(&result)?; + bytes.push(b'\n'); + std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes) + .map_err(|_| "Step 301 result write failed".to_owned()) +} diff --git a/tools/xtask/src/rshr_202_step_301_platform.rs b/tools/xtask/src/rshr_202_step_301_platform.rs @@ -0,0 +1,103 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; + +const APPLE_TOOLCHAIN_IDENTITY_SHA256: &str = + "fd9bb9af273d0a834c2abff36910edf25f3e5b60c36fcc23b45b738c5c8b2d08"; +const PROBE_SOURCE_PATH: &str = + "tools/radroots_scripts/src/radroots_scripts/verify/rshr_200_series.py"; +const PROBE_SOURCE_SHA256: &str = + "add949c6c20a037123808230625dfd09dd6fa6c5afe5a856400227191f5de5b5"; +const REQUEST_PATH: &str = ".git/rshr-step-301-platform-request-sha256"; + +fn root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("xtask must remain under tools/xtask") +} + +fn canonical(value: &Value) -> Result<Vec<u8>, String> { + serde_json::to_vec(value).map_err(|_| "Step 301 platform JSON encoding failed".to_owned()) +} + +fn sha256(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +fn uname(flag: &str) -> Result<String, String> { + let output = Command::new("/usr/bin/uname") + .arg(flag) + .output() + .map_err(|_| "Step 301 platform probe could not start uname".to_owned())?; + if !output.status.success() || !output.stderr.is_empty() { + return Err("Step 301 platform probe uname failed".to_owned()); + } + let value = std::str::from_utf8(&output.stdout) + .map_err(|_| "Step 301 platform probe uname output is not UTF-8".to_owned())? + .strip_suffix('\n') + .ok_or_else(|| "Step 301 platform probe uname output differs".to_owned())?; + if value.is_empty() || value.contains('\n') || value.contains('\r') { + return Err("Step 301 platform probe uname output differs".to_owned()); + } + Ok(value.to_owned()) +} + +pub(crate) fn run() -> Result<(), String> { + let request_bytes = fs::read(root().join(REQUEST_PATH)) + .map_err(|_| "Step 301 platform execution request is unavailable".to_owned())?; + let raw_request = std::str::from_utf8(&request_bytes) + .map_err(|_| "Step 301 platform execution request is not UTF-8".to_owned())?; + let execution_request_sha256 = raw_request.strip_suffix('\n').unwrap_or(raw_request); + if execution_request_sha256.len() != 64 + || !execution_request_sha256 + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err("Step 301 platform execution request differs".to_owned()); + } + + let kernel_name = uname("-s")?; + let kernel_release = uname("-r")?; + let kernel_version = uname("-v")?; + if kernel_name != "Darwin" || std::env::consts::ARCH != "aarch64" { + return Err("Step 301 platform identity differs".to_owned()); + } + + let os_build = json!({ + "kernel_name": kernel_name, + "kernel_release": kernel_release, + "kernel_version": kernel_version, + }); + let result = json!({ + "schema": "radroots.services-hardening.rshr-200-platform-result.v1", + "platform": "macos_aarch64", + "system": "aarch64-darwin", + "os_family": "macos", + "architecture": "aarch64", + "kernel_name": os_build["kernel_name"], + "kernel_release": os_build["kernel_release"], + "os_build_sha256": sha256(&canonical(&os_build)?), + "runner_kind": "host", + "runner_image_sha256": "none", + "apple_toolchain_identity_sha256": APPLE_TOOLCHAIN_IDENTITY_SHA256, + "probe_source_path": PROBE_SOURCE_PATH, + "probe_source_sha256": PROBE_SOURCE_SHA256, + "execution_request_sha256": execution_request_sha256, + "assertion": [ + {"id": "os_family", "result": "pass"}, + {"id": "architecture", "result": "pass"}, + {"id": "kernel_identity", "result": "pass"}, + {"id": "runner_identity", "result": "pass"}, + {"id": "apple_identity", "result": "pass"}, + ], + "result": "available", + }); + let mut bytes = canonical(&result)?; + bytes.push(b'\n'); + std::io::Write::write_all(&mut std::io::stdout().lock(), &bytes) + .map_err(|_| "Step 301 platform result write failed".to_owned()) +}