From a7f71049bd9c7281709a5bedd40a9f643e671a56 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:58:40 -0400 Subject: [PATCH] feat(attest): boot-attestation + validator-quorum verifier in Rust (L0, cross-silicon) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verification the canon runs IN the boot path (bootProbe halts on a failed Genesis quorum, before any network) cannot be a cloud TS service — it must run on-device, on whatever silicon the device is. So the canonical verifier is pure Rust in source-os/runtime, and the same source builds for aarch64 (Apple-Silicon M2), x86_64, and riscv64 sovereign silicon — zero arch-specific code. - runtime/quorumd: verify_quorum(&QuorumProof, payload_hash) — the M-of-N validator quorum, fail-closed, CONFORMING to the authoritative QuorumProof shape (the same one the prophet-platform Python verifier PP #1370 checks; twins over one schema, not two). Rejects sub-threshold, non-validator/duplicate signers, payload-hash mismatch, kind mismatch, malformed rule. (v1 checks distinct non-trivial sigs; per-sig FIDO2/NitroKey crypto is next.) - runtime/watchdog-validator/attestation: attest_boot(&BootProofRecord, &policy) — the measured-boot verifier, fail-closed: outcome=success, every stage verdict=verified, every pinned stage present+matching, NO unpinned stage (unmeasured surface), and the rootfs stage bound to the dm-verity root (verified-immutable ↔ measured-boot become ONE evidence chain). Empty policy attests nothing (anti-theater). - 19 cargo tests (8 quorum + 11 attestation). attest-verifier.yml tests natively on x86_64 and compile-checks aarch64 + riscv64 — the "any silicon" claim is enforced, not asserted. Supersedes the TS device-enrollment path (parked, unpushed) as the verify source of truth: the cloud admission calls THIS, it does not reimplement it. Conforms to BootProofRecord + QuorumProof. --- .github/workflows/attest-verifier.yml | 51 ++++ runtime/quorumd/src/lib.rs | 178 ++++++++++++++ runtime/watchdog-validator/src/attestation.rs | 230 ++++++++++++++++++ runtime/watchdog-validator/src/lib.rs | 2 + 4 files changed, 461 insertions(+) create mode 100644 .github/workflows/attest-verifier.yml diff --git a/.github/workflows/attest-verifier.yml b/.github/workflows/attest-verifier.yml new file mode 100644 index 0000000..8b3c775 --- /dev/null +++ b/.github/workflows/attest-verifier.yml @@ -0,0 +1,51 @@ +name: attest-verifier + +# The L0 verifier core (boot attestation + validator quorum) is pure Rust and MUST run on any +# silicon a device might be — an Apple-Silicon M2, an x86_64 or a RISC-V sovereign-silicon box. +# This gate tests it natively on x86_64 and compile-checks it for aarch64 and riscv64, so the +# "same binary logic, any silicon" claim is enforced, not asserted. +on: + push: + paths: + - 'runtime/quorumd/**' + - 'runtime/watchdog-validator/**' + - '.github/workflows/attest-verifier.yml' + pull_request: + paths: + - 'runtime/quorumd/**' + - 'runtime/watchdog-validator/**' + - '.github/workflows/attest-verifier.yml' + +permissions: + contents: read + +jobs: + test-x86_64: + name: test (x86_64 sovereign-silicon) + runs-on: ubuntu-latest + defaults: + run: + working-directory: runtime + steps: + - uses: actions/checkout@v4 + - name: Boot-attestation + quorum verifier tests + run: cargo test -p quorumd -p watchdog-validator + + cross-silicon-check: + name: compile-check (${{ matrix.target }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [aarch64-unknown-linux-gnu, riscv64gc-unknown-linux-gnu] + defaults: + run: + working-directory: runtime + steps: + - uses: actions/checkout@v4 + - name: Add target + run: rustup target add ${{ matrix.target }} + # `cargo check` needs the target's std, not a cross-linker — enough to prove the pure-Rust + # verifier compiles for this silicon. + - name: Compile-check the verifier for ${{ matrix.target }} + run: cargo check -p quorumd -p watchdog-validator --target ${{ matrix.target }} diff --git a/runtime/quorumd/src/lib.rs b/runtime/quorumd/src/lib.rs index 61c9dc1..b27934d 100644 --- a/runtime/quorumd/src/lib.rs +++ b/runtime/quorumd/src/lib.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Vote { @@ -6,6 +7,123 @@ pub struct Vote { pub verdict: String, } +// ── Canonical validator-quorum verifier (the on-device + CLI verifier) ────────────────────── +// +// Conforms to the authoritative QuorumProof shape (mcp-a2a-zero-trust :: +// schemas/canonical/quorum_proof.schema.json). This is the SAME contract the prophet-platform +// Python verifier (PP #1370) checks; the two are twins over one shape, not two schemas. +// +// Pure Rust, no arch-specific code — the identical binary logic runs on aarch64 (Apple Silicon), +// x86_64, and riscv64. This is why it lives at L0 in Rust and not in the cloud runtime: the +// canon runs this check IN the boot path (bootProbe halts on a failed Genesis quorum), before +// any network exists — a device decides its own trust locally, on whatever silicon it is. + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct QuorumSignature { + pub kind: String, + pub spiffe_id: String, + pub sig: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct QuorumProof { + pub rule: String, + pub validators: Vec, + pub signed_payload_hash: String, + pub signatures: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct QuorumOutcome { + pub ok: bool, + pub reasons: Vec, +} + +/// Parse a `MofN-kind` rule (e.g. "2of3-human"). Returns None on M<1, N<1, or M>N. +fn parse_rule(rule: &str) -> Option<(usize, usize, &str)> { + let (m_n, kind) = rule.split_once('-')?; + let (m, n) = m_n.split_once("of")?; + let threshold: usize = m.parse().ok()?; + let total: usize = n.parse().ok()?; + if threshold < 1 || total < 1 || threshold > total { + return None; + } + Some((threshold, total, kind)) +} + +fn is_payload_hash(s: &str) -> bool { + s.len() == 7 + 64 + && s.starts_with("sha256:") + && s[7..].bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} + +/// Verify a QuorumProof: shape + M-of-N threshold, fail-closed. When `payload_hash` is given the +/// proof must be over exactly that payload (binds the quorum to the thing being admitted). +/// +/// NOTE (v1): this checks that `threshold` DISTINCT listed validators each supplied a non-trivial +/// signature. Cryptographic verification of each `sig` against the validator's FIDO2/NitroKey +/// public key is the next step; the shape and threshold arithmetic are canonical here. +pub fn verify_quorum(proof: &QuorumProof, payload_hash: Option<&str>) -> QuorumOutcome { + let mut reasons: Vec = Vec::new(); + + let (threshold, total, kind) = match parse_rule(&proof.rule) { + Some(r) => r, + None => { + return QuorumOutcome { + ok: false, + reasons: vec![format!("rule '{}' does not parse as MofN-kind (1<=M<=N)", proof.rule)], + } + } + }; + + let vset: BTreeSet<&str> = proof.validators.iter().map(String::as_str).collect(); + if vset.len() != proof.validators.len() { + reasons.push("validators list has duplicates".into()); + } + if vset.len() < total { + reasons.push(format!("rule needs {} validators; only {} listed", total, vset.len())); + } + + if !is_payload_hash(&proof.signed_payload_hash) { + reasons.push("signed_payload_hash must be sha256:<64hex>".into()); + } else if let Some(ph) = payload_hash { + if proof.signed_payload_hash != ph { + reasons.push("signed_payload_hash does not match the admitted payload (quorum unbound)".into()); + } + } + + let mut seen: BTreeSet<&str> = BTreeSet::new(); + let mut valid = 0usize; + for (i, s) in proof.signatures.iter().enumerate() { + if s.kind != kind { + reasons.push(format!("signature[{i}] kind '{}' != rule kind '{kind}'", s.kind)); + continue; + } + if !vset.contains(s.spiffe_id.as_str()) { + reasons.push(format!("signature[{i}] signer '{}' is not a listed validator", s.spiffe_id)); + continue; + } + if seen.contains(s.spiffe_id.as_str()) { + reasons.push(format!("signature[{i}] duplicate signer '{}'", s.spiffe_id)); + continue; + } + if s.sig.len() < 16 { + reasons.push(format!("signature[{i}] sig too short / missing")); + continue; + } + seen.insert(s.spiffe_id.as_str()); + valid += 1; + } + if valid < threshold { + reasons.push(format!( + "{valid} valid distinct signature(s) < threshold {threshold} (rule {})", + proof.rule + )); + } + + QuorumOutcome { ok: reasons.is_empty(), reasons } +} + pub fn aggregate(votes: &[Vote]) -> Option { let mut counts = std::collections::BTreeMap::::new(); for vote in votes { @@ -30,4 +148,64 @@ mod tests { ]; assert_eq!(aggregate(&votes).as_deref(), Some("reseal_resume")); } + + const PH: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn validators() -> Vec { + vec!["spiffe://v/1".into(), "spiffe://v/2".into(), "spiffe://v/3".into()] + } + fn sig(v: &str) -> QuorumSignature { + QuorumSignature { kind: "human".into(), spiffe_id: v.into(), sig: "MEUCIQD".to_string() + &"f".repeat(20) } + } + fn proof(sigs: Vec) -> QuorumProof { + QuorumProof { rule: "2of3-human".into(), validators: validators(), signed_payload_hash: PH.into(), signatures: sigs } + } + + #[test] + fn valid_two_of_three_passes() { + let p = proof(vec![sig("spiffe://v/1"), sig("spiffe://v/2")]); + assert!(verify_quorum(&p, Some(PH)).ok); + } + + #[test] + fn below_threshold_fails() { + assert!(!verify_quorum(&proof(vec![sig("spiffe://v/1")]), None).ok); + } + + #[test] + fn non_validator_signer_fails() { + let p = proof(vec![sig("spiffe://v/1"), sig("spiffe://intruder")]); + let o = verify_quorum(&p, None); + assert!(!o.ok && o.reasons.iter().any(|r| r.contains("not a listed validator"))); + } + + #[test] + fn duplicate_signer_not_counted_twice() { + let p = proof(vec![sig("spiffe://v/1"), sig("spiffe://v/1")]); + let o = verify_quorum(&p, None); + assert!(!o.ok && o.reasons.iter().any(|r| r.contains("duplicate"))); + } + + #[test] + fn payload_hash_binding_enforced() { + let other = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + let o = verify_quorum(&proof(vec![sig("spiffe://v/1"), sig("spiffe://v/2")]), Some(other)); + assert!(!o.ok && o.reasons.iter().any(|r| r.contains("does not match"))); + } + + #[test] + fn malformed_rule_fails() { + let mut p = proof(vec![sig("spiffe://v/1"), sig("spiffe://v/2")]); + p.rule = "4of3-human".into(); + assert!(!verify_quorum(&p, None).ok); + } + + #[test] + fn kind_mismatch_fails() { + let p = proof(vec![ + QuorumSignature { kind: "machine".into(), spiffe_id: "spiffe://v/1".into(), sig: "x".repeat(20) }, + QuorumSignature { kind: "machine".into(), spiffe_id: "spiffe://v/2".into(), sig: "x".repeat(20) }, + ]); + assert!(!verify_quorum(&p, None).ok); + } } diff --git a/runtime/watchdog-validator/src/attestation.rs b/runtime/watchdog-validator/src/attestation.rs index 8257c06..340e14d 100644 --- a/runtime/watchdog-validator/src/attestation.rs +++ b/runtime/watchdog-validator/src/attestation.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AttestationSnapshot { @@ -15,6 +16,235 @@ pub fn snapshot() -> AttestationSnapshot { } } +// ── Measured boot + remote attestation (canon L0) ─────────────────────────────────────────── +// +// A device's boot is a MEASURED chain: each stage records the content hash of what it ran +// (BootProofRecord.stageProofs, the vendored sourceos-spec contract). Attestation verifies that +// measured chain against a pinned golden policy — fail-closed. A device is trustworthy from +// power-on only if EVERY stage it ran was pinned and matched; an unpinned stage is an +// unmeasured surface and fails. +// +// Pure Rust, no arch-specific code: the same verifier runs in the initramfs of an aarch64 M2 and +// an x86_64 / riscv64 sovereign-silicon box — only the pinned policy differs per silicon. The +// rootfs stage's measured hash is bound to the dm-verity root, so "the base is immutable" +// (verity) and "the base that booted is the pinned one" (attestation) are ONE evidence chain. + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StageProof { + pub stage_name: String, + pub content_hash: String, + pub verdict: String, // verified | skipped | failed | tampered + #[serde(default)] + pub artifact_ref: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BootProofRecord { + pub outcome: String, // success | partial | failure | aborted + #[serde(default)] + pub device_ref: String, + #[serde(default)] + pub boot_plan_ref: String, + #[serde(default)] + pub stage_proofs: Vec, + #[serde(default)] + pub signature: Option, +} + +/// One pinned stage in the golden measured-boot chain. +#[derive(Debug, Clone)] +pub struct StagePin { + pub stage_name: String, + pub content_hash: String, +} + +/// The golden measured-boot policy for one silicon/edition. +#[derive(Debug, Clone, Default)] +pub struct AttestationPolicy { + pub expected_stages: Vec, + pub rootfs_stage: Option, // defaults to "rootfs" + pub rootfs_verity_root: Option, // dm-verity root the rootfs stage must equal + pub require_signature: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AttestOutcome { + pub attested: bool, + pub reasons: Vec, + pub verity_bound: bool, +} + +fn is_sha256(s: &str) -> bool { + s.len() == 7 + 64 + && s.starts_with("sha256:") + && s[7..].bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} + +/// Attest a measured boot against a pinned policy. Fail-closed. +pub fn attest_boot(record: &BootProofRecord, policy: &AttestationPolicy) -> AttestOutcome { + let mut reasons: Vec = Vec::new(); + + // A policy that pins no stages could "attest" anything — refuse to be theater. + if policy.expected_stages.is_empty() { + return AttestOutcome { + attested: false, + reasons: vec!["attestation policy pins no stages — nothing measured".into()], + verity_bound: false, + }; + } + + // 1. the boot must have succeeded. + if record.outcome != "success" { + reasons.push(format!("boot outcome '{}' is not success", record.outcome)); + } + if record.stage_proofs.is_empty() { + reasons.push("no stageProofs — an unmeasured boot cannot be attested".into()); + } + + // 2. every stage that ran must have measured as 'verified'. + for s in &record.stage_proofs { + if s.verdict != "verified" { + reasons.push(format!("stage '{}' verdict '{}' != verified", s.stage_name, s.verdict)); + } + } + + // index measured stages by name + let measured: std::collections::BTreeMap<&str, &StageProof> = + record.stage_proofs.iter().map(|s| (s.stage_name.as_str(), s)).collect(); + let pinned: BTreeSet<&str> = policy.expected_stages.iter().map(|p| p.stage_name.as_str()).collect(); + + // 3. every pinned stage present with an exact hash match. + for pin in &policy.expected_stages { + match measured.get(pin.stage_name.as_str()) { + None => reasons.push(format!("expected stage '{}' missing from the boot proof", pin.stage_name)), + Some(s) if s.content_hash != pin.content_hash => reasons.push(format!( + "stage '{}' hash mismatch (measured {} != pinned {})", + pin.stage_name, s.content_hash, pin.content_hash + )), + Some(_) => {} + } + } + // 3b. fail-closed: no stage may run that isn't pinned (an unmeasured surface). + for s in &record.stage_proofs { + if !pinned.contains(s.stage_name.as_str()) { + reasons.push(format!( + "stage '{}' ran but is not pinned in the attestation policy (unmeasured surface)", + s.stage_name + )); + } + } + + // 4. dm-verity binding: the rootfs stage's measured hash == the pinned verity root. + let verity_bound = policy.rootfs_verity_root.is_some(); + if let Some(root) = &policy.rootfs_verity_root { + if !is_sha256(root) { + reasons.push("rootfsVerityRoot must be sha256:<64hex>".into()); + } + let stage = policy.rootfs_stage.as_deref().unwrap_or("rootfs"); + match measured.get(stage) { + None => reasons.push(format!("rootfs stage '{stage}' absent — cannot bind the dm-verity root")), + Some(s) if &s.content_hash != root => reasons.push(format!( + "rootfs hash {} != pinned dm-verity root {} (booted base is not the verified base)", + s.content_hash, root + )), + Some(_) => {} + } + } + + // 5. signed boot proof, if required. + if policy.require_signature && record.signature.as_deref().unwrap_or("").is_empty() { + reasons.push("attestation policy requires a signed boot proof".into()); + } + + AttestOutcome { attested: reasons.is_empty(), reasons, verity_bound } +} + +#[cfg(test)] +mod attest_tests { + use super::*; + + const VERITY: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn stage(name: &str, hash: &str, verdict: &str) -> StageProof { + StageProof { stage_name: name.into(), content_hash: hash.into(), verdict: verdict.into(), artifact_ref: String::new() } + } + fn good_stages() -> Vec { + vec![ + stage("firmware", "sha256:1111111111111111111111111111111111111111111111111111111111111111", "verified"), + stage("bootloader", "sha256:2222222222222222222222222222222222222222222222222222222222222222", "verified"), + stage("kernel", "sha256:3333333333333333333333333333333333333333333333333333333333333333", "verified"), + stage("rootfs", VERITY, "verified"), + ] + } + fn policy() -> AttestationPolicy { + AttestationPolicy { + expected_stages: good_stages().iter().map(|s| StagePin { stage_name: s.stage_name.clone(), content_hash: s.content_hash.clone() }).collect(), + rootfs_stage: Some("rootfs".into()), + rootfs_verity_root: Some(VERITY.into()), + require_signature: false, + } + } + fn record(stages: Vec, outcome: &str) -> BootProofRecord { + BootProofRecord { outcome: outcome.into(), device_ref: "urn:srcos:device:x".into(), boot_plan_ref: "p".into(), stage_proofs: stages, signature: None } + } + + #[test] + fn fully_measured_boot_attests() { + let o = attest_boot(&record(good_stages(), "success"), &policy()); + assert!(o.attested && o.verity_bound, "{:?}", o.reasons); + } + #[test] + fn non_success_outcome_rejected() { + assert!(!attest_boot(&record(good_stages(), "partial"), &policy()).attested); + } + #[test] + fn tampered_stage_rejected() { + let mut s = good_stages(); + s[3] = stage("rootfs", VERITY, "tampered"); + assert!(!attest_boot(&record(s, "success"), &policy()).attested); + } + #[test] + fn hash_mismatch_rejected() { + let mut s = good_stages(); + s[2] = stage("kernel", "sha256:9999999999999999999999999999999999999999999999999999999999999999", "verified"); + let o = attest_boot(&record(s, "success"), &policy()); + assert!(!o.attested && o.reasons.iter().any(|r| r.contains("hash mismatch"))); + } + #[test] + fn unpinned_stage_rejected() { + let mut s = good_stages(); + s.push(stage("mystery-blob", "sha256:7777777777777777777777777777777777777777777777777777777777777777", "verified")); + let o = attest_boot(&record(s, "success"), &policy()); + assert!(!o.attested && o.reasons.iter().any(|r| r.contains("not pinned"))); + } + #[test] + fn dm_verity_mismatch_rejected() { + let mut s = good_stages(); + s[3] = stage("rootfs", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "verified"); + let mut pol = policy(); + // repin the rootfs expected hash so ONLY the verity binding fails + pol.expected_stages = s.iter().map(|x| StagePin { stage_name: x.stage_name.clone(), content_hash: x.content_hash.clone() }).collect(); + let o = attest_boot(&record(s, "success"), &pol); + assert!(!o.attested && o.reasons.iter().any(|r| r.contains("dm-verity"))); + } + #[test] + fn empty_policy_attests_nothing() { + let pol = AttestationPolicy::default(); + assert!(!attest_boot(&record(good_stages(), "success"), &pol).attested); + } + #[test] + fn require_signature_enforced() { + let mut pol = policy(); + pol.require_signature = true; + assert!(!attest_boot(&record(good_stages(), "success"), &pol).attested); + let mut rec = record(good_stages(), "success"); + rec.signature = Some("MEUCIQD".to_string() + &"f".repeat(20)); + assert!(attest_boot(&rec, &pol).attested); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/runtime/watchdog-validator/src/lib.rs b/runtime/watchdog-validator/src/lib.rs index b749878..e7310d4 100644 --- a/runtime/watchdog-validator/src/lib.rs +++ b/runtime/watchdog-validator/src/lib.rs @@ -1,5 +1,7 @@ +pub mod attestation; pub mod audit_anchor; pub mod quarantine; +pub use attestation::{attest_boot, AttestOutcome, AttestationPolicy, BootProofRecord, StagePin, StageProof}; pub use audit_anchor::build_anchor_payload; pub use quarantine::{build_quarantine_plan, QuarantinePlan};