attestation_doc_validation/
lib.rs

1pub 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
15// Helper function to fail early on any variant of error::AttestError
16fn 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
24// Helper function to convert the embedded `ca_bundle` into a `webpki` compatible cert stack
25fn create_intermediate_cert_stack(ca_bundle: &[ByteBuf]) -> Vec<&[u8]> {
26    ca_bundle.iter().map(|cert| cert.as_slice()).collect()
27}
28
29/// Attempts to DER decode a slice of bytes to an X509 Certificate
30///
31/// # Errors
32///
33/// If DER decoding of the certificate fail
34pub fn parse_cert(given_cert: &[u8]) -> Result<X509Certificate<'_>> {
35    Ok(cert::parse_der_cert(given_cert)?)
36}
37
38/// Attests a connection to a Cage by:
39/// - Validating the cert structure
40/// - Extracting the attestation doc from the Subject Alt Names
41/// - Decoding and validating the attestation doc
42/// - Validating the signature on the attestation doc
43/// - Validating that the PCRs of the attestation doc are as expected
44///
45/// # Errors
46///
47/// Will return an error if:
48/// - The cose1 encoded attestation doc fails to parse, or its signature is invalid
49/// - The attestation document is not signed by the nitro cert chain
50/// - The public key from the certificate is not present in the attestation document's challenge
51/// - Any of the certificates are malformed
52pub fn validate_attestation_doc_in_cert(
53    given_cert: &X509Certificate<'_>,
54) -> Result<AttestationDoc> {
55    // Extract raw attestation doc from subject alt names
56    let cose_signature = cert::extract_signed_cose_sign_1_from_certificate(given_cert)?;
57
58    // Parse attestation doc from cose signature and validate structure
59    let (cose_sign_1_decoded, decoded_attestation_doc) =
60        attestation_doc::decode_attestation_document(&cose_signature)?;
61
62    // Validate that the attestation doc's signature can be tied back to the AWS Nitro CA
63    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    // Validate Cose signature over attestation doc
67    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    // Validate that the cert public key is embedded in the attestation doc
72    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
78/// Validates an attestation doc by:
79/// - Validating the cert structure
80/// - Decoding and validating the attestation doc
81/// - Validating the signature on the attestation doc
82/// - Validating the public key embedded in the attestation doc is the same public key in the cert
83/// - Validating the expiry embedded in the attestation doc is in the future
84///
85/// The `given_cert` represents the cert of the connection of which the `attestation_document` was fetched
86/// from the cage on.
87///
88/// # Errors
89///
90/// Will return an error if:
91/// - The cose1 encoded attestation doc fails to parse, or its signature is invalid
92/// - The attestation document is not signed by the nitro cert chain
93/// - The public key from the certificate is not present in the attestation document's challenge
94/// - Any of the certificates are malformed
95/// - The attestation document has no `user_data`
96/// - The binary encoded challenge cannot be decoded
97/// - The base64 encoded public key within the challenge cannot be decoded
98/// - The decoded public key raw bytes are not equal to those of the given cert's public key
99pub fn validate_attestation_doc_against_cert(
100    given_cert: &X509Certificate<'_>,
101    attestation_doc_cose_sign_1_bytes: &[u8],
102) -> Result<AttestationDoc> {
103    // Parse attestation doc from cose signature and validate structure
104    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    // Validate that the attestation doc's signature can be tied back to the AWS Nitro CA
110    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    // Validate Cose signature over attestation doc
114    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    // Validate the public key of the cert & the attestation doc match
118    let user_data = decoded_attestation_doc
119        .clone()
120        .user_data
121        .ok_or_else(|| AttestationError::MissingUserData)?;
122
123    // Validate that the public key of the given cert and that of the challenge are the same
124    true_or_invalid(
125        user_data == given_cert.public_key().raw,
126        AttestationError::InvalidPublicKey,
127    )?;
128
129    Ok(decoded_attestation_doc)
130}
131
132/// Validates an attestation doc by:
133/// - Validating the cert structure
134/// - Decoding and validating the attestation doc
135/// - Validating the signature on the attestation doc
136///
137/// # Errors
138///
139/// Will return an error if:
140/// - The cose1 encoded attestation doc fails to parse, or its signature is invalid
141/// - The attestation document is not signed by the nitro cert chain
142/// - Any of the certificates are malformed
143///
144pub fn validate_attestation_doc(
145    attestation_doc_cose_sign_1_bytes: &[u8],
146) -> error::AttestResult<()> {
147    // Parse attestation doc from cose signature and validate structure
148    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    // Validate that the attestation doc's signature can be tied back to the AWS Nitro CA
154    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    // Validate Cose signature over attestation doc
158    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
164/// Validates an attestation doc by:
165/// - Validating the cert structure
166/// - Decoding and validating the attestation doc
167/// - Validating the signature on the attestation doc
168/// Ultimately returning the parsed attestation doc.
169///
170/// # Errors
171///
172/// Will return an error if:
173/// - The cose1 encoded attestation doc fails to parse, or its signature is invalid
174/// - The attestation document is not signed by the nitro cert chain
175/// - Any of the certificates are malformed
176///
177pub fn validate_and_parse_attestation_doc(
178    attestation_doc_cose_sign_1_bytes: &[u8],
179) -> error::AttestResult<AttestationDoc> {
180    // Parse attestation doc from cose signature and validate structure
181    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    // Validate that the attestation doc's signature can be tied back to the AWS Nitro CA
187    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    // Validate Cose signature over attestation doc
191    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        // debug mode attestation docs fail due to an untrusted cert
241        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    /**
257     *
258     * The following tests act as integration tests, but require the #[cfg(test)] flag to be set in the cert module, so must be written as unit tests.
259     *
260     * Live Enclave certs are required to have the public key match with the AD challenge (which in practice prevents MITM)
261     * However, this introduces issues when testing. When the certs are more than 3 hours old, they will expire and fail
262     * our validity checks. To get around this the tests corresponding to live certs are suffixed with time_sensitive_beta, and
263     * only run in CI when the time has been spoofed to match their validity window.
264     *
265     * The certs being used were generated on January 18th 2023 at approximately 15:15. (epoch: 1674054914)
266     */
267    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            // Resolve test spec
307            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            // Read in static input file
313            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            // Perform test
317            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}