From 6fb445686cfae4242080e76085e2b7bbbb22a678 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 20 May 2026 11:37:45 +0200 Subject: [PATCH 1/4] Add experimental support for AWS Nitro --- Cargo.lock | 231 +++++++++++++- crates/attestation/Cargo.toml | 11 +- .../assets/aws-nitro-enclaves-root-g1.der | Bin 0 -> 533 bytes crates/attestation/src/lib.rs | 18 +- crates/attestation/src/measurements.rs | 290 +++++++++++++++++- crates/attestation/src/nitro.rs | 265 ++++++++++++++++ .../aws-nitro-attestation-sample.bin | Bin 0 -> 4396 bytes 7 files changed, 794 insertions(+), 21 deletions(-) create mode 100644 crates/attestation/assets/aws-nitro-enclaves-root-g1.der create mode 100644 crates/attestation/src/nitro.rs create mode 100644 crates/attestation/test-assets/aws-nitro-attestation-sample.bin diff --git a/Cargo.lock b/Cargo.lock index f9a940e..72f8815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,12 +170,56 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -301,6 +345,8 @@ dependencies = [ "hex", "http 1.4.0", "mock-tdx", + "nsm-nitro-enclave-utils", + "nsm-nitro-enclave-utils-keygen", "num-bigint", "once_cell", "openssl", @@ -375,6 +421,20 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "aws-nitro-enclaves-nsm-api" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92c1f4471b33f6a7af9ea421b249ed18a11c71156564baf6293148fa6ad1b09" +dependencies = [ + "libc", + "log", + "nix 0.26.4", + "serde", + "serde_bytes", + "serde_cbor", +] + [[package]] name = "axum" version = "0.8.8" @@ -435,7 +495,7 @@ checksum = "9b3d0900c6757c9674b05b0479236458297026e25fb505186dc8d7735091a21c" dependencies = [ "bincode 1.3.3", "jsonwebkey", - "memoffset", + "memoffset 0.9.1", "openssl", "serde", "serde-big-array", @@ -813,7 +873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half", + "half 2.7.1", ] [[package]] @@ -823,6 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -831,8 +892,22 @@ version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -856,6 +931,12 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12170080f3533d6f09a19f81596f836854d0fa4867dc32c8172b8474b4e9de61" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "console" version = "0.16.3" @@ -908,6 +989,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "coset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eb98d5e9155e2cf7cd942c8b3033097d4563b6fb0a00b9caecb74669555c058" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1442,6 +1533,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core 0.6.4", @@ -1810,6 +1902,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "half" version = "2.7.1" @@ -2254,6 +2352,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -2437,6 +2541,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2537,6 +2650,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + [[package]] name = "nix" version = "0.31.2" @@ -2547,7 +2673,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -2566,6 +2692,46 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nsm-nitro-enclave-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be8326f0a1c769ee90da2bdaf6e0859c7873b2047a5d06d97f7f2abb273dc15" +dependencies = [ + "aws-nitro-enclaves-nsm-api", + "coset", + "getrandom 0.3.4", + "hex", + "nsm-nitro-enclave-utils-keygen", + "p384", + "ring", + "rustls-pki-types", + "rustls-webpki", + "sealed", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "x509-cert", +] + +[[package]] +name = "nsm-nitro-enclave-utils-keygen" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff968e3b62edc3ac9c6fd324490a6a14278ea6289efeb7d91425feaf62592051" +dependencies = [ + "clap", + "p384", + "rand_core 0.6.4", + "sec1", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "x509-cert", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2685,6 +2851,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -3590,6 +3762,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sec1" version = "0.7.3" @@ -3670,6 +3853,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4129,6 +4322,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.50.0" @@ -4447,6 +4661,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.22.0" @@ -4490,7 +4710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba782755fc073877e567c2253c0be48e4aa9a254c232d36d3985dfae0bd5205" dependencies = [ "libc", - "nix", + "nix 0.31.2", ] [[package]] @@ -4966,7 +5186,10 @@ checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid", "der", + "sha1", + "signature", "spki", + "tls_codec", ] [[package]] diff --git a/crates/attestation/Cargo.toml b/crates/attestation/Cargo.toml index f27bcb5..d1feaa6 100644 --- a/crates/attestation/Cargo.toml +++ b/crates/attestation/Cargo.toml @@ -15,6 +15,8 @@ tokio = { workspace = true, features = ["fs"] } tokio-rustls = { workspace = true, default-features = false } anyhow = "1.0.100" +nsm-nitro-enclave-utils = { version = "0.1.3", default-features = false, features = ["nitro", "verify"] } +nsm-nitro-enclave-utils-keygen = { version = "=0.1.3", optional = true } pem-rfc7468 = { version = "0.7.0", features = ["std"] } tdx-attest = { git = "https://github.com/Dstack-TEE/dstack.git", rev = "4f602dddc0542cd34da031c90ac0b3a560f316ed" } base64 = "0.22.1" @@ -41,6 +43,8 @@ openssl = { version = "0.10.79", optional = true } [dev-dependencies] mock-tdx = { workspace = true } +nsm-nitro-enclave-utils = { version = "0.1.3", default-features = false, features = ["nitro", "pki", "seed", "verify"] } +nsm-nitro-enclave-utils-keygen = "=0.1.3" tempfile = "3.23.0" tokio-rustls = { workspace = true, default-features = true } @@ -53,7 +57,12 @@ default = [] azure = ["tss-esapi", "az-tdx-vtpm", "openssl"] # Allows mock quotes used in tests and exposes related functions for testing -mock = ["dep:mock-tdx"] +mock = [ + "dep:mock-tdx", + "dep:nsm-nitro-enclave-utils-keygen", + "nsm-nitro-enclave-utils/pki", + "nsm-nitro-enclave-utils/seed", +] [lints] workspace = true diff --git a/crates/attestation/assets/aws-nitro-enclaves-root-g1.der b/crates/attestation/assets/aws-nitro-enclaves-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* Ok(Some(nitro::get_measurements(&self.attestation)?)), } } } @@ -79,6 +81,8 @@ pub enum AttestationType { QemuTdx, /// DCAP TDX DcapTdx, + /// AWS Nitro Enclaves + AwsNitro, } impl AttestationType { @@ -90,6 +94,7 @@ impl AttestationType { AttestationType::QemuTdx => "qemu-tdx", AttestationType::GcpTdx => "gcp-tdx", AttestationType::DcapTdx => "dcap-tdx", + AttestationType::AwsNitro => "aws-nitro", } } @@ -235,6 +240,7 @@ impl AttestationGenerator { AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { dcap::create_dcap_attestation(input_data) } + AttestationType::AwsNitro => Ok(nitro::create_nitro_attestation(input_data)?), } } @@ -406,6 +412,10 @@ impl AttestationVerifier { ) .await? } + AttestationType::AwsNitro => nitro::verify_nitro_attestation( + attestation_exchange_message.attestation, + expected_input_data, + )?, }; // Do a measurement / attestation type policy check @@ -467,6 +477,10 @@ impl AttestationVerifier { pccs, )? } + AttestationType::AwsNitro => nitro::verify_nitro_attestation( + attestation_exchange_message.attestation, + expected_input_data, + )?, }; // Do a measurement / attestation type policy check @@ -586,6 +600,8 @@ pub enum AttestationError { QuoteGeneration(#[from] tdx_attest::TdxAttestError), #[error("DCAP verification: {0}")] DcapVerification(#[from] DcapVerificationError), + #[error("Nitro attestation: {0}")] + Nitro(#[from] NitroError), #[error("Attestation type not supported")] AttestationTypeNotSupported, #[error("Attestation type not accepted")] diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index db8c2c9..b495f81 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -75,12 +75,23 @@ fn parse_azure_pcr_index(value: &str) -> Result { Ok(index) } +fn parse_nitro_pcr_index(value: &str) -> Result { + let index = value.parse::()?; + + if index > 31 { + return Err(MeasurementFormatError::BadRegisterIndex); + } + + Ok(index) +} + /// Represents a set of measurements values for one of the supported CVM /// platforms #[derive(Clone, PartialEq)] pub enum MultiMeasurements { Dcap(HashMap), Azure(HashMap), + Nitro(HashMap>), NoAttestation, } @@ -93,6 +104,9 @@ impl fmt::Debug for MultiMeasurements { Self::Azure(measurements) => { f.debug_tuple("Azure").field(&AzureHexDebug(measurements)).finish() } + Self::Nitro(measurements) => { + f.debug_tuple("Nitro").field(&NitroHexDebug(measurements)).finish() + } Self::NoAttestation => f.write_str("NoAttestation"), } } @@ -132,11 +146,29 @@ impl fmt::Debug for AzureHexDebug<'_> { } } +/// Used to display Nitro measurements as hex +struct NitroHexDebug<'a>(&'a HashMap>); + +impl fmt::Debug for NitroHexDebug<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut entries: Vec<_> = self.0.iter().collect(); + entries.sort_by_key(|(index, _)| **index); + + let mut map = f.debug_map(); + for (index, value) in entries { + let hex_value = hex::encode(value); + map.entry(index, &hex_value); + } + map.finish() + } +} + /// Expected measurement values for policy enforcement #[derive(Debug, Clone, PartialEq)] pub enum ExpectedMeasurements { Dcap(HashMap>), Azure(HashMap>), + Nitro(HashMap>>), NoAttestation, } @@ -152,6 +184,10 @@ impl MultiMeasurements { .iter() .map(|(index, value)| (index.to_string(), hex::encode(value))) .collect(), + MultiMeasurements::Nitro(nitro_measurements) => nitro_measurements + .iter() + .map(|(index, value)| (index.to_string(), hex::encode(value))) + .collect(), MultiMeasurements::NoAttestation => HashMap::new(), }; @@ -163,7 +199,7 @@ impl MultiMeasurements { input: &str, attestation_type: AttestationType, ) -> Result { - let measurements_map: HashMap = serde_json::from_str(input)?; + let measurements_map: HashMap = serde_json::from_str(input)?; Ok(match attestation_type { AttestationType::None => Self::NoAttestation, @@ -172,7 +208,7 @@ impl MultiMeasurements { .into_iter() .map(|(k, v)| { Ok(( - k as u32, + k.parse::()? as u32, hex::decode(v)? .try_into() .map_err(|_| MeasurementFormatError::BadLength)?, @@ -184,8 +220,9 @@ impl MultiMeasurements { let measurements_map = measurements_map .into_iter() .map(|(k, v)| { + let index = k.parse::()?; Ok(( - k.try_into()?, + index.try_into()?, hex::decode(v)? .try_into() .map_err(|_| MeasurementFormatError::BadLength)?, @@ -194,6 +231,18 @@ impl MultiMeasurements { .collect::>()?; Self::Dcap(measurements_map) } + AttestationType::AwsNitro => Self::Nitro( + measurements_map + .into_iter() + .map(|(k, v)| { + let value = hex::decode(v)?; + if value.len() != crate::nitro::NITRO_PCR_LENGTH { + return Err(MeasurementFormatError::BadLength); + } + Ok((parse_nitro_pcr_index(&k)?, value)) + }) + .collect::>()?, + ), }) } @@ -249,9 +298,9 @@ pub enum MeasurementFormatError { AttestationTypeNotValid, #[error("Hex: {0}")] Hex(#[from] hex::FromHexError), - #[error("Expected 48 byte value")] + #[error("Unexpected measurement value length")] BadLength, - #[error("TDX quote register index must be in the ranger 0-3")] + #[error("Invalid measurement register index")] BadRegisterIndex, #[error("ParseInt: {0}")] ParseInt(#[from] std::num::ParseIntError), @@ -296,6 +345,7 @@ impl MeasurementRecord { AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { ExpectedMeasurements::Dcap(HashMap::new()) } + AttestationType::AwsNitro => ExpectedMeasurements::Nitro(HashMap::new()), }, } } @@ -337,6 +387,7 @@ impl MeasurementPolicy { MeasurementRecord::allow_any_measurement(AttestationType::QemuTdx), MeasurementRecord::allow_any_measurement(AttestationType::GcpTdx), MeasurementRecord::allow_any_measurement(AttestationType::AzureTdx), + MeasurementRecord::allow_any_measurement(AttestationType::AwsNitro), ], } } @@ -356,17 +407,30 @@ impl MeasurementPolicy { /// Expect mock measurements used in tests #[cfg(any(test, feature = "mock"))] pub fn mock() -> Self { + let nitro_measurements = match crate::nitro::mock_nitro_measurements() { + MultiMeasurements::Nitro(measurements) => { + measurements.into_iter().map(|(index, value)| (index, vec![value])).collect() + } + _ => unreachable!("mock_nitro_measurements must return Nitro measurements"), + }; + Self { - accepted_measurements: vec![MeasurementRecord { - measurement_id: "test".to_string(), - measurements: ExpectedMeasurements::Dcap(HashMap::from([ - (DcapMeasurementRegister::MRTD, vec![mock_tdx::MOCK_MRTD]), - (DcapMeasurementRegister::RTMR0, vec![mock_tdx::MOCK_RTMR0]), - (DcapMeasurementRegister::RTMR1, vec![mock_tdx::MOCK_RTMR1]), - (DcapMeasurementRegister::RTMR2, vec![mock_tdx::MOCK_RTMR2]), - (DcapMeasurementRegister::RTMR3, vec![mock_tdx::MOCK_RTMR3]), - ])), - }], + accepted_measurements: vec![ + MeasurementRecord { + measurement_id: "test".to_string(), + measurements: ExpectedMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, vec![mock_tdx::MOCK_MRTD]), + (DcapMeasurementRegister::RTMR0, vec![mock_tdx::MOCK_RTMR0]), + (DcapMeasurementRegister::RTMR1, vec![mock_tdx::MOCK_RTMR1]), + (DcapMeasurementRegister::RTMR2, vec![mock_tdx::MOCK_RTMR2]), + (DcapMeasurementRegister::RTMR3, vec![mock_tdx::MOCK_RTMR3]), + ])), + }, + MeasurementRecord { + measurement_id: "test-nitro".to_string(), + measurements: ExpectedMeasurements::Nitro(nitro_measurements), + }, + ], } } @@ -402,6 +466,18 @@ impl MeasurementPolicy { } false } + MultiMeasurements::Nitro(nitro_measurements) => { + if let ExpectedMeasurements::Nitro(expected) = &measurement_record.measurements { + for (k, v) in expected.iter() { + match nitro_measurements.get(k) { + Some(actual_value) if v.iter().any(|v| actual_value == v) => {} + _ => return false, + } + } + return true; + } + false + } MultiMeasurements::NoAttestation => { matches!(measurement_record.measurements, ExpectedMeasurements::NoAttestation) } @@ -505,6 +581,38 @@ impl MeasurementPolicy { } } + fn parse_measurement_entry_vec( + entry: &MeasurementEntry, + register_name: &str, + expected_len: usize, + ) -> Result>, MeasurementFormatError> { + let parse_hex_value = |hex_str: &str| { + let value = hex::decode(hex_str)?; + if value.len() != expected_len { + return Err(MeasurementFormatError::BadLength); + } + Ok(value) + }; + + match (&entry.expected, &entry.expected_any) { + (Some(single), None) => Ok(vec![parse_hex_value(single)?]), + (None, Some(any_list)) => { + if any_list.is_empty() { + return Err(MeasurementFormatError::EmptyExpectedAny( + register_name.to_string(), + )); + } + any_list.iter().map(|hex_str| parse_hex_value(hex_str)).collect() + } + (Some(_), Some(_)) => Err(MeasurementFormatError::BothExpectedAndExpectedAny( + register_name.to_string(), + )), + (None, None) => { + Err(MeasurementFormatError::NoExpectedValue(register_name.to_string())) + } + } + } + let records_simple: Vec = serde_json::from_slice(&json_bytes)?; let mut measurement_policy = Vec::new(); @@ -543,6 +651,23 @@ impl MeasurementPolicy { MeasurementFormatError, >>()?, ), + AttestationType::AwsNitro => ExpectedMeasurements::Nitro( + measurements + .iter() + .map(|(index_str, entry)| { + let index = parse_nitro_pcr_index(index_str)?; + Ok(( + index, + parse_measurement_entry_vec( + entry, + index_str, + crate::nitro::NITRO_PCR_LENGTH, + )?, + )) + }) + .collect::>>, MeasurementFormatError>>( + )?, + ), }; measurement_policy.push(MeasurementRecord { @@ -1018,6 +1143,134 @@ mod tests { assert!(matches!(result, Err(MeasurementFormatError::BadRegisterIndex))); } + #[tokio::test] + async fn test_parse_nitro_numeric_pcr_keys() { + let pcr0 = "11".repeat(crate::nitro::NITRO_PCR_LENGTH); + let pcr8 = "22".repeat(crate::nitro::NITRO_PCR_LENGTH); + let json = format!( + r#"[ + {{ + "measurement_id": "nitro-pcrs", + "attestation_type": "aws-nitro", + "measurements": {{ + "0": {{ "expected_any": ["{pcr0}"] }}, + "8": {{ "expected_any": ["{pcr8}"] }} + }} + }} + ]"# + ); + + let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); + let record = &policy.accepted_measurements[0]; + + if let ExpectedMeasurements::Nitro(nitro) = &record.measurements { + assert_eq!(nitro.keys().collect::>(), HashSet::from([&0, &8])); + } else { + panic!("Expected ExpectedMeasurements::Nitro"); + } + } + + #[tokio::test] + async fn test_parse_nitro_rejects_prefixed_pcr_keys() { + let pcr0 = "11".repeat(crate::nitro::NITRO_PCR_LENGTH); + let json = format!( + r#"[ + {{ + "attestation_type": "aws-nitro", + "measurements": {{ + "pcr0": {{ "expected_any": ["{pcr0}"] }} + }} + }} + ]"# + ); + + let result = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()); + assert!(matches!(result, Err(MeasurementFormatError::ParseInt(_)))); + } + + #[tokio::test] + async fn test_parse_nitro_rejects_out_of_range_pcr_keys() { + let pcr32 = "11".repeat(crate::nitro::NITRO_PCR_LENGTH); + let json = format!( + r#"[ + {{ + "attestation_type": "aws-nitro", + "measurements": {{ + "32": {{ "expected_any": ["{pcr32}"] }} + }} + }} + ]"# + ); + + let result = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()); + assert!(matches!(result, Err(MeasurementFormatError::BadRegisterIndex))); + } + + #[tokio::test] + async fn test_parse_nitro_rejects_bad_value_lengths() { + let short_pcr = "11".repeat(crate::nitro::NITRO_PCR_LENGTH - 1); + let json = format!( + r#"[ + {{ + "attestation_type": "aws-nitro", + "measurements": {{ + "0": {{ "expected_any": ["{short_pcr}"] }} + }} + }} + ]"# + ); + + let result = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()); + assert!(matches!(result, Err(MeasurementFormatError::BadLength))); + } + + #[tokio::test] + async fn test_check_nitro_measurement_with_or_semantics() { + let first = "00".repeat(crate::nitro::NITRO_PCR_LENGTH); + let second = "11".repeat(crate::nitro::NITRO_PCR_LENGTH); + let json = format!( + r#"[ + {{ + "measurement_id": "nitro-or", + "attestation_type": "aws-nitro", + "measurements": {{ + "0": {{ + "expected_any": ["{first}", "{second}"] + }} + }} + }} + ]"# + ); + + let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); + + let measurements1 = MultiMeasurements::Nitro(HashMap::from([(0, vec![0u8; 48])])); + assert!(policy.check_measurement(&measurements1).is_ok()); + + let measurements2 = MultiMeasurements::Nitro(HashMap::from([(0, vec![0x11u8; 48])])); + assert!(policy.check_measurement(&measurements2).is_ok()); + + let measurements3 = MultiMeasurements::Nitro(HashMap::from([(0, vec![0x22u8; 48])])); + assert!(policy.check_measurement(&measurements3).is_err()); + } + + #[test] + fn test_nitro_header_round_trip() { + let measurements = MultiMeasurements::Nitro(HashMap::from([ + (0, vec![0xabu8; crate::nitro::NITRO_PCR_LENGTH]), + (8, vec![0xcdu8; crate::nitro::NITRO_PCR_LENGTH]), + ])); + + let header = measurements.to_header_format().unwrap(); + let parsed = MultiMeasurements::from_header_format( + header.to_str().unwrap(), + AttestationType::AwsNitro, + ) + .unwrap(); + + assert_eq!(parsed, measurements); + } + /// Checks that the Debug implementation for MultiMeasurements displays /// them as hex #[test] @@ -1038,6 +1291,13 @@ mod tests { assert!(azure_debug.contains("Azure")); assert!(azure_debug.contains(&hex::encode(azure_register_value))); assert!(!azure_debug.contains(&format!("{azure_register_value:?}"))); + + let nitro_register_value = vec![0xabu8; crate::nitro::NITRO_PCR_LENGTH]; + let nitro = MultiMeasurements::Nitro(HashMap::from([(8u32, nitro_register_value.clone())])); + let nitro_debug = format!("{nitro:?}"); + assert!(nitro_debug.contains("Nitro")); + assert!(nitro_debug.contains(&hex::encode(&nitro_register_value))); + assert!(!nitro_debug.contains(&format!("{nitro_register_value:?}"))); } #[tokio::test] diff --git a/crates/attestation/src/nitro.rs b/crates/attestation/src/nitro.rs new file mode 100644 index 0000000..f00c0dd --- /dev/null +++ b/crates/attestation/src/nitro.rs @@ -0,0 +1,265 @@ +//! AWS Nitro Enclaves attestation generation and verification. + +use std::collections::HashMap; + +use nsm_nitro_enclave_utils::{ + api::{ + ByteBuf, + Time, + nsm::{AttestationDoc, Digest, ErrorCode, Request, Response}, + }, + driver::{Driver, nitro::Nitro}, + verify::AttestationDocVerifierExt, +}; +use thiserror::Error; + +use crate::measurements::MultiMeasurements; + +const AWS_ROOT_CERT_DER: &[u8] = include_bytes!("../assets/aws-nitro-enclaves-root-g1.der"); +pub(crate) const NITRO_PCR_LENGTH: usize = 48; + +/// Generate a Nitro attestation document using the Nitro Secure Module. +pub fn create_nitro_attestation(input_data: [u8; 64]) -> Result, NitroError> { + let nitro = Nitro::init(); + request_attestation(&nitro, input_data) +} + +fn request_attestation(driver: &impl Driver, input_data: [u8; 64]) -> Result, NitroError> { + match driver.process_request(Request::Attestation { + nonce: Some(ByteBuf::from(input_data.to_vec())), + user_data: None, + public_key: None, + }) { + Response::Attestation { document } => Ok(document), + Response::Error(error) => Err(NitroError::Nsm(error)), + response => Err(NitroError::UnexpectedResponse(format!("{response:?}"))), + } +} + +/// Verify a Nitro attestation document and return its PCR measurements. +pub fn verify_nitro_attestation( + input: Vec, + expected_input_data: [u8; 64], +) -> Result { + let doc = decode_with_accepted_roots(&input)?; + + match doc.nonce.as_ref() { + Some(nonce) if nonce.as_ref() == expected_input_data.as_slice() => {} + _ => return Err(NitroError::InputMismatch), + } + + measurements_from_doc(&doc) +} + +/// Extract Nitro PCR measurements from a verified attestation document. +pub fn get_measurements(input: &[u8]) -> Result { + let doc = decode_with_accepted_roots(input)?; + measurements_from_doc(&doc) +} + +fn decode_with_accepted_roots(input: &[u8]) -> Result { + let production_result = decode_with_root(input, AWS_ROOT_CERT_DER); + #[allow(clippy::needless_match)] + match production_result { + Ok(doc) => Ok(doc), + Err(production_error) => { + #[cfg(any(test, feature = "mock"))] + { + let mock_root = mock_nitro_root_cert_der()?; + if let Ok(doc) = decode_with_root(input, &mock_root) { + return Ok(doc); + } + } + + Err(production_error) + } + } +} + +fn decode_with_root(input: &[u8], root_cert_der: &[u8]) -> Result { + decode_with_root_at_time(input, root_cert_der, Time::default()) +} + +fn decode_with_root_at_time( + input: &[u8], + root_cert_der: &[u8], + time: Time, +) -> Result { + AttestationDoc::from_cose(input, root_cert_der, time) + .map_err(|err| NitroError::Verification(format!("{err:?}"))) +} + +fn measurements_from_doc(doc: &AttestationDoc) -> Result { + let expected_pcr_len = match doc.digest { + Digest::SHA256 => 32, + Digest::SHA384 => NITRO_PCR_LENGTH, + Digest::SHA512 => 64, + }; + + let mut measurements = HashMap::new(); + for (index, value) in &doc.pcrs { + let index = u32::try_from(*index).map_err(|_| NitroError::InvalidPcrIndex(*index))?; + if index > 31 { + return Err(NitroError::InvalidPcrIndex(index as usize)); + } + if value.as_ref().len() != expected_pcr_len { + return Err(NitroError::BadPcrLength { + index, + expected: expected_pcr_len, + actual: value.as_ref().len(), + }); + } + measurements.insert(index, value.as_ref().to_vec()); + } + + Ok(MultiMeasurements::Nitro(measurements)) +} + +#[cfg(any(test, feature = "mock"))] +fn mock_nitro_pki() -> &'static nsm_nitro_enclave_utils_keygen::NsmCertChain { + use std::time::Duration; + + use once_cell::sync::Lazy; + + static MOCK_NITRO_PKI: Lazy = Lazy::new(|| { + nsm_nitro_enclave_utils_keygen::NsmCertChain::generate(Duration::from_secs(3600)) + }); + + &MOCK_NITRO_PKI +} + +#[cfg(any(test, feature = "mock"))] +fn mock_nitro_root_cert_der() -> Result, NitroError> { + use nsm_nitro_enclave_utils_keygen::DerEncodeExt; + + mock_nitro_pki().root.to_der().map_err(|err| NitroError::MockPki(format!("{err:?}"))) +} + +#[cfg(any(test, feature = "mock"))] +fn mock_nitro_pcrs() -> nsm_nitro_enclave_utils::pcr::Pcrs { + use std::collections::BTreeMap; + + use nsm_nitro_enclave_utils::pcr::{PcrIndex, Pcrs}; + + Pcrs::seed(BTreeMap::from([ + (PcrIndex::Zero, "attested-tls-mock-nitro-pcr0".to_string()), + (PcrIndex::One, "attested-tls-mock-nitro-pcr1".to_string()), + (PcrIndex::Two, "attested-tls-mock-nitro-pcr2".to_string()), + (PcrIndex::Three, "attested-tls-mock-nitro-pcr3".to_string()), + (PcrIndex::Four, "attested-tls-mock-nitro-pcr4".to_string()), + (PcrIndex::Eight, "attested-tls-mock-nitro-pcr8".to_string()), + ])) +} + +/// Create a locally signed mock Nitro attestation document for tests and +/// local development. +#[cfg(any(test, feature = "mock"))] +pub fn create_mock_nitro_attestation(input_data: [u8; 64]) -> Result, NitroError> { + use nsm_nitro_enclave_utils::{api::SecretKey, driver::dev::DevNitro}; + use nsm_nitro_enclave_utils_keygen::DerEncodeExt; + + let pki = mock_nitro_pki(); + let signing_key_bytes = pki.end_signer.signing_key.to_bytes(); + let signing_key = SecretKey::from_slice(signing_key_bytes.as_ref()) + .map_err(|err| NitroError::MockPki(format!("{err:?}")))?; + + let nitro = DevNitro::builder( + signing_key, + ByteBuf::from( + pki.end_signer.cert.to_der().map_err(|err| NitroError::MockPki(format!("{err:?}")))?, + ), + ) + .ca_bundle(vec![ByteBuf::from( + pki.int.to_der().map_err(|err| NitroError::MockPki(format!("{err:?}")))?, + )]) + .pcrs(mock_nitro_pcrs()) + .build(); + + request_attestation(&nitro, input_data) +} + +/// Mock Nitro PCR values used in tests and local development. +#[cfg(any(test, feature = "mock"))] +pub fn mock_nitro_measurements() -> MultiMeasurements { + use nsm_nitro_enclave_utils::pcr::PcrIndex; + + let pcrs = mock_nitro_pcrs(); + MultiMeasurements::Nitro(HashMap::from([ + (0, pcrs.get(PcrIndex::Zero).as_ref().to_vec()), + (1, pcrs.get(PcrIndex::One).as_ref().to_vec()), + (2, pcrs.get(PcrIndex::Two).as_ref().to_vec()), + (3, pcrs.get(PcrIndex::Three).as_ref().to_vec()), + (4, pcrs.get(PcrIndex::Four).as_ref().to_vec()), + (8, pcrs.get(PcrIndex::Eight).as_ref().to_vec()), + ])) +} + +/// An error when generating or verifying AWS Nitro attestation. +#[derive(Error, Debug)] +pub enum NitroError { + #[error("Nitro Secure Module returned error: {0:?}")] + Nsm(ErrorCode), + #[error("Unexpected Nitro Secure Module response: {0}")] + UnexpectedResponse(String), + #[error("Nitro attestation verification: {0}")] + Verification(String), + #[error("Nitro attestation nonce is not as expected")] + InputMismatch, + #[error("Invalid Nitro PCR index: {0}")] + InvalidPcrIndex(usize), + #[error("Nitro PCR {index} has length {actual}, expected {expected}")] + BadPcrLength { index: u32, expected: usize, actual: usize }, + #[cfg(any(test, feature = "mock"))] + #[error("Mock Nitro PKI: {0}")] + MockPki(String), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AttestationExchangeMessage, AttestationType, AttestationVerifier}; + + #[test] + fn aws_signed_attestation_fixture_verifies() { + let attestation = include_bytes!("../test-assets/aws-nitro-attestation-sample.bin"); + let timestamp_millis = 1_680_010_000_000; + let doc = decode_with_root_at_time( + attestation, + AWS_ROOT_CERT_DER, + Time::new(Box::new(move || timestamp_millis)), + ) + .unwrap(); + let measurements = measurements_from_doc(&doc).unwrap(); + + if let MultiMeasurements::Nitro(pcrs) = measurements { + assert!(pcrs.contains_key(&0)); + } else { + panic!("expected Nitro measurements"); + } + } + + #[tokio::test] + async fn mock_nitro_verifier_supports_async_and_sync_verification() { + let input_data = [7u8; 64]; + let attestation = create_mock_nitro_attestation(input_data).unwrap(); + let exchange = + AttestationExchangeMessage { attestation_type: AttestationType::AwsNitro, attestation }; + let verifier = AttestationVerifier::mock(); + + let async_measurements = + verifier.verify_attestation(exchange.clone(), input_data).await.unwrap().unwrap(); + let sync_measurements = + verifier.verify_attestation_sync(exchange, input_data).unwrap().unwrap(); + + assert_eq!(async_measurements, mock_nitro_measurements()); + assert_eq!(sync_measurements, mock_nitro_measurements()); + } + + #[test] + fn nonce_mismatch_is_rejected() { + let attestation = create_mock_nitro_attestation([1u8; 64]).unwrap(); + let err = verify_nitro_attestation(attestation, [2u8; 64]).unwrap_err(); + + assert!(matches!(err, NitroError::InputMismatch)); + } +} diff --git a/crates/attestation/test-assets/aws-nitro-attestation-sample.bin b/crates/attestation/test-assets/aws-nitro-attestation-sample.bin new file mode 100644 index 0000000000000000000000000000000000000000..e8dc09f2e502965d26689c110c3e699163334ebc GIT binary patch literal 4396 zcmd6pc~lek7RQrG2w{;$3Mh!Uun8pmvL`Aio2XP#F%VE6CX)#SLI@C4XjOugT@+D4 z6b#}H3W8AepeR)8j@l|ht%$pTYY-ozZ6_iY3ch!8PTxDtIrGQ;WxkV}xu5U7H`%Kg z%buhRGCF-g92q5ylOQ2t;VOGE35b{?wty-^*h~S9&7i>~L<$26n?+?)XjDFf&lFH$ zkx(3p#L7kfv)yTIx>zobL?eEr%o2maraG9grwC)2QV6bu$oIC%_TSENz6_vYhyfNoN5 zB3rHpAMRl~x!MwHZ`Rbq=VBvrTy>sI&wTBd+c~*2^+-SrPx>~?BRy#DWi8DUBWNzs z)?7l=T%x17L|1c(p5_vL%_XBWml$X+G1OcV0V6STu}BQ_gxvbW5dMK zP<#mmm~FFiL%*t50x`ola+0Z`$RsbV{_Q#hBVF_AJ3Or^KPJqE*tPVYJ!9Ka(iF3m@hsp@b zsXddSeXO#<)^x93dd4V=-~8;tyWZZoU}$hVrD40ZPe6iv>8cA$GsnESWIy|D!Teq2 z_2v~f4wYu*W_LJkli5#~9kNc|j} z$3cYCgDZ^-#Q-4$f?4w%DISg9r07?(cX!L8!M#S0vv;rjcszYe`Q?|*Y#Lqb!z*g) zmeLdqt*$7_2Lb_WwL?P|lvnS#ZQHc%3As+ySa@dJBM;hlNrpwKVa-=8jzPxdY|^rT z9RWQvuHNQ_!F)lSR474`gCJ8BLt~JH-P(_$F{YOi7iPJ!yxnTnrK1lX|2trChG5Rn z%KjI|EWz0Rl8qjjA_|8BKF%ICV4xJy`$IZa9sZp^^@Y;EhZcA4C7w}*$?g@jZag)M zP2ChLP51r1C#EIc%Xh)ThxFY?dVaZox|U}_{r<=bZG$_bZx+nxm}n__yr^mQwO@5Y zqD*An=0wky+eX-l51L@CgO126^?bY_8O%F7qr1cd%mjv?ORoiRC;`AqePDn$!+*8W zwt7^Hme1p@4O`#E6gF);(B`}Kqm8W$)u^C+U{eOaeViBrL=Xj2veqT1`25L#Jw7J* z)-3-z7nf~~^Xy#$9}r}@auTjKtuwT zh|Z@mfnaER_-l{*0y2I3ysPG&Y4JPKQr$gy*LNP9`#R>9t1=pU%V>cqExakw2X`{K z4!iOU_mu}aP+!={+!Av3)y-CBpR^M8ZAeZGs9x8~e8ReyO15U zgO@zfE6j`^29`wqJZn4xV8B;~D3_%y36slZQ(at8uabs#d=eZZA;;3l{Eve~?PWMB z(gi(q;S1;-CV*iQOUR^==v0A_#6dVB5`#mdv0(v|C1TUb=#7J6!G&P}f->1Gk@eN? zZwz1gAV(@XRF_d2KeNjsbSL+zuW(gs#Y^djTmrrD3@sql?!K_svZxFK^RtgpT6KvN zi`-MmGre;C?0;An)#4Rsye?o>a4DWRT~g)0)Kuva(om7nIkosVd=PF7NWy)EQoMbD z;tj;}T-PqIYi43hM(Bew^5De&PiPGQ&Z4jwfWiI*IH01_6rj@-DAFm@!PI_gX(DuA z2O>V50$>phrm@KABXQ#pY)PN@AkuJGs2%b7 z`w_fB=dgq=$Ef63osaQ$Ly{BZ zw8k!>`RsAnCp)~iy&n2W#+VjSXjUSI+Mku#zHXfm{Gt#7UOpYYZe1P0Kj<91qSq~4 zm?z9A%l+UrfoxJam#E`UamXa)hu&?IVA59k#k##a83ZMxwn#)Tt$u5mVxE=;1TWql z(HU{l=C(Vp($FT@=J*hM{FA^OFcBM20bqcE9hCDa{O2vrUl=HvOE*v0e=0|j-$hRK zHVzC~J*%wqaI|NVetVkZ;fAtE2JgoY)_EDo>gA;s(fGyXK27t|Z`rKuEo>8VgKpJD zbR)q0YEflzTf6&s7uN-aWs9!m@1C>1c8#j2fN^i#`w}}~GlGvWLwx|@2N!x^&v7B(<;@;W|kjfUQ@rN{sKNU?^3$`ed=1Yr=MeP zKUMIispek#?wahDD$kBR$xY*E#y)n_4l0?}-=DDDEYJJd%y#VwWf8~2WZbTBS)4#3 zhC?Ed_#SawED{qUsDob~&c#HB^9{sY5Yzd`^2 literal 0 HcmV?d00001 From 5ad5ff6d42e94ad42f5c1a5c996d033107e2338a Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 20 May 2026 15:04:42 +0200 Subject: [PATCH 2/4] Add test to verify against observed attestation document from an actual nitro enclave --- crates/attestation/src/nitro.rs | 35 ++++++++++++++++++ .../test-assets/aws-nitro-1779281545803060420 | Bin 0 -> 4524 bytes 2 files changed, 35 insertions(+) create mode 100644 crates/attestation/test-assets/aws-nitro-1779281545803060420 diff --git a/crates/attestation/src/nitro.rs b/crates/attestation/src/nitro.rs index f00c0dd..3cad2f0 100644 --- a/crates/attestation/src/nitro.rs +++ b/crates/attestation/src/nitro.rs @@ -238,6 +238,41 @@ mod tests { } } + #[test] + fn aws_signed_attestation_fixture_has_expected_measurements() { + let attestation = include_bytes!("../test-assets/aws-nitro-attestation-sample.bin"); + let timestamp_millis = 1_680_010_000_000; + let doc = decode_with_root_at_time( + attestation, + AWS_ROOT_CERT_DER, + Time::new(Box::new(move || timestamp_millis)), + ) + .unwrap(); + let measurements = measurements_from_doc(&doc).unwrap(); + let mut expected_pcrs = HashMap::from([ + ( + 3, + hex::decode( + "e48b6ac6bab30e3717d28c2c88f2ba8b614e454590eb00b26170eef0d707b5b8e3a97662c20b2ced6192d3aaa2f5e24e", + ) + .unwrap(), + ), + ( + 4, + hex::decode( + "3413af1370600b63aef6362b3d2506bcd6b6c263c8736b913d09e83c8bf24f93eb23eb87b15672586ef78c4289594acd", + ) + .unwrap(), + ), + ]); + for index in 0..16 { + expected_pcrs.entry(index).or_insert_with(|| vec![0u8; NITRO_PCR_LENGTH]); + } + let expected = MultiMeasurements::Nitro(expected_pcrs); + + assert_eq!(measurements, expected); + } + #[tokio::test] async fn mock_nitro_verifier_supports_async_and_sync_verification() { let input_data = [7u8; 64]; diff --git a/crates/attestation/test-assets/aws-nitro-1779281545803060420 b/crates/attestation/test-assets/aws-nitro-1779281545803060420 new file mode 100644 index 0000000000000000000000000000000000000000..2c4b88dabb0f513aad08e817be934b43e0440a51 GIT binary patch literal 4524 zcmd6qcT^ME9>9ZK3MU+lk@hyvvbb<<99RPnP2Yxe1E?? z%e=Qzg$}vFmSan0(J@j@6dop%#toCffX5L?r4m#k;EQmMh%1G0IR+3B&f^OZ9$zGI zB{)(-DhtO|Y9heblPl!O)Us$YLZcOS6bf~-c;2}QQU#_|9iRjOik=r$e+i_&gcbxI zY44k=9pPDFn~k=F6#WjZ^$Waus6qa88@sh2AJ=aEF2%uQRr{^2iJ_z4IW3N4(u08i z;q4tJH;>GHOJ8cb?We*EFO-t(QQ(+wRNOP>!R6`g<23Ciw`2P$xCM4EO}4G{Wayt5 zQ-6tp{t`p|B}V#7jP;k8=r3XEFEQ0$Vy3^uTz|=23|FdUgbYK~crcUz5}`%ppytvb zDg;^RoF&ePfpw0GG~Jg1jG2axNxqftbQ%q4S0g6Ch)D~iGp(S&0K^Jd>HkOGbj2bHjm1@(lThfZ-DY1RsDmqhTC%t5bk^Vn zBVLpf>>p|~lXE={(g)WDq;b71HA;|FFkz!}0k)kA)77=U9zJiMlr3#GyLfPQ^<8bm zaN9Xp?94w3UTw(>EfrD3@eF+0^AlG72Ke!aJbPdVSY+n*R#fVHIs_=dL>HOU=`@IW zqU%o6WE4O^2xVncKMzS8{4T`!;=}v7SIkYJse5<9L4~ai#k2iVDp_8r$E4@%j94Gy zb|LOlDkde_r&34PxScJ}Jihi^x*8ukBn&D9pyT|z4p+hbrl;}$cq}^lOWN;66#@3; zb7m;15g01b$fZ&EvS6qe38E!Pq^5VmXh~^9G!b^G`|sHMTqwBT*!o@99KnR{d;1?K zvjcrPH;vshM~Dak93jHx2mznuh}RkNK7@betG=@N?a=hcI}J}ZM=0)Q)uopB2svpg zd5Yir7UlI6Z$JO34|v-Tw%oXPq9~{@=jg#$1G9Sel`M}(1MP^$X;q7VyJZv>W2Jay zYdG%uO$+M4`&AfqxDnx1}2^Mc$Mb`#sc%to6i6=k^x|^n=qr>(5+L495%hq z7~hy)mi|VWQ?)U_!7u%Tk?qZOu#kjMle(_{CqnX;xUAh;Q5@QMOtQ!(vnG(Xx<74P z+e|E9@}s*bVS{*UzoJs-QGwU?HO(t1yRx^LBxGCjCL6{f#qb@s{BmW7sdBvFZT*_t zO#8@yK1;IeUSA(qgtj|9rk#&zS~Uo2ZfFUHPLW)ckVE{5i$^~!uDbciqLS(v!k$uH z&FGrhU%6Qy>kGmMfXm_QxFE}oE8qc=6EXx+y$~dYoe07N;GsN`fZz)x1i(?edx7|K zmHP@ZyGJ@zC2-?LE@&JRoL%JEM>q}fXR1SePo|$9ni&~4h@D}a6x4ji2FXr;;wrDo zQHdEQqugr}=l5kFpkS&K?L%FznOD{hDJj2~YW&vy%FZnUklj z1=kSfV)+EGPA3LpV;vJ6BvaD}i zZg|b@Yi@7bHLd>>SfB6~aR5gE1fSX`A3;8ZK%0V$25z0qatV=CD3SnjC2;`?Fv6Ma zUw%W#Y zlTba|+IQNmlpO+FEU$H80ITFO$WJEgdkvBEb6VpX2Q(`DARYvTiw};AhJXQK1nRJN5&bW5vOyZAst(gtqJmoriJrgIk?5u5lG0M>^`EtmV ztH%v)ZM5nr^uA&`Xw^l({r4V-kB4{}<`F>JgO4cPeE`wB97-s0;RewwLh5VEF9gB{ z5B3kJ&){{lU5~!%^2PG8ypM0I^siidN%*Gs9@fR3h4G;+8`QKr`LMc4o@k%b~RF=kwnR`u(^1Ai%g@!m0?m;jke(O7&(Rqjn#ck{WsV9pgF4s zZm56N7|Zpwak+{X?3U)6RqxnYBs4jqbYC~dqaWiNy9)2g9m++;;i|Io0H<(8-_f^2 x*K9gccA#+iLX!dJXqtoDips#^#ZIy3hwgG?+2yf&$+gJ`H}3M$Y6iIN{U?D}$fy7S literal 0 HcmV?d00001 From a1a23ebdc5e2de58500fc6db47cadceffe3a3492 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 21 May 2026 14:02:38 +0200 Subject: [PATCH 3/4] Only allow 48 byte PCRs on Nitro --- crates/attestation/src/measurements.rs | 32 ++++---- crates/attestation/src/nitro.rs | 109 +++++++++++++++++++------ 2 files changed, 100 insertions(+), 41 deletions(-) diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index b495f81..8560143 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -91,7 +91,7 @@ fn parse_nitro_pcr_index(value: &str) -> Result { pub enum MultiMeasurements { Dcap(HashMap), Azure(HashMap), - Nitro(HashMap>), + Nitro(HashMap), NoAttestation, } @@ -147,7 +147,7 @@ impl fmt::Debug for AzureHexDebug<'_> { } /// Used to display Nitro measurements as hex -struct NitroHexDebug<'a>(&'a HashMap>); +struct NitroHexDebug<'a>(&'a HashMap); impl fmt::Debug for NitroHexDebug<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -168,7 +168,7 @@ impl fmt::Debug for NitroHexDebug<'_> { pub enum ExpectedMeasurements { Dcap(HashMap>), Azure(HashMap>), - Nitro(HashMap>>), + Nitro(HashMap>), NoAttestation, } @@ -239,6 +239,8 @@ impl MultiMeasurements { if value.len() != crate::nitro::NITRO_PCR_LENGTH { return Err(MeasurementFormatError::BadLength); } + let value: [u8; 48] = + value.try_into().map_err(|_| MeasurementFormatError::BadLength)?; Ok((parse_nitro_pcr_index(&k)?, value)) }) .collect::>()?, @@ -585,13 +587,13 @@ impl MeasurementPolicy { entry: &MeasurementEntry, register_name: &str, expected_len: usize, - ) -> Result>, MeasurementFormatError> { - let parse_hex_value = |hex_str: &str| { + ) -> Result, MeasurementFormatError> { + let parse_hex_value = |hex_str: &str| -> Result<[u8; 48], MeasurementFormatError> { let value = hex::decode(hex_str)?; if value.len() != expected_len { return Err(MeasurementFormatError::BadLength); } - Ok(value) + value.try_into().map_err(|_| MeasurementFormatError::BadLength) }; match (&entry.expected, &entry.expected_any) { @@ -665,7 +667,7 @@ impl MeasurementPolicy { )?, )) }) - .collect::>>, MeasurementFormatError>>( + .collect::>, MeasurementFormatError>>( )?, ), }; @@ -1244,21 +1246,21 @@ mod tests { let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); - let measurements1 = MultiMeasurements::Nitro(HashMap::from([(0, vec![0u8; 48])])); + let measurements1 = MultiMeasurements::Nitro(HashMap::from([(0, [0u8; 48])])); assert!(policy.check_measurement(&measurements1).is_ok()); - let measurements2 = MultiMeasurements::Nitro(HashMap::from([(0, vec![0x11u8; 48])])); + let measurements2 = MultiMeasurements::Nitro(HashMap::from([(0, [0x11u8; 48])])); assert!(policy.check_measurement(&measurements2).is_ok()); - let measurements3 = MultiMeasurements::Nitro(HashMap::from([(0, vec![0x22u8; 48])])); + let measurements3 = MultiMeasurements::Nitro(HashMap::from([(0, [0x22u8; 48])])); assert!(policy.check_measurement(&measurements3).is_err()); } #[test] fn test_nitro_header_round_trip() { let measurements = MultiMeasurements::Nitro(HashMap::from([ - (0, vec![0xabu8; crate::nitro::NITRO_PCR_LENGTH]), - (8, vec![0xcdu8; crate::nitro::NITRO_PCR_LENGTH]), + (0, [0xabu8; crate::nitro::NITRO_PCR_LENGTH]), + (8, [0xcdu8; crate::nitro::NITRO_PCR_LENGTH]), ])); let header = measurements.to_header_format().unwrap(); @@ -1292,11 +1294,11 @@ mod tests { assert!(azure_debug.contains(&hex::encode(azure_register_value))); assert!(!azure_debug.contains(&format!("{azure_register_value:?}"))); - let nitro_register_value = vec![0xabu8; crate::nitro::NITRO_PCR_LENGTH]; - let nitro = MultiMeasurements::Nitro(HashMap::from([(8u32, nitro_register_value.clone())])); + let nitro_register_value = [0xabu8; crate::nitro::NITRO_PCR_LENGTH]; + let nitro = MultiMeasurements::Nitro(HashMap::from([(8u32, nitro_register_value)])); let nitro_debug = format!("{nitro:?}"); assert!(nitro_debug.contains("Nitro")); - assert!(nitro_debug.contains(&hex::encode(&nitro_register_value))); + assert!(nitro_debug.contains(&hex::encode(nitro_register_value))); assert!(!nitro_debug.contains(&format!("{nitro_register_value:?}"))); } diff --git a/crates/attestation/src/nitro.rs b/crates/attestation/src/nitro.rs index 3cad2f0..9ae0623 100644 --- a/crates/attestation/src/nitro.rs +++ b/crates/attestation/src/nitro.rs @@ -90,11 +90,9 @@ fn decode_with_root_at_time( } fn measurements_from_doc(doc: &AttestationDoc) -> Result { - let expected_pcr_len = match doc.digest { - Digest::SHA256 => 32, - Digest::SHA384 => NITRO_PCR_LENGTH, - Digest::SHA512 => 64, - }; + if doc.digest != Digest::SHA384 { + return Err(NitroError::UnsupportedDigest(doc.digest)); + } let mut measurements = HashMap::new(); for (index, value) in &doc.pcrs { @@ -102,14 +100,21 @@ fn measurements_from_doc(doc: &AttestationDoc) -> Result 31 { return Err(NitroError::InvalidPcrIndex(index as usize)); } - if value.as_ref().len() != expected_pcr_len { + if value.as_ref().len() != NITRO_PCR_LENGTH { return Err(NitroError::BadPcrLength { index, - expected: expected_pcr_len, + expected: NITRO_PCR_LENGTH, actual: value.as_ref().len(), }); } - measurements.insert(index, value.as_ref().to_vec()); + measurements.insert( + index, + value.as_ref().try_into().map_err(|_| NitroError::BadPcrLength { + index, + expected: NITRO_PCR_LENGTH, + actual: value.as_ref().len(), + })?, + ); } Ok(MultiMeasurements::Nitro(measurements)) @@ -185,12 +190,12 @@ pub fn mock_nitro_measurements() -> MultiMeasurements { let pcrs = mock_nitro_pcrs(); MultiMeasurements::Nitro(HashMap::from([ - (0, pcrs.get(PcrIndex::Zero).as_ref().to_vec()), - (1, pcrs.get(PcrIndex::One).as_ref().to_vec()), - (2, pcrs.get(PcrIndex::Two).as_ref().to_vec()), - (3, pcrs.get(PcrIndex::Three).as_ref().to_vec()), - (4, pcrs.get(PcrIndex::Four).as_ref().to_vec()), - (8, pcrs.get(PcrIndex::Eight).as_ref().to_vec()), + (0, pcrs.get(PcrIndex::Zero).as_ref().try_into().unwrap()), + (1, pcrs.get(PcrIndex::One).as_ref().try_into().unwrap()), + (2, pcrs.get(PcrIndex::Two).as_ref().try_into().unwrap()), + (3, pcrs.get(PcrIndex::Three).as_ref().try_into().unwrap()), + (4, pcrs.get(PcrIndex::Four).as_ref().try_into().unwrap()), + (8, pcrs.get(PcrIndex::Eight).as_ref().try_into().unwrap()), ])) } @@ -205,6 +210,8 @@ pub enum NitroError { Verification(String), #[error("Nitro attestation nonce is not as expected")] InputMismatch, + #[error("Unsupported Nitro digest: {0:?}; expected SHA384")] + UnsupportedDigest(Digest), #[error("Invalid Nitro PCR index: {0}")] InvalidPcrIndex(usize), #[error("Nitro PCR {index} has length {actual}, expected {expected}")] @@ -220,7 +227,7 @@ mod tests { use crate::{AttestationExchangeMessage, AttestationType, AttestationVerifier}; #[test] - fn aws_signed_attestation_fixture_verifies() { + fn aws_signed_attestation_fixture_has_expected_measurements() { let attestation = include_bytes!("../test-assets/aws-nitro-attestation-sample.bin"); let timestamp_millis = 1_680_010_000_000; let doc = decode_with_root_at_time( @@ -230,18 +237,38 @@ mod tests { ) .unwrap(); let measurements = measurements_from_doc(&doc).unwrap(); - - if let MultiMeasurements::Nitro(pcrs) = measurements { - assert!(pcrs.contains_key(&0)); - } else { - panic!("expected Nitro measurements"); + let mut expected_pcrs = HashMap::from([ + ( + 3, + hex::decode( + "e48b6ac6bab30e3717d28c2c88f2ba8b614e454590eb00b26170eef0d707b5b8e3a97662c20b2ced6192d3aaa2f5e24e", + ) + .unwrap() + .try_into() + .unwrap(), + ), + ( + 4, + hex::decode( + "3413af1370600b63aef6362b3d2506bcd6b6c263c8736b913d09e83c8bf24f93eb23eb87b15672586ef78c4289594acd", + ) + .unwrap() + .try_into() + .unwrap(), + ), + ]); + for index in 0..16 { + expected_pcrs.entry(index).or_insert([0u8; NITRO_PCR_LENGTH]); } + let expected = MultiMeasurements::Nitro(expected_pcrs); + + assert_eq!(measurements, expected); } #[test] - fn aws_signed_attestation_fixture_has_expected_measurements() { - let attestation = include_bytes!("../test-assets/aws-nitro-attestation-sample.bin"); - let timestamp_millis = 1_680_010_000_000; + fn another_aws_signed_attestation_fixture_has_expected_measurements() { + let attestation = include_bytes!("../test-assets/aws-nitro-1779281545803060420"); + let timestamp_millis = 1_779_292_000_000; let doc = decode_with_root_at_time( attestation, AWS_ROOT_CERT_DER, @@ -249,24 +276,31 @@ mod tests { ) .unwrap(); let measurements = measurements_from_doc(&doc).unwrap(); + // This attestation was captured from a debug-mode enclave, so PCR0/1/2 are + // all zeros (debug mode zeroes the EIF measurements). PCR3 and PCR4 + // are non-zero. let mut expected_pcrs = HashMap::from([ ( 3, hex::decode( - "e48b6ac6bab30e3717d28c2c88f2ba8b614e454590eb00b26170eef0d707b5b8e3a97662c20b2ced6192d3aaa2f5e24e", + "ea81e40d742a2d5c5f9e099f5abce802914b55d7b2df6eeda212f8b4a96581a15689220b8dfcdda1825e3cfe2b7d6a06", ) + .unwrap() + .try_into() .unwrap(), ), ( 4, hex::decode( - "3413af1370600b63aef6362b3d2506bcd6b6c263c8736b913d09e83c8bf24f93eb23eb87b15672586ef78c4289594acd", + "50b5a4fd0bdcb66bfb04830da2a8baccf172629c3b30b8486c78ef06b18596fc4375fc0be0761e7033b41bf20ba28b41", ) + .unwrap() + .try_into() .unwrap(), ), ]); for index in 0..16 { - expected_pcrs.entry(index).or_insert_with(|| vec![0u8; NITRO_PCR_LENGTH]); + expected_pcrs.entry(index).or_insert([0u8; NITRO_PCR_LENGTH]); } let expected = MultiMeasurements::Nitro(expected_pcrs); @@ -297,4 +331,27 @@ mod tests { assert!(matches!(err, NitroError::InputMismatch)); } + + #[test] + fn unsupported_digest_is_rejected() { + use std::collections::BTreeMap; + + use nsm_nitro_enclave_utils::api::ByteBuf; + + let doc = AttestationDoc { + module_id: "test".to_string(), + digest: Digest::SHA256, + timestamp: 0, + pcrs: BTreeMap::from([(0usize, ByteBuf::from(vec![0u8; 32]))]), + certificate: ByteBuf::from(Vec::new()), + cabundle: Vec::new(), + public_key: None, + user_data: None, + nonce: None, + }; + + let err = measurements_from_doc(&doc).unwrap_err(); + + assert!(matches!(err, NitroError::UnsupportedDigest(Digest::SHA256))); + } } From 42d30b60991053580b449d5738631a45454fd3e8 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 21 May 2026 14:23:05 +0200 Subject: [PATCH 4/4] Add nitro to attestation type detection, use production enclave attestation doc in tests --- crates/attestation/src/lib.rs | 3 ++ crates/attestation/src/nitro.rs | 40 +++++++++++++++--- .../test-assets/aws-nitro-1779281545803060420 | Bin 4524 -> 0 bytes .../test-assets/aws-nitro-1779365257362730433 | Bin 0 -> 4524 bytes 4 files changed, 38 insertions(+), 5 deletions(-) delete mode 100644 crates/attestation/test-assets/aws-nitro-1779281545803060420 create mode 100644 crates/attestation/test-assets/aws-nitro-1779365257362730433 diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 0bdf544..5ddb71c 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -107,6 +107,9 @@ impl AttestationType { return Ok(AttestationType::AzureTdx); } } + if nitro::running_on_nitro() { + return Ok(AttestationType::AwsNitro); + } // Otherwise try DCAP quote - this internally checks that the quote provider // is `tdx_guest` if tdx_attest::get_quote(&[0; 64]).is_ok() { diff --git a/crates/attestation/src/nitro.rs b/crates/attestation/src/nitro.rs index 9ae0623..de37900 100644 --- a/crates/attestation/src/nitro.rs +++ b/crates/attestation/src/nitro.rs @@ -24,6 +24,12 @@ pub fn create_nitro_attestation(input_data: [u8; 64]) -> Result, NitroEr request_attestation(&nitro, input_data) } +/// Return true if we can successfully talk to the Nitro Secure Module. +pub(crate) fn running_on_nitro() -> bool { + let nitro = Nitro::init(); + matches!(nitro.process_request(Request::DescribeNSM), Response::DescribeNSM { .. }) +} + fn request_attestation(driver: &impl Driver, input_data: [u8; 64]) -> Result, NitroError> { match driver.process_request(Request::Attestation { nonce: Some(ByteBuf::from(input_data.to_vec())), @@ -267,8 +273,8 @@ mod tests { #[test] fn another_aws_signed_attestation_fixture_has_expected_measurements() { - let attestation = include_bytes!("../test-assets/aws-nitro-1779281545803060420"); - let timestamp_millis = 1_779_292_000_000; + let attestation = include_bytes!("../test-assets/aws-nitro-1779365257362730433"); + let timestamp_millis = 1_779_365_255_000; let doc = decode_with_root_at_time( attestation, AWS_ROOT_CERT_DER, @@ -276,10 +282,34 @@ mod tests { ) .unwrap(); let measurements = measurements_from_doc(&doc).unwrap(); - // This attestation was captured from a debug-mode enclave, so PCR0/1/2 are - // all zeros (debug mode zeroes the EIF measurements). PCR3 and PCR4 - // are non-zero. let mut expected_pcrs = HashMap::from([ + ( + 0, + hex::decode( + "5fd25293fa7f5682ab2290f0850da91ff42e7e37f79498a7f133dac86a66e678e3c399891a119d82ab35b2fca0d647fe", + ) + .unwrap() + .try_into() + .unwrap(), + ), + ( + 1, + hex::decode( + "0343b056cd8485ca7890ddd833476d78460aed2aa161548e4e26bedf321726696257d623e8805f3f605946b3d8b0c6aa", + ) + .unwrap() + .try_into() + .unwrap(), + ), + ( + 2, + hex::decode( + "c48f4b4ddb0711cac8c94de79f3e96e387eb52693cc3b1fb664ef90c7f9c5df602a16e7dabe6cad52e8791223ddf602b", + ) + .unwrap() + .try_into() + .unwrap(), + ), ( 3, hex::decode( diff --git a/crates/attestation/test-assets/aws-nitro-1779281545803060420 b/crates/attestation/test-assets/aws-nitro-1779281545803060420 deleted file mode 100644 index 2c4b88dabb0f513aad08e817be934b43e0440a51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4524 zcmd6qcT^ME9>9ZK3MU+lk@hyvvbb<<99RPnP2Yxe1E?? z%e=Qzg$}vFmSan0(J@j@6dop%#toCffX5L?r4m#k;EQmMh%1G0IR+3B&f^OZ9$zGI zB{)(-DhtO|Y9heblPl!O)Us$YLZcOS6bf~-c;2}QQU#_|9iRjOik=r$e+i_&gcbxI zY44k=9pPDFn~k=F6#WjZ^$Waus6qa88@sh2AJ=aEF2%uQRr{^2iJ_z4IW3N4(u08i z;q4tJH;>GHOJ8cb?We*EFO-t(QQ(+wRNOP>!R6`g<23Ciw`2P$xCM4EO}4G{Wayt5 zQ-6tp{t`p|B}V#7jP;k8=r3XEFEQ0$Vy3^uTz|=23|FdUgbYK~crcUz5}`%ppytvb zDg;^RoF&ePfpw0GG~Jg1jG2axNxqftbQ%q4S0g6Ch)D~iGp(S&0K^Jd>HkOGbj2bHjm1@(lThfZ-DY1RsDmqhTC%t5bk^Vn zBVLpf>>p|~lXE={(g)WDq;b71HA;|FFkz!}0k)kA)77=U9zJiMlr3#GyLfPQ^<8bm zaN9Xp?94w3UTw(>EfrD3@eF+0^AlG72Ke!aJbPdVSY+n*R#fVHIs_=dL>HOU=`@IW zqU%o6WE4O^2xVncKMzS8{4T`!;=}v7SIkYJse5<9L4~ai#k2iVDp_8r$E4@%j94Gy zb|LOlDkde_r&34PxScJ}Jihi^x*8ukBn&D9pyT|z4p+hbrl;}$cq}^lOWN;66#@3; zb7m;15g01b$fZ&EvS6qe38E!Pq^5VmXh~^9G!b^G`|sHMTqwBT*!o@99KnR{d;1?K zvjcrPH;vshM~Dak93jHx2mznuh}RkNK7@betG=@N?a=hcI}J}ZM=0)Q)uopB2svpg zd5Yir7UlI6Z$JO34|v-Tw%oXPq9~{@=jg#$1G9Sel`M}(1MP^$X;q7VyJZv>W2Jay zYdG%uO$+M4`&AfqxDnx1}2^Mc$Mb`#sc%to6i6=k^x|^n=qr>(5+L495%hq z7~hy)mi|VWQ?)U_!7u%Tk?qZOu#kjMle(_{CqnX;xUAh;Q5@QMOtQ!(vnG(Xx<74P z+e|E9@}s*bVS{*UzoJs-QGwU?HO(t1yRx^LBxGCjCL6{f#qb@s{BmW7sdBvFZT*_t zO#8@yK1;IeUSA(qgtj|9rk#&zS~Uo2ZfFUHPLW)ckVE{5i$^~!uDbciqLS(v!k$uH z&FGrhU%6Qy>kGmMfXm_QxFE}oE8qc=6EXx+y$~dYoe07N;GsN`fZz)x1i(?edx7|K zmHP@ZyGJ@zC2-?LE@&JRoL%JEM>q}fXR1SePo|$9ni&~4h@D}a6x4ji2FXr;;wrDo zQHdEQqugr}=l5kFpkS&K?L%FznOD{hDJj2~YW&vy%FZnUklj z1=kSfV)+EGPA3LpV;vJ6BvaD}i zZg|b@Yi@7bHLd>>SfB6~aR5gE1fSX`A3;8ZK%0V$25z0qatV=CD3SnjC2;`?Fv6Ma zUw%W#Y zlTba|+IQNmlpO+FEU$H80ITFO$WJEgdkvBEb6VpX2Q(`DARYvTiw};AhJXQK1nRJN5&bW5vOyZAst(gtqJmoriJrgIk?5u5lG0M>^`EtmV ztH%v)ZM5nr^uA&`Xw^l({r4V-kB4{}<`F>JgO4cPeE`wB97-s0;RewwLh5VEF9gB{ z5B3kJ&){{lU5~!%^2PG8ypM0I^siidN%*Gs9@fR3h4G;+8`QKr`LMc4o@k%b~RF=kwnR`u(^1Ai%g@!m0?m;jke(O7&(Rqjn#ck{WsV9pgF4s zZm56N7|Zpwak+{X?3U)6RqxnYBs4jqbYC~dqaWiNy9)2g9m++;;i|Io0H<(8-_f^2 x*K9gccA#+iLX!dJXqtoDips#^#ZIy3hwgG?+2yf&$+gJ`H}3M$Y6iIN{U?D}$fy7S diff --git a/crates/attestation/test-assets/aws-nitro-1779365257362730433 b/crates/attestation/test-assets/aws-nitro-1779365257362730433 new file mode 100644 index 0000000000000000000000000000000000000000..8e92a64c3b99b9d64c924944fafe933093b7d09c GIT binary patch literal 4524 zcmd6qc~lek7RQrG2up%7ASxo-Vzmkeen})-44xQ9IAVkg z!>~|Au|g^FaCTtX!cwI)7_*SzP%|73zuP6`qXbbX8m2gb^9F*dCEI(WmPH@7 zNO%%!m@~E0`d{27d9g?U-4i8;Aa8K(d%o*^UD}Q7 zWv;E6^OBo3JzgT6Q}mh}Buk;w;Ep`s=cy{pY;2S{FnjdIR@UdtZ=iPp|4D zZZO<)@O=5xFkyxrIO{A^{Xxo&P40IL?3NU4YA(I2xOa_AqxGdQBt;xnrt}jq;|6e3L z56l@%m_0(mBXzRje*#2+u_WDBn+oAcI#y~T&VnGs5ly;^l>SWlvIs$T38yIWgw(w7 zC+Mu7RaK5_n{2x<^pa^IC0yUhNA!ItA<@9MC@ z%9K7hfAE@sI*m`x317YIR>hSl$Fx$fANbkkzzk5Z%*~DQ_yHmWaDc3}s6-+GB3&L@ zX+j_lh#>}UM;LXh&u&k%+f&Ns3aI)RFlE%&hx4HhlsXc8pO?AUbuRx6E*kh zC!ds8_a2A90-J+14yRpWuXOs^cNZq4&^1vl4k?qlrFV)eYdWU{PS|Wu2=1G?!}4Wq zu;0UERUnE8!{s8GIK~Gu!8oLWXngV@i!|JGMtGoE!tr0s7nbKYxArU>nj-jOcxnF& zWM;s0ux3UhW5nPw0MnMiV%h>Wh7mky!mrG~^Hm?HeLuAH@qWE49f6^Z>Gu*YF0^GP zD&%qef$p%HI48dQ;ud&sZukAB%NM*SG0)^i=#Hs3s!sp>w~x&vtxGD`-LBIYgcyf* zPSA6#sU3^|xVaL=&(xPVZ7Pct#sJsi&pWf{1A9PyTYDWqzz_iDTE`gTc;dK(>8A{9 zbQZNH7bW+G?W#=6Y2YWn(y=*J%L)bve&f*HCrEKXf&r2pWwR}-tkA#ptZ?o4)GANH z)~N)?=gZL?Vfi-yU?F=tc8=G{3!S;Ij<1oP!pp_^ zef8fzAejewm_}vX>#A|Qfb?5ECfp49eakecqoLaeD#Ez9gxyZBdz4Nxt-mOscK>a; zbKRrR2W>+W`#Wp(rn>;_;=)=iux7&nJivHm0@0`l6Jw2W9^StlO?+bep z>erkM3Tukpx;UMGt#SGO0mrBt%i1a`T%xbxDmrzzhCkBSk*Qmv?R&14Q7Hv4(%EuN zX}0M z;{X(;b445$9cBtebe@*?e|A_8i=KOmdde~XwwV_9Wyd2&?LwsmvuHzr)+*u^moQrjS(i{)7Qa;6J5`NEZ z%*v!qS_tLY3F1=1rx!fRT&sAYsA`TWJQt!yU*1fC8YARfsSdFVmh}dE%aZ=OcyQU;X&BSw^!}ReF zBJ-VyxGEu3=N+0qFi`F_clll{`|@8k_2?nDhKuQY&;B`URwX>H%d(+T z-{{PP8Pa|2VeY40^O!~{cdJXI;~U438YZ{1Ea(5RAi8`1ot~$5mh(6Lixfo@%z z@r(0L)rQlyRPc{CwJa#`ov)WI0iqFvgc~jdh&bd_f>Q6fvv>|VQGwEtPcad=cD?t@t-syVa^s|AMtQM7-8NAxBZXmKH zwQNTSjtM zG2eG=d@tun5+_67O*)YeL!AXOr{X9UJ?7{te1aG{^t| literal 0 HcmV?d00001