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