sandbox: a confidential run sealed on SEV-SNP hardware, verified by a third party #412
9 changed files with 231 additions and 2 deletions
56
sandbox/proof/hardware/2026-10-21-sev-snp/README.md
Normal file
56
sandbox/proof/hardware/2026-10-21-sev-snp/README.md
Normal 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.
|
||||||
29
sandbox/proof/hardware/2026-10-21-sev-snp/attestation.json
Normal file
29
sandbox/proof/hardware/2026-10-21-sev-snp/attestation.json
Normal 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"
|
||||||
|
}
|
||||||
1
sandbox/proof/hardware/2026-10-21-sev-snp/challenge.hex
Normal file
1
sandbox/proof/hardware/2026-10-21-sev-snp/challenge.hex
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
f9a4ec56a429a6fed42839517a03745723a8317a70b737dbd78b98087ebfa6b7
|
||||||
BIN
sandbox/proof/hardware/2026-10-21-sev-snp/vcek.der
Normal file
BIN
sandbox/proof/hardware/2026-10-21-sev-snp/vcek.der
Normal file
Binary file not shown.
1
sandbox/sandbox-rs/Cargo.lock
generated
1
sandbox/sandbox-rs/Cargo.lock
generated
|
|
@ -1886,6 +1886,7 @@ dependencies = [
|
||||||
"sandbox-meter",
|
"sandbox-meter",
|
||||||
"sandbox-schema",
|
"sandbox-schema",
|
||||||
"sandbox-tee",
|
"sandbox-tee",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,4 @@ thiserror.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
p256.workspace = true
|
p256.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -126,8 +126,10 @@ pub fn verify_snp(
|
||||||
}
|
}
|
||||||
let report = SnpReport::parse(report_bytes)?;
|
let report = SnpReport::parse(report_bytes)?;
|
||||||
|
|
||||||
// 0. Version: this verifier models the v2 field layout (and v3, whose
|
// 0. Version: this verifier models the v2 field layout, which later
|
||||||
// checked fields share those offsets). Reject a version it does not model
|
// 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.
|
// rather than silently misinterpret fields at the wrong offsets.
|
||||||
const MIN_MODELED_VERSION: u32 = 2;
|
const MIN_MODELED_VERSION: u32 = 2;
|
||||||
if report.version < MIN_MODELED_VERSION {
|
if report.version < MIN_MODELED_VERSION {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue