Skip to main content

ttk_server/verifier/
dcap.rs

1//! Intel TDX and SGX evidence: ECDSA-P256 DCAP quotes (SGX v3, SGX/TDX v4 and v5).
2//!
3//! Checks performed:
4//! 1. the PCK certificate chain embedded in the quote (certification data type 5, PEM or
5//!    DER) chains to the pinned Intel SGX Root CA and is valid now;
6//! 2. the Quoting Enclave (QE) report is signed by the PCK key, and is not a debug enclave;
7//! 3. the QE report's `REPORT_DATA` binds the attestation key: `SHA-256(attestation_key ||
8//!    qe_auth_data)` followed by 32 zero bytes;
9//! 4. the quote header and body are signed by the attestation key.
10//!
11//! Not evaluated: TCB status and QE identity against Intel PCS collateral (TCB Info, QE
12//! Identity, PCK CRLs). A genuine but out-of-date or revoked platform is therefore accepted.
13
14use super::{
15    chain_algorithms, field, le_u64, verify_ecdsa, AnyKeyUsage, TeeKind, TrustStore,
16    VerifiedEvidence,
17};
18use rustls_pki_types::pem::PemObject;
19use rustls_pki_types::{CertificateDer, UnixTime};
20use sha2::{Digest, Sha256};
21use std::collections::BTreeMap;
22use x509_parser::prelude::*;
23
24const HEADER_LEN: usize = 48;
25const SGX_REPORT_BODY_LEN: usize = 384;
26const TD10_REPORT_BODY_LEN: usize = 584;
27const TD15_REPORT_BODY_LEN: usize = 648;
28
29/// Attestation key type: ECDSA-256-with-P-256 curve.
30const ATT_KEY_TYPE_ECDSA_P256: u16 = 2;
31/// Header `tee_type` values.
32const TEE_TYPE_SGX: u32 = 0x0000_0000;
33const TEE_TYPE_TDX: u32 = 0x0000_0081;
34/// Quote v5 body types.
35const BODY_TYPE_SGX: u16 = 1;
36const BODY_TYPE_TD10: u16 = 2;
37const BODY_TYPE_TD15: u16 = 3;
38/// Certification data types.
39const CERT_TYPE_PCK_CHAIN: u16 = 5;
40const CERT_TYPE_QE_REPORT: u16 = 6;
41
42/// SGX report body offsets.
43const SGX_ATTRIBUTES: usize = 48;
44const SGX_MRENCLAVE: usize = 64;
45const SGX_MRSIGNER: usize = 128;
46const SGX_REPORT_DATA: usize = 320;
47/// SGX `ATTRIBUTES.FLAGS` debug bit.
48const SGX_FLAG_DEBUG: u64 = 1 << 1;
49
50/// TDX report body offsets.
51const TD_MRSEAM: usize = 16;
52const TD_ATTRIBUTES: usize = 120;
53const TD_MRTD: usize = 136;
54const TD_MRCONFIGID: usize = 184;
55const TD_MROWNER: usize = 232;
56const TD_MROWNERCONFIG: usize = 280;
57const TD_RTMR0: usize = 328;
58const TD_REPORT_DATA: usize = 520;
59/// `TDATTRIBUTES` debug bit.
60const TD_ATTRIBUTE_DEBUG: u64 = 1;
61
62/// Verifies the DCAP `quote` produced by `tee` (TDX or SGX) at time `now`.
63pub fn verify(
64    quote: &[u8],
65    tee: TeeKind,
66    now: UnixTime,
67    trust: &TrustStore,
68) -> Result<VerifiedEvidence, String> {
69    let parts = QuoteParts::parse(quote, tee)?;
70
71    let pck_leaf = verify_pck_chain(parts.pck_chain, now, trust)?;
72    let (_, pck) = X509Certificate::from_der(&pck_leaf)
73        .map_err(|e| format!("malformed PCK certificate: {e}"))?;
74    verify_ecdsa(
75        &ring::signature::ECDSA_P256_SHA256_FIXED,
76        &pck.public_key().subject_public_key.data,
77        parts.qe_report,
78        parts.qe_report_signature,
79    )
80    .map_err(|_| "QE report signature is invalid".to_string())?;
81    if le_u64(parts.qe_report, SGX_ATTRIBUTES) & SGX_FLAG_DEBUG != 0 {
82        return Err("quote was produced by a debug-mode Quoting Enclave".into());
83    }
84
85    let mut hasher = Sha256::new();
86    hasher.update(parts.attestation_key);
87    hasher.update(parts.qe_auth_data);
88    let expected = hasher.finalize();
89    let qe_report_data = &parts.qe_report[SGX_REPORT_DATA..SGX_REPORT_DATA + 64];
90    if !super::is_bound_to(qe_report_data, &expected) {
91        return Err("QE report does not bind the quote's attestation key".into());
92    }
93
94    let mut attestation_key = Vec::with_capacity(65);
95    attestation_key.push(0x04);
96    attestation_key.extend_from_slice(parts.attestation_key);
97    verify_ecdsa(
98        &ring::signature::ECDSA_P256_SHA256_FIXED,
99        &attestation_key,
100        parts.signed,
101        parts.quote_signature,
102    )
103    .map_err(|_| "quote signature is invalid".to_string())?;
104
105    Ok(match tee {
106        TeeKind::Tdx => td_evidence(parts.body),
107        _ => sgx_evidence(parts.body),
108    })
109}
110
111/// Extracts measurements from a TDX report body.
112fn td_evidence(body: &[u8]) -> VerifiedEvidence {
113    let mut measurements = BTreeMap::from([
114        ("mrseam".to_string(), field(body, TD_MRSEAM, 48)),
115        ("mrtd".to_string(), field(body, TD_MRTD, 48)),
116        ("mrconfigid".to_string(), field(body, TD_MRCONFIGID, 48)),
117        ("mrowner".to_string(), field(body, TD_MROWNER, 48)),
118        (
119            "mrownerconfig".to_string(),
120            field(body, TD_MROWNERCONFIG, 48),
121        ),
122    ]);
123    for i in 0..4 {
124        measurements.insert(format!("rtmr{i}"), field(body, TD_RTMR0 + 48 * i, 48));
125    }
126    VerifiedEvidence {
127        tee: TeeKind::Tdx,
128        report_data: field(body, TD_REPORT_DATA, 64),
129        measurements,
130        debug: le_u64(body, TD_ATTRIBUTES) & TD_ATTRIBUTE_DEBUG != 0,
131        nitro: None,
132    }
133}
134
135/// Extracts measurements from an SGX report body.
136fn sgx_evidence(body: &[u8]) -> VerifiedEvidence {
137    VerifiedEvidence {
138        tee: TeeKind::Sgx,
139        report_data: field(body, SGX_REPORT_DATA, 64),
140        measurements: BTreeMap::from([
141            ("mrenclave".to_string(), field(body, SGX_MRENCLAVE, 32)),
142            ("mrsigner".to_string(), field(body, SGX_MRSIGNER, 32)),
143        ]),
144        debug: le_u64(body, SGX_ATTRIBUTES) & SGX_FLAG_DEBUG != 0,
145        nitro: None,
146    }
147}
148
149/// Verifies the embedded PCK chain up to the pinned Intel SGX Root CA and returns the DER of
150/// the PCK leaf certificate.
151///
152/// The chain may be PEM (as produced by the Intel quote library) or concatenated DER, in any
153/// order; the leaf is the one certificate that is not a CA.
154fn verify_pck_chain(data: &[u8], now: UnixTime, trust: &TrustStore) -> Result<Vec<u8>, String> {
155    let chain = parse_cert_chain(data)?;
156    let leaf_index = chain
157        .iter()
158        .position(|der| {
159            X509Certificate::from_der(der)
160                .map(|(_, cert)| !cert.is_ca())
161                .unwrap_or(false)
162        })
163        .ok_or("quote PCK certificate chain has no leaf certificate")?;
164    let leaf_der = &chain[leaf_index];
165    let intermediates: Vec<CertificateDer<'_>> = chain
166        .iter()
167        .enumerate()
168        .filter(|(i, _)| *i != leaf_index)
169        .map(|(_, der)| der.clone())
170        .collect();
171
172    let root_der = CertificateDer::from(trust.intel_sgx_root.as_slice());
173    let anchor = webpki::anchor_from_trusted_cert(&root_der)
174        .map_err(|e| format!("invalid Intel SGX root certificate: {e:?}"))?;
175    let leaf = webpki::EndEntityCert::try_from(leaf_der)
176        .map_err(|e| format!("invalid PCK certificate: {e:?}"))?;
177    leaf.verify_for_usage(
178        chain_algorithms(),
179        &[anchor],
180        &intermediates,
181        now,
182        AnyKeyUsage,
183        None,
184        None,
185    )
186    .map_err(|e| format!("PCK certificate chain is invalid: {e:?}"))?;
187    Ok(leaf_der.to_vec())
188}
189
190/// Splits PCK certification data (PEM or concatenated DER, optionally NUL-terminated) into
191/// DER certificates.
192fn parse_cert_chain(data: &[u8]) -> Result<Vec<CertificateDer<'static>>, String> {
193    let data = match data.iter().rposition(|b| *b != 0) {
194        Some(end) => &data[..=end],
195        None => return Err("quote PCK certificate chain is empty".into()),
196    };
197    if data.starts_with(b"-----BEGIN") {
198        return CertificateDer::pem_slice_iter(data)
199            .collect::<Result<Vec<_>, _>>()
200            .map_err(|e| format!("malformed PCK certificate chain: {e:?}"));
201    }
202    let mut chain = Vec::new();
203    let mut rest = data;
204    while !rest.is_empty() {
205        let (remaining, _) = X509Certificate::from_der(rest)
206            .map_err(|e| format!("malformed PCK certificate chain: {e}"))?;
207        let len = rest.len() - remaining.len();
208        chain.push(CertificateDer::from(rest[..len].to_vec()));
209        rest = remaining;
210    }
211    Ok(chain)
212}
213
214/// Borrowed views into a parsed DCAP quote.
215struct QuoteParts<'a> {
216    /// Header and body bytes covered by the quote signature.
217    signed: &'a [u8],
218    /// The TD or SGX report body.
219    body: &'a [u8],
220    quote_signature: &'a [u8],
221    /// Raw P-256 public key (`x || y`).
222    attestation_key: &'a [u8],
223    qe_report: &'a [u8],
224    qe_report_signature: &'a [u8],
225    qe_auth_data: &'a [u8],
226    pck_chain: &'a [u8],
227}
228
229/// Parsing of the quote layout.
230impl<'a> QuoteParts<'a> {
231    /// Splits `quote` into its parts, checking that it is an ECDSA-P256 quote from `tee`.
232    fn parse(quote: &'a [u8], tee: TeeKind) -> Result<Self, String> {
233        let mut r = Reader::new(quote);
234        let header = r.take(HEADER_LEN)?;
235        let version = u16::from_le_bytes([header[0], header[1]]);
236        let att_key_type = u16::from_le_bytes([header[2], header[3]]);
237        let tee_type = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
238
239        if att_key_type != ATT_KEY_TYPE_ECDSA_P256 {
240            return Err(format!(
241                "unsupported quote attestation key type {att_key_type}"
242            ));
243        }
244        let expected_tee_type = if tee == TeeKind::Tdx {
245            TEE_TYPE_TDX
246        } else {
247            TEE_TYPE_SGX
248        };
249        if tee_type != expected_tee_type {
250            return Err(format!(
251                "quote TEE type {tee_type:#x} does not match {tee} evidence"
252            ));
253        }
254
255        let body = match (version, tee) {
256            (3, TeeKind::Sgx) | (4, TeeKind::Sgx) => r.take(SGX_REPORT_BODY_LEN)?,
257            (4, TeeKind::Tdx) => r.take(TD10_REPORT_BODY_LEN)?,
258            (5, _) => {
259                let body_type = r.u16()?;
260                let body_len = r.u32()? as usize;
261                let expected_len = match (body_type, tee) {
262                    (BODY_TYPE_SGX, TeeKind::Sgx) => SGX_REPORT_BODY_LEN,
263                    (BODY_TYPE_TD10, TeeKind::Tdx) => TD10_REPORT_BODY_LEN,
264                    (BODY_TYPE_TD15, TeeKind::Tdx) => TD15_REPORT_BODY_LEN,
265                    _ => {
266                        return Err(format!(
267                            "unsupported quote v5 body type {body_type} for {tee}"
268                        ))
269                    }
270                };
271                if body_len != expected_len {
272                    return Err(format!(
273                        "quote v5 body is {body_len} bytes, expected {expected_len}"
274                    ));
275                }
276                r.take(body_len)?
277            }
278            _ => return Err(format!("unsupported {tee} quote version {version}")),
279        };
280        let signed = &quote[..r.pos];
281
282        let signature_data_len = r.u32()? as usize;
283        let mut s = Reader::new(r.take(signature_data_len)?);
284        let quote_signature = s.take(64)?;
285        let attestation_key = s.take(64)?;
286
287        // v3 stores the QE certification data inline; v4/v5 wrap it as certification data type 6.
288        let mut qe = if version == 3 {
289            s
290        } else {
291            let cert_type = s.u16()?;
292            if cert_type != CERT_TYPE_QE_REPORT {
293                return Err(format!(
294                    "unsupported quote certification data type {cert_type}"
295                ));
296            }
297            let len = s.u32()? as usize;
298            Reader::new(s.take(len)?)
299        };
300        let qe_report = qe.take(SGX_REPORT_BODY_LEN)?;
301        let qe_report_signature = qe.take(64)?;
302        let auth_len = qe.u16()? as usize;
303        let qe_auth_data = qe.take(auth_len)?;
304        let cert_type = qe.u16()?;
305        if cert_type != CERT_TYPE_PCK_CHAIN {
306            return Err(format!(
307                "quote does not embed a PCK certificate chain (certification data type {cert_type})"
308            ));
309        }
310        let pck_len = qe.u32()? as usize;
311        let pck_chain = qe.take(pck_len)?;
312
313        Ok(Self {
314            signed,
315            body,
316            quote_signature,
317            attestation_key,
318            qe_report,
319            qe_report_signature,
320            qe_auth_data,
321            pck_chain,
322        })
323    }
324}
325
326/// Bounds-checked little-endian cursor over a byte slice.
327struct Reader<'a> {
328    buf: &'a [u8],
329    pos: usize,
330}
331
332/// Sequential reads that fail instead of panicking on truncated input.
333impl<'a> Reader<'a> {
334    /// Starts reading at the beginning of `buf`.
335    fn new(buf: &'a [u8]) -> Self {
336        Self { buf, pos: 0 }
337    }
338
339    /// Returns the next `len` bytes.
340    fn take(&mut self, len: usize) -> Result<&'a [u8], String> {
341        let end = self
342            .pos
343            .checked_add(len)
344            .filter(|end| *end <= self.buf.len())
345            .ok_or("quote is truncated")?;
346        let bytes = &self.buf[self.pos..end];
347        self.pos = end;
348        Ok(bytes)
349    }
350
351    /// Reads a little-endian `u16`.
352    fn u16(&mut self) -> Result<u16, String> {
353        Ok(u16::from_le_bytes(
354            self.take(2)?.try_into().expect("2-byte slice"),
355        ))
356    }
357
358    /// Reads a little-endian `u32`.
359    fn u32(&mut self) -> Result<u32, String> {
360        Ok(u32::from_le_bytes(
361            self.take(4)?.try_into().expect("4-byte slice"),
362        ))
363    }
364}