use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64STD;
use sha2::{Digest, Sha256, Sha384};
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(())
}
const NITRO_PCR_HEX_LEN: usize = 96;
fn validate_expected_pcr(which: u8, value: &str) -> Result<(), ConfigAttestationVerifyError> {
let normalized = normalize_pcr_hex(value);
if normalized.len() == NITRO_PCR_HEX_LEN && normalized.bytes().all(|b| b.is_ascii_hexdigit()) {
return Ok(());
}
Err(ConfigAttestationVerifyError::InvalidExpectedPcr {
which,
value: normalized,
})
}
fn pcr_hex(parsed: &ParsedNitroQuote, idx: usize) -> String {
parsed
.pcrs
.get(&idx)
.filter(|v| v.iter().any(|b| *b != 0))
.map(|v| hex_lower(v))
.unwrap_or_default()
}
#[derive(Debug, Clone)]
pub struct AuthenticatedConfigAttestation {
module_id: String,
pcr0_hex: String,
pcr8_hex: String,
config_digest_sha384: Vec<u8>,
nonce: Vec<u8>,
config_view_json: Vec<u8>,
key_arn: Option<String>,
}
impl AuthenticatedConfigAttestation {
pub fn module_id(&self) -> &str {
&self.module_id
}
pub fn pcr0_hex(&self) -> &str {
&self.pcr0_hex
}
pub fn pcr8_hex(&self) -> &str {
&self.pcr8_hex
}
pub fn config_digest_sha384(&self) -> &[u8] {
&self.config_digest_sha384
}
pub fn nonce(&self) -> &[u8] {
&self.nonce
}
pub fn config_view_json(&self) -> &[u8] {
&self.config_view_json
}
pub fn key_arn(&self) -> Option<&str> {
self.key_arn.as_deref()
}
}
#[derive(Debug, Clone)]
pub struct VerifiedConfigAttestation {
authenticated: AuthenticatedConfigAttestation,
key_arn: String,
}
impl VerifiedConfigAttestation {
pub fn authenticated(&self) -> &AuthenticatedConfigAttestation {
&self.authenticated
}
pub fn module_id(&self) -> &str {
self.authenticated.module_id()
}
pub fn pcr0_hex(&self) -> &str {
self.authenticated.pcr0_hex()
}
pub fn pcr8_hex(&self) -> &str {
self.authenticated.pcr8_hex()
}
pub fn config_digest_sha384(&self) -> &[u8] {
self.authenticated.config_digest_sha384()
}
pub fn nonce(&self) -> &[u8] {
self.authenticated.nonce()
}
pub fn config_view_json(&self) -> &[u8] {
self.authenticated.config_view_json()
}
pub fn key_arn(&self) -> &str {
&self.key_arn
}
}
#[derive(serde::Deserialize)]
struct ConfigViewProbe {
#[serde(default)]
tee: Option<TeeProbe>,
}
#[derive(serde::Deserialize)]
struct TeeProbe {
#[serde(default)]
kms: Option<KmsProbe>,
}
#[derive(serde::Deserialize)]
struct KmsProbe {
#[serde(default)]
key_arn: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigAttestationVerifyError {
#[error("evidence base64 decode: {0}")]
Base64(String),
#[error("configView base64 decode: {0}")]
ConfigViewBase64(String),
#[error("attestation quote invalid: {0}")]
QuoteInvalid(String),
#[error("attestation quote is missing user_data (no committed config digest)")]
MissingUserData,
#[error("config view digest mismatch — SHA-384(configView) != signed user_data")]
ConfigViewDigestMismatch,
#[error("config view parse: {0}")]
ConfigViewParse(String),
#[error("key_arn mismatch — attested {actual:?}, expected {expected:?}")]
KeyArnMismatch {
expected: String,
actual: Option<String>,
},
#[error("attestation quote is missing a nonce (cannot prove freshness)")]
MissingNonce,
#[error("nonce mismatch — quote does not commit to this caller's nonce")]
NonceMismatch,
#[error(transparent)]
Pcr(#[from] PcrMismatch),
#[error(
"expected PCR{which} pin is not a well-formed Nitro PCR \
(96 hex chars, SHA-384): {value:?}"
)]
InvalidExpectedPcr { which: u8, value: String },
#[error("outer configDigestSha384 base64 decode: {0}")]
OuterDigestBase64(String),
#[error("outer configDigestSha384 does not match the signed digest")]
OuterDigestMismatch,
#[error("outer nonce does not match the signed nonce")]
OuterNonceMismatch,
}
pub fn authenticate_config_attestation(
evidence_b64: &str,
config_view_b64: &str,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
) -> Result<AuthenticatedConfigAttestation, ConfigAttestationVerifyError> {
authenticate_config_attestation_with(
evidence_b64,
config_view_b64,
expected_nonce,
expected_pcr0,
expected_pcr8,
&NitroVerifier::aws_production(OffsetDateTime::now_utc()),
)
}
pub fn authenticate_config_attestation_with(
evidence_b64: &str,
config_view_b64: &str,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
verifier: &NitroVerifier,
) -> Result<AuthenticatedConfigAttestation, ConfigAttestationVerifyError> {
validate_expected_pcr(0, expected_pcr0)?;
if let Some(pcr8) = expected_pcr8 {
validate_expected_pcr(8, pcr8)?;
}
let quote_bytes = B64STD
.decode(evidence_b64)
.map_err(|e| ConfigAttestationVerifyError::Base64(e.to_string()))?;
let config_view = B64STD
.decode(config_view_b64)
.map_err(|e| ConfigAttestationVerifyError::ConfigViewBase64(e.to_string()))?;
let parsed = verifier
.verify("e_bytes)
.map_err(|e| ConfigAttestationVerifyError::QuoteInvalid(format!("{e:?}")))?;
let user_data = parsed
.user_data
.as_deref()
.ok_or(ConfigAttestationVerifyError::MissingUserData)?;
let view_digest = Sha384::digest(&config_view);
if user_data != view_digest.as_slice() {
return Err(ConfigAttestationVerifyError::ConfigViewDigestMismatch);
}
let nonce = parsed
.nonce
.as_deref()
.ok_or(ConfigAttestationVerifyError::MissingNonce)?;
if nonce != expected_nonce {
return Err(ConfigAttestationVerifyError::NonceMismatch);
}
let attest = VerifiedAttestation {
module_id: parsed.module_id.clone(),
pcr0_hex: pcr_hex(&parsed, 0),
pcr8_hex: pcr_hex(&parsed, 8),
};
attest.check_pcrs(Some(expected_pcr0), expected_pcr8)?;
let probe: ConfigViewProbe = serde_json::from_slice(&config_view)
.map_err(|e| ConfigAttestationVerifyError::ConfigViewParse(e.to_string()))?;
let key_arn = probe.tee.and_then(|t| t.kms).and_then(|k| k.key_arn);
Ok(AuthenticatedConfigAttestation {
module_id: attest.module_id,
pcr0_hex: attest.pcr0_hex,
pcr8_hex: attest.pcr8_hex,
config_digest_sha384: user_data.to_vec(),
nonce: nonce.to_vec(),
config_view_json: config_view,
key_arn,
})
}
pub fn verify_config_attestation(
evidence_b64: &str,
config_view_b64: &str,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
expected_key_arn: &str,
) -> Result<VerifiedConfigAttestation, ConfigAttestationVerifyError> {
verify_config_attestation_with(
evidence_b64,
config_view_b64,
expected_nonce,
expected_pcr0,
expected_pcr8,
expected_key_arn,
&NitroVerifier::aws_production(OffsetDateTime::now_utc()),
)
}
#[allow(clippy::too_many_arguments)]
pub fn verify_config_attestation_with(
evidence_b64: &str,
config_view_b64: &str,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
expected_key_arn: &str,
verifier: &NitroVerifier,
) -> Result<VerifiedConfigAttestation, ConfigAttestationVerifyError> {
let authenticated = authenticate_config_attestation_with(
evidence_b64,
config_view_b64,
expected_nonce,
expected_pcr0,
expected_pcr8,
verifier,
)?;
match authenticated.key_arn.as_deref() {
Some(actual) if actual == expected_key_arn => {}
actual => {
return Err(ConfigAttestationVerifyError::KeyArnMismatch {
expected: expected_key_arn.to_string(),
actual: actual.map(|s| s.to_string()),
});
}
}
Ok(VerifiedConfigAttestation {
key_arn: expected_key_arn.to_string(),
authenticated,
})
}
impl crate::attestation_report::ConfigAttestationReport {
pub fn verify(
&self,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
expected_key_arn: &str,
) -> Result<VerifiedConfigAttestation, ConfigAttestationVerifyError> {
self.verify_with(
expected_nonce,
expected_pcr0,
expected_pcr8,
expected_key_arn,
&NitroVerifier::aws_production(OffsetDateTime::now_utc()),
)
}
#[allow(clippy::too_many_arguments)]
pub fn verify_with(
&self,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
expected_key_arn: &str,
verifier: &NitroVerifier,
) -> Result<VerifiedConfigAttestation, ConfigAttestationVerifyError> {
let verified = verify_config_attestation_with(
&self.evidence,
&self.config_view,
expected_nonce,
expected_pcr0,
expected_pcr8,
expected_key_arn,
verifier,
)?;
self.check_outer_metadata(verified.config_digest_sha384(), verified.nonce())?;
Ok(verified)
}
pub fn authenticate(
&self,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
) -> Result<AuthenticatedConfigAttestation, ConfigAttestationVerifyError> {
self.authenticate_with(
expected_nonce,
expected_pcr0,
expected_pcr8,
&NitroVerifier::aws_production(OffsetDateTime::now_utc()),
)
}
pub fn authenticate_with(
&self,
expected_nonce: &[u8],
expected_pcr0: &str,
expected_pcr8: Option<&str>,
verifier: &NitroVerifier,
) -> Result<AuthenticatedConfigAttestation, ConfigAttestationVerifyError> {
let authenticated = authenticate_config_attestation_with(
&self.evidence,
&self.config_view,
expected_nonce,
expected_pcr0,
expected_pcr8,
verifier,
)?;
self.check_outer_metadata(authenticated.config_digest_sha384(), authenticated.nonce())?;
Ok(authenticated)
}
fn check_outer_metadata(
&self,
signed_digest: &[u8],
signed_nonce: &[u8],
) -> Result<(), ConfigAttestationVerifyError> {
let outer_digest = B64STD
.decode(&self.config_digest_sha384)
.map_err(|e| ConfigAttestationVerifyError::OuterDigestBase64(e.to_string()))?;
if outer_digest != signed_digest {
return Err(ConfigAttestationVerifyError::OuterDigestMismatch);
}
if !self
.nonce
.eq_ignore_ascii_case(&crate::hex::lower(signed_nonce))
{
return Err(ConfigAttestationVerifyError::OuterNonceMismatch);
}
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, "");
}
}