sandbox: a confidential run sealed on SEV-SNP hardware, verified by a third party #412

Merged
dcharlot merged 1 commit from feat/sandbox-hardware-check into main 2026-10-21 23:05:31 -04:00
9 changed files with 231 additions and 2 deletions

View file

@ -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 <ARK-Milan> -untrusted <SEV-Milan> 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.

View file

@ -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"
}

View file

@ -0,0 +1 @@
f9a4ec56a429a6fed42839517a03745723a8317a70b737dbd78b98087ebfa6b7

Binary file not shown.

View file

@ -1886,6 +1886,7 @@ dependencies = [
"sandbox-meter",
"sandbox-schema",
"sandbox-tee",
"serde_json",
"thiserror 2.0.20",
]

View file

@ -21,3 +21,4 @@ thiserror.workspace = true
[dev-dependencies]
p256.workspace = true
serde_json.workspace = true

View file

@ -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 <challenge-hex> <vcek.der>
//! ```
//!
//! `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<String> = std::env::args().collect();
let (Some(challenge_hex), Some(vcek_path)) = (args.get(1), args.get(2)) else {
eprintln!("usage: confidential_run <challenge-hex> <vcek.der>");
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,
})
);
}

View file

@ -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 <attestation.json> <challenge-hex> <vcek.der>
//! ```
//!
//! `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<String> = 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 <attestation.json> <challenge-hex> <vcek.der>");
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);
}
}

View file

@ -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 {