use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64STD;
use sha2::{Digest, Sha256};
use time::OffsetDateTime;
use crate::sealed_transfer::{AssertionProof, AttestationQuoteAssertion, ProducerAssertion};
pub mod parse;
#[cfg(test)]
mod test_quote;
pub mod verify;
pub use parse::{NitroParseError, ParsedNitroQuote, parse_nitro_quote};
pub use verify::{
AWS_NITRO_ROOT_G1_FINGERPRINT, AWS_NITRO_ROOT_G1_PEM, NitroVerifier, NitroVerifyError,
TrustAnchor,
};
#[derive(Debug, Clone)]
pub struct VerifiedAttestation {
pub module_id: String,
pub pcr0_hex: String,
pub pcr8_hex: String,
}
#[derive(Debug, thiserror::Error)]
pub enum AttestationVerifyError {
#[error("expected an Attested proof, got {0}")]
WrongProofVariant(&'static str),
#[error("unknown attestation format: {0}")]
UnknownFormat(String),
#[error("base64 decode: {0}")]
Base64(String),
#[error("quote parse/verify failed: {0}")]
QuoteInvalid(String),
#[error("attestation quote is missing user_data")]
MissingUserData,
#[error("user_data mismatch — quote does not commit to this bundle")]
UserDataMismatch,
#[error("invalid producer did:key: {0}")]
BadProducerDid(String),
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("PCR{which} mismatch: enclave reported {actual}, operator expected {expected}")]
pub struct PcrMismatch {
pub which: u8,
pub expected: String,
pub actual: String,
}
use crate::hex::lower as hex_lower;
fn is_nitro_format(format: &str) -> bool {
matches!(
format.to_ascii_lowercase().as_str(),
"nitro" | "aws-nitro" | "aws-nitro-v1"
)
}
pub fn verify_nitro_assertion(
producer: &ProducerAssertion,
client_ed25519_pub: &[u8; 32],
nonce: &[u8; 16],
) -> Result<VerifiedAttestation, AttestationVerifyError> {
let quote = match &producer.proof {
AssertionProof::Attested(q) => q,
AssertionProof::PinnedOnly => {
return Err(AttestationVerifyError::WrongProofVariant("PinnedOnly"));
}
AssertionProof::DidSigned(_) => {
return Err(AttestationVerifyError::WrongProofVariant("DidSigned"));
}
};
verify_nitro_quote(quote, client_ed25519_pub, nonce, &producer.producer_did)
}
pub fn verify_nitro_quote(
quote: &AttestationQuoteAssertion,
client_ed25519_pub: &[u8; 32],
nonce: &[u8; 16],
producer_did: &str,
) -> Result<VerifiedAttestation, AttestationVerifyError> {
verify_nitro_quote_with(
quote,
client_ed25519_pub,
nonce,
producer_did,
&NitroVerifier::aws_production(OffsetDateTime::now_utc()),
)
}
pub fn verify_nitro_quote_with(
quote: &AttestationQuoteAssertion,
client_ed25519_pub: &[u8; 32],
nonce: &[u8; 16],
producer_did: &str,
verifier: &NitroVerifier,
) -> Result<VerifiedAttestation, AttestationVerifyError> {
if !is_nitro_format("e.format) {
return Err(AttestationVerifyError::UnknownFormat(quote.format.clone()));
}
let quote_bytes = B64STD
.decode("e.quote_b64)
.map_err(|e| AttestationVerifyError::Base64(e.to_string()))?;
let parsed = verifier
.verify("e_bytes)
.map_err(|e| AttestationVerifyError::QuoteInvalid(format!("{e:?}")))?;
let producer_ed_pub = affinidi_crypto::did_key::did_key_to_ed25519_pub(producer_did)
.map_err(|e| AttestationVerifyError::BadProducerDid(e.to_string()))?;
let mut hasher = Sha256::new();
hasher.update(client_ed25519_pub);
hasher.update(nonce);
hasher.update(producer_ed_pub);
let expected = hasher.finalize();
let user_data_bytes: &[u8] = parsed
.user_data
.as_deref()
.ok_or(AttestationVerifyError::MissingUserData)?;
if user_data_bytes != expected.as_slice() {
return Err(AttestationVerifyError::UserDataMismatch);
}
let pcr_hex = |idx: usize| -> String {
parsed
.pcrs
.get(&idx)
.filter(|v| v.iter().any(|b| *b != 0))
.map(|v| hex_lower(v))
.unwrap_or_default()
};
Ok(VerifiedAttestation {
module_id: parsed.module_id,
pcr0_hex: pcr_hex(0),
pcr8_hex: pcr_hex(8),
})
}
fn normalize_pcr_hex(s: &str) -> String {
let s = s.trim();
let s = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
s.chars()
.filter(|c| !c.is_whitespace())
.map(|c| c.to_ascii_lowercase())
.collect()
}
impl VerifiedAttestation {
pub fn check_pcrs(
&self,
expect_pcr0: Option<&str>,
expect_pcr8: Option<&str>,
) -> Result<(), PcrMismatch> {
check_pcr(0, expect_pcr0, &self.pcr0_hex)?;
check_pcr(8, expect_pcr8, &self.pcr8_hex)?;
Ok(())
}
}
fn check_pcr(which: u8, expected: Option<&str>, actual: &str) -> Result<(), PcrMismatch> {
let Some(expected) = expected else {
return Ok(());
};
let expected = normalize_pcr_hex(expected);
let actual = normalize_pcr_hex(actual);
if expected != actual {
return Err(PcrMismatch {
which,
expected,
actual,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sealed_transfer::{
AttestationQuoteAssertion, DidSignedAssertion, ProducerAssertion,
};
fn nitro_attestation(quote_b64: &str) -> AttestationQuoteAssertion {
AttestationQuoteAssertion {
format: "nitro".into(),
quote_b64: quote_b64.into(),
}
}
#[test]
fn pinned_only_assertion_rejected() {
let producer = ProducerAssertion {
producer_did: "did:key:z6MkProducer".into(),
proof: AssertionProof::PinnedOnly,
};
let err = verify_nitro_assertion(&producer, &[0u8; 32], &[0u8; 16]).unwrap_err();
assert!(
matches!(err, AttestationVerifyError::WrongProofVariant("PinnedOnly")),
"got {err:?}"
);
}
#[test]
fn did_signed_assertion_rejected() {
let producer = ProducerAssertion {
producer_did: "did:key:z6MkProducer".into(),
proof: AssertionProof::DidSigned(DidSignedAssertion {
did: "did:key:z6MkProducer".into(),
signature_b64: "sig".into(),
verification_method: "did:key:z6MkProducer#z6MkProducer".into(),
}),
};
let err = verify_nitro_assertion(&producer, &[0u8; 32], &[0u8; 16]).unwrap_err();
assert!(
matches!(err, AttestationVerifyError::WrongProofVariant("DidSigned")),
"got {err:?}"
);
}
#[test]
fn unknown_format_rejected() {
let quote = AttestationQuoteAssertion {
format: "sev-snp".into(),
quote_b64: "AAAA".into(),
};
let err = verify_nitro_quote("e, &[0u8; 32], &[0u8; 16], "did:key:z6Mk").unwrap_err();
match err {
AttestationVerifyError::UnknownFormat(f) => assert_eq!(f, "sev-snp"),
other => panic!("expected UnknownFormat, got {other:?}"),
}
}
#[test]
fn nitro_format_strings_are_case_insensitive() {
for fmt in ["nitro", "Nitro", "AWS-NITRO", "aws-nitro-v1"] {
let quote = AttestationQuoteAssertion {
format: fmt.into(),
quote_b64: "AAAA".into(), };
let err = verify_nitro_quote("e, &[0u8; 32], &[0u8; 16], "did:key:z6MkBogus")
.unwrap_err();
assert!(
!matches!(err, AttestationVerifyError::UnknownFormat(_)),
"format '{fmt}' must NOT be UnknownFormat — got {err:?}"
);
}
}
#[test]
fn malformed_base64_rejected() {
let quote = nitro_attestation("not!valid!base64!@#$");
let err =
verify_nitro_quote("e, &[0u8; 32], &[0u8; 16], "did:key:z6MkBogus").unwrap_err();
assert!(
matches!(err, AttestationVerifyError::Base64(_)),
"got {err:?}"
);
}
#[test]
fn empty_quote_bytes_rejected_as_quote_invalid() {
let quote = nitro_attestation(""); let err =
verify_nitro_quote("e, &[0u8; 32], &[0u8; 16], "did:key:z6MkBogus").unwrap_err();
assert!(
matches!(err, AttestationVerifyError::QuoteInvalid(_)),
"got {err:?}"
);
}
#[test]
fn random_bytes_rejected_as_quote_invalid() {
let quote = nitro_attestation(&B64STD.encode([0u8; 64]));
let err =
verify_nitro_quote("e, &[0u8; 32], &[0u8; 16], "did:key:z6MkBogus").unwrap_err();
assert!(
matches!(err, AttestationVerifyError::QuoteInvalid(_)),
"got {err:?}"
);
}
#[test]
fn malformed_producer_did_rejected_at_format_layer() {
let _ = AttestationVerifyError::BadProducerDid("smoke".into());
}
fn attest(pcr0: &str, pcr8: &str) -> VerifiedAttestation {
VerifiedAttestation {
module_id: "i-abc".into(),
pcr0_hex: pcr0.into(),
pcr8_hex: pcr8.into(),
}
}
#[test]
fn check_pcrs_none_is_noop() {
assert!(attest("aaaa", "bbbb").check_pcrs(None, None).is_ok());
}
#[test]
fn check_pcrs_matching_passes_case_and_prefix_insensitive() {
let a = attest("ABCD1234", " effff ");
assert!(a.check_pcrs(Some("0xabcd1234"), Some("EFFFF")).is_ok());
assert!(a.check_pcrs(Some("abcd1234"), None).is_ok());
}
#[test]
fn check_pcrs_pcr0_mismatch_is_typed() {
let err = attest("aaaa", "bbbb")
.check_pcrs(Some("dead"), None)
.expect_err("wrong PCR0 must be rejected");
assert_eq!(err.which, 0);
assert_eq!(err.expected, "dead");
assert_eq!(err.actual, "aaaa");
}
#[test]
fn check_pcrs_pcr8_mismatch_is_typed() {
let err = attest("aaaa", "bbbb")
.check_pcrs(Some("aaaa"), Some("cafe"))
.expect_err("wrong PCR8 must be rejected");
assert_eq!(err.which, 8);
}
#[test]
fn check_pcrs_expecting_an_absent_pcr_fails() {
let err = attest("", "bbbb")
.check_pcrs(Some("abcd"), None)
.expect_err("pinning an absent PCR must fail closed");
assert_eq!(err.which, 0);
assert_eq!(err.actual, "");
}
}