sandbox: TDX on the run path, through a pinned PCK #399

Merged
dcharlot merged 1 commit from feat/sandbox-tdx-run-path into main 2026-10-21 16:25:09 -04:00
8 changed files with 367 additions and 88 deletions

View file

@ -275,7 +275,13 @@ The host **MUST** normalize hostnames, paths, process tree, time, timezone, and
### 7.6 TEE evidence (S6+)
At S6 and above, `tee` **MUST** carry a summary of the hardware attestation report (`kind ∈ { SevSnp, Tdx, ArmCca }`, the launch measurement, and the platform TCB) so a verifier can establish the boundary was hardware-enforced. A host **MUST** obtain the report from the platform's attestation interface, bind it to the run through the report's 64-byte `report_data` field, and **MUST** verify the report — its signature against the platform's VCEK and its measurement/TCB/debug-state/report-data against policy — before sealing the attestation. A host **MUST NOT** present a `tee` summary it did not verify.
At S6 and above, `tee` **MUST** carry a summary of the hardware attestation report (`kind ∈ { SevSnp, Tdx, ArmCca }`, the launch measurement, and the platform TCB) so a verifier can establish the boundary was hardware-enforced. A host **MUST** obtain the report from the platform's attestation interface, bind it to the run through the report's 64-byte `report_data` field, and **MUST** verify the report — its signature through the platform's anchor and its measurement/TCB/debug-state/report-data against policy — before sealing the attestation. A host **MUST NOT** present a `tee` summary it did not verify.
**The anchor.** A report is trusted through a key the verifier obtains and pins, never through one the report carries:
- for SEV-SNP, the chip's **VCEK**, from AMD's KDS;
- for TDX, the platform's **PCK** key, from Intel's PCS, with its certificate chain checked.
A TDX quote carries its PCK certificate in its certification data, and that key **MUST** be the pinned one. Anyone can make a key, endorse a quoting-enclave report with it and assemble a quote that is consistent with itself. The host seals with a provider and an anchor of the same kind, and a third party verifies with its own anchor; evidence of another kind than the verifier's anchor is refused.
**The binding.** `report_data` is one preimage over everything a report vouches for:
@ -374,7 +380,7 @@ Each part is classified by how much of it is proven in the reference implementat
| Deterministic replay journal (§8) | **Engineering** | journal + root specified; reference records and verifies |
| Anti-evasion (§7.5) | **Engineering** | normalization + probing record specified |
| TEE report verification — SEV-SNP (§7.6) | **Solid** | `sandbox-tee` parses the SEV-SNP report, checks version/measurement/TCB/debug/report-data, and verifies the real ECDSA-P384/SHA-384 signature; vectored |
| TEE report verification — Intel TDX (§7.6) | **Solid** | `sandbox-tee::tdx` parses a DCAP v4 quote + TD10 report body and verifies the ECDSA-P256 quote signature, the QE-report endorsement of the attestation key, and the PCK signature over the QE report, then policy (MR_TD/debug/report-data); self-tested |
| TEE report verification — Intel TDX (§7.6) | **Solid** | `sandbox-tee::tdx` parses a DCAP v4 quote + TD10 report body and verifies the ECDSA-P256 quote signature, the QE-report endorsement of the attestation key, and the signature over the QE report by the **pinned** PCK key, which the quote must carry, then policy (MR_TD/debug/report-data). The run path seals and verifies TDX as it does SEV-SNP, through `TeeAnchor`; `configfs-tsm` reports on a TDX guest, and a software-grounded `LocalTdxProvider` covers wiring. Self-tested; fetching the PCK certificate from Intel's PCS and checking its chain is the deployment's step, as the VCEK from AMD's KDS is |
| Accelerator attestation — NVIDIA GPU EAT (§7.6) | **Solid** | `sandbox-tee::gpu` verifies an NRAS GPU EAT (JWT/ES384) — the real ECDSA-P384/SHA-384 signature, `alg` pinning (never `none`), and the `eat_nonce` binding — and surfaces the device claims; self-tested |
| TEE/quote/JWKS chaining to vendor roots (§7.6) | **Engineering** | the reference verifies signatures against the supplied key; fetching and pinning the endorsing key to the vendor root (AMD KDS VCEK, Intel PCS PCK, NVIDIA NRAS JWKS) is the production wiring, documented and not faked |
| TEE report generation (§2.2 S6, §7.6) | **Engineering** | the `configfs-tsm` provider requests a report on Linux confidential hardware and returns `Unavailable` elsewhere; a software-grounded provider (tagged, never claiming hardware) covers wiring |

