sui_types/
nitro_attestation.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use serde::de::{Error, MapAccess, Visitor};
5use serde::{Deserialize, Deserializer};
6use std::collections::BTreeMap;
7use std::fmt;
8use x509_parser::public_key::PublicKey;
9use x509_parser::time::ASN1Time;
10use x509_parser::x509::SubjectPublicKeyInfo;
11
12use crate::error::{SuiError, SuiErrorKind, SuiResult};
13
14use ciborium::value::{Integer, Value};
15use once_cell::sync::Lazy;
16use p384::ecdsa::signature::Verifier;
17use p384::ecdsa::{Signature, VerifyingKey};
18use x509_parser::{certificate::X509Certificate, prelude::FromDer};
19
20#[cfg(test)]
21#[path = "unit_tests/nitro_attestation_tests.rs"]
22mod nitro_attestation_tests;
23
24/// Maximum length of the certificate chain. This is to limit the absolute upper bound on execution.
25const MAX_CERT_CHAIN_LENGTH: usize = 10;
26/// Max user data length from aws nitro spec.
27const MAX_USER_DATA_LENGTH: usize = 512;
28/// Max pk length from aws nitro spec.
29const MAX_PK_LENGTH: usize = 1024;
30/// Max pcrs length from aws nitro spec.
31const MAX_PCRS_LENGTH: usize = 32;
32/// Max certificate length from aws nitro spec.
33const MAX_CERT_LENGTH: usize = 1024;
34
35/// Root certificate for AWS Nitro Attestation.
36static ROOT_CERTIFICATE: Lazy<Vec<u8>> = Lazy::new(|| {
37    let pem_bytes = include_bytes!("./nitro_root_certificate.pem");
38    let mut pem_cursor = std::io::Cursor::new(pem_bytes);
39    let cert = rustls_pemfile::certs(&mut pem_cursor)
40        .next()
41        .expect("should have root cert")
42        .expect("root cert should be valid");
43    cert.to_vec()
44});
45
46/// Error type for Nitro attestation verification.
47#[derive(Debug, PartialEq, Eq)]
48pub enum NitroAttestationVerifyError {
49    /// Invalid COSE_Sign1: {0}
50    InvalidCoseSign1(String),
51    /// Invalid signature
52    InvalidSignature,
53    /// Invalid public key
54    InvalidPublicKey,
55    /// Siganture failed to verify
56    SignatureFailedToVerify,
57    /// Invalid attestation document
58    InvalidAttestationDoc(String),
59    /// Invalid user data.
60    InvalidUserData,
61    /// Invalid certificate: {0}
62    InvalidCertificate(String),
63    /// Invalid PCRs
64    InvalidPcrs,
65}
66
67impl fmt::Display for NitroAttestationVerifyError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            NitroAttestationVerifyError::InvalidCoseSign1(msg) => {
71                write!(f, "InvalidCoseSign1: {}", msg)
72            }
73            NitroAttestationVerifyError::InvalidSignature => write!(f, "InvalidSignature"),
74            NitroAttestationVerifyError::InvalidPublicKey => write!(f, "InvalidPublicKey"),
75            NitroAttestationVerifyError::SignatureFailedToVerify => {
76                write!(f, "SignatureFailedToVerify")
77            }
78            NitroAttestationVerifyError::InvalidAttestationDoc(msg) => {
79                write!(f, "InvalidAttestationDoc: {}", msg)
80            }
81            NitroAttestationVerifyError::InvalidCertificate(msg) => {
82                write!(f, "InvalidCertificate: {}", msg)
83            }
84            NitroAttestationVerifyError::InvalidPcrs => write!(f, "InvalidPcrs"),
85            NitroAttestationVerifyError::InvalidUserData => write!(f, "InvalidUserData"),
86        }
87    }
88}
89
90impl From<NitroAttestationVerifyError> for SuiError {
91    fn from(err: NitroAttestationVerifyError) -> Self {
92        SuiErrorKind::NitroAttestationFailedToVerify(err.to_string()).into()
93    }
94}
95
96/// Given an attestation in bytes, parse it into signature, signed message and a parsed payload.
97pub fn parse_nitro_attestation(
98    attestation_bytes: &[u8],
99    is_upgraded_parsing: bool,
100    include_all_nonzero_pcrs: bool,
101) -> SuiResult<(Vec<u8>, Vec<u8>, AttestationDocument)> {
102    let cose_sign1 = CoseSign1::parse_and_validate(attestation_bytes)?;
103    let doc = AttestationDocument::parse_payload(
104        &cose_sign1.payload,
105        is_upgraded_parsing,
106        include_all_nonzero_pcrs,
107    )?;
108    let msg = cose_sign1.to_signed_message()?;
109    let signature = cose_sign1.signature;
110    Ok((signature, msg, doc))
111}
112
113/// Given the signature bytes, signed message and parsed payload, verify everything according to
114/// <https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html> and
115/// <https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md>.
116pub fn verify_nitro_attestation(
117    signature: &[u8],
118    signed_message: &[u8],
119    payload: &AttestationDocument,
120    timestamp: u64,
121) -> SuiResult<()> {
122    // Extract public key from cert and signature as P384.
123    let signature = Signature::from_slice(signature)
124        .map_err(|_| NitroAttestationVerifyError::InvalidSignature)?;
125    let cert = X509Certificate::from_der(payload.certificate.as_slice())
126        .map_err(|e| NitroAttestationVerifyError::InvalidCertificate(e.to_string()))?;
127    let pk_bytes = SubjectPublicKeyInfo::parsed(cert.1.public_key())
128        .map_err(|err| NitroAttestationVerifyError::InvalidCertificate(err.to_string()))?;
129
130    // Verify the signature against the public key and the message.
131    match pk_bytes {
132        PublicKey::EC(ec) => {
133            let verifying_key = VerifyingKey::from_sec1_bytes(ec.data())
134                .map_err(|_| NitroAttestationVerifyError::InvalidPublicKey)?;
135            verifying_key
136                .verify(signed_message, &signature)
137                .map_err(|_| NitroAttestationVerifyError::SignatureFailedToVerify)?;
138        }
139        _ => {
140            return Err(NitroAttestationVerifyError::InvalidPublicKey.into());
141        }
142    }
143
144    payload.verify_cert(timestamp)?;
145    Ok(())
146}
147
148///  Implementation of the COSE_Sign1 structure as defined in [RFC8152](https://tools.ietf.org/html/rfc8152).
149///  protected_header: See Section 3 (Note: AWS Nitro does not have unprotected header.)
150///  payload: See Section 4.2.
151///  signature: See Section 4.2.
152///  Class and trait impl adapted from <https://github.com/awslabs/aws-nitro-enclaves-cose/blob/main/src/sign.rs>
153#[derive(Debug, Clone)]
154pub struct CoseSign1 {
155    /// protected: empty_or_serialized_map,
156    protected: Vec<u8>,
157    /// unprotected: HeaderMap
158    #[allow(dead_code)]
159    unprotected: HeaderMap,
160    /// payload: bstr
161    /// The spec allows payload to be nil and transported separately, but it's not useful at the
162    /// moment, so this is just a Bytes for simplicity.
163    payload: Vec<u8>,
164    /// signature: bstr
165    signature: Vec<u8>,
166}
167
168/// Empty map wrapper for COSE headers.
169#[derive(Clone, Debug, Default)]
170pub struct HeaderMap;
171
172impl<'de> Deserialize<'de> for HeaderMap {
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: Deserializer<'de>,
176    {
177        struct MapVisitor;
178
179        impl<'de> Visitor<'de> for MapVisitor {
180            type Value = HeaderMap;
181
182            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183                formatter.write_str("a map")
184            }
185
186            fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
187            where
188                A: MapAccess<'de>,
189            {
190                let mut seen_keys = Vec::new();
191
192                // Check for duplicate keys while consuming entries
193                while let Some((key, _value)) = access.next_entry::<Value, Value>()? {
194                    if seen_keys.contains(&key) {
195                        return Err(Error::custom("duplicate key found in CBOR map"));
196                    }
197                    seen_keys.push(key);
198                }
199                Ok(HeaderMap)
200            }
201        }
202
203        deserializer.deserialize_map(MapVisitor)
204    }
205}
206
207impl<'de> Deserialize<'de> for CoseSign1 {
208    fn deserialize<D>(deserializer: D) -> Result<CoseSign1, D::Error>
209    where
210        D: Deserializer<'de>,
211    {
212        use serde::de::{Error, SeqAccess, Visitor};
213        use std::fmt;
214
215        struct CoseSign1Visitor;
216
217        impl<'de> Visitor<'de> for CoseSign1Visitor {
218            type Value = CoseSign1;
219
220            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221                f.write_str("a possibly tagged CoseSign1 structure")
222            }
223
224            fn visit_seq<A>(self, mut seq: A) -> Result<CoseSign1, A::Error>
225            where
226                A: SeqAccess<'de>,
227            {
228                fn extract_bytes(value: Value) -> Option<Vec<u8>> {
229                    match value {
230                        Value::Bytes(bytes) => Some(bytes),
231                        _ => None,
232                    }
233                }
234
235                // Get protected header bytes
236                let protected = match seq.next_element::<Value>()? {
237                    Some(v) => extract_bytes(v)
238                        .ok_or_else(|| A::Error::custom("protected header must be bytes"))?,
239                    None => return Err(A::Error::missing_field("protected")),
240                };
241
242                let unprotected = match seq.next_element()? {
243                    Some(v) => v,
244                    None => return Err(A::Error::missing_field("unprotected")),
245                };
246                // Get payload bytes
247                let payload = match seq.next_element::<Value>()? {
248                    Some(v) => {
249                        extract_bytes(v).ok_or_else(|| A::Error::custom("payload must be bytes"))?
250                    }
251                    None => return Err(A::Error::missing_field("payload")),
252                };
253
254                // Get signature bytes
255                let signature = match seq.next_element::<Value>()? {
256                    Some(v) => extract_bytes(v)
257                        .ok_or_else(|| A::Error::custom("signature must be bytes"))?,
258                    None => return Err(A::Error::missing_field("signature")),
259                };
260
261                Ok(CoseSign1 {
262                    protected,
263                    unprotected,
264                    payload,
265                    signature,
266                })
267            }
268
269            fn visit_newtype_struct<D>(self, deserializer: D) -> Result<CoseSign1, D::Error>
270            where
271                D: Deserializer<'de>,
272            {
273                // This is the tagged version: we ignore the tag part, and just go into it
274                deserializer.deserialize_seq(CoseSign1Visitor)
275            }
276        }
277
278        deserializer.deserialize_any(CoseSign1Visitor)
279    }
280}
281
282impl CoseSign1 {
283    /// Parse CBOR bytes into struct. Adapted from <https://github.com/awslabs/aws-nitro-enclaves-cose/blob/main/src/sign.rs>
284    pub fn parse_and_validate(bytes: &[u8]) -> Result<Self, NitroAttestationVerifyError> {
285        let tagged_value: ciborium::value::Value = ciborium::de::from_reader(bytes)
286            .map_err(|e| NitroAttestationVerifyError::InvalidCoseSign1(e.to_string()))?;
287
288        let (tag, value) = match tagged_value {
289            ciborium::value::Value::Tag(tag, box_value) => (Some(tag), *box_value),
290            other => (None, other),
291        };
292
293        // Validate tag (18 is the COSE_Sign1 tag)
294        match tag {
295            None | Some(18) => (),
296            Some(_) => {
297                return Err(NitroAttestationVerifyError::InvalidCoseSign1(
298                    "invalid tag".to_string(),
299                ));
300            }
301        }
302
303        // Create a buffer for serialization
304        let mut buf = Vec::new();
305
306        // Serialize the value into the buffer
307        ciborium::ser::into_writer(&value, &mut buf)
308            .map_err(|e| NitroAttestationVerifyError::InvalidCoseSign1(e.to_string()))?;
309
310        // Deserialize the COSE_Sign1 structure from the buffer
311        let cosesign1: Self = ciborium::de::from_reader(&buf[..])
312            .map_err(|e| NitroAttestationVerifyError::InvalidCoseSign1(e.to_string()))?;
313
314        // Validate protected header
315        let _: HeaderMap = ciborium::de::from_reader(cosesign1.protected.as_slice())
316            .map_err(|e| NitroAttestationVerifyError::InvalidCoseSign1(e.to_string()))?;
317
318        cosesign1.validate_header()?;
319        Ok(cosesign1)
320    }
321
322    /// Validate protected header, payload and signature length.
323    pub fn validate_header(&self) -> Result<(), NitroAttestationVerifyError> {
324        if !(Self::is_valid_protected_header(self.protected.as_slice())
325            && (1..16384).contains(&self.payload.len())
326            && self.signature.len() == 96)
327        {
328            return Err(NitroAttestationVerifyError::InvalidCoseSign1(
329                "invalid cbor header".to_string(),
330            ));
331        }
332        Ok(())
333    }
334
335    // Check protected header: https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html#COSE-CBOR
336    // 18(/* COSE_Sign1 CBOR tag is 18 */
337    //     {1: -35}, /* This is equivalent with {algorithm: ECDS 384} */
338    //     {}, /* We have nothing in unprotected */
339    //     $ATTESTATION_DOCUMENT_CONTENT /* Attestation Document */,
340    //     signature /* This is the signature */
341    // )
342    fn is_valid_protected_header(bytes: &[u8]) -> bool {
343        let expected_key: Integer = Integer::from(1);
344        let expected_val: Integer = Integer::from(-35);
345        let value: Value = match ciborium::de::from_reader(bytes) {
346            Ok(v) => v,
347            Err(_) => return false,
348        };
349        match value {
350            Value::Map(vec) => match &vec[..] {
351                [(Value::Integer(key), Value::Integer(val))] => {
352                    key == &expected_key && val == &expected_val
353                }
354                _ => false,
355            },
356            _ => false,
357        }
358    }
359
360    /// This is the content that the signature is committed over.
361    fn to_signed_message(&self) -> SuiResult<Vec<u8>> {
362        let value = Value::Array(vec![
363            Value::Text("Signature1".to_string()),
364            Value::Bytes(self.protected.as_slice().to_vec()),
365            Value::Bytes(vec![]),
366            Value::Bytes(self.payload.as_slice().to_vec()),
367        ]);
368        // 17 for extra metadata bytes
369        let mut bytes = Vec::with_capacity(self.protected.len() + self.payload.len() + 17);
370        ciborium::ser::into_writer(&value, &mut bytes).map_err(|_| {
371            SuiErrorKind::NitroAttestationFailedToVerify("cannot parse message".to_string())
372        })?;
373        Ok(bytes)
374    }
375}
376
377/// The AWS Nitro Attestation Document, see <https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html#doc-def>
378#[allow(dead_code)]
379#[derive(Debug, Clone)]
380pub struct AttestationDocument {
381    pub module_id: String,
382    pub timestamp: u64,
383    pub digest: String,
384    pub pcr_vec: Vec<Vec<u8>>,
385    pub pcr_map: BTreeMap<u8, Vec<u8>>,
386    certificate: Vec<u8>,
387    cabundle: Vec<Vec<u8>>,
388    pub public_key: Option<Vec<u8>>,
389    pub user_data: Option<Vec<u8>>,
390    pub nonce: Option<Vec<u8>>,
391}
392
393impl AttestationDocument {
394    /// Validate and parse the payload of the attestation document.
395    /// Adapted from <https://github.com/EternisAI/remote-attestation-verifier/blob/main/src/lib.rs>
396    pub fn parse_payload(
397        payload: &[u8],
398        is_upgraded_parsing: bool,
399        include_all_nonzero_pcrs: bool,
400    ) -> Result<AttestationDocument, NitroAttestationVerifyError> {
401        let document_map = Self::to_map(payload, is_upgraded_parsing)?;
402        Self::validate_document_map(&document_map, is_upgraded_parsing, include_all_nonzero_pcrs)
403    }
404
405    fn to_map(
406        payload: &[u8],
407        is_upgraded_parsing: bool,
408    ) -> Result<BTreeMap<String, Value>, NitroAttestationVerifyError> {
409        let document_data: ciborium::value::Value =
410            ciborium::de::from_reader(payload).map_err(|err| {
411                NitroAttestationVerifyError::InvalidAttestationDoc(format!(
412                    "cannot parse payload CBOR: {}",
413                    err
414                ))
415            })?;
416
417        let document_map: BTreeMap<String, Value> = match document_data {
418            ciborium::value::Value::Map(map) => {
419                let map_size = map.len();
420                let result = map
421                    .into_iter()
422                    .map(|(k, v)| {
423                        let k = k.as_text().ok_or(
424                            NitroAttestationVerifyError::InvalidAttestationDoc(format!(
425                                "invalid key type: {:?}",
426                                k
427                            )),
428                        )?;
429                        Ok((k.to_string(), v))
430                    })
431                    .collect::<Result<BTreeMap<String, Value>, NitroAttestationVerifyError>>()?;
432
433                if is_upgraded_parsing && result.len() != map_size {
434                    return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
435                        "duplicate keys found in attestation document".to_string(),
436                    ));
437                }
438                result
439            }
440            _ => {
441                return Err(NitroAttestationVerifyError::InvalidAttestationDoc(format!(
442                    "expected map, got {:?}",
443                    document_data
444                )));
445            }
446        };
447        Ok(document_map)
448    }
449
450    fn validate_document_map(
451        document_map: &BTreeMap<String, Value>,
452        is_upgraded_parsing: bool,
453        include_all_nonzero_pcrs: bool,
454    ) -> Result<AttestationDocument, NitroAttestationVerifyError> {
455        let module_id = document_map
456            .get("module_id")
457            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
458                "module id not found".to_string(),
459            ))?
460            .as_text()
461            .filter(|s| !s.is_empty())
462            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
463                "invalid module id".to_string(),
464            ))?
465            .to_string();
466
467        let digest = document_map
468            .get("digest")
469            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
470                "digest not found".to_string(),
471            ))?
472            .as_text()
473            .filter(|s| s == &"SHA384")
474            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
475                "invalid digest".to_string(),
476            ))?
477            .to_string();
478
479        let certificate = document_map
480            .get("certificate")
481            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
482                "certificate not found".to_string(),
483            ))?
484            .as_bytes()
485            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
486                "invalid certificate".to_string(),
487            ))?
488            .to_vec();
489
490        if certificate.is_empty() || certificate.len() > MAX_CERT_LENGTH {
491            return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
492                "invalid certificate".to_string(),
493            ));
494        }
495
496        let timestamp = document_map
497            .get("timestamp")
498            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
499                "timestamp not found".to_string(),
500            ))?
501            .as_integer()
502            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
503                "timestamp is not an integer".to_string(),
504            ))
505            .and_then(|integer| {
506                u64::try_from(integer).map_err(|_| {
507                    NitroAttestationVerifyError::InvalidAttestationDoc(
508                        "timestamp not u64".to_string(),
509                    )
510                })
511            })?;
512
513        let public_key = document_map
514            .get("public_key")
515            .and_then(|v| v.as_bytes())
516            .map(|bytes| bytes.to_vec());
517
518        if let Some(data) = &public_key {
519            if is_upgraded_parsing {
520                if data.len() > MAX_PK_LENGTH {
521                    return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
522                        "invalid public key".to_string(),
523                    ));
524                }
525            } else if data.is_empty() || data.len() > MAX_PK_LENGTH {
526                return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
527                    "invalid public key".to_string(),
528                ));
529            }
530        }
531
532        let user_data = document_map
533            .get("user_data")
534            .and_then(|v| v.as_bytes())
535            .map(|bytes| bytes.to_vec());
536
537        if let Some(data) = &user_data
538            && data.len() > MAX_USER_DATA_LENGTH
539        {
540            return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
541                "invalid user data".to_string(),
542            ));
543        }
544
545        let nonce = document_map
546            .get("nonce")
547            .and_then(|v| v.as_bytes())
548            .map(|bytes| bytes.to_vec());
549
550        if let Some(data) = &nonce
551            && data.len() > MAX_USER_DATA_LENGTH
552        {
553            return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
554                "invalid nonce".to_string(),
555            ));
556        }
557
558        let (pcr_vec, pcr_map) = document_map
559            .get("pcrs")
560            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
561                "pcrs not found".to_string(),
562            ))?
563            .as_map()
564            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
565                "invalid pcrs format".to_string(),
566            ))
567            .and_then(|pairs| {
568                if pairs.len() > MAX_PCRS_LENGTH {
569                    return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
570                        "invalid PCRs length".to_string(),
571                    ));
572                }
573                let mut pcr_vec = Vec::with_capacity(pairs.len());
574                let mut pcr_map = BTreeMap::new();
575                for (k, v) in pairs.iter() {
576                    let key = k.as_integer().ok_or(
577                        NitroAttestationVerifyError::InvalidAttestationDoc(
578                            "invalid PCR key format".to_string(),
579                        ),
580                    )?;
581                    let value =
582                        v.as_bytes()
583                            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
584                                "invalid PCR value format".to_string(),
585                            ))?;
586
587                    if value.len() != 32 && value.len() != 48 && value.len() != 64 {
588                        return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
589                            "invalid PCR value length".to_string(),
590                        ));
591                    }
592
593                    // legacy parsing that populates a vector.
594                    let key_u64 = u64::try_from(key).map_err(|_| {
595                        NitroAttestationVerifyError::InvalidAttestationDoc(
596                            "invalid PCR index".to_string(),
597                        )
598                    })?;
599                    for i in [0, 1, 2, 3, 4, 8] {
600                        if key_u64 == i {
601                            pcr_vec.push(value.to_vec());
602                        }
603                    }
604
605                    // when upgraded parsing is enabled, use btreemap to avoid dup.
606                    if is_upgraded_parsing {
607                        // valid key is 0..31, can parse with u8.
608                        let key_u8 = u8::try_from(key).map_err(|_| {
609                            NitroAttestationVerifyError::InvalidAttestationDoc(
610                                "invalid PCR index".to_string(),
611                            )
612                        })?;
613
614                        if pcr_map.contains_key(&key_u8) {
615                            return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
616                                format!("duplicate PCR index {}", key_u8),
617                            ));
618                        }
619
620                        if include_all_nonzero_pcrs {
621                            // If flag=true, parse all 0..31 PCRs, but skip all-zero values.
622                            // See: <https://github.com/aws/aws-nitro-enclaves-nsm-api/issues/18#issuecomment-970172662>
623                            // Also: <https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/nsm-test/src/bin/nsm-check.rs#L193-L199>
624                            if key_u8 <= 31 && !value.iter().all(|&b| b == 0) {
625                                pcr_map.insert(key_u8, value.to_vec());
626                            }
627                        } else {
628                            // In legacy mode (flag=false): Parse only specific PCRs (0, 1, 2, 3, 4, 8), including zero values.
629                            // See: <https://docs.aws.amazon.com/enclaves/latest/user/set-up-attestation.html#where>
630                            if matches!(key_u8, 0 | 1 | 2 | 3 | 4 | 8) {
631                                pcr_map.insert(key_u8, value.to_vec());
632                            }
633                        }
634                    }
635                }
636                Ok((pcr_vec, pcr_map))
637            })?;
638
639        let cabundle = document_map
640            .get("cabundle")
641            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
642                "cabundle not found".to_string(),
643            ))?
644            .as_array()
645            .map(|arr| {
646                if arr.is_empty() || arr.len() > MAX_CERT_CHAIN_LENGTH {
647                    return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
648                        "invalid ca chain length".to_string(),
649                    ));
650                }
651                let mut certs = Vec::with_capacity(arr.len());
652                for cert in arr.iter() {
653                    let cert_bytes = cert.as_bytes().ok_or(
654                        NitroAttestationVerifyError::InvalidAttestationDoc(
655                            "invalid certificate bytes".to_string(),
656                        ),
657                    )?;
658                    if cert_bytes.is_empty() || cert_bytes.len() > MAX_CERT_LENGTH {
659                        return Err(NitroAttestationVerifyError::InvalidAttestationDoc(
660                            "invalid ca length".to_string(),
661                        ));
662                    }
663                    certs.push(cert_bytes.to_vec());
664                }
665                Ok(certs)
666            })
667            .ok_or(NitroAttestationVerifyError::InvalidAttestationDoc(
668                "invalid cabundle".to_string(),
669            ))??;
670
671        let doc = AttestationDocument {
672            module_id,
673            timestamp,
674            digest,
675            pcr_vec,
676            pcr_map,
677            certificate,
678            cabundle,
679            public_key,
680            user_data,
681            nonce,
682        };
683        Ok(doc)
684    }
685
686    /// Verify the certificate against AWS Nitro root of trust and checks expiry.
687    /// Assume the cabundle is in order.
688    fn verify_cert(&self, now: u64) -> Result<(), NitroAttestationVerifyError> {
689        // Create chain starting with leaf cert all the way to root.
690        let mut chain = Vec::with_capacity(1 + self.cabundle.len());
691        chain.push(self.certificate.as_slice());
692        chain.extend(self.cabundle.iter().rev().map(|cert| cert.as_slice()));
693        verify_cert_chain(&chain, now)
694    }
695
696    /// Get the length of the certificate chain.
697    pub fn get_cert_chain_length(&self) -> usize {
698        self.cabundle.len()
699    }
700}
701/// Verify the certificate chain against the root of trust.
702fn verify_cert_chain(cert_chain: &[&[u8]], now_ms: u64) -> Result<(), NitroAttestationVerifyError> {
703    let root_cert = X509Certificate::from_der(ROOT_CERTIFICATE.as_slice())
704        .map_err(|e| NitroAttestationVerifyError::InvalidCertificate(e.to_string()))?
705        .1;
706
707    let now_secs =
708        ASN1Time::from_timestamp((now_ms as i64).checked_div(1000).ok_or_else(|| {
709            NitroAttestationVerifyError::InvalidAttestationDoc("timestamp overflow".to_string())
710        })?)
711        .map_err(|e| NitroAttestationVerifyError::InvalidCertificate(e.to_string()))?;
712
713    // Validate the chain starting from the leaf
714    for i in 0..cert_chain.len() {
715        let cert = X509Certificate::from_der(cert_chain[i])
716            .map_err(|e| NitroAttestationVerifyError::InvalidCertificate(e.to_string()))?
717            .1;
718
719        // Check key usage for all certificates
720        if let Ok(Some(key_usage)) = cert.key_usage() {
721            if i == 0 {
722                // Target certificate must have digitalSignature
723                if !key_usage.value.digital_signature() {
724                    return Err(NitroAttestationVerifyError::InvalidCertificate(
725                        "Target certificate missing digitalSignature key usage".to_string(),
726                    ));
727                }
728            } else {
729                // CA certificates must have keyCertSign
730                if !key_usage.value.key_cert_sign() {
731                    return Err(NitroAttestationVerifyError::InvalidCertificate(
732                        "CA certificate missing keyCertSign key usage".to_string(),
733                    ));
734                }
735            }
736        } else {
737            return Err(NitroAttestationVerifyError::InvalidCertificate(
738                "Missing key usage extension".to_string(),
739            ));
740        }
741
742        if i != 0 {
743            // all ca certs must have basic contraint, must be critical, ca flag must be true,
744            // pathLenConstraint is optional but if present must be checked.
745            if let Ok(Some(bc)) = cert.basic_constraints() {
746                if !bc.critical || !bc.value.ca {
747                    return Err(NitroAttestationVerifyError::InvalidCertificate(
748                        "CA certificate invalid".to_string(),
749                    ));
750                }
751                if let Some(path_len) = bc.value.path_len_constraint {
752                    // path_len_constraint is the maximum number of CA certificates
753                    // that may follow this certificate. A value of zero indicates
754                    // that only an end-entity certificate may follow in the path.
755                    if i - 1 > path_len as usize {
756                        return Err(NitroAttestationVerifyError::InvalidCertificate(
757                            "Cert chain exceeds pathLenConstraint".to_string(),
758                        ));
759                    }
760                }
761            } else {
762                return Err(NitroAttestationVerifyError::InvalidCertificate(
763                    "missing basic constraint".to_string(),
764                ));
765            }
766        } else if let Ok(Some(bc)) = cert.basic_constraints() {
767            // end entity can have basic constraint optionally, if present, ca must be false and
768            // pathLenConstraint is undefined.
769            if bc.value.path_len_constraint.is_some() || bc.value.ca {
770                return Err(NitroAttestationVerifyError::InvalidCertificate(
771                    "Cert chain exceeds pathLenConstraint".to_string(),
772                ));
773            }
774        }
775
776        // Check timestamp validity
777        if !cert.validity().is_valid_at(now_secs) {
778            return Err(NitroAttestationVerifyError::InvalidCertificate(
779                "Certificate timestamp not valid".to_string(),
780            ));
781        }
782
783        // Get issuer cert from either next in chain or root
784        let issuer_cert = if i < cert_chain.len() - 1 {
785            X509Certificate::from_der(cert_chain[i + 1])
786                .map_err(|e| NitroAttestationVerifyError::InvalidCertificate(e.to_string()))?
787                .1
788        } else {
789            root_cert.clone()
790        };
791
792        // Verify issuer/subject chaining
793        if cert.issuer() != issuer_cert.subject() {
794            return Err(NitroAttestationVerifyError::InvalidCertificate(
795                "certificate chain issuer mismatch".to_string(),
796            ));
797        }
798
799        // Verify signature
800        cert.verify_signature(Some(issuer_cert.public_key()))
801            .map_err(|_| {
802                NitroAttestationVerifyError::InvalidCertificate(
803                    "certificate fails to verify".to_string(),
804                )
805            })?;
806    }
807
808    Ok(())
809}