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 000000000..b2563cd0f Binary files /dev/null and b/sandbox/proof/hardware/2026-10-21-sev-snp/vcek.der differ diff --git a/sandbox/sandbox-rs/Cargo.lock b/sandbox/sandbox-rs/Cargo.lock index 8d2a2b380..78adb144f 100644 --- a/sandbox/sandbox-rs/Cargo.lock +++ b/sandbox/sandbox-rs/Cargo.lock @@ -1886,6 +1886,7 @@ dependencies = [ "sandbox-meter", "sandbox-schema", "sandbox-tee", + "serde_json", "thiserror 2.0.20", ] diff --git a/sandbox/sandbox-rs/crates/sandbox-runtime/Cargo.toml b/sandbox/sandbox-rs/crates/sandbox-runtime/Cargo.toml index 7196d853f..dc3235e6f 100644 --- a/sandbox/sandbox-rs/crates/sandbox-runtime/Cargo.toml +++ b/sandbox/sandbox-rs/crates/sandbox-runtime/Cargo.toml @@ -21,3 +21,4 @@ thiserror.workspace = true [dev-dependencies] p256.workspace = true +serde_json.workspace = true diff --git a/sandbox/sandbox-rs/crates/sandbox-runtime/examples/confidential_run.rs b/sandbox/sandbox-rs/crates/sandbox-runtime/examples/confidential_run.rs new file mode 100644 index 000000000..b30a88281 --- /dev/null +++ b/sandbox/sandbox-rs/crates/sandbox-runtime/examples/confidential_run.rs @@ -0,0 +1,93 @@ +//! Seal a confidential run on real hardware and verify it as a third party +//! would (SPEC §7.6). +//! +//! Run as root inside an SEV-SNP guest with `configfs-tsm`: +//! +//! ```text +//! confidential_run +//! ``` +//! +//! `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 {