View file

@ -1879,6 +1879,7 @@ dependencies = [
"blake3",
"ed25519-dalek",
"hex",
"p256",
"p384",
"sandbox-attest",
"sandbox-capability",

View file

@ -98,7 +98,7 @@ fn confidential() {
.with_tee(TeeConfig {
provider: Box::new(provider),
policy,
vcek,
anchor: sandbox_tee::policy::TeeAnchor::SevSnp(vcek),
})
.with_locality(Locality {
region: "us-east-1".into(),

View file

@ -18,3 +18,6 @@ p384.workspace = true
blake3.workspace = true
hex.workspace = true
thiserror.workspace = true
[dev-dependencies]
p256.workspace = true

View file

@ -13,7 +13,6 @@
//! that a host built on any executor seals runs the same way.
use ed25519_dalek::{SigningKey, VerifyingKey};
use p384::ecdsa::VerifyingKey as VcekKey;
use sandbox_attest::{
Attestation, AttestError, AuditLog, DeviceIdentity, Journal, Locality, Mechanism, Outcome,
};
@ -24,7 +23,7 @@ pub use sandbox_attest::Mechanism as MechanismRef;
use sandbox_capability::Grants;
use sandbox_meter::{DeviceEnergy, Receipt, ReceiptError, StorageActivity};
use sandbox_schema::{ContentId, DeviceClass, Tier};
use sandbox_tee::policy::{verify_snp, TeePolicy};
use sandbox_tee::policy::{verify_report, TeeAnchor, TeePolicy};
use sandbox_tee::provider::AttestationProvider;
use std::sync::Mutex;
@ -244,12 +243,13 @@ pub struct SealedRun {
}
/// The confidential-tier configuration a host needs to seal S6+ runs: a TEE
/// report provider, the policy a report must satisfy, and the VCEK key to verify
/// it against (SPEC §7.6).
/// report provider, the policy a report must satisfy, and the anchor to verify
/// it against, a VCEK for SEV-SNP or a PCK key for TDX (SPEC §7.6). The
/// provider and the anchor must be of one kind.
pub struct TeeConfig {
pub provider: Box<dyn AttestationProvider>,
pub policy: TeePolicy,
pub vcek: VcekKey,
pub anchor: TeeAnchor,
}
/// The reference host. Holds the steward identity (for receipt JCR-1 `kid`) and
@ -429,7 +429,14 @@ impl Host {
let report_bytes = cfg.provider.report(&report_data)?;
// Pin the report-data binding for this run, then verify.
let policy = cfg.policy.clone().with_report_data(report_data);
let verified = verify_snp(&report_bytes, &policy, &cfg.vcek)?;
if cfg.provider.kind() != cfg.anchor.kind() {
return Err(RunError::Tee(sandbox_tee::TeeError::Policy(format!(
"a {:?} report provider with a {:?} anchor",
cfg.provider.kind(),
cfg.anchor.kind()
))));
}
let verified = verify_report(&report_bytes, &policy, &cfg.anchor)?;
// The report travels with its summary, so that a party other than
// this host can verify it, and the summary against it.
let mut evidence = verified.to_evidence();
@ -588,19 +595,23 @@ pub const RUN_REPORT_DATA_CONTEXT: &str = "sandbox/1 run report_data v1";
///
/// `challenge` is the one the verifier issued, not the one the attestation
/// records, so a report sealed for another request is refused. Evidence that
/// carries no report cannot be verified this way and is refused.
/// carries no report cannot be verified this way and is refused. `anchor` is
/// the verifier's own, and evidence of another kind than its own is refused.
pub fn verify_run_evidence(
attestation: &Attestation,
challenge: Option<&[u8; 32]>,
policy: &TeePolicy,
vcek: &VcekKey,
anchor: &TeeAnchor,
) -> Result<sandbox_tee::policy::VerifiedReport, RunError> {
let evidence = |why: &str| RunError::Tee(sandbox_tee::TeeError::Policy(why.into()));
let tee = attestation.tee.as_ref().ok_or_else(|| evidence("the attestation carries no TEE evidence"))?;
if tee.kind != anchor.kind() {
return Err(evidence("the evidence is not of the anchor's kind"));
}
let report_hex = tee.report_hex.as_ref().ok_or_else(|| evidence("the evidence carries no report"))?;
let report = hex::decode(report_hex).map_err(|_| evidence("the report is not hex"))?;
let expected = run_report_data(&attestation.unit_id, &attestation.receipt_id, &attestation.outcome, challenge);
let verified = verify_snp(&report, &policy.clone().with_report_data(expected), vcek)?;
let verified = verify_report(&report, &policy.clone().with_report_data(expected), anchor)?;
let summary = verified.to_evidence();
if (summary.kind, &summary.measurement_hex, &summary.tcb) != (tee.kind, &tee.measurement_hex, &tee.tcb) {
return Err(evidence("the summary is not the report's"));
@ -907,7 +918,7 @@ mod tests {
.with_tee(TeeConfig {
provider: Box::new(provider),
policy,
vcek,
anchor: TeeAnchor::SevSnp(vcek),
})
.with_locality(Locality {
region: "us-east-1".into(),
@ -960,10 +971,10 @@ mod tests {
.unwrap()
}
fn verifier() -> (sandbox_tee::policy::TeePolicy, VcekKey) {
fn verifier() -> (sandbox_tee::policy::TeePolicy, TeeAnchor) {
let provider = sandbox_tee::provider::LocalSnpProvider::new([0x11; 48], 7);
let policy = sandbox_tee::policy::TeePolicy::sev_snp_baseline().with_measurement([0x11; 48]);
(policy, provider.vcek())
(policy, TeeAnchor::SevSnp(provider.vcek()))
}
/// The report a confidential run carries binds the run's unit, receipt and
@ -1004,10 +1015,80 @@ mod tests {
// The report does not verify under another key.
let other = p384::ecdsa::SigningKey::from_slice(&[0x07; 48]).unwrap();
let stranger = VcekKey::from(&other);
let stranger = TeeAnchor::SevSnp(p384::ecdsa::VerifyingKey::from(&other));
assert!(verify_run_evidence(a, Some(&ours), &policy, &stranger).is_err());
}
/// A host on a TDX guest: a software-grounded TDX provider, its policy and
/// the PCK key its quotes are endorsed by.
fn tdx_host(anchor: Option<TeeAnchor>) -> (Host, TeeAnchor) {
use sandbox_tee::provider::LocalTdxProvider;
let provider = LocalTdxProvider::new([0x5D; 48]);
let pinned = TeeAnchor::Tdx(provider.pck());
let host = Host::new(
"host-tdx",
"steward.tdx",
SigningKey::from_bytes(&[3u8; 32]),
SigningKey::from_bytes(&[4u8; 32]),
)
.with_tee(TeeConfig {
provider: Box::new(provider),
policy: sandbox_tee::policy::TeePolicy::tdx_baseline().with_measurement([0x5D; 48]),
anchor: anchor.unwrap_or_else(|| pinned.clone()),
});
(host, pinned)
}
/// A confidential run on TDX is sealed and verified as one on SEV-SNP is:
/// the quote binds the run and the challenge, and a third party verifies
/// it through the PCK it pinned. Before, the run path verified SEV-SNP
/// reports only.
#[test]
fn a_confidential_run_on_tdx_is_sealed_and_verified() {
let (host, pck) = tdx_host(None);
let policy = sandbox_tee::policy::TeePolicy::tdx_baseline().with_measurement([0x5D; 48]);
let ours = [0xD1; 32];
let sealed = sealed_s6(&host, Some(ours));
let a = &sealed.attestation;
assert_eq!(a.tee.as_ref().unwrap().kind, sandbox_attest::TeeKind::Tdx);
verify_run_evidence(a, Some(&ours), &policy, &pck).unwrap();
assert!(verify_run_evidence(a, Some(&[0xD2; 32]), &policy, &pck).is_err(), "another challenge");
let mut other_outcome = a.clone();
other_outcome.outcome = Outcome::Failed;
assert!(verify_run_evidence(&other_outcome, Some(&ours), &policy, &pck).is_err(), "the outcome is bound");
let forger = p256::ecdsa::SigningKey::from_slice(&[9u8; 32]).unwrap();
let other_pck = TeeAnchor::Tdx(*forger.verifying_key());
assert!(verify_run_evidence(a, Some(&ours), &policy, &other_pck).is_err(), "another platform's PCK");
let (snp_policy, snp) = verifier();
let refused = verify_run_evidence(a, Some(&ours), &snp_policy, &snp).unwrap_err();
assert!(format!("{refused}").contains("anchor's kind"), "an SEV-SNP anchor: {refused}");
}
/// A provider and an anchor of different kinds cannot seal a confidential
/// run: a TDX quote is never handed to the SEV-SNP verifier, nor the
/// other way round.
#[test]
fn a_provider_and_an_anchor_of_different_kinds_cannot_seal() {
let (_, snp) = verifier();
let (host, _) = tdx_host(Some(snp));
let out = host.seal_run_with_mechanism(
ContentId::of_bytes(b"u"),
ContentId::of_bytes(b"i"),
&Grants::default(),
Tier::S6,
cpu_report(),
0,
[0; 8],
1,
2,
None,
None,
);
let Err(err) = out else { panic!("sealed with mismatched kinds") };
assert!(format!("{err}").contains("report provider with a"), "{err}");
}
/// A run sealed without a challenge verifies without one, and is refused
/// to a verifier that issued one: it cannot be passed off as fresh.
#[test]

View file

@ -86,6 +86,34 @@ impl VerifiedReport {
}
}
/// What a verifier trusts a report's signer through (SPEC §7.6): for SEV-SNP
/// the VCEK, from AMD's KDS; for TDX the PCK key, from Intel's PCS. The
/// verifier obtains it and pins it. A report is never trusted through a key it
/// carries itself.
#[derive(Clone, Debug)]
pub enum TeeAnchor {
SevSnp(VerifyingKey),
Tdx(p256::ecdsa::VerifyingKey),
}
impl TeeAnchor {
/// The kind of report this anchor verifies.
pub fn kind(&self) -> TeeKind {
match self {
TeeAnchor::SevSnp(_) => TeeKind::SevSnp,
TeeAnchor::Tdx(_) => TeeKind::Tdx,
}
}
}
/// Verify a report of `anchor`'s kind against it and `policy`.
pub fn verify_report(report: &[u8], policy: &TeePolicy, anchor: &TeeAnchor) -> Result<VerifiedReport, TeeError> {
match anchor {
TeeAnchor::SevSnp(vcek) => verify_snp(report, policy, vcek),
TeeAnchor::Tdx(pck) => crate::tdx::verify_tdx(report, policy, pck),
}
}
/// Verify a SEV-SNP report: signature against `vcek`, then policy. Returns the
/// verified report on success, or the first violated check.
pub fn verify_snp(

View file

@ -31,19 +31,56 @@ pub trait AttestationProvider {
/// confidential-computing guest with the interface mounted.
pub struct ConfigfsTsmProvider {
base: String,
kind: TeeKind,
}
impl Default for ConfigfsTsmProvider {
/// The provider on an SEV-SNP guest.
fn default() -> Self {
ConfigfsTsmProvider {
base: "/sys/kernel/config/tsm/report".into(),
ConfigfsTsmProvider::for_kind(TeeKind::SevSnp)
}
}
impl ConfigfsTsmProvider {
/// The provider on a guest of `kind`: `configfs-tsm` is the same interface
/// on an SEV-SNP guest and a TDX guest, and the report it returns is that
/// platform's. A report is refused when the platform's TSM provider is not
/// of this kind, rather than handed to the wrong verifier.
pub fn for_kind(kind: TeeKind) -> Self {
ConfigfsTsmProvider { base: "/sys/kernel/config/tsm/report".into(), kind }
}
/// The kernel names the platform's provider in each report directory. A
/// report of another kind than this provider's is refused, rather than
/// handed to a verifier for the wrong format.
fn check_platform(&self, dir: &str) -> Result<(), TeeError> {
let Ok(name) = std::fs::read_to_string(format!("{dir}/provider")) else {
return Ok(());
};
if tsm_provider_kind(&name) != Some(self.kind) {
return Err(TeeError::Unavailable(format!(
"the platform's TSM provider is {}, not a {:?} provider",
name.trim(),
self.kind
)));
}
Ok(())
}
}
/// The kind of report a `configfs-tsm` provider produces, from the name the
/// kernel gives it in a report directory's `provider` attribute.
pub fn tsm_provider_kind(name: &str) -> Option<TeeKind> {
match name.trim() {
"sev_guest" => Some(TeeKind::SevSnp),
"tdx_guest" => Some(TeeKind::Tdx),
_ => None,
}
}
impl AttestationProvider for ConfigfsTsmProvider {
fn kind(&self) -> TeeKind {
TeeKind::SevSnp
self.kind
}
fn grounding(&self) -> Grounding {
@ -63,6 +100,7 @@ impl AttestationProvider for ConfigfsTsmProvider {
std::fs::create_dir(&dir)
.map_err(|e| TeeError::Unavailable(format!("create {dir}: {e}")))?;
let result = (|| {
self.check_platform(&dir)?;
std::fs::write(format!("{dir}/inblob"), report_data)
.map_err(|e| TeeError::Unavailable(format!("write inblob: {e}")))?;
std::fs::read(format!("{dir}/outblob"))
@ -132,6 +170,49 @@ impl AttestationProvider for LocalSnpProvider {
}
}
/// A software-grounded TDX provider for wiring and tests. It mints a DCAP v4
/// quote over a TD report with a configured `MR_TD`, binds the requested
/// `report_data`, signs it with a local attestation key and endorses that key
/// with a local PCK key: real ECDSA-P256, but **not** a hardware root of trust
/// ([`Grounding::Software`]).
pub struct LocalTdxProvider {
attestation_key: p256::ecdsa::SigningKey,
pck: p256::ecdsa::SigningKey,
mr_td: [u8; 48],
}
impl LocalTdxProvider {
/// A provider with the given launch measurement (`MR_TD`).
pub fn new(mr_td: [u8; 48]) -> Self {
LocalTdxProvider {
attestation_key: p256::ecdsa::SigningKey::from_slice(&[3u8; 32]).expect("a valid scalar"),
pck: p256::ecdsa::SigningKey::from_slice(&[7u8; 32]).expect("a valid scalar"),
mr_td,
}
}
/// The PCK key a verifier pins for this provider's quotes. Because it is a
/// local dev key (not a PCK certified by Intel's PCS), trusting it tells a
/// verifier the quote is software-grounded.
pub fn pck(&self) -> p256::ecdsa::VerifyingKey {
*self.pck.verifying_key()
}
}
impl AttestationProvider for LocalTdxProvider {
fn kind(&self) -> TeeKind {
TeeKind::Tdx
}
fn grounding(&self) -> Grounding {
Grounding::Software
}
fn report(&self, report_data: &[u8; 64]) -> Result<Vec<u8>, TeeError> {
Ok(crate::tdx::local_quote(&self.attestation_key, &self.pck, self.mr_td, *report_data, false))
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -157,7 +238,42 @@ mod tests {
// On a host without the configfs-tsm interface, honest Unavailable.
let p = ConfigfsTsmProvider {
base: "/nonexistent/tsm/report".into(),
kind: TeeKind::SevSnp,
};
assert!(matches!(p.report(&[0; 64]), Err(TeeError::Unavailable(_))));
}
/// The kernel's provider names map to report kinds, and nothing else does.
#[test]
fn a_tsm_provider_name_names_its_report_kind() {
assert_eq!(tsm_provider_kind("sev_guest\n"), Some(TeeKind::SevSnp));
assert_eq!(tsm_provider_kind("tdx_guest"), Some(TeeKind::Tdx));
assert_eq!(tsm_provider_kind("arm_cca_guest"), None);
assert_eq!(ConfigfsTsmProvider::default().kind(), TeeKind::SevSnp);
assert_eq!(ConfigfsTsmProvider::for_kind(TeeKind::Tdx).kind(), TeeKind::Tdx);
}
/// On a TDX guest, a provider configured for SEV-SNP refuses to report,
/// and one configured for TDX goes ahead.
#[test]
fn a_report_of_another_platform_kind_is_refused() {
let dir = std::env::temp_dir().join(format!("tsm-kind-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("provider"), "tdx_guest\n").unwrap();
let d = dir.to_str().unwrap();
assert!(matches!(ConfigfsTsmProvider::default().check_platform(d), Err(TeeError::Unavailable(_))));
ConfigfsTsmProvider::for_kind(TeeKind::Tdx).check_platform(d).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
}
/// A software TDX quote verifies against the provider's PCK and binds the
/// requested report data.
#[test]
fn a_local_tdx_quote_verifies_against_its_pck() {
let p = LocalTdxProvider::new([0x33; 48]);
let q = p.report(&[0x44; 64]).unwrap();
let v = crate::tdx::verify_tdx(&q, &TeePolicy::tdx_baseline().with_report_data([0x44; 64]), &p.pck()).unwrap();
assert_eq!((v.kind, v.measurement), (TeeKind::Tdx, [0x33; 48]));
assert_eq!(p.grounding(), Grounding::Software);
}
}

View file

@ -24,8 +24,8 @@
use crate::TeeError;
use crate::policy::{TeePolicy, VerifiedReport};
use p256::ecdsa::signature::Verifier;
use p256::ecdsa::{Signature, VerifyingKey};
use p256::ecdsa::signature::{Signer, Verifier};
use p256::ecdsa::{Signature, SigningKey as P256SigningKey, VerifyingKey};
use sandbox_attest::TeeKind;
use sha2::{Digest, Sha256};
@ -213,7 +213,14 @@ fn pem_first_cert(bytes: &[u8]) -> Option<Vec<u8>> {
/// Verify a TDX quote: the quote signature, the QE-report endorsement of the
/// attestation key, the PCK signature over the QE report, then policy. Returns
/// the verified report (`kind = Tdx`, measurement = `MR_TD`).
pub fn verify_tdx(bytes: &[u8], policy: &TeePolicy) -> Result<VerifiedReport, TeeError> {
///
/// `pck` is the platform's PCK key as the verifier obtained it, from Intel's
/// PCS with its certificate chain checked, the way a VCEK comes from AMD's KDS
/// (SPEC §7.6). The quote's own certification data must carry that same key.
/// A quote is never trusted through a key it carries: anyone can make a key,
/// endorse a QE report with it and assemble a quote that is consistent with
/// itself.
pub fn verify_tdx(bytes: &[u8], policy: &TeePolicy, pck: &VerifyingKey) -> Result<VerifiedReport, TeeError> {
if !policy.allowed_kinds.contains(&TeeKind::Tdx) {
return Err(TeeError::Policy("Tdx not an allowed kind".into()));
}
@ -239,9 +246,12 @@ pub fn verify_tdx(bytes: &[u8], policy: &TeePolicy) -> Result<VerifiedReport, Te
return Err(TeeError::Policy("QE report does not bind the attestation key".into()));
}
// 3. PCK signature over the QE report (the platform endorses the QE).
let pck = pck_pubkey(&q.pck_chain)?;
p256_verify(&pck, &q.qe_report, &q.qe_report_sig)?;
// 3. PCK signature over the QE report (the platform endorses the QE), by
// the PCK the verifier pinned, which the quote must name.
if pck_pubkey(&q.pck_chain)? != *pck {
return Err(TeeError::Policy("the quote's PCK is not the one the verifier pinned".into()));
}
p256_verify(pck, &q.qe_report, &q.qe_report_sig)?;
// 4. Policy.
if policy.require_debug_disabled && q.td.debug_enabled() {
@ -269,69 +279,88 @@ pub fn verify_tdx(bytes: &[u8], policy: &TeePolicy) -> Result<VerifiedReport, Te
})
}
/// Build a self-consistent software-grounded TDX quote: a DCAP v4 quote over
/// a TD report with `mr_td` and `report_data`, signed with the attestation
/// key `ak`, whose QE report the PCK key `pck` endorses. Real ECDSA-P256,
/// but no hardware root: it is what [`crate::provider::LocalTdxProvider`]
/// mints, analogous to the SEV-SNP `LocalSnpProvider`.
pub(crate) fn local_quote(
ak: &P256SigningKey,
pck: &P256SigningKey,
mr_td: [u8; 48],
report_data: [u8; 64],
debug: bool,
) -> Vec<u8> {
let ak_xy = ak.verifying_key().to_encoded_point(false).as_bytes()[1..].to_vec();
let pck_xy = pck.verifying_key().to_encoded_point(false).as_bytes()[1..].to_vec();
// Header (48).
let mut q = vec![0u8; HEADER_LEN];
q[0..2].copy_from_slice(&4u16.to_le_bytes()); // version
q[2..4].copy_from_slice(&2u16.to_le_bytes()); // ak type ECDSA-P256
q[4..8].copy_from_slice(&TEE_TYPE_TDX.to_le_bytes());
// TD report (584).
let mut td = vec![0u8; TD_REPORT_LEN];
if debug {
td[TDR_TD_ATTRIBUTES] = 0x01;
}
td[TDR_MR_TD..TDR_MR_TD + 48].copy_from_slice(&mr_td);
td[TDR_REPORT_DATA..TDR_REPORT_DATA + 64].copy_from_slice(&report_data);
q.extend_from_slice(&td);
// Quote signature over header ‖ report.
let sig: Signature = ak.sign(&q);
let quote_sig = sig.to_bytes();
// QE report (384): report_data[0..32] = SHA256(ak_xy ‖ qe_auth(empty)).
let mut qe = vec![0u8; QE_REPORT_LEN];
let mut h = Sha256::new();
h.update(&ak_xy);
let bind = h.finalize();
qe[QE_REPORT_DATA_OFF..QE_REPORT_DATA_OFF + 32].copy_from_slice(&bind);
let qe_sig: Signature = pck.sign(&qe);
let qe_sig_b = qe_sig.to_bytes();
// PCK certification data: a minimal DER-ish blob carrying the point.
let mut pck_blob = vec![0x30u8, 0x00, 0x03, 0x42, 0x00, 0x04];
pck_blob.extend_from_slice(&pck_xy);
// Assemble signature section.
let mut sigsec = Vec::new();
sigsec.extend_from_slice(&quote_sig);
sigsec.extend_from_slice(&ak_xy);
sigsec.extend_from_slice(&6u16.to_le_bytes()); // qe cert type
sigsec.extend_from_slice(&0u32.to_le_bytes()); // qe cert size (unused here)
sigsec.extend_from_slice(&qe);
sigsec.extend_from_slice(&qe_sig_b);
sigsec.extend_from_slice(&0u16.to_le_bytes()); // qe auth len = 0
sigsec.extend_from_slice(&5u16.to_le_bytes()); // pck cert type
sigsec.extend_from_slice(&(pck_blob.len() as u32).to_le_bytes());
sigsec.extend_from_slice(&pck_blob);
q.extend_from_slice(&(sigsec.len() as u32).to_le_bytes());
q.extend_from_slice(&sigsec);
q
}
#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::signature::Signer;
use p256::ecdsa::SigningKey as P256SigningKey;
/// Build a self-consistent software-grounded TDX quote (the honest test
/// fixture, analogous to the SEV-SNP `LocalSnpProvider`). Signs with a dev
/// attestation key and a dev PCK key so the verifier exercises real ECDSA.
/// The dev attestation and PCK keys the software-grounded quotes are made
/// with.
fn dev_keys() -> (P256SigningKey, P256SigningKey) {
(P256SigningKey::from_slice(&[3u8; 32]).unwrap(), P256SigningKey::from_slice(&[7u8; 32]).unwrap())
}
fn synthetic_quote(mr_td: [u8; 48], report_data: [u8; 64], debug: bool) -> Vec<u8> {
let ak = P256SigningKey::from_slice(&[3u8; 32]).unwrap();
let pck = P256SigningKey::from_slice(&[7u8; 32]).unwrap();
let ak_xy = ak.verifying_key().to_encoded_point(false).as_bytes()[1..].to_vec();
let pck_xy = pck.verifying_key().to_encoded_point(false).as_bytes()[1..].to_vec();
let (ak, pck) = dev_keys();
local_quote(&ak, &pck, mr_td, report_data, debug)
}
// Header (48).
let mut q = vec![0u8; HEADER_LEN];
q[0..2].copy_from_slice(&4u16.to_le_bytes()); // version
q[2..4].copy_from_slice(&2u16.to_le_bytes()); // ak type ECDSA-P256
q[4..8].copy_from_slice(&TEE_TYPE_TDX.to_le_bytes());
// TD report (584).
let mut td = vec![0u8; TD_REPORT_LEN];
if debug {
td[TDR_TD_ATTRIBUTES] = 0x01;
}
td[TDR_MR_TD..TDR_MR_TD + 48].copy_from_slice(&mr_td);
td[TDR_REPORT_DATA..TDR_REPORT_DATA + 64].copy_from_slice(&report_data);
q.extend_from_slice(&td);
// Quote signature over header ‖ report.
let sig: Signature = ak.sign(&q);
let quote_sig = sig.to_bytes();
// QE report (384): report_data[0..32] = SHA256(ak_xy ‖ qe_auth(empty)).
let mut qe = vec![0u8; QE_REPORT_LEN];
let mut h = Sha256::new();
h.update(&ak_xy);
let bind = h.finalize();
qe[QE_REPORT_DATA_OFF..QE_REPORT_DATA_OFF + 32].copy_from_slice(&bind);
let qe_sig: Signature = pck.sign(&qe);
let qe_sig_b = qe_sig.to_bytes();
// PCK certification data: a minimal DER-ish blob carrying the point.
let mut pck_blob = vec![0x30u8, 0x00, 0x03, 0x42, 0x00, 0x04];
pck_blob.extend_from_slice(&pck_xy);
// Assemble signature section.
let mut sigsec = Vec::new();
sigsec.extend_from_slice(&quote_sig);
sigsec.extend_from_slice(&ak_xy);
sigsec.extend_from_slice(&6u16.to_le_bytes()); // qe cert type
sigsec.extend_from_slice(&0u32.to_le_bytes()); // qe cert size (unused here)
sigsec.extend_from_slice(&qe);
sigsec.extend_from_slice(&qe_sig_b);
sigsec.extend_from_slice(&0u16.to_le_bytes()); // qe auth len = 0
sigsec.extend_from_slice(&5u16.to_le_bytes()); // pck cert type
sigsec.extend_from_slice(&(pck_blob.len() as u32).to_le_bytes());
sigsec.extend_from_slice(&pck_blob);
q.extend_from_slice(&(sigsec.len() as u32).to_le_bytes());
q.extend_from_slice(&sigsec);
q
fn dev_pck() -> VerifyingKey {
*dev_keys().1.verifying_key()
}
fn baseline() -> TeePolicy {
@ -347,7 +376,7 @@ mod tests {
#[test]
fn a_valid_tdx_quote_verifies() {
let q = synthetic_quote([0xAA; 48], [0x11; 64], false);
let v = verify_tdx(&q, &baseline().with_measurement([0xAA; 48])).unwrap();
let v = verify_tdx(&q, &baseline().with_measurement([0xAA; 48]), &dev_pck()).unwrap();
assert_eq!(v.kind, TeeKind::Tdx);
assert_eq!(v.measurement, [0xAA; 48]);
assert_eq!(v.report_data, [0x11; 64]);
@ -356,13 +385,13 @@ mod tests {
#[test]
fn wrong_measurement_is_rejected() {
let q = synthetic_quote([0xAA; 48], [0x11; 64], false);
assert!(verify_tdx(&q, &baseline().with_measurement([0xBB; 48])).is_err());
assert!(verify_tdx(&q, &baseline().with_measurement([0xBB; 48]), &dev_pck()).is_err());
}
#[test]
fn debug_td_is_rejected() {
let q = synthetic_quote([0xAA; 48], [0; 64], true);
let err = verify_tdx(&q, &baseline()).unwrap_err();
let err = verify_tdx(&q, &baseline(), &dev_pck()).unwrap_err();
assert!(format!("{err}").contains("debug"));
}
@ -370,13 +399,28 @@ mod tests {
fn report_data_binding_is_enforced() {
let q = synthetic_quote([0xAA; 48], [0x11; 64], false);
let policy = baseline().with_report_data([0x22; 64]); // wrong binding
assert!(verify_tdx(&q, &policy).is_err());
assert!(verify_tdx(&q, &policy, &dev_pck()).is_err());
}
#[test]
fn a_tampered_td_report_breaks_the_quote_signature() {
let mut q = synthetic_quote([0xAA; 48], [0x11; 64], false);
q[HEADER_LEN + TDR_MR_TD] ^= 0xFF; // flip an MR_TD byte after signing
assert!(verify_tdx(&q, &baseline()).is_err());
assert!(verify_tdx(&q, &baseline(), &dev_pck()).is_err());
}
/// A quote that is consistent with itself but endorsed by a PCK key its
/// maker generated is refused: the verifier trusts the PCK it pinned, not
/// the one the quote carries. Before, the key was read from the quote, so
/// any self-made quote verified.
#[test]
fn a_quote_endorsed_by_its_own_pck_is_refused() {
let (ak, _) = dev_keys();
let forger = P256SigningKey::from_slice(&[9u8; 32]).unwrap();
let forged = local_quote(&ak, &forger, [0xAA; 48], [0x11; 64], false);
let own = *forger.verifying_key();
verify_tdx(&forged, &baseline(), &own).expect("consistent with itself");
let err = verify_tdx(&forged, &baseline(), &dev_pck()).unwrap_err();
assert!(format!("{err}").contains("pinned"));
}
}