use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64STD;
use nitro_attest::UnparsedAttestationDoc;
use sha2::{Digest, Sha256};
use time::OffsetDateTime;
use crate::sealed_transfer::{AssertionProof, AttestationQuoteAssertion, ProducerAssertion};
#[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),
}
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> {
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 = UnparsedAttestationDoc::from(quote_bytes.as_slice())
.parse_and_verify(OffsetDateTime::now_utc())
.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_ref()
.map(|b| b.as_ref())
.ok_or(AttestationVerifyError::MissingUserData)?;
if user_data_bytes != expected.as_slice() {
return Err(AttestationVerifyError::UserDataMismatch);
}
let pcr0_hex = parsed
.pcrs
.get(&0)
.map(|d| hex_lower(&d.value))
.unwrap_or_default();
let pcr8_hex = parsed
.pcrs
.get(&8)
.map(|d| hex_lower(&d.value))
.unwrap_or_default();
Ok(VerifiedAttestation {
module_id: parsed.module_id,
pcr0_hex,
pcr8_hex,
})
}
#[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());
}
}