From 981768a7cdedeb9c90f916fcfca3959196b84cc9 Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Fri, 13 Feb 2026 16:12:46 +0100 Subject: [PATCH 1/9] Add nitro verification --- CLAUDE.md | 48 +++ assets/aws_nitro_root_g1.der | Bin 0 -> 533 bytes assets/tests/nitro/attestation_doc.bin | Bin 0 -> 4461 bytes assets/tests/nitro/attestation_doc_2.bin | Bin 0 -> 7539 bytes .../tests/nitro/attestation_doc_bad_sig.bin | Bin 0 -> 4461 bytes assets/tests/nitro/crl_root.der | Bin 0 -> 299 bytes assets/tests/nitro/crl_zonal.der | Bin 0 -> 326 bytes src/nitro.rs | 25 ++ src/nitro/attestation.rs | 325 ++++++++++++++++++ tests/nitro/mod.rs | 198 +++++++++++ 10 files changed, 596 insertions(+) create mode 100644 CLAUDE.md create mode 100644 assets/aws_nitro_root_g1.der create mode 100644 assets/tests/nitro/attestation_doc.bin create mode 100644 assets/tests/nitro/attestation_doc_2.bin create mode 100644 assets/tests/nitro/attestation_doc_bad_sig.bin create mode 100644 assets/tests/nitro/crl_root.der create mode 100644 assets/tests/nitro/crl_zonal.der create mode 100644 src/nitro.rs create mode 100644 src/nitro/attestation.rs create mode 100644 tests/nitro/mod.rs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..74e4c28 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,48 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is a monorepo of a Rust library that implements verification functionalities for various TEE frameworks (currently limited to Intel TDX, but open to expansions). + +## Rust Toolchain + +This repo is meant to be compiled with a stable Rust toolchain. The nightly toolchain is only used for checking unneeded dependencies with udeps. This is a no_std crate, and to help verify that we are not unintentionally importing indirect std dependencies, we test the compilation on a bare metal configuration. + +## Build Commands + +```bash +# Build the library +cargo build --release + +# Build bare metal +cargo make build-bare-metal +``` + +## Formatting and linting + +```bash +cargo fmt +cargo clippy --release +``` + +## Check dependencies + +```bash +cargo make udeps +``` + +## Testing + +```bash +cargo test --release +``` + +## Development Guidelines + +The main usage of this repo is to implement TEE verification pallets in zkVerify (https://github.com/zkVerify/zkVerify, for now only in the `dr/add_tee_verifier_crl` branch.) + +In that regard, we want to minimize the size of the resulting runtime wasm, and to minimize the chances of indirectly pulling in dependencies on std, for example caused by shared sub-dependencies with other crates used by zkVerify, where std is not disabled (features are additive!). Long story short, try to minimize the number of external crates imported, and alway try to pick the smallest/simplest external crate when needed. + +Always check that the bare metal build works, that tests pass, and that the format/linting is accepted before finishing any coding task. diff --git a/assets/aws_nitro_root_g1.der b/assets/aws_nitro_root_g1.der new file mode 100644 index 0000000000000000000000000000000000000000..994d128b7b49970854fa25e6ee96dd1fb408e632 GIT binary patch literal 533 zcmXqLViGiHVw|>snTe5!Ns!^EVQGf+g!K<)-Og-z^881b0T&yGR-4B;TNY+!15ZP4 z15P&PP!={}rqEzRegj?*hl_{JF*mU)KhKZ{s2V81&BN>%9&9LWAPM3z^9UuD7whF^ zmK5ddrsgH*B$lNX8_0?C8d@3}7+Dw^8(A2bMv3#9K)5DgE;X$xGZ0{72m67Ek&RWm zk%d8tIf;Sg4^znNWgG`TXB0e|b!);tHw&Xl#d)3H|GyR8>~!%C2z+X?WaGEnj}PpL zkTTl7v5cMXp3t>f_OBGB)1C*Nuf2GeGd^Fq;EOni^UXT~jEYarCo^hrrn$7AtW0V# z@I7k(WwDcig8@G<1Z0I78UM4e8ZZMX16hy&ABz}^$OP4`yf<0hpHDkH?N`y<^D|f8 z_nv0J2a@Iosb>L3NE>qKGiNdwq%j#XEUwp|xbWoOxaT{PYQ$z-3T5t-XLkM>oxC!s z$JVl8s_R0zJ^S^nLvO5mSGeWywY8stS{8ktEylX3Kx64$?**A#8S_%N{Jz)!fK4_l cSh9Z3t*aKWu25~NC#$wVR{EC~n|1frlQs8vA$ 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 literal 0 HcmV?d00001 diff --git a/assets/tests/nitro/attestation_doc_2.bin b/assets/tests/nitro/attestation_doc_2.bin new file mode 100644 index 0000000000000000000000000000000000000000..38e0b633ebcbe3e98f63d92abd2bb7516d9e1984 GIT binary patch literal 7539 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 literal 0 HcmV?d00001 diff --git a/assets/tests/nitro/crl_root.der b/assets/tests/nitro/crl_root.der new file mode 100644 index 0000000000000000000000000000000000000000..aebd798e29542ee77c18c33ddade0e7c046c67bc GIT binary patch literal 299 zcmXqLVpKP1T*t)7Xu!qBq1EPb&X$Fl$-vW)+klgeIh2J>m?<>akl%n8#Npy$bIeVw z%Fi?80jdTHaPu%bh6fu;8%Tn9%sfJg<;8k=nI%Q}x~X}|If-Sd#p1k1W(G!vMg}0# zJPOJ+HZTWr&7u~V7#JDw12xNvuy8Q3G0q8C5p%;p9wea5B4HrbATmL9EALHK_vh0N zPy1Cg_x#M2_r0efJC!+u!622%kYR<&Uzsz%eB8ULu9QmXyY&_6|L<0wJxxXGfXZct z#SPvKo+(H28GUqGO!D6dm)G+$8L;2nRlkJ&>ZKyZy$j2FE}aeWVY;ZT^ZL!Xht>tl Z_9r}F@v-OWwdGzj(o(k`FKdip1^{1fUef>o literal 0 HcmV?d00001 diff --git a/assets/tests/nitro/crl_zonal.der b/assets/tests/nitro/crl_zonal.der new file mode 100644 index 0000000000000000000000000000000000000000..3cab515a6c9dab296ac243a139f48865ff023383 GIT binary patch literal 326 zcmXqLVstWSJjuk!Xu!qBq1EPb&X$Fl*&xM`+klgeIh2J>m?<>akl%n8#Npy$bIeVw z%Fi?80jdTHaPu%bh6fv(8JK{0%sjdl2FaFbsTQUNsb*}B}MtVsd>pciDjw9;=D#ihK7bl1_nlkMwU@fuCak3n7hElz{r3fXs)aX3kMS$ zV@qak_FMybkbp9agn?Lt$kJ5T<4sGX&*f%MmOr=R@63{@eWA#{V9sDLNM$l)n3LKa zwRDaxl<8`#W?3!!ohmXb+2VVi#s_sK0~VRZ zD=#i#5;x>nD&Xg!nb3Cb!>b;ar{Qnn^4kud3)|GZt;6rd4}2dfZc6$O01Dq_ AcK`qY literal 0 HcmV?d00001 diff --git a/src/nitro.rs b/src/nitro.rs new file mode 100644 index 0000000..6ad63bc --- /dev/null +++ b/src/nitro.rs @@ -0,0 +1,25 @@ +// Copyright 2025, Horizen Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +static ROOT_CERT: &[u8] = include_bytes!("../assets/aws_nitro_root_g1.der"); + +mod attestation; + +pub use attestation::{NitroAttestation, NitroParseError, NitroVerificationError}; + +/// Parse a Nitro attestation document from binary COSE_Sign1 data. +pub fn parse_nitro_attestation(input: &[u8]) -> Result { + attestation::parse_nitro_attestation(input) +} diff --git a/src/nitro/attestation.rs b/src/nitro/attestation.rs new file mode 100644 index 0000000..62de515 --- /dev/null +++ b/src/nitro/attestation.rs @@ -0,0 +1,325 @@ +// Copyright 2025, Horizen Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate alloc; +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +use coset::{CborSerializable, CoseSign1, TaggedCborSerializable}; +use p384::ecdsa::signature::Verifier; +use x509_verify::x509_cert::Certificate; + +/// Errors that can occur when parsing a Nitro attestation document. +#[derive(Debug)] +pub enum NitroParseError { + /// The input is not a valid COSE_Sign1 structure. + InvalidCoseSign1, + /// The COSE_Sign1 payload is missing or invalid. + InvalidPayload, + /// A mandatory field is missing from the attestation document. + MissingField(&'static str), + /// A field has an unexpected type or value. + InvalidField(&'static str), + /// A certificate in the document could not be parsed. + InvalidCertificate, + /// The COSE protected header specifies an unsupported algorithm. + UnsupportedAlgorithm, +} + +/// Errors that can occur when verifying a Nitro attestation document. +#[derive(Debug)] +pub enum NitroVerificationError { + /// The ECDSA signature over the COSE Sig_structure is invalid. + BadSignature, + /// The certificate chain verification failed. + CertificateChain, + /// A certificate in the chain has expired or is not yet valid. + CertificateExpired, + /// The enclave is running in debug mode. + DebugMode, + /// The leaf certificate's public key could not be extracted. + InvalidPublicKey, + /// The attestation document's digest algorithm is not "SHA384". + UnsupportedDigest, +} + +/// A parsed AWS Nitro Enclave attestation document. +pub struct NitroAttestation { + /// The issuing Nitro enclave module ID. + pub module_id: String, + /// The digest algorithm used (must be "SHA384"). + pub digest: String, + /// Timestamp in milliseconds since Unix epoch. + pub timestamp: u64, + /// Platform Configuration Registers (index → value). + pub pcrs: BTreeMap>, + /// DER-encoded leaf certificate that signed this document. + pub certificate: Vec, + /// DER-encoded CA bundle certificates (root-first). + pub cabundle: Vec>, + /// Optional public key provided by the enclave. + pub public_key: Option>, + /// Optional user data provided by the enclave. + pub user_data: Option>, + /// Optional nonce for replay protection. + pub nonce: Option>, + + // Internal fields for COSE_Sign1 verification + protected_header_bytes: Vec, + payload: Vec, + signature: Vec, +} + +/// Parse a Nitro attestation document from binary COSE_Sign1 data. +pub fn parse_nitro_attestation(input: &[u8]) -> Result { + // Decode COSE_Sign1 — accept both tagged (CBOR tag 18) and untagged forms + let cose_sign1 = CoseSign1::from_tagged_slice(input) + .or_else(|_| CoseSign1::from_slice(input)) + .map_err(|_| NitroParseError::InvalidCoseSign1)?; + + // Verify the algorithm is ES384 + let alg = cose_sign1 + .protected + .header + .alg + .as_ref() + .ok_or(NitroParseError::UnsupportedAlgorithm)?; + match alg { + coset::RegisteredLabelWithPrivate::Assigned(coset::iana::Algorithm::ES384) => {} + _ => return Err(NitroParseError::UnsupportedAlgorithm), + } + + // Extract payload + let payload_bytes = cose_sign1 + .payload + .as_ref() + .ok_or(NitroParseError::InvalidPayload)? + .clone(); + + // Decode payload as CBOR map + let payload_value: ciborium::Value = ciborium::from_reader(payload_bytes.as_slice()) + .map_err(|_| NitroParseError::InvalidPayload)?; + + let map = match &payload_value { + ciborium::Value::Map(m) => m, + _ => return Err(NitroParseError::InvalidPayload), + }; + + // Extract mandatory fields + let module_id = get_text_field(map, "module_id")?; + let digest = get_text_field(map, "digest")?; + let timestamp = get_integer_field(map, "timestamp")?; + let pcrs = get_pcrs(map)?; + let certificate = get_bytes_field(map, "certificate")?; + let cabundle = get_cabundle(map)?; + + // Extract optional fields + let public_key = get_optional_bytes_field(map, "public_key"); + let user_data = get_optional_bytes_field(map, "user_data"); + let nonce = get_optional_bytes_field(map, "nonce"); + + // Store internal COSE components for later verification + let protected_header_bytes = cose_sign1 + .protected + .original_data + .as_deref() + .unwrap_or(&[]) + .to_vec(); + let signature = cose_sign1.signature.clone(); + + Ok(NitroAttestation { + module_id, + digest, + timestamp, + pcrs, + certificate, + cabundle, + public_key, + user_data, + nonce, + protected_header_bytes, + payload: payload_bytes, + signature, + }) +} + +impl NitroAttestation { + /// Verify the attestation document against the embedded AWS Nitro root CA. + /// + /// `crl` is an optional certificate revocation list to check against. + /// `now` is the current time as seconds since Unix epoch. + pub fn verify( + &self, + crl: Option<&crate::cert::Crl>, + now: u64, + ) -> Result<(), NitroVerificationError> { + if self.digest != "SHA384" { + return Err(NitroVerificationError::UnsupportedDigest); + } + + // Build the certificate chain: cabundle (root → intermediates) + leaf + // cabundle is root-first, so the chain for verification is: + // leaf → last_intermediate → ... → first_intermediate → cabundle_root → embedded_root + let mut chain_refs: Vec<&[u8]> = Vec::new(); + chain_refs.push(self.certificate.as_slice()); + for cert_der in self.cabundle.iter().rev() { + chain_refs.push(cert_der.as_slice()); + } + + // Verify the certificate chain against the embedded AWS root + let leaf = crate::cert::verify_der_cert_chain( + &chain_refs, + Some(crate::nitro::ROOT_CERT), + crl, + now, + ) + .map_err(|_| NitroVerificationError::CertificateChain)?; + + // Build COSE Sig_structure for verification: + // Sig_structure = ["Signature1", protected, external_aad, payload] + let protected_header = coset::ProtectedHeader::from_cbor_bstr( + coset::cbor::value::Value::Bytes(self.protected_header_bytes.clone()), + ) + .map_err(|_| NitroVerificationError::BadSignature)?; + let sig_structure = coset::sig_structure_data( + coset::SignatureContext::CoseSign1, + protected_header, + None, + &[], + &self.payload, + ); + + // Extract the public key from the leaf certificate and verify the signature + self.verify_es384_signature(&leaf, &sig_structure)?; + + Ok(()) + } + + fn verify_es384_signature( + &self, + cert: &Certificate, + data: &[u8], + ) -> Result<(), NitroVerificationError> { + let pk_bytes = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .ok_or(NitroVerificationError::InvalidPublicKey)?; + + let point = p384::EncodedPoint::from_bytes(pk_bytes) + .map_err(|_| NitroVerificationError::InvalidPublicKey)?; + let verifying_key = p384::ecdsa::VerifyingKey::from_encoded_point(&point) + .map_err(|_| NitroVerificationError::InvalidPublicKey)?; + + // COSE signatures use raw (R || S) format, not DER + let signature = p384::ecdsa::Signature::from_slice(&self.signature) + .map_err(|_| NitroVerificationError::BadSignature)?; + + verifying_key + .verify(data, &signature) + .map_err(|_| NitroVerificationError::BadSignature)?; + + Ok(()) + } +} + +// ============================================================================= +// CBOR field extraction helpers +// ============================================================================= + +type CborMap = Vec<(ciborium::Value, ciborium::Value)>; + +fn find_field<'a>(map: &'a CborMap, key: &str) -> Option<&'a ciborium::Value> { + map.iter() + .find(|(k, _)| matches!(k, ciborium::Value::Text(s) if s == key)) + .map(|(_, v)| v) +} + +fn get_text_field(map: &CborMap, key: &'static str) -> Result { + match find_field(map, key) { + Some(ciborium::Value::Text(s)) => Ok(s.clone()), + Some(_) => Err(NitroParseError::InvalidField(key)), + None => Err(NitroParseError::MissingField(key)), + } +} + +fn get_integer_field(map: &CborMap, key: &'static str) -> Result { + match find_field(map, key) { + Some(ciborium::Value::Integer(i)) => { + let val: i128 = (*i).into(); + u64::try_from(val).map_err(|_| NitroParseError::InvalidField(key)) + } + Some(_) => Err(NitroParseError::InvalidField(key)), + None => Err(NitroParseError::MissingField(key)), + } +} + +fn get_bytes_field(map: &CborMap, key: &'static str) -> Result, NitroParseError> { + match find_field(map, key) { + Some(ciborium::Value::Bytes(b)) => Ok(b.clone()), + Some(_) => Err(NitroParseError::InvalidField(key)), + None => Err(NitroParseError::MissingField(key)), + } +} + +fn get_optional_bytes_field(map: &CborMap, key: &str) -> Option> { + match find_field(map, key) { + Some(ciborium::Value::Bytes(b)) => Some(b.clone()), + Some(ciborium::Value::Null) => None, + _ => None, + } +} + +fn get_pcrs(map: &CborMap) -> Result>, NitroParseError> { + let pcrs_value = find_field(map, "pcrs").ok_or(NitroParseError::MissingField("pcrs"))?; + let pcrs_map = match pcrs_value { + ciborium::Value::Map(m) => m, + _ => return Err(NitroParseError::InvalidField("pcrs")), + }; + + let mut result = BTreeMap::new(); + for (k, v) in pcrs_map { + let index = match k { + ciborium::Value::Integer(i) => { + let val: i128 = (*i).into(); + u32::try_from(val).map_err(|_| NitroParseError::InvalidField("pcrs"))? + } + _ => return Err(NitroParseError::InvalidField("pcrs")), + }; + let value = match v { + ciborium::Value::Bytes(b) => b.clone(), + _ => return Err(NitroParseError::InvalidField("pcrs")), + }; + result.insert(index, value); + } + Ok(result) +} + +fn get_cabundle(map: &CborMap) -> Result>, NitroParseError> { + let value = find_field(map, "cabundle").ok_or(NitroParseError::MissingField("cabundle"))?; + let arr = match value { + ciborium::Value::Array(a) => a, + _ => return Err(NitroParseError::InvalidField("cabundle")), + }; + + arr.iter() + .map(|v| match v { + ciborium::Value::Bytes(b) => Ok(b.clone()), + _ => Err(NitroParseError::InvalidField("cabundle")), + }) + .collect() +} diff --git a/tests/nitro/mod.rs b/tests/nitro/mod.rs new file mode 100644 index 0000000..2a814f9 --- /dev/null +++ b/tests/nitro/mod.rs @@ -0,0 +1,198 @@ +// Copyright 2025, Horizen Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Integration tests for Nitro attestation verification. +//! +//! These tests verify the parsing and verification of AWS Nitro Enclave +//! attestation documents using real attestation fixtures. + +use std::fs::File; +use std::io::Read; + +use assert_ok::assert_ok; + +use tee_verifier::{ + parse_der_crl, parse_nitro_attestation, Crl, NitroParseError, NitroVerificationError, +}; + +/// Helper function to load a file into a byte vector +fn load_file(path: &str) -> Vec { + let mut f = File::open(path).unwrap(); + let mut buf = Vec::new(); + f.read_to_end(&mut buf).unwrap(); + buf +} + +// ============================================================================= +// Nitro Attestation Parsing Tests +// ============================================================================= + +mod nitro_parsing { + use super::*; + + #[test] + fn parse_empty_input_fails() { + let result = parse_nitro_attestation(&[]); + assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); + } + + #[test] + fn parse_invalid_cbor_fails() { + let result = parse_nitro_attestation(&[0xFF, 0xFF, 0xFF]); + assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); + } + + #[test] + fn parse_valid_attestation_doc() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = assert_ok!(parse_nitro_attestation(&data)); + + assert_eq!(att.module_id, "i-04fd167167daacf3b-enc01845e97d0b3eb4e"); + assert_eq!(att.digest, "SHA384"); + // 2022-11-09T22:52:00.696Z + assert_eq!(att.timestamp, 1668034320696); + assert_eq!(att.pcrs.len(), 16); + assert_eq!(att.cabundle.len(), 4); + assert!(att.public_key.is_none()); + assert!(att.user_data.is_some()); + assert!(att.nonce.is_some()); + } + + #[test] + fn parse_valid_attestation_doc_2() { + let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); + let att = assert_ok!(parse_nitro_attestation(&data)); + + assert_eq!(att.module_id, "i-0a22e5c5f24d22174-enc0191cceb4289903f"); + assert_eq!(att.digest, "SHA384"); + // 2024-09-07T14:37:39.545Z + assert_eq!(att.timestamp, 1725719859545); + assert_eq!(att.pcrs.len(), 16); + assert!(att.public_key.is_some()); + assert!(att.nonce.is_some()); + } +} + +// ============================================================================= +// End-to-End Nitro Attestation Verification Tests +// ============================================================================= + +mod end_to_end { + use super::*; + + #[test] + fn verify_attestation_doc() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = parse_nitro_attestation(&data).unwrap(); + + // Use the document's own timestamp (converted from ms to seconds) + // since the embedded certificates have a short validity window. + // 2022-11-09T22:52:00Z + let now = att.timestamp / 1000; + assert_ok!(att.verify(None, now)); + } + + #[test] + fn verify_attestation_doc_2() { + let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); + let att = parse_nitro_attestation(&data).unwrap(); + + // 2024-09-07T14:37:39Z + let now = att.timestamp / 1000; + assert_ok!(att.verify(None, now)); + } + + #[test] + fn verify_attestation_doc_with_crl() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let nitro_root = load_file("assets/aws_nitro_root_g1.der"); + let att = parse_nitro_attestation(&data).unwrap(); + + // 2022-11-09T22:52:00Z + let now = att.timestamp / 1000; + + // Parse CRLs using signing certificate chains from the attestation's cabundle. + // Chains are concatenated DER: leaf-first, root-last. + // Root CRL: signed by cabundle[0] (root). + // Zonal CRL: signed by cabundle[1] (first intermediate), + // chain is cabundle[1] || cabundle[0]. + let root_crl_data = load_file("assets/tests/nitro/crl_root.der"); + let zonal_crl_data = load_file("assets/tests/nitro/crl_zonal.der"); + + let zonal_chain: Vec = att.cabundle[0..2] + .iter() + .rev() + .flat_map(|c| c.iter().copied()) + .collect(); + + let (_time1, crl1) = + parse_der_crl(&root_crl_data, &att.cabundle[0], Some(&nitro_root), now).unwrap(); + let (_time2, crl2) = + parse_der_crl(&zonal_crl_data, &zonal_chain, Some(&nitro_root), now).unwrap(); + + let mut crl: Crl = crl1; + crl.extend(crl2); + + assert_ok!(att.verify(Some(&crl), now)); + } +} + +// ============================================================================= +// Nitro Attestation Verification Error Tests +// ============================================================================= + +mod verification_errors { + use super::*; + + #[test] + fn verify_bad_signature_fails() { + let data = load_file("assets/tests/nitro/attestation_doc_bad_sig.bin"); + let att = parse_nitro_attestation(&data).unwrap(); + + // 2022-11-09T22:52:00Z + let now = att.timestamp / 1000; + let result = att.verify(None, now); + assert!(matches!(result, Err(NitroVerificationError::BadSignature))); + } + + #[test] + fn verify_with_expired_timestamp_fails() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = parse_nitro_attestation(&data).unwrap(); + + // One year after the document timestamp — certificates will have expired. + // ~2023-11-09 + let now = att.timestamp / 1000 + 365 * 24 * 3600; + let result = att.verify(None, now); + assert!(matches!( + result, + Err(NitroVerificationError::CertificateChain) + )); + } + + #[test] + fn verify_with_past_timestamp_fails() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = parse_nitro_attestation(&data).unwrap(); + + // 1970-01-01T00:00:00Z — certificates will not yet be valid. + let now = 0; + let result = att.verify(None, now); + assert!(matches!( + result, + Err(NitroVerificationError::CertificateChain) + )); + } +} From e99a3e614d480726db30f47055c06dc5f7dd2bb6 Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Fri, 13 Feb 2026 18:10:42 +0100 Subject: [PATCH 2/9] Add DER certificates --- Cargo.toml | 3 + src/cert.rs | 216 ++++++++++++++++++++++++++++++------- src/intel/collaterals.rs | 5 +- src/lib.rs | 32 +++++- tests/integration_tests.rs | 1 + tests/intel/mod.rs | 4 +- 6 files changed, 215 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 73d5f64..4656aad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,10 @@ sha2 = { version = "0.10.9", default-features = false } serde = { version = "1.0.228", default-features = false, features = ["derive", "alloc"] } serde-json-core = { version = "0.6.0", default-features = false, features = ["heapless"] } p256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "alloc"] } +p384 = { version = "0.13.1", default-features = false, features = ["ecdsa", "alloc"] } pem = { version = "3.0.6", default-features = false } +ciborium = { version = "0.2.2", default-features = false } +coset = { version = "0.4.1", default-features = false } spki = { version = "0.7.3", default-features = false } x509-verify = { version = "0.4.8", default-features = false, features = [ "x509", diff --git a/src/cert.rs b/src/cert.rs index de1c604..4959e17 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -22,7 +22,7 @@ use spki::ObjectIdentifier; use x509_verify::{ x509_cert::{ crl::CertificateList, - der::{Decode, Encode}, + der::{Decode, Encode, Reader, SliceReader}, Certificate, }, Signature, VerifyInfo, VerifyingKey, @@ -91,8 +91,30 @@ pub fn verify_pem_cert_chain( .into_iter() .map(|pem| Certificate::from_der(pem.contents())) .collect(); - let mut certs = certs.map_err(|_| CertificateError::Parse)?; + let certs = certs.map_err(|_| CertificateError::Parse)?; + verify_cert_chain(certs, root_cert, crl, now) +} +pub fn verify_der_cert_chain( + der_certs: &[&[u8]], + root_cert: Option<&[u8]>, + crl: Option<&Crl>, + now: u64, +) -> Result { + let certs: Result, _> = der_certs + .iter() + .map(|der| Certificate::from_der(der)) + .collect(); + let certs = certs.map_err(|_| CertificateError::Parse)?; + verify_cert_chain(certs, root_cert, crl, now) +} + +fn verify_cert_chain( + mut certs: Vec, + root_cert: Option<&[u8]>, + crl: Option<&Crl>, + now: u64, +) -> Result { if certs.is_empty() { return Err(CertificateError::EmptyChain); } @@ -286,8 +308,45 @@ pub fn verify_signature( Ok(()) } +/// Verify a CRL's signature and extract its revoked certificate entries. +fn process_crl( + crl: &CertificateList, + sign_key: &VerifyingKey, +) -> Result<(u64, Crl), CertificateError> { + verify_crl(crl, sign_key)?; + + let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); + + let issuer = crl + .tbs_cert_list + .issuer + .to_der() + .map_err(|_| CertificateError::Parse)?; + + let mut revoked_certs: Crl = Vec::new(); + if let Some(revoked) = &crl.tbs_cert_list.revoked_certificates { + for entry in revoked { + revoked_certs.push(RevokedCertId { + issuer: issuer.clone(), + serial_number: entry.serial_number.as_bytes().to_vec(), + }); + } + } + + Ok((this_update, revoked_certs)) +} + +fn signing_key_from_cert(cert: &Certificate) -> Result { + cert.tbs_certificate + .subject_public_key_info + .clone() + .try_into() + .map_err(|_| CertificateError::KeyVerification) +} + // PUBLIC INTERFACE -pub fn parse_crl( + +pub fn parse_pem_crl( crl_pem: &Vec, pck_certificate_chain_pem: &Vec, root_cert: Option<&[u8]>, @@ -301,53 +360,56 @@ pub fn parse_crl( let crls = crls.map_err(|_| CertificateError::Parse)?; let sign_cert = verify_pem_cert_chain(pck_certificate_chain_pem, root_cert, None, now)?; - let sign_key: VerifyingKey = sign_cert - .tbs_certificate - .subject_public_key_info - .clone() - .try_into() - .map_err(|_| CertificateError::KeyVerification)?; + let sign_key = signing_key_from_cert(&sign_cert)?; let mut revoked_certs: Crl = Vec::new(); let mut latest_this_update: u64 = 0; for crl in &crls { - verify_crl(crl, &sign_key)?; - - // Extract this_update and track the latest one - let this_update_duration = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); - - latest_this_update = match this_update_duration { - current if current > this_update_duration => current, - _ => this_update_duration, - }; - - let issuer = crl - .tbs_cert_list - .issuer - .to_der() - .map_err(|_| CertificateError::Parse)?; - - if let Some(revoked) = &crl.tbs_cert_list.revoked_certificates { - for entry in revoked { - revoked_certs.push(RevokedCertId { - issuer: issuer.clone(), - serial_number: entry.serial_number.as_bytes().to_vec(), - }); - } + let (this_update, entries) = process_crl(crl, &sign_key)?; + if this_update > latest_this_update { + latest_this_update = this_update; } + revoked_certs.extend(entries); } Ok((latest_this_update, revoked_certs)) } +pub fn parse_der_crl( + crl_der: &[u8], + signing_cert_chain_der: &[u8], + root_cert: Option<&[u8]>, + now: u64, +) -> Result<(u64, Crl), CertificateError> { + let crl = CertificateList::from_der(crl_der).map_err(|_| CertificateError::Parse)?; + let certs = parse_concat_der_certs(signing_cert_chain_der)?; + let sign_cert = verify_cert_chain(certs, root_cert, None, now)?; + let sign_key = signing_key_from_cert(&sign_cert)?; + process_crl(&crl, &sign_key) +} + +fn parse_concat_der_certs(data: &[u8]) -> Result, CertificateError> { + let mut reader = SliceReader::new(data).map_err(|_| CertificateError::Parse)?; + let mut certs = Vec::new(); + while !reader.is_finished() { + let cert = Certificate::decode(&mut reader).map_err(|_| CertificateError::Parse)?; + certs.push(cert); + } + Ok(certs) +} + #[cfg(test)] mod should { - use crate::cert::{parse_crl, CertificateError}; + use crate::cert::{parse_der_crl, parse_pem_crl, verify_der_cert_chain, CertificateError}; + use crate::nitro; use chrono::DateTime; use rstest::rstest; use std::{fs::File, io::Read}; + /// Attestation doc 1 timestamp as seconds since epoch (2022-11-09T22:52:00Z). + const NITRO_ATT_DOC_1_TIMESTAMP: u64 = 1668034320; + fn load_file(path: &str) -> Vec { let mut f = File::open(path).unwrap(); let mut buf = Vec::new(); @@ -355,10 +417,24 @@ mod should { buf } - fn load_root_cert() -> Vec { + fn load_intel_root_cert() -> Vec { load_file("assets/Intel_SGX_Provisioning_Certification_RootCA.cer") } + fn load_nitro_root_cert() -> Vec { + load_file("assets/aws_nitro_root_g1.der") + } + + /// Parse the Nitro attestation doc and return the DER cert chain + /// (leaf first, then cabundle reversed) ready for verify_der_cert_chain. + fn nitro_cert_chain() -> Vec> { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = nitro::parse_nitro_attestation(&data).unwrap(); + let mut chain = vec![att.certificate]; + chain.extend(att.cabundle.into_iter().rev()); + chain + } + #[rstest] #[case( "assets/tests/intel/crl.pem", @@ -379,14 +455,14 @@ mod should { fn parse_quote(#[case] crl_path: &str, #[case] crl_chain_path: &str, #[case] exp_date: &str) { let crl_buf = load_file(crl_path); let crl_chain_buf = load_file(crl_chain_path); - let root_buf = load_root_cert(); + let root_buf = load_intel_root_cert(); let now = DateTime::parse_from_rfc3339(exp_date) .unwrap() .timestamp() .try_into() .unwrap(); - let (date, _crl) = parse_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now).unwrap(); + let (date, _crl) = parse_pem_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now).unwrap(); assert_eq!(date, now); } @@ -404,11 +480,11 @@ mod should { ) { let crl_buf = load_file(crl_path); let crl_chain_buf = load_file(crl_chain_path); - let root_buf = load_root_cert(); + let root_buf = load_intel_root_cert(); let now = DateTime::parse_from_rfc3339(timestamp).unwrap().timestamp() as u64; - let result = parse_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now); + let result = parse_pem_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now); assert!(matches!( result, Err(CertificateError::CertificateNotYetValid) @@ -429,11 +505,71 @@ mod should { ) { let crl_buf = load_file(crl_path); let crl_chain_buf = load_file(crl_chain_path); - let root_buf = load_root_cert(); + let root_buf = load_intel_root_cert(); let now = DateTime::parse_from_rfc3339(timestamp).unwrap().timestamp() as u64; - let result = parse_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now); + let result = parse_pem_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now); assert!(matches!(result, Err(CertificateError::CertificateExpired))); } + + #[test] + fn verify_nitro_der_cert_chain() { + let chain = nitro_cert_chain(); + let refs: Vec<&[u8]> = chain.iter().map(|c| c.as_slice()).collect(); + let nitro_root = load_nitro_root_cert(); + let now = NITRO_ATT_DOC_1_TIMESTAMP; + let leaf = verify_der_cert_chain(&refs, Some(&nitro_root), None, now).unwrap(); + assert!(leaf + .tbs_certificate + .subject + .to_string() + .contains("i-04fd167167daacf3b")); + } + + #[test] + fn reject_nitro_chain_with_wrong_root() { + let chain = nitro_cert_chain(); + let refs: Vec<&[u8]> = chain.iter().map(|c| c.as_slice()).collect(); + let now = NITRO_ATT_DOC_1_TIMESTAMP; + // Use the Intel root cert instead of the Nitro one + let intel_root = load_intel_root_cert(); + let result = verify_der_cert_chain(&refs, Some(&intel_root), None, now); + assert!(matches!(result, Err(CertificateError::KeyVerification))); + } + + /// Concatenate DER certs into a flat byte buffer. + fn concat_der(certs: &[&[u8]]) -> Vec { + certs.iter().flat_map(|c| c.iter().copied()).collect() + } + + #[test] + fn parse_nitro_der_crl() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = nitro::parse_nitro_attestation(&data).unwrap(); + let nitro_root = load_nitro_root_cert(); + let now = NITRO_ATT_DOC_1_TIMESTAMP; + + // Zonal CRL signed by cabundle[1] (first intermediate); + // its signing chain is cabundle[1] → cabundle[0] → root. + let crl_buf = load_file("assets/tests/nitro/crl_zonal.der"); + let sign_chain = concat_der(&[&att.cabundle[1], &att.cabundle[0]]); + let (date, crl) = parse_der_crl(&crl_buf, &sign_chain, Some(&nitro_root), now).unwrap(); + // 2022-11-12T00:21:29Z + assert_eq!(date, 1668212489); + assert!(crl.is_empty()); + } + + #[test] + fn reject_nitro_crl_with_wrong_signer() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = nitro::parse_nitro_attestation(&data).unwrap(); + let nitro_root = load_nitro_root_cert(); + let now = NITRO_ATT_DOC_1_TIMESTAMP; + + // Zonal CRL verified against the wrong cert (cabundle[0] = root, not the intermediate) + let crl_buf = load_file("assets/tests/nitro/crl_zonal.der"); + let result = parse_der_crl(&crl_buf, &att.cabundle[0], Some(&nitro_root), now); + assert!(matches!(result, Err(CertificateError::KeyVerification))); + } } diff --git a/src/intel/collaterals.rs b/src/intel/collaterals.rs index a6abeed..f86de9f 100644 --- a/src/intel/collaterals.rs +++ b/src/intel/collaterals.rs @@ -230,7 +230,10 @@ pub fn compare_tcb_levels(quote_tcb: &TcbSvn, levels: &Vec) -> (TcbSta /// Compare SGX TCB SVNs extracted from the PCK certificate against the /// sgxtcbcomponents in each TCB level of the collateral. -pub fn compare_sgx_tcb_levels(cert_tcb: &TcbSvn, levels: &Vec) -> (TcbStatus, PceSvn) { +pub fn compare_sgx_tcb_levels( + cert_tcb: &TcbSvn, + levels: &Vec, +) -> (TcbStatus, PceSvn) { for tcb_level in levels { let Some(coll_tcb) = tcb_level.tcb.sgx_components.as_ref() else { continue; diff --git a/src/lib.rs b/src/lib.rs index ab1ae67..c6de6bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,27 +24,44 @@ use alloc::vec::Vec; mod cert; mod intel; +mod nitro; pub use crate::{ cert::{CertificateError, Crl, RevokedCertId}, intel::{CollateralError, ParseError, QuoteV4, TcbResponse, VerificationError}, + nitro::{NitroAttestation, NitroParseError, NitroVerificationError}, }; // ============================================================================= // Generic // ============================================================================= -/// Parse a CRL from PEM data and validates its signature. +/// Parse CRLs from PEM data and validate their signatures against a PEM certificate chain. /// Returns: /// - the most recent issue date for all the included CRLs /// - a Vec of revoked certificates, identified by the (issuer, serial_number) pair. -pub fn parse_crl( +pub fn parse_pem_crl( crl_pem: &Vec, pck_certificate_chain_pem: &Vec, root_cert: Option<&[u8]>, now: u64, ) -> Result<(u64, Crl), CertificateError> { - cert::parse_crl(crl_pem, pck_certificate_chain_pem, root_cert, now) + cert::parse_pem_crl(crl_pem, pck_certificate_chain_pem, root_cert, now) +} + +/// Parse a CRL from DER data and validate its signature against a DER certificate chain. +/// The signing chain is a flat byte slice of concatenated DER-encoded certificates +/// (leaf first, root last), matching the PEM variant's flat-buffer approach. +/// Returns: +/// - the CRL's issue date +/// - a Vec of revoked certificates, identified by the (issuer, serial_number) pair. +pub fn parse_der_crl( + crl_der: &[u8], + signing_cert_chain_der: &[u8], + root_cert: Option<&[u8]>, + now: u64, +) -> Result<(u64, Crl), CertificateError> { + cert::parse_der_crl(crl_der, signing_cert_chain_der, root_cert, now) } // ============================================================================= @@ -60,3 +77,12 @@ pub fn parse_tcb_response(input: &[u8]) -> Result pub fn parse_quote(input: &[u8]) -> Result { intel::parse_quote(input) } + +// ============================================================================= +// Nitro Specific +// ============================================================================= + +/// Parse an AWS Nitro Enclave attestation document from binary COSE_Sign1 data. +pub fn parse_nitro_attestation(input: &[u8]) -> Result { + nitro::parse_nitro_attestation(input) +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 5a6a6e8..457f44f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -14,3 +14,4 @@ // limitations under the License. mod intel; +mod nitro; diff --git a/tests/intel/mod.rs b/tests/intel/mod.rs index 8aa15e9..50221d5 100644 --- a/tests/intel/mod.rs +++ b/tests/intel/mod.rs @@ -25,7 +25,7 @@ use std::io::Read; use assert_ok::assert_ok; use chrono::DateTime; -use tee_verifier::{parse_crl, parse_quote, parse_tcb_response, Crl, VerificationError}; +use tee_verifier::{parse_pem_crl, parse_quote, parse_tcb_response, Crl, VerificationError}; /// Helper function to load a file into a byte vector fn load_file(path: &str) -> Vec { @@ -70,7 +70,7 @@ mod end_to_end { let now = parse_timestamp("2026-02-03T09:32:53Z"); - let (_crl_time, crl) = parse_crl(&crl_data, &crl_chain, Some(&root_cert), now).unwrap(); + let (_crl_time, crl) = parse_pem_crl(&crl_data, &crl_chain, Some(&root_cert), now).unwrap(); // Verify that the tcb data is valid and signed let tcb_response = parse_tcb_response(&tcb_data).unwrap(); From 34bf17f256ae4ee039e3e0ee5781a46979ace495 Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Wed, 11 Mar 2026 13:21:39 +0100 Subject: [PATCH 3/9] Enforce only one CRL per parse --- src/cert.rs | 48 ++++++++++++++++++++++++++++------------------ src/intel/quote.rs | 26 ++++++++++++++----------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/cert.rs b/src/cert.rs index 4959e17..9536253 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -49,6 +49,8 @@ pub enum CertificateError { CertificateNotYetValid, /// The certificate has expired. CertificateExpired, + /// Expected exactly one CRL, but found multiple. + MultipleCrls, } /// Identifies a revoked certificate by its issuer and serial number. @@ -315,8 +317,6 @@ fn process_crl( ) -> Result<(u64, Crl), CertificateError> { verify_crl(crl, sign_key)?; - let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); - let issuer = crl .tbs_cert_list .issuer @@ -333,6 +333,7 @@ fn process_crl( } } + let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); Ok((this_update, revoked_certs)) } @@ -353,27 +354,19 @@ pub fn parse_pem_crl( now: u64, ) -> Result<(u64, Crl), CertificateError> { let pems = pem::parse_many(crl_pem).map_err(|_| CertificateError::Parse)?; - let crls: Result, _> = pems - .into_iter() - .map(|pem| CertificateList::from_der(pem.contents())) - .collect(); - let crls = crls.map_err(|_| CertificateError::Parse)?; + if pems.is_empty() { + return Err(CertificateError::Parse); + } + if pems.len() > 1 { + return Err(CertificateError::MultipleCrls); + } + let crl = + CertificateList::from_der(pems[0].contents()).map_err(|_| CertificateError::Parse)?; let sign_cert = verify_pem_cert_chain(pck_certificate_chain_pem, root_cert, None, now)?; let sign_key = signing_key_from_cert(&sign_cert)?; - let mut revoked_certs: Crl = Vec::new(); - let mut latest_this_update: u64 = 0; - - for crl in &crls { - let (this_update, entries) = process_crl(crl, &sign_key)?; - if this_update > latest_this_update { - latest_this_update = this_update; - } - revoked_certs.extend(entries); - } - - Ok((latest_this_update, revoked_certs)) + process_crl(&crl, &sign_key) } pub fn parse_der_crl( @@ -513,6 +506,23 @@ mod should { assert!(matches!(result, Err(CertificateError::CertificateExpired))); } + #[test] + fn reject_multiple_crls() { + let crl_buf = load_file("assets/tests/intel/crl.pem"); + let crl_chain_buf = load_file("assets/tests/intel/crl_chain.pem"); + let root_buf = load_intel_root_cert(); + let now = DateTime::parse_from_rfc3339("2026-02-03T09:32:53Z") + .unwrap() + .timestamp() as u64; + + // Concatenate two copies of the same CRL PEM + let mut double_crl = crl_buf.clone(); + double_crl.extend_from_slice(&crl_buf); + + let result = parse_pem_crl(&double_crl, &crl_chain_buf, Some(&root_buf), now); + assert!(matches!(result, Err(CertificateError::MultipleCrls))); + } + #[test] fn verify_nitro_der_cert_chain() { let chain = nitro_cert_chain(); diff --git a/src/intel/quote.rs b/src/intel/quote.rs index fef2acd..0ba463e 100644 --- a/src/intel/quote.rs +++ b/src/intel/quote.rs @@ -713,20 +713,24 @@ mod should { } #[rstest] - #[case( - "assets/tests/intel/quote_b0.dat", - "assets/tests/intel/tcb_info_b0.json" - )] + fn verify_quote() { + let quote_buf = load_file("assets/tests/intel/quote_b0.dat"); + let q = parse_quote("e_buf).unwrap(); + + let tcb_buf = load_file("assets/tests/intel/tcb_info_b0.json"); + let tcb = parse_tcb_response(&tcb_buf).unwrap(); + let crl: crate::cert::Crl = vec![]; + let now: u64 = 1769529377; // Tue Jan 27 2026 15:56:17 GMT+0000 + assert_ok!(q.verify(&tcb.tcb_info, &crl, now)); + } + + #[rstest] #[should_panic(expected = "BadTcbStatus")] - #[case( - "assets/tests/intel/quote_90.dat", - "assets/tests/intel/tcb_info_90.json" - )] - fn verify_quote(#[case] quote_path: &str, #[case] coll_path: &str) { - let quote_buf = load_file(quote_path); + fn verify_quote_with_bad_tcb_status() { + let quote_buf = load_file("assets/tests/intel/quote_90.dat"); let q = parse_quote("e_buf).unwrap(); - let tcb_buf = load_file(coll_path); + let tcb_buf = load_file("assets/tests/intel/tcb_info_90.json"); let tcb = parse_tcb_response(&tcb_buf).unwrap(); let crl: crate::cert::Crl = vec![]; let now: u64 = 1769529377; // Tue Jan 27 2026 15:56:17 GMT+0000 From 6624b65f262e36b518ed866b1ddc7313c0239adb Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Wed, 11 Mar 2026 18:03:20 +0100 Subject: [PATCH 4/9] Cosmetics --- src/cert.rs | 39 +++++++++++++++++++-------------------- src/intel/collaterals.rs | 7 ++----- src/intel/quote.rs | 2 +- src/lib.rs | 16 ++++++++-------- src/nitro.rs | 4 ++-- src/nitro/attestation.rs | 4 ++-- tests/intel/mod.rs | 18 +++++++++--------- tests/nitro/mod.rs | 26 +++++++++++++------------- 8 files changed, 56 insertions(+), 60 deletions(-) diff --git a/src/cert.rs b/src/cert.rs index 9536253..95cf6b6 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -82,7 +82,7 @@ fn verify_crl(crl: &CertificateList, key: &VerifyingKey) -> Result<(), Certifica .map_err(|_| CertificateError::KeyVerification) } -pub fn verify_pem_cert_chain( +pub fn verify_cert_chain_pem( pck_certificate_chain_pem: &Vec, root_cert: Option<&[u8]>, crl: Option<&Crl>, @@ -97,7 +97,7 @@ pub fn verify_pem_cert_chain( verify_cert_chain(certs, root_cert, crl, now) } -pub fn verify_der_cert_chain( +pub fn verify_cert_chain_der( der_certs: &[&[u8]], root_cert: Option<&[u8]>, crl: Option<&Crl>, @@ -347,7 +347,7 @@ fn signing_key_from_cert(cert: &Certificate) -> Result, pck_certificate_chain_pem: &Vec, root_cert: Option<&[u8]>, @@ -360,16 +360,15 @@ pub fn parse_pem_crl( if pems.len() > 1 { return Err(CertificateError::MultipleCrls); } - let crl = - CertificateList::from_der(pems[0].contents()).map_err(|_| CertificateError::Parse)?; + let crl = CertificateList::from_der(pems[0].contents()).map_err(|_| CertificateError::Parse)?; - let sign_cert = verify_pem_cert_chain(pck_certificate_chain_pem, root_cert, None, now)?; + let sign_cert = verify_cert_chain_pem(pck_certificate_chain_pem, root_cert, None, now)?; let sign_key = signing_key_from_cert(&sign_cert)?; process_crl(&crl, &sign_key) } -pub fn parse_der_crl( +pub fn parse_crl_der( crl_der: &[u8], signing_cert_chain_der: &[u8], root_cert: Option<&[u8]>, @@ -394,7 +393,7 @@ fn parse_concat_der_certs(data: &[u8]) -> Result, CertificateEr #[cfg(test)] mod should { - use crate::cert::{parse_der_crl, parse_pem_crl, verify_der_cert_chain, CertificateError}; + use crate::cert::{parse_crl_der, parse_crl_pem, verify_cert_chain_der, CertificateError}; use crate::nitro; use chrono::DateTime; use rstest::rstest; @@ -419,10 +418,10 @@ mod should { } /// Parse the Nitro attestation doc and return the DER cert chain - /// (leaf first, then cabundle reversed) ready for verify_der_cert_chain. + /// (leaf first, then cabundle reversed) ready for verify_cert_chain_der. fn nitro_cert_chain() -> Vec> { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = nitro::parse_nitro_attestation(&data).unwrap(); + let att = nitro::parse_attestation(&data).unwrap(); let mut chain = vec![att.certificate]; chain.extend(att.cabundle.into_iter().rev()); chain @@ -455,7 +454,7 @@ mod should { .timestamp() .try_into() .unwrap(); - let (date, _crl) = parse_pem_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now).unwrap(); + let (date, _crl) = parse_crl_pem(&crl_buf, &crl_chain_buf, Some(&root_buf), now).unwrap(); assert_eq!(date, now); } @@ -477,7 +476,7 @@ mod should { let now = DateTime::parse_from_rfc3339(timestamp).unwrap().timestamp() as u64; - let result = parse_pem_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now); + let result = parse_crl_pem(&crl_buf, &crl_chain_buf, Some(&root_buf), now); assert!(matches!( result, Err(CertificateError::CertificateNotYetValid) @@ -502,7 +501,7 @@ mod should { let now = DateTime::parse_from_rfc3339(timestamp).unwrap().timestamp() as u64; - let result = parse_pem_crl(&crl_buf, &crl_chain_buf, Some(&root_buf), now); + let result = parse_crl_pem(&crl_buf, &crl_chain_buf, Some(&root_buf), now); assert!(matches!(result, Err(CertificateError::CertificateExpired))); } @@ -519,7 +518,7 @@ mod should { let mut double_crl = crl_buf.clone(); double_crl.extend_from_slice(&crl_buf); - let result = parse_pem_crl(&double_crl, &crl_chain_buf, Some(&root_buf), now); + let result = parse_crl_pem(&double_crl, &crl_chain_buf, Some(&root_buf), now); assert!(matches!(result, Err(CertificateError::MultipleCrls))); } @@ -529,7 +528,7 @@ mod should { let refs: Vec<&[u8]> = chain.iter().map(|c| c.as_slice()).collect(); let nitro_root = load_nitro_root_cert(); let now = NITRO_ATT_DOC_1_TIMESTAMP; - let leaf = verify_der_cert_chain(&refs, Some(&nitro_root), None, now).unwrap(); + let leaf = verify_cert_chain_der(&refs, Some(&nitro_root), None, now).unwrap(); assert!(leaf .tbs_certificate .subject @@ -544,7 +543,7 @@ mod should { let now = NITRO_ATT_DOC_1_TIMESTAMP; // Use the Intel root cert instead of the Nitro one let intel_root = load_intel_root_cert(); - let result = verify_der_cert_chain(&refs, Some(&intel_root), None, now); + let result = verify_cert_chain_der(&refs, Some(&intel_root), None, now); assert!(matches!(result, Err(CertificateError::KeyVerification))); } @@ -556,7 +555,7 @@ mod should { #[test] fn parse_nitro_der_crl() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = nitro::parse_nitro_attestation(&data).unwrap(); + let att = nitro::parse_attestation(&data).unwrap(); let nitro_root = load_nitro_root_cert(); let now = NITRO_ATT_DOC_1_TIMESTAMP; @@ -564,7 +563,7 @@ mod should { // its signing chain is cabundle[1] → cabundle[0] → root. let crl_buf = load_file("assets/tests/nitro/crl_zonal.der"); let sign_chain = concat_der(&[&att.cabundle[1], &att.cabundle[0]]); - let (date, crl) = parse_der_crl(&crl_buf, &sign_chain, Some(&nitro_root), now).unwrap(); + let (date, crl) = parse_crl_der(&crl_buf, &sign_chain, Some(&nitro_root), now).unwrap(); // 2022-11-12T00:21:29Z assert_eq!(date, 1668212489); assert!(crl.is_empty()); @@ -573,13 +572,13 @@ mod should { #[test] fn reject_nitro_crl_with_wrong_signer() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = nitro::parse_nitro_attestation(&data).unwrap(); + let att = nitro::parse_attestation(&data).unwrap(); let nitro_root = load_nitro_root_cert(); let now = NITRO_ATT_DOC_1_TIMESTAMP; // Zonal CRL verified against the wrong cert (cabundle[0] = root, not the intermediate) let crl_buf = load_file("assets/tests/nitro/crl_zonal.der"); - let result = parse_der_crl(&crl_buf, &att.cabundle[0], Some(&nitro_root), now); + let result = parse_crl_der(&crl_buf, &att.cabundle[0], Some(&nitro_root), now); assert!(matches!(result, Err(CertificateError::KeyVerification))); } } diff --git a/src/intel/collaterals.rs b/src/intel/collaterals.rs index f86de9f..77a4365 100644 --- a/src/intel/collaterals.rs +++ b/src/intel/collaterals.rs @@ -152,7 +152,7 @@ impl TcbResponse { crl: &crate::cert::Crl, now: u64, ) -> Result<(), CollateralError> { - let cert = crate::cert::verify_pem_cert_chain( + let cert = crate::cert::verify_cert_chain_pem( &certs, Some(crate::intel::ROOT_CERT), Some(crl), @@ -230,10 +230,7 @@ pub fn compare_tcb_levels(quote_tcb: &TcbSvn, levels: &Vec) -> (TcbSta /// Compare SGX TCB SVNs extracted from the PCK certificate against the /// sgxtcbcomponents in each TCB level of the collateral. -pub fn compare_sgx_tcb_levels( - cert_tcb: &TcbSvn, - levels: &Vec, -) -> (TcbStatus, PceSvn) { +pub fn compare_sgx_tcb_levels(cert_tcb: &TcbSvn, levels: &Vec) -> (TcbStatus, PceSvn) { for tcb_level in levels { let Some(coll_tcb) = tcb_level.tcb.sgx_components.as_ref() else { continue; diff --git a/src/intel/quote.rs b/src/intel/quote.rs index 0ba463e..e5aabe9 100644 --- a/src/intel/quote.rs +++ b/src/intel/quote.rs @@ -461,7 +461,7 @@ impl QeCertificationData { ) -> Result<(), VerificationError> { match self.certification_data_type { CERT_DATA_TYPE_PCK_CHAIN => { - let cert = crate::cert::verify_pem_cert_chain( + let cert = crate::cert::verify_cert_chain_pem( &self.certification_data, Some(crate::intel::ROOT_CERT), Some(crl), diff --git a/src/lib.rs b/src/lib.rs index c6de6bd..a51fd3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,13 +40,13 @@ pub use crate::{ /// Returns: /// - the most recent issue date for all the included CRLs /// - a Vec of revoked certificates, identified by the (issuer, serial_number) pair. -pub fn parse_pem_crl( +pub fn parse_crl_pem( crl_pem: &Vec, pck_certificate_chain_pem: &Vec, root_cert: Option<&[u8]>, now: u64, ) -> Result<(u64, Crl), CertificateError> { - cert::parse_pem_crl(crl_pem, pck_certificate_chain_pem, root_cert, now) + cert::parse_crl_pem(crl_pem, pck_certificate_chain_pem, root_cert, now) } /// Parse a CRL from DER data and validate its signature against a DER certificate chain. @@ -55,13 +55,13 @@ pub fn parse_pem_crl( /// Returns: /// - the CRL's issue date /// - a Vec of revoked certificates, identified by the (issuer, serial_number) pair. -pub fn parse_der_crl( +pub fn parse_crl_der( crl_der: &[u8], signing_cert_chain_der: &[u8], root_cert: Option<&[u8]>, now: u64, ) -> Result<(u64, Crl), CertificateError> { - cert::parse_der_crl(crl_der, signing_cert_chain_der, root_cert, now) + cert::parse_crl_der(crl_der, signing_cert_chain_der, root_cert, now) } // ============================================================================= @@ -69,12 +69,12 @@ pub fn parse_der_crl( // ============================================================================= /// Parse a TCB response from JSON bytes. -pub fn parse_tcb_response(input: &[u8]) -> Result { +pub fn intel_parse_tcb_response(input: &[u8]) -> Result { intel::parse_tcb_response(input) } /// Parse an Intel TDX quote from binary data. -pub fn parse_quote(input: &[u8]) -> Result { +pub fn intel_parse_quote(input: &[u8]) -> Result { intel::parse_quote(input) } @@ -83,6 +83,6 @@ pub fn parse_quote(input: &[u8]) -> Result { // ============================================================================= /// Parse an AWS Nitro Enclave attestation document from binary COSE_Sign1 data. -pub fn parse_nitro_attestation(input: &[u8]) -> Result { - nitro::parse_nitro_attestation(input) +pub fn nitro_parse_attestation(input: &[u8]) -> Result { + nitro::parse_attestation(input) } diff --git a/src/nitro.rs b/src/nitro.rs index 6ad63bc..b966b7d 100644 --- a/src/nitro.rs +++ b/src/nitro.rs @@ -20,6 +20,6 @@ mod attestation; pub use attestation::{NitroAttestation, NitroParseError, NitroVerificationError}; /// Parse a Nitro attestation document from binary COSE_Sign1 data. -pub fn parse_nitro_attestation(input: &[u8]) -> Result { - attestation::parse_nitro_attestation(input) +pub fn parse_attestation(input: &[u8]) -> Result { + attestation::parse_attestation(input) } diff --git a/src/nitro/attestation.rs b/src/nitro/attestation.rs index 62de515..adf4a81 100644 --- a/src/nitro/attestation.rs +++ b/src/nitro/attestation.rs @@ -84,7 +84,7 @@ pub struct NitroAttestation { } /// Parse a Nitro attestation document from binary COSE_Sign1 data. -pub fn parse_nitro_attestation(input: &[u8]) -> Result { +pub fn parse_attestation(input: &[u8]) -> Result { // Decode COSE_Sign1 — accept both tagged (CBOR tag 18) and untagged forms let cose_sign1 = CoseSign1::from_tagged_slice(input) .or_else(|_| CoseSign1::from_slice(input)) @@ -180,7 +180,7 @@ impl NitroAttestation { } // Verify the certificate chain against the embedded AWS root - let leaf = crate::cert::verify_der_cert_chain( + let leaf = crate::cert::verify_cert_chain_der( &chain_refs, Some(crate::nitro::ROOT_CERT), crl, diff --git a/tests/intel/mod.rs b/tests/intel/mod.rs index 50221d5..df1851c 100644 --- a/tests/intel/mod.rs +++ b/tests/intel/mod.rs @@ -25,7 +25,7 @@ use std::io::Read; use assert_ok::assert_ok; use chrono::DateTime; -use tee_verifier::{parse_pem_crl, parse_quote, parse_tcb_response, Crl, VerificationError}; +use tee_verifier::{parse_crl_pem, intel_parse_quote, intel_parse_tcb_response, Crl, VerificationError}; /// Helper function to load a file into a byte vector fn load_file(path: &str) -> Vec { @@ -70,14 +70,14 @@ mod end_to_end { let now = parse_timestamp("2026-02-03T09:32:53Z"); - let (_crl_time, crl) = parse_pem_crl(&crl_data, &crl_chain, Some(&root_cert), now).unwrap(); + let (_crl_time, crl) = parse_crl_pem(&crl_data, &crl_chain, Some(&root_cert), now).unwrap(); // Verify that the tcb data is valid and signed - let tcb_response = parse_tcb_response(&tcb_data).unwrap(); + let tcb_response = intel_parse_tcb_response(&tcb_data).unwrap(); assert_ok!(tcb_response.verify(tcb_chain, &crl, now)); // Verify the quote - let quote = parse_quote("e_data).unwrap(); + let quote = intel_parse_quote("e_data).unwrap(); assert_ok!(quote.verify(&tcb_response.tcb_info, &crl, now)); } } @@ -92,7 +92,7 @@ mod quote_parsing { #[test] fn parse_valid_quote_90() { let quote_data = load_file("assets/tests/intel/quote_90.dat"); - assert_ok!(parse_quote("e_data)); + assert_ok!(intel_parse_quote("e_data)); } } @@ -108,8 +108,8 @@ mod quote_verification_errors { let quote_data = load_file("assets/tests/intel/quote_no_cert.dat"); let tcb_data = load_file("assets/tests/intel/tcb_info_90.json"); - let quote = parse_quote("e_data).unwrap(); - let tcb_response = parse_tcb_response(&tcb_data).unwrap(); + let quote = intel_parse_quote("e_data).unwrap(); + let tcb_response = intel_parse_tcb_response(&tcb_data).unwrap(); let crl: Crl = vec![]; let now = parse_timestamp("2026-02-03T09:32:53Z"); @@ -126,8 +126,8 @@ mod quote_verification_errors { // And the tcb data for the FMSPC starting with B0 let tcb_data = load_file("assets/tests/intel/tcb_info_b0.json"); - let quote = parse_quote("e_data).unwrap(); - let tcb_response = parse_tcb_response(&tcb_data).unwrap(); + let quote = intel_parse_quote("e_data).unwrap(); + let tcb_response = intel_parse_tcb_response(&tcb_data).unwrap(); let crl: Crl = vec![]; let now = parse_timestamp("2026-02-03T09:32:53Z"); diff --git a/tests/nitro/mod.rs b/tests/nitro/mod.rs index 2a814f9..292b612 100644 --- a/tests/nitro/mod.rs +++ b/tests/nitro/mod.rs @@ -24,7 +24,7 @@ use std::io::Read; use assert_ok::assert_ok; use tee_verifier::{ - parse_der_crl, parse_nitro_attestation, Crl, NitroParseError, NitroVerificationError, + parse_crl_der, nitro_parse_attestation, Crl, NitroParseError, NitroVerificationError, }; /// Helper function to load a file into a byte vector @@ -44,20 +44,20 @@ mod nitro_parsing { #[test] fn parse_empty_input_fails() { - let result = parse_nitro_attestation(&[]); + let result = nitro_parse_attestation(&[]); assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); } #[test] fn parse_invalid_cbor_fails() { - let result = parse_nitro_attestation(&[0xFF, 0xFF, 0xFF]); + let result = nitro_parse_attestation(&[0xFF, 0xFF, 0xFF]); assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); } #[test] fn parse_valid_attestation_doc() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = assert_ok!(parse_nitro_attestation(&data)); + let att = assert_ok!(nitro_parse_attestation(&data)); assert_eq!(att.module_id, "i-04fd167167daacf3b-enc01845e97d0b3eb4e"); assert_eq!(att.digest, "SHA384"); @@ -73,7 +73,7 @@ mod nitro_parsing { #[test] fn parse_valid_attestation_doc_2() { let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); - let att = assert_ok!(parse_nitro_attestation(&data)); + let att = assert_ok!(nitro_parse_attestation(&data)); assert_eq!(att.module_id, "i-0a22e5c5f24d22174-enc0191cceb4289903f"); assert_eq!(att.digest, "SHA384"); @@ -95,7 +95,7 @@ mod end_to_end { #[test] fn verify_attestation_doc() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = parse_nitro_attestation(&data).unwrap(); + let att = nitro_parse_attestation(&data).unwrap(); // Use the document's own timestamp (converted from ms to seconds) // since the embedded certificates have a short validity window. @@ -107,7 +107,7 @@ mod end_to_end { #[test] fn verify_attestation_doc_2() { let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); - let att = parse_nitro_attestation(&data).unwrap(); + let att = nitro_parse_attestation(&data).unwrap(); // 2024-09-07T14:37:39Z let now = att.timestamp / 1000; @@ -118,7 +118,7 @@ mod end_to_end { fn verify_attestation_doc_with_crl() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); let nitro_root = load_file("assets/aws_nitro_root_g1.der"); - let att = parse_nitro_attestation(&data).unwrap(); + let att = nitro_parse_attestation(&data).unwrap(); // 2022-11-09T22:52:00Z let now = att.timestamp / 1000; @@ -138,9 +138,9 @@ mod end_to_end { .collect(); let (_time1, crl1) = - parse_der_crl(&root_crl_data, &att.cabundle[0], Some(&nitro_root), now).unwrap(); + parse_crl_der(&root_crl_data, &att.cabundle[0], Some(&nitro_root), now).unwrap(); let (_time2, crl2) = - parse_der_crl(&zonal_crl_data, &zonal_chain, Some(&nitro_root), now).unwrap(); + parse_crl_der(&zonal_crl_data, &zonal_chain, Some(&nitro_root), now).unwrap(); let mut crl: Crl = crl1; crl.extend(crl2); @@ -159,7 +159,7 @@ mod verification_errors { #[test] fn verify_bad_signature_fails() { let data = load_file("assets/tests/nitro/attestation_doc_bad_sig.bin"); - let att = parse_nitro_attestation(&data).unwrap(); + let att = nitro_parse_attestation(&data).unwrap(); // 2022-11-09T22:52:00Z let now = att.timestamp / 1000; @@ -170,7 +170,7 @@ mod verification_errors { #[test] fn verify_with_expired_timestamp_fails() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = parse_nitro_attestation(&data).unwrap(); + let att = nitro_parse_attestation(&data).unwrap(); // One year after the document timestamp — certificates will have expired. // ~2023-11-09 @@ -185,7 +185,7 @@ mod verification_errors { #[test] fn verify_with_past_timestamp_fails() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = parse_nitro_attestation(&data).unwrap(); + let att = nitro_parse_attestation(&data).unwrap(); // 1970-01-01T00:00:00Z — certificates will not yet be valid. let now = 0; From 16ec8aa55613027dfd0c802eafcd9ca1b46140cc Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Fri, 13 Mar 2026 16:01:15 +0100 Subject: [PATCH 5/9] Replace test attestation with a vela one --- assets/tests/nitro/attestation_doc_2.bin | Bin 7539 -> 4617 bytes tests/intel/mod.rs | 4 +++- tests/nitro/mod.rs | 13 +++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/assets/tests/nitro/attestation_doc_2.bin b/assets/tests/nitro/attestation_doc_2.bin index 38e0b633ebcbe3e98f63d92abd2bb7516d9e1984..40b88dd5c3b09f117331cd7bb9f06518d62ae87f 100644 GIT binary patch delta 2201 zcma)+c~DbV8^*J-$R;3$MOG_eOTxW3A&a44MUhri!~lt?kmUw~AwZB#`3xe02uA4r zG$NY{MFcBgD57j4Di#%FQ*ptCMXFL%#2xv9+Wyh8GvApxe>`*MJ#*gQ^PG2bapoeA zv}H%ojD?4}Mujcn$S7f)1P&1k6CK1PfX)LMFkeU!@4RpCh=r1PQiZ=2n)`5TS6=4>M}5xIqWb-+3`E6ofQa~hoCOVlKnh5Mm>{s;R3G{xnQBQvo_)`;||INhz= z8tr3#tNQun2Wz{%PcM+kCTFV+w-=8{KzRk5Rr+Uh>pQB3EP;!=zI<*yE97PpqR=49 z#VA080a3jTSNiUqgjtrw8q+Ii5`9NKL{XWU{7Xe=`{V1IQ9B$CaZ~Mz{wQxR-S?)= zY7I+W(w33xdO*XgwYn3vP0Bv%_=+4J@Er{|boXM;bYnD-NcM_dDSPPznffhgOR3p3 z5-#BJSA#YJQqc!g3{6{%;gwP;=HqDRvkZ9aDVCE+g!pL&FPOy#Fd)bc za-)5v2{9nbtV9O27vn(zl}`uhOe&oR@ZETVzrAn`tzO8`CGGX@Py`1D9*~roS91Dn z%68jw@lS^|ZKd=mM@|15#`m?KMK97@*4Of@!=ZHF!b=;jO)O)jn?ogJ_ds*+l0_z` z|C8pT48l3JPq?0mdArc>(r{0CXR@I9_(r5@j<*S%d7!&f&|BgalYR8VLZAH`i1}i~ zR3}FcqB1F)M7#&yb^eEtBADAlWX?$J}b&1>6+FgucID#jB4yU z7i$irVrMbtf61#6s%jvYIL}}xO#73Sd?REtgGR`@%m1Cy=fp1m5>X072k3t#mI*LF zOJGMP=ngE_#0KE7dX(8gAp%7_H&7sAP-zf(Ym}5H`J1^Za>&3JS*2$h>fJ!-avn9V znA^N#_bAuGL3Q$Y-uYdRL6_y`=DX<|ch=R9{z)au0^dyM3}WIH4vujFPZ}y^N%57L zUNWPnVM(udHl|s6j;KWaMN?frG}sde@2FwxANZqE(rS!vn<5wV94Oyn4_K+6YGr`K z<4m%G4!E(|5sS53exE$C!o`uYzl4lf>$`@2{;siGNFa%q5KEGZx5%PqKcR#d566;Y zDPP)|Jty-bd9xiR#F7P3k*)%n#1-OE85AZ&B?;YlOcE7l3Q1}uAb~t4K!G5ph)IQz zTphgH|EvAyO;TP6g6jphW=tDSqjh?&=^X5saMk;72_>gG;b2!8s|>$$^^Hs2kG2Th z;YbX?Il96o;o`A!i~6GCOXznym%7Z8bq4^JlcrkhKH@g%$`9s0R?dq6wD&ye2*0Cq zj*Dqid*7no%fI+PA2s187;Ur6*sa%#9k;&5f5z-uq%nxBGO)1xKfLo>@O5^96bj?7 zBv3)xXNh!Sh5tVKTtoNW7*Q6aRbx90KmFnIy`uFgYnNFWoL*0hy4gSgY)^n!(mErZi1EQUscQYu*d^^^v`dCE! z?z>Lyxt{Y814D{D=soDNlobNgxmmc7gv5bRU;7TgX4dlWaAp8L<(sAhI-1(f={}v` zZDS>9vpp0gx)E4!)% zTA*ZA-BVAxw_2N8T=rEKhvnTTy4U^>h3}3y literal 7539 zcmeH~3s6&M7RQsD5Z>>CA_7`0y3mC0CLtsiK@kDvAqr?>)p&Dr1JNWPNqG1ei2_Qk z7F6(w0;y746~qTcd{Gn@A0QyCv?vcjTBvMS(A5QHZ^Q>w+?~vHcRC}PNoLNMd+z;m zzTf|UPLh0bi0qHEd3L%-#nLFDQiAct!uY9TDnRIT%nfxD(V0Rz9d>6@F&PSA4veCh zfJtX_IDjD%3B?hZTpB`>^V>>-oEQYy!*Sd>VwxE`gtXh3rMpI7Trur zz80T$_YT8P8t-fH&^ZSQ+aBz6v6(*3Ni100;P`VQ-)jZW_tc$&@^2w7sBgN^WuQKg z=$aK?Rof*CQvCWWU+=}tt9o0UIF6pHpK zy4t7cX`iC6eTsqhDTdmo7-^qktbK}!_9>>?r>sJ;7=>6QMiB+ZgEoMT&|3UW%qBrZ z2r|4sv6LLtFmQ(&xFF)*Uck|JyHON{Wl;tPgGq*;qYVGeNDL?k0H ziZRfXaw>+%6;zmp#K~zgu_7i)V-^x57Lx<}amI8ez;6IE8SV_$k~g6{9&(ld)z1IL zhCPAD(1zVc+3;viHu6V+SYSrceOXk9NYQaokqIA@1O)O#*W$m~FQ0$5B2AQsMID{l znK5Y7ZP-(B_V0~dha3~{zYt6-*)yv)HgB1SE_a%wNxBxL9sGy+F>I1eLFs;`$d}*U zH*b^Q(R%#~og53nY=^};C0{?klCay4b!vS~a+%r=*a9Pb=XRDv;vg9U1YoF%Ovz*t zM7cctq)CwkfI%?faJ}`jYYp+`R@-K%t`4C76YClZmsd;4Zl^dE)m8Zg%3JL>g&hy_ z=1OWgn^@%o5C}cF^`{z}P0tV4oGZ;s4!hkNJc-v^QpOHrx2`KIx?g;SRTvltyCy!QY)8)neizzxTUITYfp2oL|rN4@9l2cWR-BYLI1k(oMP^)w9hJ>TQ0)gUWC&F5&MvuerDrOMW`b z>)5j6FPzJFKZpH?@9d8}tk?F!#O;Odrr|>4HgqQJ_A&%Qtq^1Z<`9BZ45hPCfdD}; zzy=(qaHK!{-sIkc4Ay7TarkjlY0BB~pQ!N9A{au>0gH2lKhunhSe;97H!ZO#rNq5O>7 z*VD|tSl$2W)Y@N6$G62k_MF@@v-#`(!C+aNjz`UlZ~l6Z#H8aC*B%V9ng0Fr=8o9E zRO`mg$3XL0n~a`hLQ|VfpiN)p0gt5sb<)j5g`h$Cb$D(7!y;CN!UwNh1V z6`|Vaq$HdWU*yTD@5n%vIHZFF5C@dm^6b?qQLl7>V@rKA2vOfvg+>u?z8^&!Ob&{O zgziEri^YPeOd%7aBBv8$b2|=(?X4>-ZZAJxn@kkc zb?7Ho%(p5{UGL^_L+{?U_GiRAxuMHcb*XF`54wWO;__RAw<6pU*QA??mP|^vqS$|8 z&o`AE3FsLik3SQbZ$*Ub=Is!Q4-+rLKi|~+o`$kDO%f*ee9;q?NnXBpZHx!$P-;t8 z|IXXAjveNAg%{E+xExeRx*=IUFK*F5pb~x<`*W}-uT$v3 z!wOPvxj8sOcBATQ*k6{{bIScm<@x;MDV|YPg6wtWQfkY=y{UQMZQ5R!=Q=OY_(a3_ zg3=&^)z Vec { diff --git a/tests/nitro/mod.rs b/tests/nitro/mod.rs index 292b612..86f6228 100644 --- a/tests/nitro/mod.rs +++ b/tests/nitro/mod.rs @@ -24,7 +24,7 @@ use std::io::Read; use assert_ok::assert_ok; use tee_verifier::{ - parse_crl_der, nitro_parse_attestation, Crl, NitroParseError, NitroVerificationError, + nitro_parse_attestation, parse_crl_der, Crl, NitroParseError, NitroVerificationError, }; /// Helper function to load a file into a byte vector @@ -75,13 +75,14 @@ mod nitro_parsing { let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); let att = assert_ok!(nitro_parse_attestation(&data)); - assert_eq!(att.module_id, "i-0a22e5c5f24d22174-enc0191cceb4289903f"); + assert_eq!(att.module_id, "i-07a18ebd3db1ea8f6-enc019c28bed89f04b9"); assert_eq!(att.digest, "SHA384"); - // 2024-09-07T14:37:39.545Z - assert_eq!(att.timestamp, 1725719859545); + // 2026-02-13T16:29:13.369Z + assert_eq!(att.timestamp, 1771000153369); assert_eq!(att.pcrs.len(), 16); assert!(att.public_key.is_some()); - assert!(att.nonce.is_some()); + assert!(att.user_data.is_some()); + assert!(att.nonce.is_none()); } } @@ -109,7 +110,7 @@ mod end_to_end { let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); let att = nitro_parse_attestation(&data).unwrap(); - // 2024-09-07T14:37:39Z + // 2026-02-13T16:29:13Z let now = att.timestamp / 1000; assert_ok!(att.verify(None, now)); } From 5385427ed887294e8b40b0f5e2587ac7ae4edfa8 Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Fri, 13 Mar 2026 16:22:20 +0100 Subject: [PATCH 6/9] Fix headers --- src/nitro.rs | 2 +- src/nitro/attestation.rs | 2 +- tests/nitro/mod.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/nitro.rs b/src/nitro.rs index b966b7d..1e26af8 100644 --- a/src/nitro.rs +++ b/src/nitro.rs @@ -1,4 +1,4 @@ -// Copyright 2025, Horizen Labs, Inc. +// Copyright 2026, Horizen Labs, Inc. // SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/nitro/attestation.rs b/src/nitro/attestation.rs index adf4a81..50066b9 100644 --- a/src/nitro/attestation.rs +++ b/src/nitro/attestation.rs @@ -1,4 +1,4 @@ -// Copyright 2025, Horizen Labs, Inc. +// Copyright 2026, Horizen Labs, Inc. // SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/nitro/mod.rs b/tests/nitro/mod.rs index 86f6228..f89f664 100644 --- a/tests/nitro/mod.rs +++ b/tests/nitro/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2025, Horizen Labs, Inc. +// Copyright 2026, Horizen Labs, Inc. // SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -173,7 +173,7 @@ mod verification_errors { let data = load_file("assets/tests/nitro/attestation_doc.bin"); let att = nitro_parse_attestation(&data).unwrap(); - // One year after the document timestamp — certificates will have expired. + // One year after the document timestamp certificates will have expired. // ~2023-11-09 let now = att.timestamp / 1000 + 365 * 24 * 3600; let result = att.verify(None, now); @@ -188,7 +188,7 @@ mod verification_errors { let data = load_file("assets/tests/nitro/attestation_doc.bin"); let att = nitro_parse_attestation(&data).unwrap(); - // 1970-01-01T00:00:00Z — certificates will not yet be valid. + // 1970-01-01T00:00:00Z certificates will not yet be valid. let now = 0; let result = att.verify(None, now); assert!(matches!( From 37e5ef053460e8e9b5d13f51fed71e1ccd83efcd Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Wed, 18 Mar 2026 14:52:53 +0100 Subject: [PATCH 7/9] Move nitro tests to unit tests when sensible --- src/nitro/attestation.rs | 133 +++++++++++++++++++++++++++++++++++++++ tests/nitro/mod.rs | 109 +------------------------------- 2 files changed, 135 insertions(+), 107 deletions(-) diff --git a/src/nitro/attestation.rs b/src/nitro/attestation.rs index 50066b9..c9a13b6 100644 --- a/src/nitro/attestation.rs +++ b/src/nitro/attestation.rs @@ -323,3 +323,136 @@ fn get_cabundle(map: &CborMap) -> Result>, NitroParseError> { }) .collect() } + +#[cfg(test)] +mod should { + use super::*; + use assert_ok::assert_ok; + use std::fs::File; + use std::io::Read; + + /// Helper function to load a file into a byte vector + fn load_file(path: &str) -> Vec { + let mut f = File::open(path).unwrap(); + let mut buf = Vec::new(); + f.read_to_end(&mut buf).unwrap(); + buf + } + + mod parse { + use super::*; + + #[test] + fn parse_empty_input_fails() { + let result = parse_attestation(&[]); + assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); + } + + #[test] + fn parse_invalid_cbor_fails() { + let result = parse_attestation(&[0xFF, 0xFF, 0xFF]); + assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); + } + + #[test] + fn parse_valid_attestation_doc() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = assert_ok!(parse_attestation(&data)); + + assert_eq!(att.module_id, "i-04fd167167daacf3b-enc01845e97d0b3eb4e"); + assert_eq!(att.digest, "SHA384"); + // 2022-11-09T22:52:00.696Z + assert_eq!(att.timestamp, 1668034320696); + assert_eq!(att.pcrs.len(), 16); + assert_eq!(att.cabundle.len(), 4); + assert!(att.public_key.is_none()); + assert!(att.user_data.is_some()); + assert!(att.nonce.is_some()); + } + + #[test] + fn parse_valid_attestation_doc_2() { + let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); + let att = assert_ok!(parse_attestation(&data)); + + assert_eq!(att.module_id, "i-07a18ebd3db1ea8f6-enc019c28bed89f04b9"); + assert_eq!(att.digest, "SHA384"); + // 2026-02-13T16:29:13.369Z + assert_eq!(att.timestamp, 1771000153369); + assert_eq!(att.pcrs.len(), 16); + assert!(att.public_key.is_some()); + assert!(att.user_data.is_some()); + assert!(att.nonce.is_none()); + } + } + + mod verify { + use super::*; + + #[test] + fn verify_attestation_doc() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = parse_attestation(&data).unwrap(); + + // Use the document's own timestamp (converted from ms to seconds) + // since the embedded certificates have a short validity window. + // 2022-11-09T22:52:00Z + let now = att.timestamp / 1000; + assert_ok!(att.verify(None, now)); + } + + #[test] + fn verify_attestation_doc_2() { + let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); + let att = parse_attestation(&data).unwrap(); + + // 2026-02-13T16:29:13Z + let now = att.timestamp / 1000; + assert_ok!(att.verify(None, now)); + } + } + + mod verification_errors { + use super::*; + + #[test] + fn verify_bad_signature_fails() { + let data = load_file("assets/tests/nitro/attestation_doc_bad_sig.bin"); + let att = parse_attestation(&data).unwrap(); + + // 2022-11-09T22:52:00Z + let now = att.timestamp / 1000; + let result = att.verify(None, now); + assert!(matches!(result, Err(NitroVerificationError::BadSignature))); + } + + #[test] + fn verify_with_expired_timestamp_fails() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = parse_attestation(&data).unwrap(); + + // One year after the document timestamp certificates will have expired. + // ~2023-11-09 + let now = att.timestamp / 1000 + 365 * 24 * 3600; + let result = att.verify(None, now); + assert!(matches!( + result, + Err(NitroVerificationError::CertificateChain) + )); + } + + #[test] + fn verify_with_past_timestamp_fails() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let att = parse_attestation(&data).unwrap(); + + // 1970-01-01T00:00:00Z certificates will not yet be valid. + let now = 0; + let result = att.verify(None, now); + assert!(matches!( + result, + Err(NitroVerificationError::CertificateChain) + )); + } + } +} diff --git a/tests/nitro/mod.rs b/tests/nitro/mod.rs index f89f664..f4d9e2f 100644 --- a/tests/nitro/mod.rs +++ b/tests/nitro/mod.rs @@ -24,7 +24,7 @@ use std::io::Read; use assert_ok::assert_ok; use tee_verifier::{ - nitro_parse_attestation, parse_crl_der, Crl, NitroParseError, NitroVerificationError, + nitro_parse_attestation, parse_crl_der, Crl, }; /// Helper function to load a file into a byte vector @@ -42,47 +42,10 @@ fn load_file(path: &str) -> Vec { mod nitro_parsing { use super::*; - #[test] - fn parse_empty_input_fails() { - let result = nitro_parse_attestation(&[]); - assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); - } - - #[test] - fn parse_invalid_cbor_fails() { - let result = nitro_parse_attestation(&[0xFF, 0xFF, 0xFF]); - assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); - } - #[test] fn parse_valid_attestation_doc() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = assert_ok!(nitro_parse_attestation(&data)); - - assert_eq!(att.module_id, "i-04fd167167daacf3b-enc01845e97d0b3eb4e"); - assert_eq!(att.digest, "SHA384"); - // 2022-11-09T22:52:00.696Z - assert_eq!(att.timestamp, 1668034320696); - assert_eq!(att.pcrs.len(), 16); - assert_eq!(att.cabundle.len(), 4); - assert!(att.public_key.is_none()); - assert!(att.user_data.is_some()); - assert!(att.nonce.is_some()); - } - - #[test] - fn parse_valid_attestation_doc_2() { - let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); - let att = assert_ok!(nitro_parse_attestation(&data)); - - assert_eq!(att.module_id, "i-07a18ebd3db1ea8f6-enc019c28bed89f04b9"); - assert_eq!(att.digest, "SHA384"); - // 2026-02-13T16:29:13.369Z - assert_eq!(att.timestamp, 1771000153369); - assert_eq!(att.pcrs.len(), 16); - assert!(att.public_key.is_some()); - assert!(att.user_data.is_some()); - assert!(att.nonce.is_none()); + assert_ok!(nitro_parse_attestation(&data)); } } @@ -93,28 +56,6 @@ mod nitro_parsing { mod end_to_end { use super::*; - #[test] - fn verify_attestation_doc() { - let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = nitro_parse_attestation(&data).unwrap(); - - // Use the document's own timestamp (converted from ms to seconds) - // since the embedded certificates have a short validity window. - // 2022-11-09T22:52:00Z - let now = att.timestamp / 1000; - assert_ok!(att.verify(None, now)); - } - - #[test] - fn verify_attestation_doc_2() { - let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); - let att = nitro_parse_attestation(&data).unwrap(); - - // 2026-02-13T16:29:13Z - let now = att.timestamp / 1000; - assert_ok!(att.verify(None, now)); - } - #[test] fn verify_attestation_doc_with_crl() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); @@ -150,50 +91,4 @@ mod end_to_end { } } -// ============================================================================= -// Nitro Attestation Verification Error Tests -// ============================================================================= - -mod verification_errors { - use super::*; - - #[test] - fn verify_bad_signature_fails() { - let data = load_file("assets/tests/nitro/attestation_doc_bad_sig.bin"); - let att = nitro_parse_attestation(&data).unwrap(); - - // 2022-11-09T22:52:00Z - let now = att.timestamp / 1000; - let result = att.verify(None, now); - assert!(matches!(result, Err(NitroVerificationError::BadSignature))); - } - #[test] - fn verify_with_expired_timestamp_fails() { - let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = nitro_parse_attestation(&data).unwrap(); - - // One year after the document timestamp certificates will have expired. - // ~2023-11-09 - let now = att.timestamp / 1000 + 365 * 24 * 3600; - let result = att.verify(None, now); - assert!(matches!( - result, - Err(NitroVerificationError::CertificateChain) - )); - } - - #[test] - fn verify_with_past_timestamp_fails() { - let data = load_file("assets/tests/nitro/attestation_doc.bin"); - let att = nitro_parse_attestation(&data).unwrap(); - - // 1970-01-01T00:00:00Z certificates will not yet be valid. - let now = 0; - let result = att.verify(None, now); - assert!(matches!( - result, - Err(NitroVerificationError::CertificateChain) - )); - } -} From 13dce6f6e83df647aa9e5191de042dfee91a8952 Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Wed, 18 Mar 2026 14:59:09 +0100 Subject: [PATCH 8/9] Update README --- README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ae6a382..b0a2da7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A Rust library for TEE (Trusted Execution Environment) attestation quote verific ## Supported platforms - **Intel TDX / SGX** — Quote v4 parsing and ECDSA-P256 signature verification, X.509 certificate chain validation with CRL support, TCB collateral verification +- **AWS Nitro Enclaves** — COSE_Sign1 attestation document parsing with ECDSA-P384 signature verification, certificate chain validation against AWS Nitro root CA ## Features @@ -20,13 +21,13 @@ Add the dependency to your `Cargo.toml`: tee-verifier = { git = "https://github.com/zkVerify/tee-verifier" } ``` -### Example: Intel TDX/SGX quote verification +### Intel TDX/SGX quote verification ```rust -use tee_verifier::{parse_crl, parse_quote, parse_tcb_response}; +use tee_verifier::{intel_parse_quote, intel_parse_tcb_response, parse_crl_pem}; // 1. Parse the CRL and validate its signature against the certificate chain -let (crl_issue_time, crl) = parse_crl( +let (crl_issue_time, crl) = parse_crl_pem( &crl_pem, &pck_certificate_chain_pem, Some(&intel_root_cert_der), @@ -34,14 +35,31 @@ let (crl_issue_time, crl) = parse_crl( ).unwrap(); // 2. Parse and verify TCB collateral -let tcb_response = parse_tcb_response(&tcb_json).unwrap(); +let tcb_response = intel_parse_tcb_response(&tcb_json).unwrap(); tcb_response.verify(tcb_signing_chain_pem, &crl, now_unix_timestamp).unwrap(); // 3. Parse and verify the attestation quote -let quote = parse_quote(&raw_quote_bytes).unwrap(); +let quote = intel_parse_quote(&raw_quote_bytes).unwrap(); quote.verify(&tcb_response.tcb_info, &crl, now_unix_timestamp).unwrap(); ``` +### AWS Nitro Enclaves attestation verification + +```rust +use tee_verifier::nitro_parse_attestation; + +// 1. Parse the COSE_Sign1 attestation document +let attestation = nitro_parse_attestation(&raw_attestation_bytes).unwrap(); + +// 2. Verify the signature and certificate chain against the AWS Nitro root CA +attestation.verify(None, now_unix_timestamp).unwrap(); + +// 3. Access attestation fields +let module_id = &attestation.module_id; +let pcrs = &attestation.pcrs; +let user_data = &attestation.user_data; +``` + ### `no_std` Disable default features to use in a `no_std` environment: From b1657a37ac5bd155f870678a59f6436dace73a08 Mon Sep 17 00:00:00 2001 From: Daniele Rogora Date: Wed, 18 Mar 2026 15:18:42 +0100 Subject: [PATCH 9/9] Add synthetic tests for nitro attestations --- src/nitro/attestation.rs | 150 +++++++++++++++++++++++++++++++++------ tests/nitro/mod.rs | 6 +- 2 files changed, 131 insertions(+), 25 deletions(-) diff --git a/src/nitro/attestation.rs b/src/nitro/attestation.rs index c9a13b6..ed58132 100644 --- a/src/nitro/attestation.rs +++ b/src/nitro/attestation.rs @@ -33,8 +33,6 @@ pub enum NitroParseError { MissingField(&'static str), /// A field has an unexpected type or value. InvalidField(&'static str), - /// A certificate in the document could not be parsed. - InvalidCertificate, /// The COSE protected header specifies an unsupported algorithm. UnsupportedAlgorithm, } @@ -46,10 +44,6 @@ pub enum NitroVerificationError { BadSignature, /// The certificate chain verification failed. CertificateChain, - /// A certificate in the chain has expired or is not yet valid. - CertificateExpired, - /// The enclave is running in debug mode. - DebugMode, /// The leaf certificate's public key could not be extracted. InvalidPublicKey, /// The attestation document's digest algorithm is not "SHA384". @@ -343,19 +337,7 @@ mod should { use super::*; #[test] - fn parse_empty_input_fails() { - let result = parse_attestation(&[]); - assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); - } - - #[test] - fn parse_invalid_cbor_fails() { - let result = parse_attestation(&[0xFF, 0xFF, 0xFF]); - assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); - } - - #[test] - fn parse_valid_attestation_doc() { + fn valid_attestation_doc() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); let att = assert_ok!(parse_attestation(&data)); @@ -371,7 +353,7 @@ mod should { } #[test] - fn parse_valid_attestation_doc_2() { + fn valid_attestation_doc_2() { let data = load_file("assets/tests/nitro/attestation_doc_2.bin"); let att = assert_ok!(parse_attestation(&data)); @@ -441,6 +423,20 @@ mod should { )); } + #[test] + fn verify_unsupported_digest_fails() { + let data = load_file("assets/tests/nitro/attestation_doc.bin"); + let mut att = parse_attestation(&data).unwrap(); + att.digest = String::from("SHA256"); + + let now = att.timestamp / 1000; + let result = att.verify(None, now); + assert!(matches!( + result, + Err(NitroVerificationError::UnsupportedDigest) + )); + } + #[test] fn verify_with_past_timestamp_fails() { let data = load_file("assets/tests/nitro/attestation_doc.bin"); @@ -455,4 +451,118 @@ mod should { )); } } + + mod parse_errors { + use super::*; + + fn build_cose_sign1(payload: &[u8]) -> Vec { + let mut sign1 = CoseSign1::default(); + sign1.protected.header.alg = Some(coset::RegisteredLabelWithPrivate::Assigned( + coset::iana::Algorithm::ES384, + )); + sign1.payload = Some(payload.to_vec()); + sign1.signature = vec![0u8; 96]; + sign1.to_tagged_vec().unwrap() + } + + fn build_payload(overrides: &[(&str, Option)]) -> Vec { + let mut fields: Vec<(&str, ciborium::Value)> = vec![ + ("module_id", ciborium::Value::Text("test-module".into())), + ("digest", ciborium::Value::Text("SHA384".into())), + ("timestamp", ciborium::Value::Integer(1000000u64.into())), + ( + "pcrs", + ciborium::Value::Map(vec![( + ciborium::Value::Integer(0.into()), + ciborium::Value::Bytes(vec![0u8; 48]), + )]), + ), + ("certificate", ciborium::Value::Bytes(vec![0u8; 32])), + ( + "cabundle", + ciborium::Value::Array(vec![ciborium::Value::Bytes(vec![0u8; 32])]), + ), + ]; + + for (key, val) in overrides { + fields.retain(|(k, _)| k != key); + if let Some(v) = val { + fields.push((key, v.clone())); + } + } + + let map = ciborium::Value::Map( + fields + .into_iter() + .map(|(k, v)| (ciborium::Value::Text(k.into()), v)) + .collect(), + ); + let mut buf = Vec::new(); + ciborium::into_writer(&map, &mut buf).unwrap(); + buf + } + + #[test] + fn empty_input() { + let result = parse_attestation(&[]); + assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); + } + + #[test] + fn invalid_cose_sign1() { + let result = parse_attestation(&[0xFF, 0xFF, 0xFF]); + assert!(matches!(result, Err(NitroParseError::InvalidCoseSign1))); + } + + #[test] + fn unsupported_algorithm() { + let mut sign1 = CoseSign1::default(); + sign1.protected.header.alg = Some(coset::RegisteredLabelWithPrivate::Assigned( + coset::iana::Algorithm::ES256, + )); + sign1.payload = Some(vec![0u8]); + sign1.signature = vec![0u8; 64]; + let data = sign1.to_tagged_vec().unwrap(); + + let result = parse_attestation(&data); + assert!(matches!(result, Err(NitroParseError::UnsupportedAlgorithm))); + } + + #[test] + fn invalid_payload() { + let mut buf = Vec::new(); + ciborium::into_writer(&ciborium::Value::Array(vec![]), &mut buf).unwrap(); + let data = build_cose_sign1(&buf); + + let result = parse_attestation(&data); + assert!(matches!(result, Err(NitroParseError::InvalidPayload))); + } + + #[test] + fn missing_field() { + let payload = build_payload(&[("module_id", None)]); + let data = build_cose_sign1(&payload); + + let result = parse_attestation(&data); + assert!(matches!( + result, + Err(NitroParseError::MissingField("module_id")) + )); + } + + #[test] + fn invalid_field() { + let payload = build_payload(&[( + "timestamp", + Some(ciborium::Value::Text("not a number".into())), + )]); + let data = build_cose_sign1(&payload); + + let result = parse_attestation(&data); + assert!(matches!( + result, + Err(NitroParseError::InvalidField("timestamp")) + )); + } + } } diff --git a/tests/nitro/mod.rs b/tests/nitro/mod.rs index f4d9e2f..50fc5c1 100644 --- a/tests/nitro/mod.rs +++ b/tests/nitro/mod.rs @@ -23,9 +23,7 @@ use std::io::Read; use assert_ok::assert_ok; -use tee_verifier::{ - nitro_parse_attestation, parse_crl_der, Crl, -}; +use tee_verifier::{nitro_parse_attestation, parse_crl_der, Crl}; /// Helper function to load a file into a byte vector fn load_file(path: &str) -> Vec { @@ -90,5 +88,3 @@ mod end_to_end { assert_ok!(att.verify(Some(&crl), now)); } } - -