Skip to main content

ttk_server/verifier/
nitro.rs

1//! AWS Nitro Enclaves evidence: an NSM attestation document (COSE_Sign1, ES384) whose signing
2//! certificate chains to the AWS Nitro Enclaves root.
3
4use super::{
5    chain_algorithms, verify_ecdsa, AnyKeyUsage, Policy, TeeKind, TrustStore, VerifiedEvidence,
6};
7use ciborium::Value;
8use rustls_pki_types::{CertificateDer, UnixTime};
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::time::Duration;
12use x509_parser::prelude::*;
13
14/// COSE algorithm identifier for ECDSA P-384 with SHA-384 (RFC 9053).
15const COSE_ALG_ES384: i128 = -35;
16
17/// Tolerated clock skew when checking that the document is not from the future.
18const MAX_CLOCK_SKEW: Duration = Duration::from_secs(5 * 60);
19
20/// Decoded payload of an AWS Nitro attestation document.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AttestationDocument {
23    pub module_id: String,
24    pub timestamp: u64,
25    pub digest: String,
26    pub pcrs: BTreeMap<usize, Vec<u8>>,
27    pub certificate: Vec<u8>,
28    pub cabundle: Vec<Vec<u8>>,
29    #[serde(default)]
30    pub public_key: Option<Vec<u8>>,
31    #[serde(default)]
32    pub user_data: Option<Vec<u8>>,
33    #[serde(default)]
34    pub nonce: Option<Vec<u8>>,
35}
36
37/// Structural checks on a decoded attestation document.
38impl AttestationDocument {
39    /// Checks the mandatory fields per the AWS Nitro Enclaves attestation document spec.
40    fn check_sanity(&self) -> Result<(), String> {
41        if self.module_id.is_empty() {
42            return Err("attestation module_id is empty".into());
43        }
44        if self.digest != "SHA384" {
45            return Err(format!("unsupported attestation digest {}", self.digest));
46        }
47        if self.pcrs.is_empty() || self.pcrs.len() > 32 {
48            return Err("attestation document has an invalid number of PCRs".into());
49        }
50        if self.pcrs.values().any(|v| ![32, 48, 64].contains(&v.len())) {
51            return Err("attestation document has a PCR of invalid length".into());
52        }
53        if self.certificate.is_empty() {
54            return Err("attestation signing certificate is empty".into());
55        }
56        Ok(())
57    }
58}
59
60/// Verifies the Nitro attestation document `doc_bytes` at time `now`.
61pub fn verify(
62    doc_bytes: &[u8],
63    now: UnixTime,
64    trust: &TrustStore,
65    policy: Policy,
66) -> Result<VerifiedEvidence, String> {
67    let cose = CoseSign1Parts::parse(doc_bytes)?;
68    let doc: AttestationDocument = ciborium::from_reader(cose.payload.as_slice())
69        .map_err(|e| format!("invalid attestation document payload: {e}"))?;
70    doc.check_sanity()?;
71
72    if doc.timestamp > (now.as_secs() + MAX_CLOCK_SKEW.as_secs()) * 1000 {
73        return Err("attestation document timestamp is in the future".into());
74    }
75
76    let root = trusted_root(&doc, trust, policy)?;
77    verify_chain(&doc, root)?;
78    cose.verify_signature(&doc.certificate)?;
79
80    // Debug-mode enclaves report all-zero PCRs.
81    let debug = doc.pcrs.get(&0).is_some_and(|p| p.iter().all(|b| *b == 0));
82    let measurements = doc
83        .pcrs
84        .iter()
85        .map(|(i, v)| (format!("pcr{i}"), v.clone()))
86        .collect();
87
88    Ok(VerifiedEvidence {
89        tee: TeeKind::AwsNitro,
90        report_data: doc.user_data.clone().unwrap_or_default(),
91        measurements,
92        debug,
93        nitro: Some(doc),
94    })
95}
96
97/// `module_id` the server's mock provider puts in its documents.
98const MOCK_MODULE_ID: &str = "aws-nitro-enclaves-mock";
99
100/// Returns the pinned root that `doc`'s `cabundle` must start with: the AWS Nitro root, or the
101/// mock root when mock attestation is allowed.
102fn trusted_root<'a>(
103    doc: &AttestationDocument,
104    trust: &'a TrustStore,
105    policy: Policy,
106) -> Result<&'a [u8], String> {
107    let root = doc.cabundle.first();
108    let is_mock = doc.module_id == MOCK_MODULE_ID || root == Some(&trust.mock_nitro_root);
109    if is_mock && !policy.allow_mock {
110        return Err(
111            "the server presented MOCK attestation evidence, which is only accepted for local \
112             development: set TTK_ALLOW_MOCK_ATTESTATION=1 for the client binary, or use \
113             EnclaveCertVerifier::allow_mock()"
114                .into(),
115        );
116    }
117    let root = root.ok_or(if is_mock {
118        "the mock attestation document is unsigned (empty cabundle): rebuild and restart the \
119         server to get signed mock evidence"
120    } else {
121        "attestation cabundle is empty"
122    })?;
123
124    if *root == trust.aws_nitro_root {
125        return Ok(&trust.aws_nitro_root);
126    }
127    if *root == trust.mock_nitro_root {
128        log::warn!("Accepting MOCK attestation evidence signed by the TTKServer mock root CA");
129        return Ok(&trust.mock_nitro_root);
130    }
131    Err("attestation cabundle is not rooted at the AWS Nitro root CA".into())
132}
133
134/// Verifies the document's signing certificate up to the pinned `root`.
135///
136/// The chain is validated at the document's timestamp: Nitro signing certificates are
137/// short-lived, while the server reuses one document for the lifetime of its TLS certificate.
138fn verify_chain(doc: &AttestationDocument, root: &[u8]) -> Result<(), String> {
139    let root_der = CertificateDer::from(root);
140    let anchor = webpki::anchor_from_trusted_cert(&root_der)
141        .map_err(|e| format!("invalid attestation root certificate: {e:?}"))?;
142    let intermediates: Vec<CertificateDer<'_>> = doc.cabundle[1..]
143        .iter()
144        .map(|c| CertificateDer::from(c.as_slice()))
145        .collect();
146    let leaf_der = CertificateDer::from(doc.certificate.as_slice());
147    let leaf = webpki::EndEntityCert::try_from(&leaf_der)
148        .map_err(|e| format!("invalid attestation signing certificate: {e:?}"))?;
149
150    leaf.verify_for_usage(
151        chain_algorithms(),
152        &[anchor],
153        &intermediates,
154        UnixTime::since_unix_epoch(Duration::from_millis(doc.timestamp)),
155        AnyKeyUsage,
156        None,
157        None,
158    )
159    .map_err(|e| format!("attestation certificate chain is invalid: {e:?}"))?;
160    Ok(())
161}
162
163/// The parts of a COSE_Sign1 structure (RFC 9052) needed for verification.
164struct CoseSign1Parts {
165    protected: Vec<u8>,
166    payload: Vec<u8>,
167    signature: Vec<u8>,
168}
169
170/// Parsing and signature verification of COSE_Sign1 documents.
171impl CoseSign1Parts {
172    /// Parses a tagged (tag 18) or untagged COSE_Sign1 array.
173    fn parse(bytes: &[u8]) -> Result<Self, String> {
174        let value: Value =
175            ciborium::from_reader(bytes).map_err(|e| format!("invalid COSE_Sign1 CBOR: {e}"))?;
176        let items = match value {
177            Value::Tag(18, inner) => match *inner {
178                Value::Array(items) => items,
179                _ => return Err("COSE_Sign1 tag does not contain an array".into()),
180            },
181            Value::Array(items) => items,
182            _ => return Err("attestation document is not a COSE_Sign1 structure".into()),
183        };
184        let [protected, _unprotected, payload, signature] = <[Value; 4]>::try_from(items)
185            .map_err(|_| "COSE_Sign1 structure must have 4 elements".to_string())?;
186        let bytes_of = |v: Value, what: &str| match v {
187            Value::Bytes(b) => Ok(b),
188            _ => Err(format!("COSE_Sign1 {what} is not a byte string")),
189        };
190        Ok(Self {
191            protected: bytes_of(protected, "protected header")?,
192            payload: bytes_of(payload, "payload")?,
193            signature: bytes_of(signature, "signature")?,
194        })
195    }
196
197    /// Verifies the ES384 signature with the public key of `signing_cert_der`.
198    fn verify_signature(&self, signing_cert_der: &[u8]) -> Result<(), String> {
199        let header: Value = ciborium::from_reader(self.protected.as_slice())
200            .map_err(|e| format!("invalid COSE protected header: {e}"))?;
201        let alg = header.as_map().and_then(|m| {
202            m.iter().find_map(|(k, v)| match (k, v) {
203                (Value::Integer(k), Value::Integer(v)) if i128::from(*k) == 1 => {
204                    Some(i128::from(*v))
205                }
206                _ => None,
207            })
208        });
209        if alg != Some(COSE_ALG_ES384) {
210            return Err("attestation document is not signed with ES384".into());
211        }
212
213        // Sig_structure = ["Signature1", protected, external_aad, payload] (RFC 9052 ยง4.4)
214        let sig_structure = Value::Array(vec![
215            Value::Text("Signature1".into()),
216            Value::Bytes(self.protected.clone()),
217            Value::Bytes(Vec::new()),
218            Value::Bytes(self.payload.clone()),
219        ]);
220        let mut to_verify = Vec::new();
221        ciborium::into_writer(&sig_structure, &mut to_verify)
222            .map_err(|e| format!("failed to encode COSE Sig_structure: {e}"))?;
223
224        let (_, signing_cert) = X509Certificate::from_der(signing_cert_der)
225            .map_err(|e| format!("malformed attestation signing certificate: {e}"))?;
226        verify_ecdsa(
227            &ring::signature::ECDSA_P384_SHA384_FIXED,
228            &signing_cert.public_key().subject_public_key.data,
229            &to_verify,
230            &self.signature,
231        )
232        .map_err(|_| "attestation document signature is invalid".to_string())
233    }
234}