sandbox-tee: check a TDX quote's PCK chain to a pinned Intel root #415
7 changed files with 351 additions and 9 deletions
6
.github/workflows/standards-build.yml
vendored
6
.github/workflows/standards-build.yml
vendored
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
316
sandbox/sandbox-rs/crates/sandbox-tee/src/intel_pcs.rs
Normal file
316
sandbox/sandbox-rs/crates/sandbox-tee/src/intel_pcs.rs
Normal file
|
|
@ -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<Vec<Vec<u8>>, 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<VerifyingKey, TeeError> {
|
||||
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<Signature, TeeError> {
|
||||
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<VerifyingKey, TeeError> {
|
||||
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<VerifyingKey, TeeError> {
|
||||
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<u8> {
|
||||
let int = |b: &[u8]| {
|
||||
let mut v: Vec<u8> = 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<u8> {
|
||||
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<u8>,
|
||||
ca: Vec<u8>,
|
||||
leaf: Vec<u8>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ impl AttestationProvider for LocalTdxProvider {
|
|||
}
|
||||
|
||||
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))
|
||||
Ok(crate::tdx::local_quote(&self.attestation_key, &self.pck, self.mr_td, *report_data, false, None))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,14 @@ pub struct TdxQuote {
|
|||
pck_chain: Vec<u8>,
|
||||
}
|
||||
|
||||
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<const N: usize>(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<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();
|
||||
|
|
@ -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<u8> {
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue