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-schema",
|
||||
"sandbox-tee",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@ thiserror.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
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)?;
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue