attestation_doc_validation/
lib.rs1pub mod attestation_doc;
2pub mod cert;
3pub mod error;
4mod nsm;
5mod time;
6
7pub use attestation_doc::{validate_expected_nonce, validate_expected_pcrs, PCRProvider};
8use aws_nitro_enclaves_nsm_api::api::AttestationDoc;
9use error::{AttestResult as Result, AttestationError};
10
11use nsm::CryptoClient;
12use serde_bytes::ByteBuf;
13use x509_parser::certificate::X509Certificate;
14
15fn true_or_invalid<E: Into<error::AttestError>>(check: bool, err: E) -> std::result::Result<(), E> {
17 if check {
18 Ok(())
19 } else {
20 Err(err)
21 }
22}
23
24fn create_intermediate_cert_stack(ca_bundle: &[ByteBuf]) -> Vec<&[u8]> {
26 ca_bundle.iter().map(|cert| cert.as_slice()).collect()
27}
28
29pub fn parse_cert(given_cert: &[u8]) -> Result<X509Certificate<'_>> {
35 Ok(cert::parse_der_cert(given_cert)?)
36}
37
38pub fn validate_attestation_doc_in_cert(
53 given_cert: &X509Certificate<'_>,
54) -> Result<AttestationDoc> {
55 let cose_signature = cert::extract_signed_cose_sign_1_from_certificate(given_cert)?;
57
58 let (cose_sign_1_decoded, decoded_attestation_doc) =
60 attestation_doc::decode_attestation_document(&cose_signature)?;
61
62 let intermediate_certs = create_intermediate_cert_stack(&decoded_attestation_doc.cabundle);
64 cert::validate_cert_trust_chain(&decoded_attestation_doc.certificate, &intermediate_certs)?;
65
66 let cert = cert::parse_der_cert(&decoded_attestation_doc.certificate)?;
68 let pub_key: nsm::PublicKey = cert.public_key().try_into()?;
69 attestation_doc::validate_cose_signature::<CryptoClient>(&pub_key, &cose_sign_1_decoded)?;
70
71 let cage_cert_public_key = cert::export_public_key_to_der(given_cert);
73 attestation_doc::validate_expected_challenge(&decoded_attestation_doc, cage_cert_public_key)?;
74
75 Ok(decoded_attestation_doc)
76}
77
78pub fn validate_attestation_doc_against_cert(
100 given_cert: &X509Certificate<'_>,
101 attestation_doc_cose_sign_1_bytes: &[u8],
102) -> Result<AttestationDoc> {
103 let (cose_sign_1_decoded, decoded_attestation_doc) =
105 attestation_doc::decode_attestation_document(attestation_doc_cose_sign_1_bytes)?;
106 attestation_doc::validate_attestation_document_structure(&decoded_attestation_doc)?;
107 let attestation_doc_signing_cert = cert::parse_der_cert(&decoded_attestation_doc.certificate)?;
108
109 let intermediate_certs = create_intermediate_cert_stack(&decoded_attestation_doc.cabundle);
111 cert::validate_cert_trust_chain(&decoded_attestation_doc.certificate, &intermediate_certs)?;
112
113 let pub_key: nsm::PublicKey = attestation_doc_signing_cert.public_key().try_into()?;
115 attestation_doc::validate_cose_signature::<CryptoClient>(&pub_key, &cose_sign_1_decoded)?;
116
117 let user_data = decoded_attestation_doc
119 .clone()
120 .user_data
121 .ok_or_else(|| AttestationError::MissingUserData)?;
122
123 true_or_invalid(
125 user_data == given_cert.public_key().raw,
126 AttestationError::InvalidPublicKey,
127 )?;
128
129 Ok(decoded_attestation_doc)
130}
131
132pub fn validate_attestation_doc(
145 attestation_doc_cose_sign_1_bytes: &[u8],
146) -> error::AttestResult<()> {
147 let (cose_sign_1_decoded, decoded_attestation_doc) =
149 attestation_doc::decode_attestation_document(attestation_doc_cose_sign_1_bytes)?;
150 attestation_doc::validate_attestation_document_structure(&decoded_attestation_doc)?;
151 let attestation_doc_signing_cert = cert::parse_der_cert(&decoded_attestation_doc.certificate)?;
152
153 let intermediate_certs = create_intermediate_cert_stack(&decoded_attestation_doc.cabundle);
155 cert::validate_cert_trust_chain(&decoded_attestation_doc.certificate, &intermediate_certs)?;
156
157 let pub_key: nsm::PublicKey = attestation_doc_signing_cert.public_key().try_into()?;
159 attestation_doc::validate_cose_signature::<CryptoClient>(&pub_key, &cose_sign_1_decoded)?;
160
161 Ok(())
162}
163
164pub fn validate_and_parse_attestation_doc(
178 attestation_doc_cose_sign_1_bytes: &[u8],
179) -> error::AttestResult<AttestationDoc> {
180 let (cose_sign_1_decoded, decoded_attestation_doc) =
182 attestation_doc::decode_attestation_document(attestation_doc_cose_sign_1_bytes)?;
183 attestation_doc::validate_attestation_document_structure(&decoded_attestation_doc)?;
184 let attestation_doc_signing_cert = cert::parse_der_cert(&decoded_attestation_doc.certificate)?;
185
186 let intermediate_certs = create_intermediate_cert_stack(&decoded_attestation_doc.cabundle);
188 cert::validate_cert_trust_chain(&decoded_attestation_doc.certificate, &intermediate_certs)?;
189
190 let pub_key: nsm::PublicKey = attestation_doc_signing_cert.public_key().try_into()?;
192 attestation_doc::validate_cose_signature::<CryptoClient>(&pub_key, &cose_sign_1_decoded)?;
193
194 Ok(decoded_attestation_doc)
195}
196
197#[cfg(test)]
198mod test {
199 use super::*;
200 use cert::get_subject_alt_names_from_cert;
201 use x509_parser::extensions::GeneralName;
202
203 use rcgen::generate_simple_self_signed;
204
205 fn embed_attestation_doc_in_cert(hostname: &str, cose_bytes: &[u8]) -> rcgen::Certificate {
206 let subject_alt_names = vec![
207 hostname.to_string(),
208 format!("{}.{hostname}", hex::encode(cose_bytes)),
209 ];
210
211 generate_simple_self_signed(subject_alt_names).unwrap()
212 }
213
214 fn rcgen_cert_to_der(cert: rcgen::Certificate) -> Vec<u8> {
215 cert.serialize_der().unwrap()
216 }
217
218 #[test]
219 fn test_der_cert_parsing() {
220 let sample_cose_sign_1_bytes = std::fs::read(std::path::Path::new(
221 "../test-data/beta/valid-attestation-doc-bytes",
222 ))
223 .unwrap();
224 let hostname = "debug.cage.com".to_string();
225 let cert = embed_attestation_doc_in_cert(&hostname, &sample_cose_sign_1_bytes);
226 let der_cert = rcgen_cert_to_der(cert);
227 let parsed_cert = parse_cert(der_cert.as_ref()).unwrap();
228 let subject_alt_names = get_subject_alt_names_from_cert(&parsed_cert).unwrap();
229 let matched_hostname = subject_alt_names.into_iter().any(|entries| {
230 let GeneralName::DNSName(san) = entries else {
231 return false;
232 };
233 san == hostname
234 });
235 assert!(matched_hostname);
236 }
237
238 #[test]
239 fn validate_debug_mode_attestation_doc() {
240 let sample_cose_sign_1_bytes = std::fs::read(std::path::Path::new(
242 "../test-data/beta/debug-mode-attestation-doc-bytes",
243 ))
244 .unwrap();
245 let attestable_cert =
246 embed_attestation_doc_in_cert("test-cage.localhost:6789", &sample_cose_sign_1_bytes);
247 let cert = rcgen_cert_to_der(attestable_cert);
248 let cert = parse_cert(&cert).unwrap();
249 let err = validate_attestation_doc_in_cert(&cert).unwrap_err();
250 assert!(matches!(
251 err,
252 error::AttestError::CertError(error::CertError::InvalidTrustChain(_))
253 ));
254 }
255
256 use serde::Deserialize;
268
269 #[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
270 #[serde(rename_all = "camelCase")]
271 pub struct TestPCRs {
272 pub pcr_0: Option<String>,
273 pub pcr_1: Option<String>,
274 pub pcr_2: Option<String>,
275 pub pcr_8: Option<String>,
276 }
277
278 impl PCRProvider for TestPCRs {
279 fn pcr_0(&self) -> Option<&str> {
280 self.pcr_0.as_deref()
281 }
282 fn pcr_1(&self) -> Option<&str> {
283 self.pcr_1.as_deref()
284 }
285 fn pcr_2(&self) -> Option<&str> {
286 self.pcr_2.as_deref()
287 }
288 fn pcr_8(&self) -> Option<&str> {
289 self.pcr_8.as_deref()
290 }
291 }
292
293 #[derive(Deserialize)]
294 #[serde(rename_all = "camelCase")]
295 struct TestSpec {
296 file: String,
297 pcrs: TestPCRs,
298 is_attestation_doc_valid: bool,
299 should_pcrs_match: bool,
300 }
301
302 const TEST_BASE_PATH: &'static str = "..";
303
304 macro_rules! evaluate_test_from_spec {
305 ($test_spec:literal) => {
306 let test_def_filepath = format!("{}/test-specs/beta/{}", TEST_BASE_PATH, $test_spec);
308 let test_definition = std::fs::read(std::path::Path::new(&test_def_filepath)).unwrap();
309 let test_def_str = std::str::from_utf8(&test_definition).unwrap();
310 let test_spec: TestSpec = serde_json::from_str(test_def_str).unwrap();
311
312 let test_input_file = format!("{}/{}", TEST_BASE_PATH, test_spec.file);
314 let input_bytes = std::fs::read(std::path::Path::new(&test_input_file)).unwrap();
315
316 let is_pem_cert = test_spec.file.ends_with(".pem");
318 let cert_content = if is_pem_cert {
319 let pem_cert = pem::parse(input_bytes).unwrap();
320 pem_cert.contents.clone()
321 } else {
322 input_bytes
323 };
324 let cert = parse_cert(&cert_content).unwrap();
325 let maybe_attestation_doc = validate_attestation_doc_in_cert(&cert);
326 if test_spec.is_attestation_doc_valid {
327 assert!(maybe_attestation_doc.is_ok());
328 let pcrs_match =
329 validate_expected_pcrs(&maybe_attestation_doc.unwrap(), &test_spec.pcrs);
330 assert_eq!(pcrs_match.is_ok(), test_spec.should_pcrs_match);
331
332 if !test_spec.should_pcrs_match {
333 let returned_error = pcrs_match.unwrap_err();
334 assert!(matches!(
335 returned_error,
336 error::AttestationError::UnexpectedPCRs(_, _)
337 ));
338 }
339 } else {
340 assert!(maybe_attestation_doc.is_err());
341 }
342 };
343 }
344
345 #[test]
346 fn validate_valid_attestation_doc_in_cert_time_sensitive_beta() {
347 evaluate_test_from_spec!("valid_attestation_doc_in_cert_time_sensitive.json");
348 }
349
350 #[test]
351 fn validate_valid_attestation_doc_in_non_debug_mode_with_correct_pcrs_time_sensitive_beta() {
352 evaluate_test_from_spec!(
353 "valid_attestation_doc_in_non_debug_mode_with_correct_pcrs_time_sensitive.json"
354 );
355 }
356
357 #[test]
358 fn validate_valid_attestation_doc_in_cert_incorrect_pcrs_time_sensitive_beta() {
359 evaluate_test_from_spec!(
360 "valid_attestation_doc_in_cert_incorrect_pcrs_time_sensitive.json"
361 );
362 }
363
364 #[test]
365 fn validate_valid_attestation_doc_in_cert_der_encoding_time_sensitive_beta() {
366 evaluate_test_from_spec!("valid_attestation_doc_in_cert_der_encoding_time_sensitive.json");
367 }
368
369 #[test]
370 fn valid_attestation_check_pcr8_only_time_sensitive_beta() {
371 evaluate_test_from_spec!("valid_attestation_doc_check_pcr8_only_time_sensitive.json");
372 }
373
374 #[test]
375 fn validate_valid_attestation_doc_in_cert_time_sensitive_ga() {
376 use base64::{engine::general_purpose, Engine as _};
377 let attestation_doc = std::fs::read(std::path::Path::new(
378 &"../test-data/valid-attestation-doc-base64",
379 ))
380 .unwrap();
381 let attestation_doc_str = std::str::from_utf8(&attestation_doc).unwrap();
382 let attestation_doc_bytes = general_purpose::STANDARD
383 .decode(&attestation_doc_str)
384 .unwrap();
385
386 let input_bytes =
387 std::fs::read(std::path::Path::new("../test-data/valid-certificate.der")).unwrap();
388
389 let cert = parse_cert(&input_bytes).unwrap();
390 let maybe_attestation_doc =
391 validate_attestation_doc_against_cert(&cert, &attestation_doc_bytes);
392 assert!(maybe_attestation_doc.is_ok());
393 }
394}