lib

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

commit 73bc3f01f5a8a8d20aeaac9fc31d2283b8ceedd6
parent 9a9520c4ada7caf0492497fce19edac9e2648da9
Author: triesap <tyson@radroots.org>
Date:   Sun,  6 Sep 2026 23:55:20 +0000

test: bind the Step 299 Nix boundary

- Verify exact output inventories across both governed Nix systems.
- Reject excluded systems and fixture-owned public defaults.
- Bind helper security assertions to exact checked-in source bytes.
- Keep the installed bundle inspector closure minimal and runnable.

Diffstat:
Mbuild/nix/library.nix | 18+++++++-----------
Acontracts/rshr-202-step-299-gates.v1.json | 1+
Mtools/xtask/src/main.rs | 35+++++++++++++++++++++++++++++++++++
Atools/xtask/src/rshr_202_step_299_gate.rs | 374+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 417 insertions(+), 11 deletions(-)

diff --git a/build/nix/library.nix b/build/nix/library.nix @@ -25,22 +25,18 @@ let {"artifact":"public_library_workspace_release_bundle","package":"radroots","version":"${version}"} EOF ''; - inspector = pkgs.writeShellApplication { - name = "radroots-lib-bundle-inspect"; - runtimeInputs = [ pkgs.coreutils ]; - text = '' - set -euo pipefail - test -f ${package}/share/radroots-lib/Cargo.toml - test -f ${package}/share/radroots-lib/Cargo.lock - exec cat ${package}/share/radroots-lib/release-bundle.json - ''; - }; + inspector = pkgs.writeShellScript "radroots-lib-bundle-inspect" '' + set -euo pipefail + ${pkgs.coreutils}/bin/test -f ${package}/share/radroots-lib/Cargo.toml + ${pkgs.coreutils}/bin/test -f ${package}/share/radroots-lib/Cargo.lock + exec ${pkgs.coreutils}/bin/cat ${package}/share/radroots-lib/release-bundle.json + ''; in { inherit package; app = { type = "app"; - program = "${inspector}/bin/radroots-lib-bundle-inspect"; + program = "${inspector}"; meta.description = "Inspect the installed Radroots Lib release bundle"; }; check = pkgs.runCommand "radroots-lib-release-bundle-check" { } '' diff --git a/contracts/rshr-202-step-299-gates.v1.json b/contracts/rshr-202-step-299-gates.v1.json @@ -0,0 +1 @@ +{"gate_command_contract":[{"argv_template":["cargo","extbuild","run","--","cargo","run","--offline","--locked","-q","-p","xtask","--","rshr-step-299-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_299_gate_01_aaf288b5bf9102722f61bd8160188dc80bf84a62f48ddaef9023a4417654d8ba"],"check_id":"gate-01-aaf288b5bf9102722f61bd8160188dc80bf84a62f48ddaef9023a4417654d8ba","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":"aaf288b5bf9102722f61bd8160188dc80bf84a62f48ddaef9023a4417654d8ba","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":299,"verifier_path":"tools/xtask/src/rshr_202_step_299_gate.rs","verifier_sha256":"106164ac8f32512ab9ea288c49e7ae5dc2bd08d97bcfb05a22fc8b0b4774b7d1"}],"schema":"radroots.lib.rshr-202-step-299-gates.v1","step":[299]} diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -40,6 +40,7 @@ mod release_preflight; mod release_qualification; mod rshr_202_step_298_gate; mod rshr_202_step_298_platform; +mod rshr_202_step_299_gate; mod safe_artifact_io; #[cfg_attr(coverage_nightly, coverage(off))] mod safety_qualification; @@ -149,6 +150,23 @@ enum XtaskCommand { }, #[command(name = "rshr-step-298-platform-probe", hide = true)] RshrStep298PlatformProbe, + #[command(name = "rshr-step-299-gate", hide = true)] + RshrStep299Gate { + #[arg(long)] + step: u16, + #[arg(long)] + check_id: String, + #[arg(long)] + source_revision: String, + #[arg(long)] + source_tree: String, + #[arg(long)] + candidate_digest: String, + #[arg(long)] + platform: String, + #[arg(long)] + execution_request_sha256: String, + }, SourceLock { #[arg(long)] consumer_root: PathBuf, @@ -610,6 +628,23 @@ fn run(args: &[String]) -> Result<(), String> { execution_request_sha256, }), XtaskCommand::RshrStep298PlatformProbe => rshr_202_step_298_platform::run(), + XtaskCommand::RshrStep299Gate { + step, + check_id, + source_revision, + source_tree, + candidate_digest, + platform, + execution_request_sha256, + } => rshr_202_step_299_gate::run(rshr_202_step_299_gate::Arguments { + step, + check_id, + source_revision, + source_tree, + candidate_digest, + platform, + execution_request_sha256, + }), XtaskCommand::SourceLock { consumer_root } => { build_control::validate_consumer(&consumer_root).map(|_| ()) } diff --git a/tools/xtask/src/rshr_202_step_299_gate.rs b/tools/xtask/src/rshr_202_step_299_gate.rs @@ -0,0 +1,374 @@ +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 = 299; +const GATE_DIGEST: &str = "aaf288b5bf9102722f61bd8160188dc80bf84a62f48ddaef9023a4417654d8ba"; +const NIX_SHA256: &str = "a59ab70f97f6d571642d13c7506aafec0a4275520d53daee2d8451be7c495cd1"; +const NIX_VERSION_SHA256: &str = "6db806391ffaea4cdb08ade0031feac399c0cd08474b3bfde8cb33f88a36c8e1"; +const MAX_OUTPUT_BYTES: usize = 32 * 1024 * 1024; + +const EXACT_SOURCES: &[(&str, &str)] = &[ + ( + "flake.nix", + "de3d2258f2c48ae8b1493b676ab3d1466cd5f1a59015f65283efbfe12447e501", + ), + ( + "build/nix/library.nix", + "ff425fe6361bad78c105e36d3e900950ce22b0034e26add55e5ed287aa868765", + ), + ( + "build/nix/service/native-inputs.nix", + "09aed688d6ad6c5c824aa771b871cc0c4857d8378d1330221c8d5a05509d9391", + ), + ( + "build/nix/service/nixos-module.nix", + "20b42dd6972c59fcaaf5a282219644d83822ea03a2805bfe8e6e9b8f7b628622", + ), + ( + "build/nix/service/fixture.nix", + "f3fa0cbef4f6a86feee9b2319165d97c51526236b1426ffa83ffb435ed90cd97", + ), + ( + "build/nix/service/systems.nix", + "d16e21827022a2315234f4c5e4b485017a36ecd90a5559b01d23331cdd505e46", + ), +]; + +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 299 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 299 Nix client is unavailable".to_owned()); + } + let path = env::var_os("PATH").ok_or_else(|| "Step 299 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 299 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 299 {label} is not an object"))? + .keys() + .cloned() + .collect::<Vec<_>>(); + keys.sort_unstable(); + Ok(keys) +} + +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 299 Nix output inventory", + )?; + let inventory: Value = serde_json::from_slice(&show.stdout) + .map_err(|_| "Step 299 Nix output inventory is invalid".to_owned())?; + let systems = ["aarch64-darwin", "x86_64-linux"]; + for family in ["apps", "checks", "devShells", "formatter", "packages"] { + if object_keys(&inventory[family], family)? != systems { + return Err(format!("Step 299 {family} systems differ")); + } + } + if inventory.pointer("/overlays/default/type") != Some(&json!("nixpkgs-overlay")) { + return Err("Step 299 default overlay is absent".to_owned()); + } + for system in systems { + let packages = &inventory["packages"][system]; + if object_keys(packages, "packages")? != ["default", "xtask"] + || packages["default"]["name"] != "radroots-lib-release-bundle-0.1.0-alpha" + { + return Err("Step 299 package inventory differs".to_owned()); + } + if inventory["apps"][system]["default"]["description"] + != "Inspect the installed Radroots Lib release bundle" + || inventory["devShells"][system].get("default").is_none() + || inventory["checks"][system].get("release-bundle").is_none() + { + return Err("Step 299 production output is absent".to_owned()); + } + for family in ["apps", "devShells", "packages"] { + if object_keys(&inventory[family][system], family)? + .iter() + .any(|name| name.contains("fixture")) + { + return Err("Step 299 fixture escaped its test-only surface".to_owned()); + } + } + let fixture_checks = object_keys(&inventory["checks"][system], "checks")? + .into_iter() + .filter(|name| name.contains("fixture")) + .collect::<Vec<_>>(); + if fixture_checks.is_empty() + || fixture_checks + .iter() + .any(|name| !name.starts_with("service-fixture-")) + { + return Err("Step 299 fixture check names differ".to_owned()); + } + } + + let supported = bounded( + Command::new(nix).args(["--offline", "eval", "--json", ".#lib.supportedSystems"]), + "Step 299 shared-helper systems", + )?; + if supported.stdout != b"[\"aarch64-darwin\",\"x86_64-linux\"]\n" { + return Err("Step 299 shared-helper systems differ".to_owned()); + } + let helper_type = bounded( + Command::new(nix).args([ + "--offline", + "eval", + "--raw", + "--apply", + "f: builtins.typeOf f", + ".#lib.mkServiceHelpers", + ]), + "Step 299 shared-helper export", + )?; + if helper_type.stdout != b"lambda" { + return Err("Step 299 shared-helper export 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 299 excluded-system evaluation", + )?; + } + Ok(()) +} + +fn require_nix() -> Result<(), String> { + let executable = resolve_nix()?; + if sha256(&fs::read(&executable).map_err(|_| "Step 299 Nix client is unreadable")?) + != NIX_SHA256 + { + return Err("Step 299 Nix client identity differs".to_owned()); + } + let version = bounded( + Command::new(&executable).arg("--version"), + "Step 299 Nix version", + )?; + if sha256(&version.stdout) != NIX_VERSION_SHA256 { + return Err("Step 299 Nix version differs".to_owned()); + } + bounded( + Command::new(&executable).args([ + "--offline", + "flake", + "check", + "--all-systems", + "--no-build", + "--no-write-lock-file", + ]), + "Step 299 Nix evaluation", + )?; + require_outputs(&executable) +} + +fn expected_contract(verifier_sha256: &str) -> Value { + json!({ + "argv_template": [ + "cargo", "extbuild", "run", "--", "cargo", "run", "--offline", "--locked", + "-q", "-p", "xtask", "--", "rshr-step-299-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_299_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_299_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 299 gate arguments differ".to_owned()); + } + let root = root(); + if root.join(".github").exists() { + return Err("forbidden .github surface is present".to_owned()); + } + for (relative, expected) in EXACT_SOURCES { + let bytes = fs::read(root.join(relative)) + .map_err(|_| "Step 299 governed source is unreadable".to_owned())?; + if sha256(&bytes) != *expected { + return Err("Step 299 governed source bytes differ".to_owned()); + } + } + + let verifier_path = root.join("tools/xtask/src/rshr_202_step_299_gate.rs"); + let verifier_sha256 = + sha256(&fs::read(verifier_path).map_err(|_| "Step 299 verifier is unreadable".to_owned())?); + let authority_path = root.join("contracts/rshr-202-step-299-gates.v1.json"); + let authority_bytes = + fs::read(authority_path).map_err(|_| "Step 299 gate authority is unreadable".to_owned())?; + let authority: Value = serde_json::from_slice(&authority_bytes) + .map_err(|_| "Step 299 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 299 gate contract is absent".to_owned())?; + if authority_bytes != canonical_authority + || authority.get("schema") + != Some(&Value::String( + "radroots.lib.rshr-202-step-299-gates.v1".to_owned(), + )) + || authority.get("step") != Some(&json!([STEP])) + || contracts.as_slice() != [expected_contract(&verifier_sha256)] + { + return Err("Step 299 gate authority differs".to_owned()); + } + + bounded( + Command::new("cargo").args(["+1.97.1", "fmt", "--all", "--", "--check"]), + "Step 299 formatting", + )?; + bounded( + Command::new("cargo").args(["+1.97.1", "check", "--offline", "--locked", "-p", "xtask"]), + "Step 299 verifier check", + )?; + require_nix()?; + + let contract = &contracts[0]; + let assertion = json!([{ + "id": format!("step_299_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 299 result write failed".to_owned()) +}