From 328d89b67623a830bf0543199c3c357639102259 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 12:56:47 -0400 Subject: [PATCH] sandbox: a confidential run sealed on SEV-SNP hardware, verified by a third party MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run binding of #392 had only met software-grounded reports. This runs the reference host inside an SEV-SNP guest and records the result as evidence anyone can re-check offline. sandbox-runtime examples: - confidential_run, run inside the guest. It takes reports through configfs-tsm and seals an S6 run against a challenge; the report_data is the run binding of SPEC §7.6. It then checks the attestation through the chip's VCEK: verified against the challenge, refused against another and against none. - verify_run: the third party's check, from the attestation, the challenge issued and the VCEK alone. sandbox/proof/hardware/2026-10-21-sev-snp: the attestation (with the 1184-byte v5 report), the challenge, the chip's VCEK and AMD's cert chain, and a README saying what it shows and what it does not. The unit is a placeholder, so this exercises the binding and verifier on silicon, not isolation. Checked on our side, away from the guest: - The VCEK was fetched from AMD's KDS by the chip id and TCB inside the report, and openssl verifies it to ARK-Milan. - verify_run on the build host verifies the run, and refuses another challenge and no challenge. - An attestation with its outcome changed is refused ("report-data binding mismatch"): the AMD signature covers the outcome through the binding. sandbox-tee: the version comment now says what the code does (v2 layout and later; a v5 report verified on hardware). Co-Authored-By: Claude Opus 5.5 --- .../hardware/2026-10-21-sev-snp/README.md | 56 +++++++++++ .../2026-10-21-sev-snp/attestation.json | 29 ++++++ .../hardware/2026-10-21-sev-snp/challenge.hex | 1 + .../hardware/2026-10-21-sev-snp/vcek.der | Bin 0 -> 1351 bytes sandbox/sandbox-rs/Cargo.lock | 1 + .../crates/sandbox-runtime/Cargo.toml | 1 + .../examples/confidential_run.rs | 93 ++++++++++++++++++ .../sandbox-runtime/examples/verify_run.rs | 46 +++++++++ .../crates/sandbox-tee/src/policy.rs | 6 +- 9 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 sandbox/proof/hardware/2026-10-21-sev-snp/README.md create mode 100644 sandbox/proof/hardware/2026-10-21-sev-snp/attestation.json create mode 100644 sandbox/proof/hardware/2026-10-21-sev-snp/challenge.hex create mode 100644 sandbox/proof/hardware/2026-10-21-sev-snp/vcek.der create mode 100644 sandbox/sandbox-rs/crates/sandbox-runtime/examples/confidential_run.rs create mode 100644 sandbox/sandbox-rs/crates/sandbox-runtime/examples/verify_run.rs diff --git a/sandbox/proof/hardware/2026-10-21-sev-snp/README.md b/sandbox/proof/hardware/2026-10-21-sev-snp/README.md new file mode 100644 index 000000000..ba140149b --- /dev/null +++ b/sandbox/proof/hardware/2026-10-21-sev-snp/README.md @@ -0,0 +1,56 @@ +# A confidential run on SEV-SNP hardware, 2026-10-21 + +The report-data binding of SPEC §7.6, exercised on genuine AMD silicon and +verified by a party other than the host. + +## What ran + +- **Platform:** a confidential VM with AMD SEV-SNP, on an AMD Milan processor + (`n2d-standard-2`, image `ubuntu-2404-noble-amd64-v20261016`). It was created + for the run and deleted after it. +- **Inside the guest:** the reference host + ([`confidential_run`](../../../sandbox-rs/crates/sandbox-runtime/examples/confidential_run.rs)). + It took a report through `configfs-tsm` (the kernel named its provider + `sev_guest`) and sealed an S6 run. Its `report_data` is the run binding: + the unit, the receipt, the outcome, and a 32-byte challenge chosen by the + party that started the run ([`challenge.hex`](challenge.hex)). +- **The report:** version 5, 1184 bytes, signed with the chip's VCEK. It + travels in the attestation as `tee.report_hex`. + +## What it shows + +Verified away from the guest, from these files alone: + +```text +cd ../../../sandbox-rs +cargo run -p sandbox-runtime --example verify_run -- \ + ../proof/hardware/2026-10-21-sev-snp/attestation.json \ + "$(cat ../proof/hardware/2026-10-21-sev-snp/challenge.hex)" \ + ../proof/hardware/2026-10-21-sev-snp/vcek.der +``` + +- The report verifies through the VCEK. Its `report_data` is the binding of + this attestation's unit, receipt and outcome, and of the challenge issued. +- It is refused for any other challenge, and without one. +- An attestation whose outcome is changed is refused: the AMD signature covers + the outcome through the binding. + +The VCEK ([`vcek.der`](vcek.der)) was fetched from AMD's KDS by the chip id and +reported TCB inside the report. It chains to AMD's root +([`cert_chain.pem`](cert_chain.pem): `SEV-Milan`, then `ARK-Milan`): + +```text +openssl x509 -inform der -in vcek.der -out vcek.pem +openssl verify -CAfile -untrusted vcek.pem +``` + +The ARK's SHA-256 fingerprint is +`69:D0:63:B4:53:44:D2:6A:2E:94:E1:F4:21:0D:E4:9E:F5:55:30:82:87:D4:C1:74:44:5C:95:63:9A:54:0B:CD`. +Compare it with the one AMD publishes, not with this file. + +## What it does not show + +- The unit is a fixed placeholder. This exercises the binding and the verifier + on real hardware, not an executor's isolation of a workload. +- The receipt and Supervisor keys are fixed development keys. The trust here + rests on the hardware report, not on those signatures. diff --git a/sandbox/proof/hardware/2026-10-21-sev-snp/attestation.json b/sandbox/proof/hardware/2026-10-21-sev-snp/attestation.json new file mode 100644 index 000000000..13ff8e0ab --- /dev/null +++ b/sandbox/proof/hardware/2026-10-21-sev-snp/attestation.json @@ -0,0 +1,29 @@ +{ + "audit_prev": null, + "devices": [ + { + "class": "Cpu", + "driver": "host", + "model": "confidential guest" + } + ], + "finished_at_us": 1792601988753099, + "grants_id": "b3:ej-h9yIoVIBjnlA-OMDiy95_Jlk7nqn6djvXYNv035w", + "host_id": "gcp-confidential-vm", + "image_id": "b3:sfsM4tYpFQT10UkAZVEdN5C2user1jDf-76b0oYeI8o", + "journal_root": null, + "locality": null, + "mechanism": null, + "outcome": "Completed", + "receipt_id": "b3:kCkhM2_jsKJyBwLL17ivdqhPhyPEIJaWjPUVDFRR6hU", + "started_at_us": 1792601988753098, + "tee": { + "challenge_hex": "f9a4ec56a429a6fed42839517a03745723a8317a70b737dbd78b98087ebfa6b7", + "kind": "SevSnp", + "measurement_hex": "0e017d2fba7c4964575fdd8c770061abf07f0753148d030f94e969a2d1fa5a5747ba0ced9174f2d44b5f6663fa2de00f", + "report_hex": "05000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000400000000001dde25000000000000000000000000000000ebf35a85d4fa49a70da07099763c8d8c299bd3beb5d8fa3a1b750c77adfa4d10a3090fd326f7ac07a686890c31ee2f3488e10a58e5dcb842a524b9bab29889460e017d2fba7c4964575fdd8c770061abf07f0753148d030f94e969a2d1fa5a5747ba0ced9174f2d44b5f6663fa2de00f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c575dabffb4e8e772ab8d8d531fd886c90ef814ca39680247c0a7efceba25d4affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0400000000001dde190101000000000000000000000000000000000000000000c1928ae552b597a350103ac5999da841754ab728de34dd6feed77fb9d779f5e0570b1d194f707494b9349f6dc8930d12c1944bbd10938e7ad4c57997b1af2efa0400000000001dde013a0100013a01000400000000001dde0b000000000000000b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c8f38068e4c86327fb0cc7f352c855ef49be254d45c4ec57e5a6667175a29804b0860bb83ed47f46b01fe8fae56754e00000000000000000000000000000000000000000000000049f2f008ed6ed6e75b44a397d6893fa882ab5c2a83471ca4af25816fe78ec66788705659fb0ae0d50b897f9756410fb30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "tcb": "0xde1d000000000004" + }, + "tier": "S6", + "unit_id": "b3:StLoFp1GgfFDM6RVELdOlbduOMrjpLZGE6kSdYoLFNg" +} diff --git a/sandbox/proof/hardware/2026-10-21-sev-snp/challenge.hex b/sandbox/proof/hardware/2026-10-21-sev-snp/challenge.hex new file mode 100644 index 000000000..b325ca4b9 --- /dev/null +++ b/sandbox/proof/hardware/2026-10-21-sev-snp/challenge.hex @@ -0,0 +1 @@ +f9a4ec56a429a6fed42839517a03745723a8317a70b737dbd78b98087ebfa6b7 diff --git a/sandbox/proof/hardware/2026-10-21-sev-snp/vcek.der b/sandbox/proof/hardware/2026-10-21-sev-snp/vcek.der new file mode 100644 index 0000000000000000000000000000000000000000..b2563cd0fe7f32fa15e5370cd367129945850347 GIT binary patch literal 1351 zcmXqLVs$oXV*0*-nTe5!iIKs;k&RQU&7t88*&?Pf<)PbnL>kMO4xb0 zgA?;g5*3_t5{nXHN;r6!oE;724P-&;xp>4JQ_2$al2cO@d^3}a@)cZC%QBNwiw%Vg z1V9Ryc{qbz!*qQ!a}x6m)ZI6(t`h~qdw zjtg^k^)@I2x|baoy60?}7};308(A2Xn3EV-^nc9V8S$uY&Ni*)J9o;ix{JsCIwE4$ zJWs1gBCW5rEZh32a%s`;=FF1NW7cOk2{$ta7`Q5i=Y9Si_M!S_Vtk2&*6+ugR|-m= zI=B1gjyjc>~7FsAiGO6c5xGiE2a<|rVu-(5HmZEoYeI+XzTRF0RmP>XU<*WSn9Q1HK08V!p>p0-ea}_RGU|m1b1(Na z2_-YgcuL9a4GFJ}ndl$#XHr+na(3R+0uSe~=_-HK+Snvsd|>w-)9{ixo;}AVo<9_% zmug>Sd`A3P&GW!rmrqxvuM+9`8g}=!c=)qy>E#jUYYpd!EjY9$sPyh;#!Fs5Mbnb5 z*IyC66tgToug}_V-QF+zLe96Zwq3UAd+_TitX|(*r#`$X{pf0!)O6k5n+y&;&QRSr zWsgfCf3#0Z)3*<-9onWlh0Y4!`Zo87(}ld|l#qtt4e9SaX6=7fAJFK(qN_Pmv2w@E zv$>Ie^&fptntbBBWp#2w2lw}L@(#RHE8jF}WZ&D+8+On5W|ih%(=#%YWFFYcOMKvC z`uX%plwy?Q#jP8kc(`5>xM8}EEu{SJgdoln(=X*{Bu|?AC;J#r>YDO +//! ``` +//! +//! `vcek.der` is the chip's VCEK, from AMD's KDS. The host seals an S6 run whose +//! report the platform signs over the run's binding and the challenge. The +//! attestation is then verified through the VCEK alone, against the verifier's +//! own challenge, and against another challenge, which must fail. The run's +//! unit is a fixed placeholder: what this exercises is the binding and the +//! verifier on genuine silicon. It prints the verdicts and the attestation as +//! JSON. + +use ed25519_dalek::SigningKey; +use sandbox_attest::{DeviceIdentity, Outcome}; +use sandbox_capability::Grants; +use sandbox_meter::{DeviceEnergy, Provenance}; +use sandbox_runtime::{verify_run_evidence, Host, RunReport, TeeConfig}; +use sandbox_schema::{ContentId, DeviceClass, Tier}; +use sandbox_tee::policy::{TeeAnchor, TeePolicy}; +use sandbox_tee::provider::ConfigfsTsmProvider; + +fn main() { + let args: Vec = std::env::args().collect(); + let (Some(challenge_hex), Some(vcek_path)) = (args.get(1), args.get(2)) else { + eprintln!("usage: confidential_run "); + std::process::exit(2); + }; + let challenge: [u8; 32] = + hex::decode(challenge_hex).ok().and_then(|v| v.try_into().ok()).expect("a 32-byte hex challenge"); + let der = std::fs::read(vcek_path).expect("read the VCEK"); + // The P-384 public point in the certificate's SubjectPublicKeyInfo: the + // BIT STRING `03 62 00 04` followed by x ‖ y. + let at = der.windows(4).position(|w| w == [0x03, 0x62, 0x00, 0x04]).expect("a P-384 point in the VCEK"); + let vcek = p384::ecdsa::VerifyingKey::from_sec1_bytes(&der[at + 3..at + 3 + 97]).expect("a P-384 key"); + let anchor = TeeAnchor::SevSnp(vcek); + let policy = TeePolicy::sev_snp_baseline(); + + let host = Host::new("gcp-confidential-vm", "steward.hardware-check", SigningKey::from_bytes(&[1; 32]), SigningKey::from_bytes(&[2; 32])) + .with_tee(TeeConfig { provider: Box::new(ConfigfsTsmProvider::default()), policy: policy.clone(), anchor: anchor.clone() }); + let report = RunReport { + device_energy: vec![DeviceEnergy { class: DeviceClass::Cpu, measured_uj: None, estimated_uj: 1, provenance: Provenance::Estimator }], + device_identity: vec![DeviceIdentity { class: DeviceClass::Cpu, model: "confidential guest".into(), driver: "host".into() }], + outcome: Outcome::Completed, + journal: None, + storage: None, + }; + let now_us = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_micros() as u64); + let sealed = match host.seal_run_with_mechanism( + ContentId::of_bytes(b"hardware check unit"), + ContentId::of_bytes(b"hardware check image"), + &Grants::default(), + Tier::S6, + report, + 0, + [0; 8], + now_us, + now_us + 1, + None, + Some(challenge), + ) { + Ok(s) => s, + Err(e) => { + println!("{}", serde_json::json!({ "sealed": false, "error": e.to_string() })); + std::process::exit(1); + } + }; + let a = &sealed.attestation; + let tee = a.tee.as_ref().expect("an S6 run carries TEE evidence"); + let verified = verify_run_evidence(a, Some(&challenge), &policy, &anchor); + let mut other = challenge; + other[0] ^= 1; + let refused_other = verify_run_evidence(a, Some(&other), &policy, &anchor).is_err(); + let refused_none = verify_run_evidence(a, None, &policy, &anchor).is_err(); + println!( + "{}", + serde_json::json!({ + "sealed": true, + "verified": verified.is_ok(), + "verify_error": verified.err().map(|e| e.to_string()), + "refused_another_challenge": refused_other, + "refused_no_challenge": refused_none, + "kind": format!("{:?}", tee.kind), + "measurement_hex": tee.measurement_hex, + "tcb": tee.tcb, + "attestation": a, + }) + ); +} diff --git a/sandbox/sandbox-rs/crates/sandbox-runtime/examples/verify_run.rs b/sandbox/sandbox-rs/crates/sandbox-runtime/examples/verify_run.rs new file mode 100644 index 000000000..edacb54c6 --- /dev/null +++ b/sandbox/sandbox-rs/crates/sandbox-runtime/examples/verify_run.rs @@ -0,0 +1,46 @@ +//! Verify a sealed run's TEE evidence as a third party (SPEC §7.6): from the +//! attestation, the challenge the verifier issued, and the platform's anchor, +//! with nothing from the host that sealed it. +//! +//! ```text +//! verify_run +//! ``` +//! +//! `vcek.der` is the chip's VCEK, as AMD's KDS issues it. The run is verified +//! against the challenge, and must be refused against any other challenge and +//! against none. Exits non-zero unless all three hold. + +use sandbox_attest::Attestation; +use sandbox_runtime::verify_run_evidence; +use sandbox_tee::policy::{TeeAnchor, TeePolicy}; + +fn main() { + let args: Vec = std::env::args().collect(); + let (Some(path), Some(challenge_hex), Some(vcek_path)) = (args.get(1), args.get(2), args.get(3)) else { + eprintln!("usage: verify_run "); + std::process::exit(2); + }; + let attestation: Attestation = + serde_json::from_str(&std::fs::read_to_string(path).expect("read the attestation")).expect("an attestation"); + let challenge: [u8; 32] = + hex::decode(challenge_hex.trim()).ok().and_then(|v| v.try_into().ok()).expect("a 32-byte hex challenge"); + let der = std::fs::read(vcek_path).expect("read the VCEK"); + let at = der.windows(4).position(|w| w == [0x03, 0x62, 0x00, 0x04]).expect("a P-384 point in the VCEK"); + let anchor = TeeAnchor::SevSnp(p384::ecdsa::VerifyingKey::from_sec1_bytes(&der[at + 3..at + 3 + 97]).expect("a P-384 key")); + let policy = TeePolicy::sev_snp_baseline(); + + let verified = verify_run_evidence(&attestation, Some(&challenge), &policy, &anchor); + let mut other = challenge; + other[31] ^= 0x80; + let refused_other = verify_run_evidence(&attestation, Some(&other), &policy, &anchor).is_err(); + let refused_none = verify_run_evidence(&attestation, None, &policy, &anchor).is_err(); + match &verified { + Ok(v) => println!("verified: {:?} run, measurement {}", v.kind, hex::encode(v.measurement)), + Err(e) => println!("NOT verified: {e}"), + } + println!("refused for another challenge: {refused_other}"); + println!("refused without a challenge: {refused_none}"); + if verified.is_err() || !refused_other || !refused_none { + std::process::exit(1); + } +} diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/src/policy.rs b/sandbox/sandbox-rs/crates/sandbox-tee/src/policy.rs index 50cb0fc1e..aab1fdd2d 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/src/policy.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/src/policy.rs @@ -126,8 +126,10 @@ pub fn verify_snp( } let report = SnpReport::parse(report_bytes)?; - // 0. Version: this verifier models the v2 field layout (and v3, whose - // checked fields share those offsets). Reject a version it does not model + // 0. Version: this verifier models the v2 field layout, which later + // versions keep for the fields checked here: a v5 report from SEV-SNP + // hardware verified (sandbox/proof/hardware/2026-10-21-sev-snp). Reject a + // version it does not model // rather than silently misinterpret fields at the wrong offsets. const MIN_MODELED_VERSION: u32 = 2; if report.version < MIN_MODELED_VERSION { -- 2.54.0 (Apple Git-157)