use std::collections::BTreeMap;
use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest as NsmDigest};
use coset::{CborSerializable, CoseSign1Builder, HeaderBuilder, iana};
use ring::digest::{SHA256, digest};
use ring::rand::SystemRandom;
use ring::signature::{ECDSA_P384_SHA384_FIXED_SIGNING, EcdsaKeyPair};
use time::{Duration, OffsetDateTime};
use super::verify::{NitroVerifier, TrustAnchor};
pub(crate) struct SyntheticQuote {
pub bytes: Vec<u8>,
pub root_fingerprint: [u8; 32],
pub valid_now: OffsetDateTime,
pub pcr0: Vec<u8>,
pub pcr8: Vec<u8>,
pub module_id: String,
}
impl SyntheticQuote {
pub(crate) fn verifier(&self) -> NitroVerifier {
NitroVerifier {
anchor: TrustAnchor::RootFingerprint(self.root_fingerprint),
now: self.valid_now,
}
}
}
pub(crate) fn build(user_data: Vec<u8>) -> SyntheticQuote {
build_inner(user_data, true)
}
pub(crate) fn build_with_bad_signature(user_data: Vec<u8>) -> SyntheticQuote {
build_inner(user_data, false)
}
fn build_inner(user_data: Vec<u8>, good_signature: bool) -> SyntheticQuote {
let chain = nitro_attest::builder::chain(); let der: Vec<Vec<u8>> = chain
.iter()
.map(|c| c.cert.der().as_ref().to_vec())
.collect();
let root_fingerprint: [u8; 32] = digest(&SHA256, &der[0])
.as_ref()
.try_into()
.expect("sha256 is 32 bytes");
let leaf_der = der.last().expect("chain has a leaf").clone();
let cabundle: Vec<Vec<u8>> = der[..der.len() - 1].to_vec();
let module_id = "i-synthetic-enclave".to_string();
let pcr0 = vec![0x11u8; 48];
let pcr8 = vec![0x88u8; 48];
let mut pcrs: BTreeMap<usize, Vec<u8>> = BTreeMap::new();
pcrs.insert(0, pcr0.clone());
pcrs.insert(8, pcr8.clone());
let timestamp_ms: u64 = 1_577_836_800_000;
let doc = AttestationDoc::new(
module_id.clone(),
NsmDigest::SHA384,
timestamp_ms,
pcrs,
leaf_der,
cabundle,
Some(user_data),
None,
None,
);
let payload = doc.to_binary();
let leaf_keypair = &chain.last().expect("leaf").keys;
let pkcs8 = leaf_keypair.serialize_der();
let rng = SystemRandom::new();
let signing_key = EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &pkcs8, &rng)
.expect("leaf PKCS#8 loads into ring");
let protected = HeaderBuilder::new()
.algorithm(iana::Algorithm::ES384)
.build();
let sign1 = CoseSign1Builder::new()
.protected(protected)
.payload(payload)
.create_signature(&[], |tbs| {
let to_sign = if good_signature {
tbs.to_vec()
} else {
let mut corrupted = tbs.to_vec();
corrupted[0] ^= 0xff;
corrupted
};
signing_key
.sign(&rng, &to_sign)
.expect("ring ES384 sign")
.as_ref()
.to_vec()
})
.build();
let bytes = sign1.to_vec().expect("COSE_Sign1 serializes");
SyntheticQuote {
bytes,
root_fingerprint,
valid_now: OffsetDateTime::now_utc(),
pcr0,
pcr8,
module_id,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attestation::parse::parse_nitro_quote;
use crate::attestation::verify::{
AWS_NITRO_ROOT_G1_FINGERPRINT, AWS_NITRO_ROOT_G1_PEM, NitroVerifyError,
};
use crate::hex::lower as hex_lower;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64STD;
#[test]
fn parse_extracts_fields_without_verifying() {
let q = build(b"commitment".to_vec());
let parsed = parse_nitro_quote(&q.bytes).expect("synthetic quote parses");
assert_eq!(parsed.module_id, q.module_id);
assert_eq!(parsed.user_data.as_deref(), Some(b"commitment".as_ref()));
assert_eq!(parsed.pcrs.get(&0), Some(&q.pcr0));
assert_eq!(parsed.pcrs.get(&8), Some(&q.pcr8));
}
#[test]
fn parse_is_total_on_garbage() {
for bytes in [
vec![],
vec![0u8; 64],
vec![0xff; 1024],
b"not cbor at all".to_vec(),
] {
assert!(parse_nitro_quote(&bytes).is_err());
}
}
#[test]
fn verifier_accepts_with_matching_anchor_and_clock() {
let q = build(b"ud".to_vec());
let parsed = q.verifier().verify(&q.bytes).expect("verifies");
assert_eq!(parsed.module_id, q.module_id);
}
#[test]
fn verifier_rejects_wrong_anchor() {
let q = build(b"ud".to_vec());
let v = NitroVerifier {
anchor: TrustAnchor::RootFingerprint([0u8; 32]),
now: q.valid_now,
};
assert!(matches!(
v.verify(&q.bytes),
Err(NitroVerifyError::RootFingerprintMismatch { .. })
));
}
#[test]
fn verifier_rejects_aws_anchor_for_synthetic_chain() {
let q = build(b"ud".to_vec());
let v = NitroVerifier::aws_production(q.valid_now);
assert!(matches!(
v.verify(&q.bytes),
Err(NitroVerifyError::RootFingerprintMismatch { .. })
));
}
#[test]
fn verifier_rejects_expired_and_not_yet_valid() {
let q = build(b"ud".to_vec());
let expired = NitroVerifier {
anchor: TrustAnchor::RootFingerprint(q.root_fingerprint),
now: q.valid_now + Duration::hours(2),
};
assert!(matches!(
expired.verify(&q.bytes),
Err(NitroVerifyError::CertExpired { .. })
));
let early = NitroVerifier {
anchor: TrustAnchor::RootFingerprint(q.root_fingerprint),
now: q.valid_now - Duration::hours(2),
};
assert!(matches!(
early.verify(&q.bytes),
Err(NitroVerifyError::CertNotYetValid { .. })
));
}
#[test]
fn verifier_rejects_bad_cose_signature() {
let q = build_with_bad_signature(b"ud".to_vec());
assert!(matches!(
q.verifier().verify(&q.bytes),
Err(NitroVerifyError::CoseSignatureInvalid)
));
}
#[test]
fn aws_anchor_fingerprint_matches_vendored_pem() {
let der = pem_to_der(AWS_NITRO_ROOT_G1_PEM);
let computed: [u8; 32] = digest(&SHA256, &der).as_ref().try_into().unwrap();
assert_eq!(computed, AWS_NITRO_ROOT_G1_FINGERPRINT);
assert_eq!(
hex_lower(&AWS_NITRO_ROOT_G1_FINGERPRINT),
"641a0321a3e244efe456463195d606317ed7cdcc3c1756e09893f3c68f79bb5b"
);
}
#[test]
fn aws_anchor_fingerprint_matches_upstream_baked_value() {
let q = build(b"ud".to_vec());
let err = nitro_attest::UnparsedAttestationDoc::from(q.bytes.as_slice())
.parse_and_verify(q.valid_now)
.expect_err("synthetic root is not the AWS root");
match err {
nitro_attest::Error::CertificateRootInvalid { want, .. } => {
assert_eq!(want, hex_lower(&AWS_NITRO_ROOT_G1_FINGERPRINT));
}
other => panic!("expected CertificateRootInvalid, got {other:?}"),
}
}
#[test]
fn parse_failure_implies_upstream_verify_failure() {
let now = OffsetDateTime::now_utc();
for bytes in [
vec![],
vec![0u8; 32],
vec![0xab; 200],
b"\x84garbage".to_vec(),
] {
if parse_nitro_quote(&bytes).is_err() {
assert!(
nitro_attest::UnparsedAttestationDoc::from(bytes.as_slice())
.parse_and_verify(now)
.is_err(),
"upstream accepted bytes our parser rejected: {bytes:?}"
);
}
}
}
#[test]
fn verify_nitro_quote_with_end_to_end() {
use crate::attestation::verify_nitro_quote_with;
use crate::sealed_transfer::AttestationQuoteAssertion;
let client_ed = [7u8; 32];
let nonce = [9u8; 16];
let (_sk, producer_ed) = crate::sealed_transfer::generate_ed25519_keypair();
let producer_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&producer_ed);
let mut h = ring::digest::Context::new(&SHA256);
h.update(&client_ed);
h.update(&nonce);
h.update(&producer_ed);
let commitment = h.finish().as_ref().to_vec();
let q = build(commitment);
let assertion = AttestationQuoteAssertion {
format: "nitro".into(),
quote_b64: B64STD.encode(&q.bytes),
};
let verified =
verify_nitro_quote_with(&assertion, &client_ed, &nonce, &producer_did, &q.verifier())
.expect("end-to-end verify");
assert_eq!(verified.module_id, q.module_id);
assert_eq!(verified.pcr0_hex, hex_lower(&q.pcr0));
assert_eq!(verified.pcr8_hex, hex_lower(&q.pcr8));
let wrong_client = [8u8; 32];
assert!(matches!(
verify_nitro_quote_with(
&assertion,
&wrong_client,
&nonce,
&producer_did,
&q.verifier()
),
Err(crate::attestation::AttestationVerifyError::UserDataMismatch)
));
}
#[test]
#[ignore = "requires a real AWS Nitro quote fixture (live enclave)"]
fn real_quote_field_parity_with_upstream() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/nitro-quote.cose"
);
let Ok(bytes) = std::fs::read(path) else {
eprintln!("no fixture at {path}; drop a captured quote there to run this");
return;
};
let parsed = parse_nitro_quote(&bytes).expect("real quote parses");
let now = OffsetDateTime::from_unix_timestamp((parsed.timestamp_ms / 1000) as i64)
.expect("valid timestamp");
let ours = NitroVerifier::aws_production(now)
.verify(&bytes)
.expect("our verifier accepts the real quote");
let theirs = nitro_attest::UnparsedAttestationDoc::from(bytes.as_slice())
.parse_and_verify(now)
.expect("upstream accepts the real quote");
assert_eq!(ours.module_id, theirs.module_id);
assert_eq!(
ours.user_data.as_deref(),
theirs.user_data.as_ref().map(|b| b.as_ref())
);
assert_eq!(
ours.timestamp_ms / 1000,
theirs.timestamp.unix_timestamp() as u64
);
for (idx, digest) in &theirs.pcrs {
assert_eq!(
ours.pcrs.get(&(*idx as usize)).map(Vec::as_slice),
Some(digest.value.as_slice()),
"PCR{idx} differs"
);
}
}
#[test]
#[ignore = "seed-corpus generator; run explicitly with --ignored"]
fn gen_nitro_fuzz_seed() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../fuzz/seeds/nitro-quote");
std::fs::create_dir_all(dir).expect("create seed dir");
let q = build(b"fuzz-seed-commitment".to_vec());
q.verifier().verify(&q.bytes).expect("seed quote verifies");
let path = format!("{dir}/synthetic.cose");
std::fs::write(&path, &q.bytes).expect("write seed");
println!("wrote {} ({} bytes)", path, q.bytes.len());
}
fn pem_to_der(pem: &str) -> Vec<u8> {
let body: String = pem
.lines()
.filter(|l| !l.starts_with("-----"))
.collect::<Vec<_>>()
.join("");
B64STD.decode(body.trim()).expect("valid PEM base64")
}
}