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