sandbox: TDX on the run path, through a pinned PCK #399
8 changed files with 367 additions and 88 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
1
sandbox/sandbox-rs/Cargo.lock
generated
1
sandbox/sandbox-rs/Cargo.lock
generated
|
|
@ -1879,6 +1879,7 @@ dependencies = [
|
|||
"blake3",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"p256",
|
||||
"p384",
|
||||
"sandbox-attest",
|
||||
"sandbox-capability",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -18,3 +18,6 @@ p384.workspace = true
|
|||
blake3.workspace = true
|
||||
hex.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
p256.workspace = true
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("e_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("e_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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue