From ff06055e5ce210d320421f1e51e83be842db4d4b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 19:11:54 -0400 Subject: [PATCH] sandbox-tee: check a TDX quote's PCK chain to a pinned Intel root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #399 made verify_tdx trust only a PCK key the verifier pins, but left pinning it to the deployment: a hand pin, with nothing checking the quote's certificate chain to Intel's root. The SEV-SNP side has amd_kds for exactly this. intel_pcs (feature `intel-pcs`): - verify_pck_chain(trusted_root_der, chain_pem, now_unix) checks the chain a quote carries, the PCK leaf then its issuing CA, optionally ending in the root, to the pinned root. It checks: - ECDSA-P256-SHA256 signatures, with the X.509 (r, s) decoded via der; - the declared algorithm; - issuer and subject names linking up the chain; - each certificate valid at now. A presented root must equal the pinned one byte for byte; a longer chain is refused. It returns the PCK key, the TeeAnchor::Tdx a quote is verified by. - pck_from_quote derives that anchor from the quote's own chain, checked to the root. The trust comes from the root, never from the quote. - Revocation lists are the deployment's to consult, as stated in the module. tdx: TdxQuote::pck_chain(); local_quote takes optional certification data, so a software quote can carry a real chain. CI: the sandbox-rs job now runs `cargo test -p sandbox-tee --features amd-kds,intel-pcs`. The amd-kds tests had never run in CI: a plain `cargo test` does not enable the feature. SPEC §7.6 and the §9 readiness row say the chain is checked. Tests: 5, against a self-generated chain of Intel's structure: - a chain to the pinned root yields the PCK, with or without the root; - a chain to another root is refused, pinned or presented; - every link must hold (a forged leaf or CA, a misnamed issuer, a missing CA, a chain too long, a relabeled algorithm); - a chain outside its validity is refused; - the anchor from a quote's chain verifies that quote and no other root yields one. Mutation testing: 10 mutants, all killed. Verified on the build host: sandbox-tee with amd-kds and intel-pcs, 51 tests; the sandbox workspace, 434; clippy -D warnings clean. Repo checks pass, including workflow concurrency and lockfiles. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/standards-build.yml | 6 +- sandbox/SPEC.md | 4 +- .../sandbox-rs/crates/sandbox-tee/Cargo.toml | 4 + .../crates/sandbox-tee/src/intel_pcs.rs | 316 ++++++++++++++++++ .../sandbox-rs/crates/sandbox-tee/src/lib.rs | 2 + .../crates/sandbox-tee/src/provider.rs | 2 +- .../sandbox-rs/crates/sandbox-tee/src/tdx.rs | 26 +- 7 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 sandbox/sandbox-rs/crates/sandbox-tee/src/intel_pcs.rs diff --git a/.github/workflows/standards-build.yml b/.github/workflows/standards-build.yml index d76353d29..ac5bbb0b1 100644 --- a/.github/workflows/standards-build.yml +++ b/.github/workflows/standards-build.yml @@ -78,7 +78,11 @@ jobs: env: WORKSPACES: | - { name: jcp-rs, dir: jcp/jcp-rs, runner: linux, conformance: jcp-conformance, feature_tests: "-p jcp-energy --features nvml" } - - { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance } + # sandbox-tee's certificate-chain verifiers sit behind features (their + # X.509 and RSA dependencies are heavy), so a plain `cargo test` never + # runs their tests: `amd-kds` (SEV-SNP's VCEK to AMD's root) and + # `intel-pcs` (TDX's PCK to Intel's root). + - { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance, feature_tests: "-p sandbox-tee --features amd-kds,intel-pcs" } - { name: smart-byte-rs, dir: smart-byte/smart-byte-rs, runner: linux } - { name: pool-rs, dir: pool/pool-rs, runner: linux } - { name: openpay, dir: openpay, runner: linux-arm64 } diff --git a/sandbox/SPEC.md b/sandbox/SPEC.md index 80b8cc67f..a1ad228d1 100644 --- a/sandbox/SPEC.md +++ b/sandbox/SPEC.md @@ -298,7 +298,7 @@ At S6 and above, `tee` **MUST** carry a summary of the hardware attestation repo - 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. +A TDX quote carries its PCK certificate chain in its certification data, and the PCK key **MUST** be the pinned one. A verifier may come to pin it by checking that chain to Intel's root, which it pins in turn: the reference does so in `sandbox-tee::intel_pcs` (signatures, issuer and subject names, validity at a given time; revocation lists are the deployment's to consult), as `sandbox-tee::amd_kds` checks a VCEK to AMD's root. 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: @@ -397,7 +397,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 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 | +| 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. The PCK chain is checked to a pinned Intel root by `sandbox-tee::intel_pcs` (feature `intel-pcs`). Self-tested against a chain of Intel's structure; pinning Intel's real root and consulting revocation lists are the deployment's steps | | 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 | diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml b/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml index b0aad61f6..5de3ff8a1 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml +++ b/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml @@ -32,6 +32,10 @@ signature = { version = "2", optional = true } # AMD KDS cert-chain verification (SEV-SNP): turns a genuine AMD ARK->ASK->VCEK # chain into a hardware-grounded verifying key for the `chain`/grounding machinery. amd-kds = ["dep:rsa", "dep:x509-cert", "dep:spki", "dep:der", "dep:signature"] +# Intel PCK cert-chain verification (TDX): checks the PCK chain a quote carries +# to a pinned Intel root, so the TDX anchor is derived rather than pinned by hand. +# ECDSA-P256 throughout, so it needs the X.509 parsing and not the RSA stack. +intel-pcs = ["dep:x509-cert", "dep:spki", "dep:der"] # Dev-only, for the `attested_efficiency` example: composes this crate's energy # attestation with the efficiency/1 receipt (jcp-rs), over a figure this host's diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/src/intel_pcs.rs b/sandbox/sandbox-rs/crates/sandbox-tee/src/intel_pcs.rs new file mode 100644 index 000000000..003c31952 --- /dev/null +++ b/sandbox/sandbox-rs/crates/sandbox-tee/src/intel_pcs.rs @@ -0,0 +1,316 @@ +//! Intel PCK cert-chain verification for TDX (the `intel-pcs` feature). +//! +//! A TDX quote's QE report is signed by the platform's **PCK** key, and the +//! quote carries the PCK's certificate chain in its certification data: the +//! PCK leaf, then the Intel CA that issued it (the platform or processor CA), +//! then the root. [`crate::tdx::verify_tdx`] trusts the PCK the verifier pins, +//! never the one a quote presents. This module is how a verifier comes to pin +//! it: by checking the presented chain to Intel's root, which the relying +//! party pins, as SEV-SNP's VCEK is checked to AMD's ARK +//! ([`crate::amd_kds`]). +//! +//! Every link is ECDSA-P256 with SHA-256. The checks are signatures, issuer +//! and subject names linking up the chain, and each certificate valid at the +//! time given. +//! +//! **Not checked here:** revocation. Intel publishes CRLs for the PCK CAs, and +//! consulting them is the deployment's step. +//! +//! **Tested against:** a self-generated chain of Intel's structure, which +//! exercises the cryptographic constructions. Pinning Intel's real root is +//! the relying party's step. + +use crate::TeeError; +use der::asn1::UintRef; +use der::{Decode, Encode, Reader, SliceReader}; +use p256::ecdsa::signature::Verifier; +use p256::ecdsa::{Signature, VerifyingKey}; +use x509_cert::Certificate; + +/// The OID of `ecdsa-with-SHA256`. +const ECDSA_WITH_SHA256: &str = "1.2.840.10045.4.3.2"; + +/// The certificates of a PEM bundle, as DER, in order. +fn pem_certs(bytes: &[u8]) -> Result>, TeeError> { + use base64::Engine; + let text = core::str::from_utf8(bytes).map_err(|_| TeeError::Malformed("the PCK chain is not PEM".into()))?; + let (begin, end) = ("-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"); + let mut out = Vec::new(); + let mut rest = text; + while let Some(b) = rest.find(begin) { + let after = &rest[b + begin.len()..]; + let e = after.find(end).ok_or_else(|| TeeError::Malformed("an unterminated certificate".into()))?; + let b64: String = after[..e].chars().filter(|c| !c.is_whitespace()).collect(); + out.push( + base64::engine::general_purpose::STANDARD + .decode(b64) + .map_err(|e| TeeError::Malformed(format!("certificate base64: {e}")))?, + ); + rest = &after[e + end.len()..]; + } + Ok(out) +} + +/// The P-256 key in a certificate's SubjectPublicKeyInfo. +fn subject_key(cert: &Certificate) -> Result { + let point = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .ok_or_else(|| TeeError::Malformed("an unaligned public key".into()))?; + VerifyingKey::from_sec1_bytes(point).map_err(|e| TeeError::Key(format!("P-256 key: {e}"))) +} + +/// An X.509 ECDSA signature: DER `SEQUENCE { r INTEGER, s INTEGER }`. +fn ecdsa_signature(der: &[u8]) -> Result { + let bad = |e: der::Error| TeeError::Signature(format!("ECDSA signature: {e}")); + let mut reader = SliceReader::new(der).map_err(bad)?; + let (r, s) = reader + .sequence(|seq| Ok((UintRef::decode(seq)?, UintRef::decode(seq)?))) + .map_err(bad)?; + let field = |n: UintRef<'_>| -> Result<[u8; 32], TeeError> { + let b = n.as_bytes(); + if b.len() > 32 { + return Err(TeeError::Signature("an ECDSA scalar wider than P-256".into())); + } + let mut out = [0u8; 32]; + out[32 - b.len()..].copy_from_slice(b); + Ok(out) + }; + Signature::from_scalars(field(r)?, field(s)?).map_err(|e| TeeError::Signature(format!("ECDSA signature: {e}"))) +} + +/// Whether `cert` is signed by `issuer` (ECDSA-P256-SHA256 over its TBS), names +/// `issuer` as its issuer, and is valid at `now_unix`. +fn check_link(cert: &Certificate, issuer: &Certificate, now_unix: u64, what: &str) -> Result<(), TeeError> { + let fail = |why: String| TeeError::Policy(format!("{what}: {why}")); + if cert.signature_algorithm.oid.to_string() != ECDSA_WITH_SHA256 { + return Err(fail(format!("signed with {}, not ecdsa-with-SHA256", cert.signature_algorithm.oid))); + } + if cert.tbs_certificate.issuer != issuer.tbs_certificate.subject { + return Err(fail("its issuer is not the next certificate's subject".into())); + } + let validity = &cert.tbs_certificate.validity; + let (from, until) = (validity.not_before.to_unix_duration().as_secs(), validity.not_after.to_unix_duration().as_secs()); + if now_unix < from || now_unix > until { + return Err(fail(format!("not valid at {now_unix} (valid {from}..{until})"))); + } + let tbs = cert.tbs_certificate.to_der().map_err(|e| TeeError::Malformed(format!("{what} TBS: {e}")))?; + let sig = ecdsa_signature( + cert.signature.as_bytes().ok_or_else(|| TeeError::Malformed(format!("{what}: an unaligned signature")))?, + )?; + subject_key(issuer)?.verify(&tbs, &sig).map_err(|_| fail("the signature does not verify".into())) +} + +/// Verify a PCK certificate chain to the pinned Intel root, and return the PCK +/// key: the anchor [`crate::tdx::verify_tdx`] checks a quote's QE report by. +/// +/// - `trusted_root_der`: Intel's published root CA certificate, which the +/// relying party pins. +/// - `chain_pem`: the chain as a quote carries it, the PCK leaf first, then +/// its issuing CA, optionally ending in the root. A root the chain presents +/// must be the pinned one, byte for byte. The pinned root is used either way. +/// - `now_unix`: when the chain must be valid. +pub fn verify_pck_chain(trusted_root_der: &[u8], chain_pem: &[u8], now_unix: u64) -> Result { + let root = Certificate::from_der(trusted_root_der).map_err(|e| TeeError::Malformed(format!("root: {e}")))?; + let certs = pem_certs(chain_pem)?; + let (leaf_der, rest) = certs.split_first().ok_or_else(|| TeeError::Malformed("an empty PCK chain".into()))?; + let (ca_der, rest) = rest.split_first().ok_or_else(|| TeeError::Malformed("a PCK chain without its CA".into()))?; + match rest { + [] => {} + [presented] if presented.as_slice() == trusted_root_der => {} + [_] => return Err(TeeError::Policy("the chain ends in another root than the pinned one".into())), + _ => return Err(TeeError::Malformed("a PCK chain longer than leaf, CA and root".into())), + } + let leaf = Certificate::from_der(leaf_der).map_err(|e| TeeError::Malformed(format!("PCK: {e}")))?; + let ca = Certificate::from_der(ca_der).map_err(|e| TeeError::Malformed(format!("PCK CA: {e}")))?; + check_link(&ca, &root, now_unix, "the PCK CA")?; + check_link(&leaf, &ca, now_unix, "the PCK certificate")?; + subject_key(&leaf) +} + +/// The anchor for a TDX quote, derived from the chain the quote itself carries +/// and checked to the pinned Intel root ([`verify_pck_chain`]). The trust +/// comes from the root, never from the quote. +pub fn pck_from_quote(quote: &[u8], trusted_root_der: &[u8], now_unix: u64) -> Result { + let q = crate::tdx::parse(quote)?; + verify_pck_chain(trusted_root_der, q.pck_chain(), now_unix) +} + +#[cfg(test)] +mod tests { + use super::*; + use der::asn1::{BitString, ObjectIdentifier, UtcTime}; + use der::Any; + use p256::ecdsa::signature::Signer; + use p256::ecdsa::SigningKey; + use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; + use std::str::FromStr; + use std::time::Duration; + use x509_cert::certificate::{TbsCertificate, Version}; + use x509_cert::name::Name; + use x509_cert::serial_number::SerialNumber; + use x509_cert::time::{Time, Validity}; + + const NOW: u64 = 1_800_000_000; + const YEAR: u64 = 365 * 24 * 3600; + + /// An ECDSA signature as X.509 carries it: DER `SEQUENCE { r, s }`. + fn der_signature(sig: &Signature) -> Vec { + let int = |b: &[u8]| { + let mut v: Vec = b.iter().copied().skip_while(|&x| x == 0).collect(); + if v.first().is_none_or(|&x| x & 0x80 != 0) { + v.insert(0, 0); + } + [vec![0x02, v.len() as u8], v].concat() + }; + let (r, s) = (sig.r().to_bytes(), sig.s().to_bytes()); + let body = [int(&r), int(&s)].concat(); + [vec![0x30, body.len() as u8], body].concat() + } + + /// A certificate for `key`, named `subject`, issued by `issuer` and signed + /// by `signer`, valid from `from` until `until`. + fn cert(subject: &str, issuer: &str, key: &VerifyingKey, signer: &SigningKey, from: u64, until: u64) -> Vec { + let sig_alg = AlgorithmIdentifierOwned { oid: ObjectIdentifier::new_unwrap(ECDSA_WITH_SHA256), parameters: None }; + let spki = SubjectPublicKeyInfoOwned { + algorithm: AlgorithmIdentifierOwned { + oid: ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"), + parameters: Some(Any::encode_from(&ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7")).unwrap()), + }, + subject_public_key: BitString::from_bytes(key.to_encoded_point(false).as_bytes()).unwrap(), + }; + let at = |s: u64| Time::UtcTime(UtcTime::from_unix_duration(Duration::from_secs(s)).unwrap()); + let tbs = TbsCertificate { + version: Version::V3, + serial_number: SerialNumber::new(&[1]).unwrap(), + signature: sig_alg.clone(), + issuer: Name::from_str(&format!("CN={issuer}")).unwrap(), + validity: Validity { not_before: at(from), not_after: at(until) }, + subject: Name::from_str(&format!("CN={subject}")).unwrap(), + subject_public_key_info: spki, + issuer_unique_id: None, + subject_unique_id: None, + extensions: None, + }; + let sig: Signature = signer.sign(&tbs.to_der().unwrap()); + let signature = BitString::from_bytes(&der_signature(&sig)).unwrap(); + Certificate { tbs_certificate: tbs, signature_algorithm: sig_alg, signature }.to_der().unwrap() + } + + fn pem(der: &[u8]) -> String { + use base64::Engine; + let b64 = base64::engine::general_purpose::STANDARD.encode(der); + format!("-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n") + } + + struct Chain { + root: Vec, + ca: Vec, + leaf: Vec, + pck: SigningKey, + } + + /// A chain of Intel's structure: a self-signed root, a platform CA it + /// signs, and a PCK leaf the CA signs. + fn chain() -> Chain { + let root_key = SigningKey::from_slice(&[0x31; 32]).unwrap(); + let ca_key = SigningKey::from_slice(&[0x32; 32]).unwrap(); + let pck = SigningKey::from_slice(&[0x33; 32]).unwrap(); + let (from, until) = (NOW - YEAR, NOW + YEAR); + Chain { + root: cert("Root CA", "Root CA", root_key.verifying_key(), &root_key, from, until), + ca: cert("Platform CA", "Root CA", ca_key.verifying_key(), &root_key, from, until), + leaf: cert("PCK Certificate", "Platform CA", pck.verifying_key(), &ca_key, from, until), + pck, + } + } + + /// A genuine chain yields the PCK key, whether or not it ends in the root. + #[test] + fn a_chain_to_the_pinned_root_yields_the_pck() { + let c = chain(); + let leaf_ca = pem(&c.leaf) + &pem(&c.ca); + assert_eq!(verify_pck_chain(&c.root, leaf_ca.as_bytes(), NOW).unwrap(), *c.pck.verifying_key()); + let with_root = leaf_ca + &pem(&c.root); + assert_eq!(verify_pck_chain(&c.root, with_root.as_bytes(), NOW).unwrap(), *c.pck.verifying_key()); + } + + /// A chain to another root is refused, whether the verifier pins another + /// root or the chain presents one. + #[test] + fn a_chain_to_another_root_is_refused() { + let c = chain(); + let other = chain_from(0x41); + let leaf_ca = pem(&c.leaf) + &pem(&c.ca); + assert!(verify_pck_chain(&other.root, leaf_ca.as_bytes(), NOW).is_err(), "another root pinned"); + let presenting_other = leaf_ca + &pem(&other.root); + let err = verify_pck_chain(&c.root, presenting_other.as_bytes(), NOW).unwrap_err(); + assert!(format!("{err}").contains("pinned"), "{err}"); + } + + /// A link that does not hold is refused: a leaf its CA did not sign, a + /// CA another key signed, a leaf naming another issuer. + #[test] + fn every_link_must_hold() { + let c = chain(); + let stranger = SigningKey::from_slice(&[0x51; 32]).unwrap(); + let forged_leaf = cert("PCK Certificate", "Platform CA", c.pck.verifying_key(), &stranger, NOW - YEAR, NOW + YEAR); + assert!(verify_pck_chain(&c.root, (pem(&forged_leaf) + &pem(&c.ca)).as_bytes(), NOW).is_err()); + let ca_key = SigningKey::from_slice(&[0x32; 32]).unwrap(); + let forged_ca = cert("Platform CA", "Root CA", ca_key.verifying_key(), &stranger, NOW - YEAR, NOW + YEAR); + assert!(verify_pck_chain(&c.root, (pem(&c.leaf) + &pem(&forged_ca)).as_bytes(), NOW).is_err()); + let misnamed = cert("PCK Certificate", "Processor CA", c.pck.verifying_key(), &ca_key, NOW - YEAR, NOW + YEAR); + let err = verify_pck_chain(&c.root, (pem(&misnamed) + &pem(&c.ca)).as_bytes(), NOW).unwrap_err(); + assert!(format!("{err}").contains("issuer"), "{err}"); + assert!(verify_pck_chain(&c.root, pem(&c.leaf).as_bytes(), NOW).is_err(), "a chain without its CA"); + let long = pem(&c.leaf) + &pem(&c.ca) + &pem(&c.root) + &pem(&c.root); + assert!(verify_pck_chain(&c.root, long.as_bytes(), NOW).is_err(), "a chain longer than leaf, CA and root"); + + // A certificate declaring another signature algorithm is refused, even + // though its signature still verifies as ECDSA-P256-SHA256. + let mut relabeled = Certificate::from_der(&c.leaf).unwrap(); + relabeled.signature_algorithm.oid = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3"); + let relabeled = relabeled.to_der().unwrap(); + let err = verify_pck_chain(&c.root, (pem(&relabeled) + &pem(&c.ca)).as_bytes(), NOW).unwrap_err(); + assert!(format!("{err}").contains("signed with"), "{err}"); + } + + /// Every certificate must be valid at the time given. + #[test] + fn a_chain_outside_its_validity_is_refused() { + let c = chain(); + let leaf_ca = pem(&c.leaf) + &pem(&c.ca); + assert!(verify_pck_chain(&c.root, leaf_ca.as_bytes(), NOW + 2 * YEAR).is_err(), "expired"); + assert!(verify_pck_chain(&c.root, leaf_ca.as_bytes(), NOW - 2 * YEAR).is_err(), "not yet valid"); + } + + /// The anchor derived from a quote's own chain verifies that quote, and + /// a quote whose chain does not reach the pinned root yields none. + #[test] + fn the_anchor_comes_from_the_quotes_chain_checked_to_the_root() { + let c = chain(); + let ak = SigningKey::from_slice(&[3u8; 32]).unwrap(); + let chain_pem = pem(&c.leaf) + &pem(&c.ca) + &pem(&c.root); + let quote = crate::tdx::local_quote(&ak, &c.pck, [0xAA; 48], [0x11; 64], false, Some(chain_pem.as_bytes())); + let pck = pck_from_quote("e, &c.root, NOW).unwrap(); + let policy = crate::policy::TeePolicy::tdx_baseline().with_report_data([0x11; 64]); + crate::tdx::verify_tdx("e, &policy, &pck).unwrap(); + assert!(pck_from_quote("e, &chain_from(0x41).root, NOW).is_err()); + } + + /// Another chain of the same shape, from other keys. + fn chain_from(seed: u8) -> Chain { + let root_key = SigningKey::from_slice(&[seed; 32]).unwrap(); + let ca_key = SigningKey::from_slice(&[seed + 1; 32]).unwrap(); + let pck = SigningKey::from_slice(&[seed + 2; 32]).unwrap(); + let (from, until) = (NOW - YEAR, NOW + YEAR); + Chain { + root: cert("Root CA", "Root CA", root_key.verifying_key(), &root_key, from, until), + ca: cert("Platform CA", "Root CA", ca_key.verifying_key(), &root_key, from, until), + leaf: cert("PCK Certificate", "Platform CA", pck.verifying_key(), &ca_key, from, until), + pck, + } + } +} diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-tee/src/lib.rs index aa64d5b6b..50d0ab2d0 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/src/lib.rs @@ -17,6 +17,8 @@ #[cfg(feature = "amd-kds")] pub mod amd_kds; +#[cfg(feature = "intel-pcs")] +pub mod intel_pcs; pub mod chain; pub mod energy; pub mod gpu; diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/src/provider.rs b/sandbox/sandbox-rs/crates/sandbox-tee/src/provider.rs index 41c3896c2..4c0fd7ad7 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/src/provider.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/src/provider.rs @@ -209,7 +209,7 @@ impl AttestationProvider for LocalTdxProvider { } fn report(&self, report_data: &[u8; 64]) -> Result, TeeError> { - Ok(crate::tdx::local_quote(&self.attestation_key, &self.pck, self.mr_td, *report_data, false)) + Ok(crate::tdx::local_quote(&self.attestation_key, &self.pck, self.mr_td, *report_data, false, None)) } } diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/src/tdx.rs b/sandbox/sandbox-rs/crates/sandbox-tee/src/tdx.rs index f3c1bf8fc..91614bbbf 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/src/tdx.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/src/tdx.rs @@ -88,6 +88,14 @@ pub struct TdxQuote { pck_chain: Vec, } +impl TdxQuote { + /// The certification data the quote carries for its PCK: a PEM chain, the + /// PCK leaf first, for [`crate::intel_pcs::verify_pck_chain`]. + pub fn pck_chain(&self) -> &[u8] { + &self.pck_chain + } +} + fn arr(b: &[u8], off: usize) -> Result<[u8; N], TeeError> { b.get(off..off + N) .and_then(|s| s.try_into().ok()) @@ -290,6 +298,7 @@ pub(crate) fn local_quote( mr_td: [u8; 48], report_data: [u8; 64], debug: bool, + certification: Option<&[u8]>, ) -> Vec { 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(); @@ -322,9 +331,16 @@ pub(crate) fn local_quote( 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); + // PCK certification data: the chain given, or a minimal DER-ish blob + // carrying the point. + let pck_blob = match certification { + Some(chain) => chain.to_vec(), + None => { + let mut blob = vec![0x30u8, 0x00, 0x03, 0x42, 0x00, 0x04]; + blob.extend_from_slice(&pck_xy); + blob + } + }; // Assemble signature section. let mut sigsec = Vec::new(); @@ -356,7 +372,7 @@ mod tests { fn synthetic_quote(mr_td: [u8; 48], report_data: [u8; 64], debug: bool) -> Vec { let (ak, pck) = dev_keys(); - local_quote(&ak, &pck, mr_td, report_data, debug) + local_quote(&ak, &pck, mr_td, report_data, debug, None) } fn dev_pck() -> VerifyingKey { @@ -417,7 +433,7 @@ mod tests { 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 forged = local_quote(&ak, &forger, [0xAA; 48], [0x11; 64], false, None); let own = *forger.verifying_key(); verify_tdx(&forged, &baseline(), &own).expect("consistent with itself"); let err = verify_tdx(&forged, &baseline(), &dev_pck()).unwrap_err(); -- 2.54.0 (Apple Git-157)