1use 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
14const COSE_ALG_ES384: i128 = -35;
16
17const MAX_CLOCK_SKEW: Duration = Duration::from_secs(5 * 60);
19
20#[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
37impl AttestationDocument {
39 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
60pub 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 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
97const MOCK_MODULE_ID: &str = "aws-nitro-enclaves-mock";
99
100fn 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
134fn 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
163struct CoseSign1Parts {
165 protected: Vec<u8>,
166 payload: Vec<u8>,
167 signature: Vec<u8>,
168}
169
170impl CoseSign1Parts {
172 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 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 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}