sandbox: a confidential run sealed on TDX hardware, its PCK checked to Intel's root #436
7 changed files with 214 additions and 7 deletions
65
sandbox/proof/hardware/2026-10-21-tdx/README.md
Normal file
65
sandbox/proof/hardware/2026-10-21-tdx/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# A confidential run on TDX hardware, 2026-10-21
|
||||
|
||||
The report-data binding of SPEC §7.6 on genuine Intel silicon, with the PCK
|
||||
anchor derived from the quote's own certificate chain checked to Intel's
|
||||
root, and the run verified by a party other than the host.
|
||||
|
||||
## What ran
|
||||
|
||||
- **Platform:** a confidential VM with Intel TDX (`c3-standard-4`, kernel
|
||||
`7.0.0-1014-gcp`), created for the run and deleted after it.
|
||||
- **Inside the guest:** the reference host
|
||||
([`confidential_run_tdx`](../../../sandbox-rs/crates/sandbox-runtime/examples/confidential_run_tdx.rs)).
|
||||
- It took quotes through `configfs-tsm` (provider `tdx_guest`).
|
||||
- It derived the anchor from a probe quote's PCK chain, checked to Intel's
|
||||
root, which the verifier's side uploaded rather than the guest fetching it.
|
||||
- It sealed an S6 run whose `report_data` binds the unit, the receipt, the
|
||||
outcome and a 32-byte challenge ([`challenge.hex`](challenge.hex)).
|
||||
- **The quote:** DCAP version 4, TEE type `0x81`. The interface returns it in
|
||||
an 8000-byte buffer, 4935 bytes of quote followed by padding. It travels in
|
||||
the attestation as `tee.report_hex`.
|
||||
|
||||
The quote's certification data carries three certificates:
|
||||
|
||||
```text
|
||||
CN=Intel SGX PCK Certificate issued by CN=Intel SGX PCK Platform CA
|
||||
CN=Intel SGX PCK Platform CA issued by CN=Intel SGX Root CA
|
||||
CN=Intel SGX Root CA self-signed
|
||||
```
|
||||
|
||||
The root it carries is byte-identical to Intel's published root
|
||||
([`intel-root.der`](intel-root.der)), whose SHA-256 fingerprint is
|
||||
`44:A0:19:6B:2B:99:F8:89:B8:E1:49:E9:5B:80:7A:35:0E:74:24:96:43:99:E8:85:A7:CB:B8:CC:FA:B6:74:D3`.
|
||||
Compare it with the one Intel publishes, not with this file.
|
||||
|
||||
## 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-tdx/attestation.json \
|
||||
"$(cat ../proof/hardware/2026-10-21-tdx/challenge.hex)" \
|
||||
../proof/hardware/2026-10-21-tdx/intel-root.der
|
||||
```
|
||||
|
||||
- The quote's PCK chain checks to Intel's root (`sandbox-tee::intel_pcs`), and
|
||||
the quote verifies through that PCK: the quote signature, the QE report's
|
||||
endorsement of the attestation key, and the PCK's signature over the QE
|
||||
report.
|
||||
- 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.
|
||||
- Given another certificate in place of Intel's root, no anchor is derived.
|
||||
|
||||
## What it does not show
|
||||
|
||||
- The unit is a fixed placeholder. This exercises the binding, the chain
|
||||
check and the verifier on real hardware, not an executor's isolation of a
|
||||
workload.
|
||||
- Revocation was not consulted: Intel publishes CRLs for the PCK CAs, and
|
||||
checking them is the deployment's step.
|
||||
- The receipt and Supervisor keys are fixed development keys. The trust here
|
||||
rests on the quote, not on those signatures.
|
||||
29
sandbox/proof/hardware/2026-10-21-tdx/attestation.json
Normal file
29
sandbox/proof/hardware/2026-10-21-tdx/attestation.json
Normal file
File diff suppressed because one or more lines are too long
1
sandbox/proof/hardware/2026-10-21-tdx/challenge.hex
Normal file
1
sandbox/proof/hardware/2026-10-21-tdx/challenge.hex
Normal file
|
|
@ -0,0 +1 @@
|
|||
8a7e3b2032714567827ad2e65ae0739125dad921db483725436d3af51d8cd4dc
|
||||
BIN
sandbox/proof/hardware/2026-10-21-tdx/intel-root.der
Normal file
BIN
sandbox/proof/hardware/2026-10-21-tdx/intel-root.der
Normal file
Binary file not shown.
|
|
@ -21,4 +21,6 @@ thiserror.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
p256.workspace = true
|
||||
# The examples check a TDX quote's PCK chain to Intel's root.
|
||||
sandbox-tee = { workspace = true, features = ["intel-pcs"] }
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
//! Seal a confidential run on TDX hardware and verify it as a third party
|
||||
//! would (SPEC §7.6): the TDX counterpart of `confidential_run`.
|
||||
//!
|
||||
//! Run as root inside a TDX guest with `configfs-tsm`:
|
||||
//!
|
||||
//! ```text
|
||||
//! confidential_run_tdx <challenge-hex> <intel-root.der>
|
||||
//! ```
|
||||
//!
|
||||
//! `intel-root.der` is Intel's published root CA certificate, which the
|
||||
//! verifier pins. The PCK anchor is derived from the chain a quote carries,
|
||||
//! checked to that root, so the trust comes from the root and not from the
|
||||
//! quote. The host seals an S6 run whose quote the platform makes over the
|
||||
//! run's binding and the challenge, and the attestation is verified against
|
||||
//! the challenge, against another, and against none. It prints the verdicts
|
||||
//! and the attestation as JSON.
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use sandbox_attest::{DeviceIdentity, Outcome, TeeKind};
|
||||
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::{AttestationProvider, ConfigfsTsmProvider};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let (Some(challenge_hex), Some(root_path)) = (args.get(1), args.get(2)) else {
|
||||
eprintln!("usage: confidential_run_tdx <challenge-hex> <intel-root.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 root = std::fs::read(root_path).expect("read Intel's root certificate");
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("a clock after 1970");
|
||||
|
||||
// A probe quote carries the platform's PCK chain; check it to the root.
|
||||
let probe = ConfigfsTsmProvider::for_kind(TeeKind::Tdx).report(&[0; 64]).expect("a TDX quote");
|
||||
let pck = sandbox_tee::intel_pcs::pck_from_quote(&probe, &root, now.as_secs()).expect("a PCK chain to Intel's root");
|
||||
let anchor = TeeAnchor::Tdx(pck);
|
||||
let policy = TeePolicy::tdx_baseline();
|
||||
|
||||
let host = Host::new("confidential-vm", "steward.hardware-check", SigningKey::from_bytes(&[1; 32]), SigningKey::from_bytes(&[2; 32]))
|
||||
.with_tee(TeeConfig {
|
||||
provider: Box::new(ConfigfsTsmProvider::for_kind(TeeKind::Tdx)),
|
||||
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 = now.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(), "probe_hex": hex::encode(&probe) }));
|
||||
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;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"sealed": true,
|
||||
"verified": verified.is_ok(),
|
||||
"verify_error": verified.err().map(|e| e.to_string()),
|
||||
"refused_another_challenge": verify_run_evidence(a, Some(&other), &policy, &anchor).is_err(),
|
||||
"refused_no_challenge": verify_run_evidence(a, None, &policy, &anchor).is_err(),
|
||||
"kind": format!("{:?}", tee.kind),
|
||||
"measurement_hex": tee.measurement_hex,
|
||||
"tcb": tee.tcb,
|
||||
"attestation": a,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -3,14 +3,16 @@
|
|||
//! with nothing from the host that sealed it.
|
||||
//!
|
||||
//! ```text
|
||||
//! verify_run <attestation.json> <challenge-hex> <vcek.der>
|
||||
//! verify_run <attestation.json> <challenge-hex> <anchor.der>
|
||||
//! ```
|
||||
//!
|
||||
//! `vcek.der` is the chip's VCEK, as AMD's KDS issues it. The run is verified
|
||||
//! For SEV-SNP evidence, `anchor.der` is the chip's VCEK, as AMD's KDS issues
|
||||
//! it. For TDX evidence, it is Intel's root CA certificate: the PCK anchor is
|
||||
//! derived from the chain the quote carries, checked to that root. 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_attest::{Attestation, TeeKind};
|
||||
use sandbox_runtime::verify_run_evidence;
|
||||
use sandbox_tee::policy::{TeeAnchor, TeePolicy};
|
||||
|
||||
|
|
@ -24,10 +26,22 @@ fn main() {
|
|||
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 der = std::fs::read(vcek_path).expect("read the anchor");
|
||||
let tee = attestation.tee.as_ref().expect("TEE evidence to verify");
|
||||
let (anchor, policy) = match tee.kind {
|
||||
TeeKind::SevSnp => {
|
||||
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");
|
||||
(TeeAnchor::SevSnp(vcek), TeePolicy::sev_snp_baseline())
|
||||
}
|
||||
TeeKind::Tdx => {
|
||||
let quote = hex::decode(tee.report_hex.as_ref().expect("the quote")).expect("a hex quote");
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("a clock").as_secs();
|
||||
let pck = sandbox_tee::intel_pcs::pck_from_quote("e, &der, now).expect("a PCK chain to Intel's root");
|
||||
(TeeAnchor::Tdx(pck), TeePolicy::tdx_baseline())
|
||||
}
|
||||
TeeKind::ArmCca => panic!("no ArmCca verifier"),
|
||||
};
|
||||
|
||||
let verified = verify_run_evidence(&attestation, Some(&challenge), &policy, &anchor);
|
||||
let mut other = challenge;
|
||||
|
|
|
|||
Loading…
Reference in a new issue