ttk_server/verifier/mod.rs
1//! Verification of TEE attestation evidence carried in an RFC 9711 EAT.
2//!
3//! The server embeds exactly one TEE-specific evidence blob in the EAT `submods` map, under a
4//! label from [`submod`]. [`verify_evidence`] dispatches on that label:
5//!
6//! | Label | TEE | Evidence | Verified by |
7//! |-------------|----------------|--------------------------------------------|---------------|
8//! | `aws_nitro` | AWS Nitro | NSM attestation document (COSE_Sign1) | [`nitro`] |
9//! | `sev_snp` | AMD SEV-SNP | map `{report, vcek}`: report + VCEK (DER) | [`sev_snp`] |
10//! | `tdx` | Intel TDX | DCAP quote v4/v5 with PCK chain | [`dcap`] |
11//! | `sgx` | Intel SGX | DCAP quote v3/v4/v5 with PCK chain | [`dcap`] |
12//!
13//! Every verifier checks the vendor signature chain up to a root in the [`TrustStore`] and
14//! returns a [`VerifiedEvidence`]. [`verify_evidence`] then enforces the [`Policy`] and checks
15//! that the evidence's report data is bound to the expected hash (the SHA-256 of the RA-TLS
16//! certificate's public key).
17
18pub mod dcap;
19pub mod nitro;
20pub mod sev_snp;
21
22use crate::attestation::eat::EatClaimsSet;
23use ciborium::Value;
24use rustls_pki_types::{SignatureVerificationAlgorithm, UnixTime};
25use std::collections::BTreeMap;
26use std::fmt;
27
28/// EAT `submods` labels identifying the TEE that produced the nested evidence.
29pub mod submod {
30 /// AWS Nitro Enclaves attestation document.
31 pub const AWS_NITRO: &str = "aws_nitro";
32 /// AMD SEV-SNP attestation report and VCEK certificate.
33 pub const SEV_SNP: &str = "sev_snp";
34 /// Intel TDX DCAP quote.
35 pub const TDX: &str = "tdx";
36 /// Intel SGX DCAP quote.
37 pub const SGX: &str = "sgx";
38}
39
40/// AWS Nitro Enclaves root certificate (G1), from the AWS Nitro Enclaves documentation.
41const AWS_NITRO_ROOT: &[u8] = include_bytes!("certs/aws_nitro_root_g1.der");
42/// Mock root CA used by the server's `mock` provider. Its private key is public, so it is
43/// trusted only when [`Policy::allow_mock`] is set.
44const MOCK_NITRO_ROOT: &[u8] = include_bytes!("certs/mock_nitro_root.der");
45/// Intel SGX Root CA, which also roots TDX PCK certificate chains.
46const INTEL_SGX_ROOT: &[u8] = include_bytes!("certs/intel_sgx_root_ca.der");
47
48/// The trusted execution environment that produced a piece of evidence.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum TeeKind {
51 /// AWS Nitro Enclaves.
52 AwsNitro,
53 /// AMD SEV-SNP.
54 SevSnp,
55 /// Intel TDX.
56 Tdx,
57 /// Intel SGX.
58 Sgx,
59}
60
61/// Human-readable TEE names.
62impl fmt::Display for TeeKind {
63 /// Writes the TEE's common name.
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(match self {
66 Self::AwsNitro => "AWS Nitro",
67 Self::SevSnp => "AMD SEV-SNP",
68 Self::Tdx => "Intel TDX",
69 Self::Sgx => "Intel SGX",
70 })
71 }
72}
73
74/// Mapping between TEE kinds and their EAT submodule labels.
75impl TeeKind {
76 /// Returns the TEE whose evidence is stored under the submodule `label`, if any.
77 pub fn from_submod(label: &str) -> Option<Self> {
78 match label {
79 submod::AWS_NITRO => Some(Self::AwsNitro),
80 submod::SEV_SNP => Some(Self::SevSnp),
81 submod::TDX => Some(Self::Tdx),
82 submod::SGX => Some(Self::Sgx),
83 _ => None,
84 }
85 }
86}
87
88/// Evidence whose signature chain has been verified.
89#[derive(Debug, Clone)]
90pub struct VerifiedEvidence {
91 /// The TEE that produced the evidence.
92 pub tee: TeeKind,
93 /// Data bound into the evidence by the attester: Nitro `user_data`, or the 64-byte
94 /// `REPORT_DATA` of an SEV-SNP report or DCAP quote.
95 pub report_data: Vec<u8>,
96 /// Named measurements, compared against expected values by the caller. Names per TEE:
97 /// Nitro `pcr0`..`pcrN`; SEV-SNP `measurement`, `host_data`, `id_key_digest`,
98 /// `author_key_digest`; TDX `mrtd`, `rtmr0`..`rtmr3`, `mrseam`, `mrconfigid`, `mrowner`,
99 /// `mrownerconfig`; SGX `mrenclave`, `mrsigner`.
100 pub measurements: BTreeMap<String, Vec<u8>>,
101 /// `true` if the TEE runs in debug mode, where its memory is not confidential.
102 pub debug: bool,
103 /// The decoded Nitro attestation document, for Nitro evidence.
104 pub nitro: Option<nitro::AttestationDocument>,
105}
106
107/// Trust anchors for each vendor's attestation signing chain.
108#[derive(Debug, Clone)]
109pub struct TrustStore {
110 /// DER of the AWS Nitro Enclaves root certificate.
111 pub aws_nitro_root: Vec<u8>,
112 /// DER of the TTKServer mock root CA, trusted only when [`Policy::allow_mock`] is set.
113 pub mock_nitro_root: Vec<u8>,
114 /// DER of the Intel SGX Root CA (roots both SGX and TDX PCK chains).
115 pub intel_sgx_root: Vec<u8>,
116 /// AMD root (ARK) and signing (ASK) certificates per processor family.
117 pub amd: Vec<sev_snp::AmdRoots>,
118}
119
120/// Construction of the trust store.
121impl TrustStore {
122 /// The vendor roots embedded in this crate, downloaded from AWS, Intel and AMD KDS.
123 pub fn builtin() -> Self {
124 Self {
125 aws_nitro_root: AWS_NITRO_ROOT.to_vec(),
126 mock_nitro_root: MOCK_NITRO_ROOT.to_vec(),
127 intel_sgx_root: INTEL_SGX_ROOT.to_vec(),
128 amd: sev_snp::AmdRoots::builtin(),
129 }
130 }
131}
132
133/// Defaults to [`TrustStore::builtin`].
134impl Default for TrustStore {
135 /// Returns the built-in vendor roots.
136 fn default() -> Self {
137 Self::builtin()
138 }
139}
140
141/// Relaxations of the default (strict) verification policy.
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
143pub struct Policy {
144 /// Accept mock Nitro documents: additionally trusts the mock root CA (whose private key is
145 /// public) and implies `allow_debug`. Mock documents are still fully verified. For local
146 /// development only.
147 pub allow_mock: bool,
148 /// Accept TEEs running in debug mode.
149 pub allow_debug: bool,
150}
151
152/// Verifies the TEE evidence in `eat_bytes` at time `now` and checks that it is bound to
153/// `binding`, the SHA-256 of the RA-TLS certificate's public key.
154pub fn verify_evidence(
155 eat_bytes: &[u8],
156 binding: &[u8],
157 now: UnixTime,
158 trust: &TrustStore,
159 policy: Policy,
160) -> Result<VerifiedEvidence, String> {
161 let claims =
162 EatClaimsSet::from_bytes(eat_bytes).map_err(|e| format!("invalid EAT token: {e}"))?;
163 let submods = claims.submods.ok_or("EAT token has no submods")?;
164 let entries = submods.into_map().map_err(|_| "EAT submods is not a map")?;
165
166 let mut known: Vec<(TeeKind, Value)> = entries
167 .into_iter()
168 .filter_map(|(label, value)| {
169 let tee = TeeKind::from_submod(label.as_text()?)?;
170 Some((tee, value))
171 })
172 .collect();
173 let (tee, value) = match known.len() {
174 0 => return Err("EAT token contains no supported TEE evidence".into()),
175 1 => known.remove(0),
176 _ => return Err("EAT token contains evidence from more than one TEE".into()),
177 };
178
179 let evidence = match tee {
180 TeeKind::AwsNitro => nitro::verify(&bytes_of(value, tee)?, now, trust, policy)?,
181 TeeKind::SevSnp => sev_snp::verify(&value, now, trust)?,
182 TeeKind::Tdx | TeeKind::Sgx => dcap::verify(&bytes_of(value, tee)?, tee, now, trust)?,
183 };
184
185 if evidence.debug && !(policy.allow_debug || policy.allow_mock) {
186 return Err(format!("{tee} evidence comes from a debug-mode TEE"));
187 }
188 if !is_bound_to(&evidence.report_data, binding) {
189 return Err(format!(
190 "{tee} report data does not match the certificate's public key"
191 ));
192 }
193 Ok(evidence)
194}
195
196/// Unwraps a CBOR byte string holding `tee` evidence.
197fn bytes_of(value: Value, tee: TeeKind) -> Result<Vec<u8>, String> {
198 value
199 .into_bytes()
200 .map_err(|_| format!("{tee} evidence is not a byte string"))
201}
202
203/// Returns `true` if `report_data` equals `binding`, or starts with it and is zero-padded.
204pub fn is_bound_to(report_data: &[u8], binding: &[u8]) -> bool {
205 report_data.len() >= binding.len()
206 && report_data[..binding.len()] == *binding
207 && report_data[binding.len()..].iter().all(|b| *b == 0)
208}
209
210/// Signature algorithms accepted when validating vendor certificate chains.
211fn chain_algorithms() -> &'static [&'static dyn SignatureVerificationAlgorithm] {
212 rustls::crypto::ring::default_provider()
213 .signature_verification_algorithms
214 .all
215}
216
217/// Verifies a fixed-size (`r || s`) ECDSA `signature` over `message` with the uncompressed
218/// point `public_key`.
219fn verify_ecdsa(
220 alg: &'static ring::signature::EcdsaVerificationAlgorithm,
221 public_key: &[u8],
222 message: &[u8],
223 signature: &[u8],
224) -> Result<(), ()> {
225 ring::signature::UnparsedPublicKey::new(alg, public_key)
226 .verify(message, signature)
227 .map_err(|_| ())
228}
229
230/// Returns the `len` bytes at `offset` as an owned vector. Callers check the buffer length.
231fn field(buf: &[u8], offset: usize, len: usize) -> Vec<u8> {
232 buf[offset..offset + len].to_vec()
233}
234
235/// Reads a little-endian `u64` at `offset`. Callers check the buffer length.
236fn le_u64(buf: &[u8], offset: usize) -> u64 {
237 u64::from_le_bytes(buf[offset..offset + 8].try_into().expect("8-byte slice"))
238}
239
240/// EKU policy for vendor attestation chains: they sign evidence, not TLS sessions, so no
241/// particular Extended Key Usage is required.
242struct AnyKeyUsage;
243
244/// Accepts any (well-formed) Extended Key Usage extension.
245impl webpki::ExtendedKeyUsageValidator for AnyKeyUsage {
246 /// Only rejects a malformed EKU extension.
247 fn validate(&self, iter: webpki::KeyPurposeIdIter<'_, '_>) -> Result<(), webpki::Error> {
248 for eku in iter {
249 eku?;
250 }
251 Ok(())
252 }
253}