use attestation_doc_validation::{
PCRProvider, attestation_doc::decode_attestation_document,
attestation_doc::get_pcrs as att_get_pcrs, validate_and_parse_attestation_doc,
validate_expected_nonce, validate_expected_pcrs,
};
use aws_nitro_enclaves_nsm_api::api::AttestationDoc;
use base64::Engine;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pcrs {
pub pcr0: Vec<u8>,
pub pcr1: Vec<u8>,
pub pcr2: Vec<u8>,
}
impl Pcrs {
pub fn from_hex(pcr0: &str, pcr1: &str, pcr2: &str) -> Result<Self, AttestationError> {
fn decode(idx: usize, s: &str) -> Result<Vec<u8>, AttestationError> {
let bytes =
hex::decode(s.trim()).map_err(|_| AttestationError::InvalidPcrHex(idx))?;
if !matches!(bytes.len(), 32 | 48 | 64) {
return Err(AttestationError::InvalidPcrLength { idx, len: bytes.len() });
}
Ok(bytes)
}
Ok(Pcrs {
pcr0: decode(0, pcr0)?,
pcr1: decode(1, pcr1)?,
pcr2: decode(2, pcr2)?,
})
}
pub fn digest(&self) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(&self.pcr0);
hasher.update(&self.pcr1);
hasher.update(&self.pcr2);
hasher.finalize().into()
}
}
#[derive(Debug, thiserror::Error)]
pub enum AttestationError {
#[error("attestation document validation failed: {0}")]
Validation(String),
#[error("attestation document PCR {idx} has unexpected length {len}")]
InvalidPcrLength {
idx: usize,
len: usize,
},
#[error("attestation document PCR {0} is not valid hex")]
InvalidPcrHex(usize),
#[error(
"attestation document user_data is missing or not a 65-byte uncompressed SEC1 P-256 pubkey"
)]
InvalidControlPubkey,
#[error("attestation document user_data does not match sha256(payload)")]
PayloadBindingMismatch,
#[error("attestation document user_data is not a 32-byte control nonce")]
InvalidControlNonce,
#[error("attestation document PCRs match none of the expected values")]
PcrsNotExpected,
}
pub const CONTROL_PUBKEY_LEN: usize = 65;
pub const NON_UPGRADABLE_CONTROL_KEY_DST: &[u8] =
b"enclavia/synchronizer/non-upgradable-control-key/v1";
pub const NON_UPGRADABLE_CONTROL_KEY: [u8; CONTROL_PUBKEY_LEN] = [
0x04, 0x22, 0x18, 0xad, 0x29, 0x17, 0x7d, 0x9a, 0x5c, 0xb3, 0x52, 0xc4, 0x78, 0x64, 0x06, 0xfa,
0x76, 0x57, 0xaa, 0xc1, 0x6c, 0xe4, 0xb2, 0xe8, 0x19, 0xcd, 0xbd, 0x7f, 0x6e, 0xbd, 0xfa, 0x5a,
0x8e, 0xb1, 0x1a, 0xf7, 0x68, 0x69, 0x3a, 0xd6, 0x5f, 0xc5, 0xb2, 0x21, 0x10, 0x3f, 0x10, 0x8a,
0xe9, 0x50, 0x87, 0xb3, 0x1d, 0x68, 0x54, 0xe8, 0x13, 0x51, 0x60, 0x6d, 0xc4, 0xe2, 0xd4, 0xf7,
0xda,
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttestedIdentity {
pub pcrs: Pcrs,
pub control_pubkey: [u8; CONTROL_PUBKEY_LEN],
}
pub fn verify_against(
attestation_data: &[u8],
handshake_hash: &[u8],
expected_pcrs: &Pcrs,
debug_mode: bool,
) -> Result<(), AttestationError> {
let pcrs_hex = PcrsHex::from_pcrs(expected_pcrs);
let doc = parse_and_validate(attestation_data, debug_mode)?;
check_nonce(&doc, handshake_hash)?;
validate_expected_pcrs(&doc, &pcrs_hex)
.map_err(|e| AttestationError::Validation(e.to_string()))?;
Ok(())
}
pub fn verify_control_nonce_attestation(
attestation_data: &[u8],
handshake_hash: &[u8],
expected_pcrs: &Pcrs,
debug_mode: bool,
) -> Result<[u8; 32], AttestationError> {
let pcrs_hex = PcrsHex::from_pcrs(expected_pcrs);
let doc = parse_and_validate(attestation_data, debug_mode)?;
check_nonce(&doc, handshake_hash)?;
validate_expected_pcrs(&doc, &pcrs_hex)
.map_err(|e| AttestationError::Validation(e.to_string()))?;
let user_data = doc
.user_data
.as_ref()
.ok_or(AttestationError::InvalidControlNonce)?;
user_data
.as_slice()
.try_into()
.map_err(|_| AttestationError::InvalidControlNonce)
}
pub fn verify_and_extract(
attestation_data: &[u8],
handshake_hash: &[u8],
debug_mode: bool,
) -> Result<AttestedIdentity, AttestationError> {
let doc = parse_and_validate(attestation_data, debug_mode)?;
check_nonce(&doc, handshake_hash)?;
let hex_pcrs = att_get_pcrs(&doc).map_err(|e| AttestationError::Validation(e.to_string()))?;
let pcrs = Pcrs {
pcr0: decode_pcr(&hex_pcrs.pcr_0, 0)?,
pcr1: decode_pcr(&hex_pcrs.pcr_1, 1)?,
pcr2: decode_pcr(&hex_pcrs.pcr_2, 2)?,
};
let user_data = doc
.user_data
.as_ref()
.ok_or(AttestationError::InvalidControlPubkey)?;
let control_pubkey: [u8; CONTROL_PUBKEY_LEN] = user_data
.as_slice()
.try_into()
.map_err(|_| AttestationError::InvalidControlPubkey)?;
if control_pubkey[0] != 0x04 {
return Err(AttestationError::InvalidControlPubkey);
}
Ok(AttestedIdentity {
pcrs,
control_pubkey,
})
}
pub fn verify_and_extract_pcrs(
attestation_data: &[u8],
handshake_hash: &[u8],
expected: &[Pcrs],
debug_mode: bool,
) -> Result<Pcrs, AttestationError> {
let doc = parse_and_validate(attestation_data, debug_mode)?;
check_nonce(&doc, handshake_hash)?;
let hex_pcrs = att_get_pcrs(&doc).map_err(|e| AttestationError::Validation(e.to_string()))?;
let pcrs = Pcrs {
pcr0: decode_pcr(&hex_pcrs.pcr_0, 0)?,
pcr1: decode_pcr(&hex_pcrs.pcr_1, 1)?,
pcr2: decode_pcr(&hex_pcrs.pcr_2, 2)?,
};
if !expected.iter().any(|e| e == &pcrs) {
return Err(AttestationError::PcrsNotExpected);
}
Ok(pcrs)
}
pub fn extract_own_pcrs(attestation_data: &[u8]) -> Result<Pcrs, AttestationError> {
let doc = parse_and_validate(attestation_data, true)?;
let hex_pcrs = att_get_pcrs(&doc).map_err(|e| AttestationError::Validation(e.to_string()))?;
Ok(Pcrs {
pcr0: decode_pcr(&hex_pcrs.pcr_0, 0)?,
pcr1: decode_pcr(&hex_pcrs.pcr_1, 1)?,
pcr2: decode_pcr(&hex_pcrs.pcr_2, 2)?,
})
}
pub fn verify_chain_attestation(
attestation_data: &[u8],
payload: &[u8],
expected_pcrs: &Pcrs,
debug_mode: bool,
) -> Result<(), AttestationError> {
let pcrs_hex = PcrsHex::from_pcrs(expected_pcrs);
let doc = parse_and_validate(attestation_data, debug_mode)?;
let user_data = doc
.user_data
.as_ref()
.ok_or(AttestationError::PayloadBindingMismatch)?;
let expected: [u8; 32] = {
let mut hasher = Sha256::new();
hasher.update(payload);
hasher.finalize().into()
};
if user_data.as_slice() != expected {
return Err(AttestationError::PayloadBindingMismatch);
}
validate_expected_pcrs(&doc, &pcrs_hex)
.map_err(|e| AttestationError::Validation(e.to_string()))?;
Ok(())
}
fn parse_and_validate(
attestation_data: &[u8],
debug_mode: bool,
) -> Result<AttestationDoc, AttestationError> {
if debug_mode {
let (_, doc) = decode_attestation_document(attestation_data)
.map_err(|e| AttestationError::Validation(e.to_string()))?;
Ok(doc)
} else {
validate_and_parse_attestation_doc(attestation_data)
.map_err(|e| AttestationError::Validation(e.to_string()))
}
}
fn check_nonce(doc: &AttestationDoc, handshake_hash: &[u8]) -> Result<(), AttestationError> {
let nonce_b64 = base64::engine::general_purpose::STANDARD.encode(handshake_hash);
validate_expected_nonce(doc, &nonce_b64)
.map_err(|e| AttestationError::Validation(e.to_string()))
}
fn decode_pcr(hex_str: &str, idx: usize) -> Result<Vec<u8>, AttestationError> {
let bytes = hex::decode(hex_str).map_err(|_| AttestationError::InvalidPcrHex(idx))?;
if ![32usize, 48, 64].contains(&bytes.len()) {
return Err(AttestationError::InvalidPcrLength {
idx,
len: bytes.len(),
});
}
Ok(bytes)
}
struct PcrsHex {
pcr0: String,
pcr1: String,
pcr2: String,
}
impl PcrsHex {
fn from_pcrs(pcrs: &Pcrs) -> Self {
Self {
pcr0: hex::encode(&pcrs.pcr0),
pcr1: hex::encode(&pcrs.pcr1),
pcr2: hex::encode(&pcrs.pcr2),
}
}
}
impl PCRProvider for PcrsHex {
fn pcr_0(&self) -> Option<&str> {
Some(&self.pcr0)
}
fn pcr_1(&self) -> Option<&str> {
Some(&self.pcr1)
}
fn pcr_2(&self) -> Option<&str> {
Some(&self.pcr2)
}
fn pcr_8(&self) -> Option<&str> {
None
}
}
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils {
use std::collections::BTreeMap;
use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
use ciborium::value::Value as CborValue;
pub struct FakeAttestation {
pub pcr0: Vec<u8>,
pub pcr1: Vec<u8>,
pub pcr2: Vec<u8>,
pub handshake_hash: Vec<u8>,
pub control_pubkey: [u8; super::CONTROL_PUBKEY_LEN],
}
impl FakeAttestation {
pub fn with_seed(seed: u8, handshake_hash: Vec<u8>) -> Self {
let mut control_pubkey = [seed.wrapping_add(0x80); super::CONTROL_PUBKEY_LEN];
control_pubkey[0] = 0x04;
Self {
pcr0: vec![seed; 48],
pcr1: vec![seed.wrapping_add(1); 48],
pcr2: vec![seed.wrapping_add(2); 48],
handshake_hash,
control_pubkey,
}
}
pub fn with_seed_and_pubkey(
seed: u8,
handshake_hash: Vec<u8>,
control_pubkey: [u8; super::CONTROL_PUBKEY_LEN],
) -> Self {
let mut fake = Self::with_seed(seed, handshake_hash);
fake.control_pubkey = control_pubkey;
fake
}
pub fn encode(&self) -> Vec<u8> {
assert_eq!(self.pcr0.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
assert_eq!(self.pcr1.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
assert_eq!(self.pcr2.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
let mut pcrs = BTreeMap::new();
pcrs.insert(0usize, self.pcr0.clone());
pcrs.insert(1usize, self.pcr1.clone());
pcrs.insert(2usize, self.pcr2.clone());
pcrs.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcrs,
vec![0u8; 64],
vec![vec![0u8; 64]],
Some(self.control_pubkey.to_vec()),
Some(self.handshake_hash.clone()),
None,
);
let mut payload = Vec::new();
ciborium::into_writer(&doc, &mut payload).expect("ciborium encode AttestationDoc");
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(payload),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut out = Vec::new();
ciborium::into_writer(&cose, &mut out).expect("ciborium encode COSE_Sign1");
out
}
}
pub struct FakeControlNonceAttestation {
pub pcr0: Vec<u8>,
pub pcr1: Vec<u8>,
pub pcr2: Vec<u8>,
pub handshake_hash: Vec<u8>,
pub control_nonce: Vec<u8>,
}
impl FakeControlNonceAttestation {
pub fn with_seed(seed: u8, handshake_hash: Vec<u8>, control_nonce: [u8; 32]) -> Self {
Self {
pcr0: vec![seed; 48],
pcr1: vec![seed.wrapping_add(1); 48],
pcr2: vec![seed.wrapping_add(2); 48],
handshake_hash,
control_nonce: control_nonce.to_vec(),
}
}
pub fn encode(&self) -> Vec<u8> {
assert_eq!(self.pcr0.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
assert_eq!(self.pcr1.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
assert_eq!(self.pcr2.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
let mut pcrs = BTreeMap::new();
pcrs.insert(0usize, self.pcr0.clone());
pcrs.insert(1usize, self.pcr1.clone());
pcrs.insert(2usize, self.pcr2.clone());
pcrs.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcrs,
vec![0u8; 64],
vec![vec![0u8; 64]],
Some(self.control_nonce.clone()),
Some(self.handshake_hash.clone()),
None,
);
let mut payload = Vec::new();
ciborium::into_writer(&doc, &mut payload).expect("ciborium encode AttestationDoc");
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(payload),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut out = Vec::new();
ciborium::into_writer(&cose, &mut out).expect("ciborium encode COSE_Sign1");
out
}
}
pub struct FakeChainAttestation {
pub pcr0: Vec<u8>,
pub pcr1: Vec<u8>,
pub pcr2: Vec<u8>,
pub user_data: Vec<u8>,
}
impl FakeChainAttestation {
pub fn for_payload(seed: u8, payload: &[u8]) -> Self {
use sha2::Digest as _;
let mut hasher = sha2::Sha256::new();
hasher.update(payload);
let user_data: Vec<u8> = hasher.finalize().to_vec();
Self {
pcr0: vec![seed; 48],
pcr1: vec![seed.wrapping_add(1); 48],
pcr2: vec![seed.wrapping_add(2); 48],
user_data,
}
}
pub fn encode(&self) -> Vec<u8> {
assert_eq!(self.pcr0.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
assert_eq!(self.pcr1.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
assert_eq!(self.pcr2.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
let mut pcrs = BTreeMap::new();
pcrs.insert(0usize, self.pcr0.clone());
pcrs.insert(1usize, self.pcr1.clone());
pcrs.insert(2usize, self.pcr2.clone());
pcrs.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcrs,
vec![0u8; 64],
vec![vec![0u8; 64]],
Some(self.user_data.clone()),
Some(vec![0u8; 32]),
None,
);
let mut payload = Vec::new();
ciborium::into_writer(&doc, &mut payload).expect("ciborium encode AttestationDoc");
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(payload),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut out = Vec::new();
ciborium::into_writer(&cose, &mut out).expect("ciborium encode COSE_Sign1");
out
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hh() -> Vec<u8> {
(0u8..32).collect()
}
#[test]
fn verify_and_extract_returns_doc_identity_in_debug_mode() {
let fake = test_utils::FakeAttestation::with_seed(0x11, hh());
let bytes = fake.encode();
let identity = verify_and_extract(&bytes, &hh(), true).expect("verify");
assert_eq!(identity.pcrs.pcr0, fake.pcr0);
assert_eq!(identity.pcrs.pcr1, fake.pcr1);
assert_eq!(identity.pcrs.pcr2, fake.pcr2);
assert_eq!(identity.control_pubkey, fake.control_pubkey);
}
#[test]
fn extract_own_pcrs_returns_doc_pcrs_without_nonce_or_chain() {
let fake = test_utils::FakeAttestation::with_seed(0x5a, hh());
let bytes = fake.encode();
let pcrs = extract_own_pcrs(&bytes).expect("extract own pcrs");
assert_eq!(pcrs.pcr0, fake.pcr0);
assert_eq!(pcrs.pcr1, fake.pcr1);
assert_eq!(pcrs.pcr2, fake.pcr2);
let verified = verify_and_extract(&bytes, &hh(), true).expect("verify");
assert_eq!(pcrs.digest(), verified.pcrs.digest());
}
#[test]
fn extract_own_pcrs_ignores_the_nonce_entirely() {
let fake = test_utils::FakeAttestation::with_seed(0x77, vec![0xde; 32]);
let pcrs = extract_own_pcrs(&fake.encode()).expect("extract own pcrs");
assert_eq!(pcrs.pcr0, fake.pcr0);
}
#[test]
fn extract_own_pcrs_rejects_garbage_bytes() {
let err = extract_own_pcrs(b"not a cose document").unwrap_err();
assert!(
matches!(err, AttestationError::Validation(_)),
"expected Validation, got {err:?}"
);
}
#[test]
fn verify_control_nonce_attestation_returns_attested_nonce() {
let nonce = [0xab; 32];
let fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), nonce);
let expected = Pcrs {
pcr0: fake.pcr0.clone(),
pcr1: fake.pcr1.clone(),
pcr2: fake.pcr2.clone(),
};
let got = verify_control_nonce_attestation(&fake.encode(), &hh(), &expected, true)
.expect("verify");
assert_eq!(got, nonce);
}
#[test]
fn verify_control_nonce_attestation_rejects_wrong_pcrs() {
let fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), [0xab; 32]);
let wrong = Pcrs {
pcr0: vec![0xff; 48],
pcr1: fake.pcr1.clone(),
pcr2: fake.pcr2.clone(),
};
let err =
verify_control_nonce_attestation(&fake.encode(), &hh(), &wrong, true).unwrap_err();
assert!(matches!(err, AttestationError::Validation(_)), "{err}");
}
#[test]
fn verify_control_nonce_attestation_rejects_wrong_handshake_hash() {
let fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), [0xab; 32]);
let expected = Pcrs {
pcr0: fake.pcr0.clone(),
pcr1: fake.pcr1.clone(),
pcr2: fake.pcr2.clone(),
};
let other_hh: Vec<u8> = (100u8..132).collect();
let err = verify_control_nonce_attestation(&fake.encode(), &other_hh, &expected, true)
.unwrap_err();
assert!(matches!(err, AttestationError::Validation(_)), "{err}");
}
#[test]
fn verify_control_nonce_attestation_rejects_non_32_byte_user_data() {
let mut fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), [0xab; 32]);
fake.control_nonce = vec![0xab; 16]; let expected = Pcrs {
pcr0: fake.pcr0.clone(),
pcr1: fake.pcr1.clone(),
pcr2: fake.pcr2.clone(),
};
let err =
verify_control_nonce_attestation(&fake.encode(), &hh(), &expected, true).unwrap_err();
assert!(
matches!(err, AttestationError::InvalidControlNonce),
"{err}"
);
}
#[test]
fn verify_and_extract_rejects_doc_without_user_data() {
use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
use ciborium::value::Value as CborValue;
use std::collections::BTreeMap;
let mut pcrs = BTreeMap::new();
pcrs.insert(0usize, vec![0x11u8; 48]);
pcrs.insert(1usize, vec![0x12u8; 48]);
pcrs.insert(2usize, vec![0x13u8; 48]);
pcrs.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcrs,
vec![0u8; 64],
vec![vec![0u8; 64]],
None, Some(hh()),
None,
);
let mut payload = Vec::new();
ciborium::into_writer(&doc, &mut payload).unwrap();
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(payload),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut bytes = Vec::new();
ciborium::into_writer(&cose, &mut bytes).unwrap();
let err = verify_and_extract(&bytes, &hh(), true).unwrap_err();
assert!(
matches!(err, AttestationError::InvalidControlPubkey),
"expected InvalidControlPubkey, got {err:?}"
);
}
#[test]
fn verify_and_extract_rejects_doc_with_wrong_size_user_data() {
let mut fake = test_utils::FakeAttestation::with_seed(0x22, hh());
use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
use ciborium::value::Value as CborValue;
use std::collections::BTreeMap;
let _ = &mut fake;
let mut pcrs = BTreeMap::new();
pcrs.insert(0usize, vec![0x22u8; 48]);
pcrs.insert(1usize, vec![0x23u8; 48]);
pcrs.insert(2usize, vec![0x24u8; 48]);
pcrs.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcrs,
vec![0u8; 64],
vec![vec![0u8; 64]],
Some(vec![0u8; 16]), Some(hh()),
None,
);
let mut payload = Vec::new();
ciborium::into_writer(&doc, &mut payload).unwrap();
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(payload),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut bytes = Vec::new();
ciborium::into_writer(&cose, &mut bytes).unwrap();
let err = verify_and_extract(&bytes, &hh(), true).unwrap_err();
assert!(
matches!(err, AttestationError::InvalidControlPubkey),
"expected InvalidControlPubkey, got {err:?}"
);
}
fn seed_pcrs(seed: u8) -> Pcrs {
Pcrs {
pcr0: vec![seed; 48],
pcr1: vec![seed.wrapping_add(1); 48],
pcr2: vec![seed.wrapping_add(2); 48],
}
}
#[test]
fn verify_and_extract_pcrs_returns_doc_pcrs_in_debug_mode() {
let fake = test_utils::FakeAttestation::with_seed(0x66, hh());
let pcrs = verify_and_extract_pcrs(&fake.encode(), &hh(), &[seed_pcrs(0x66)], true)
.expect("verify");
assert_eq!(pcrs.pcr0, fake.pcr0);
assert_eq!(pcrs.pcr1, fake.pcr1);
assert_eq!(pcrs.pcr2, fake.pcr2);
}
#[test]
fn verify_and_extract_pcrs_rejects_unexpected_pcrs() {
let fake = test_utils::FakeAttestation::with_seed(0x66, hh());
let err =
verify_and_extract_pcrs(&fake.encode(), &hh(), &[seed_pcrs(0x99)], true).unwrap_err();
assert!(
matches!(err, AttestationError::PcrsNotExpected),
"expected PcrsNotExpected, got {err:?}"
);
let err = verify_and_extract_pcrs(&fake.encode(), &hh(), &[], true).unwrap_err();
assert!(
matches!(err, AttestationError::PcrsNotExpected),
"expected PcrsNotExpected, got {err:?}"
);
}
#[test]
fn verify_and_extract_pcrs_rejects_wrong_handshake_hash() {
let fake = test_utils::FakeAttestation::with_seed(0x67, hh());
let wrong: Vec<u8> = vec![0xab; 32];
let err =
verify_and_extract_pcrs(&fake.encode(), &wrong, &[seed_pcrs(0x67)], true).unwrap_err();
assert!(
matches!(err, AttestationError::Validation(_)),
"expected Validation, got {err:?}"
);
}
#[test]
fn verify_and_extract_pcrs_rejects_garbage_bytes() {
let err = verify_and_extract_pcrs(b"not a cose document", &hh(), &[seed_pcrs(0x66)], true)
.unwrap_err();
assert!(
matches!(err, AttestationError::Validation(_)),
"expected Validation, got {err:?}"
);
}
#[test]
fn verify_and_extract_pcrs_accepts_doc_without_user_data() {
use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
use ciborium::value::Value as CborValue;
use std::collections::BTreeMap;
let mut pcrs = BTreeMap::new();
pcrs.insert(0usize, vec![0x68u8; 48]);
pcrs.insert(1usize, vec![0x69u8; 48]);
pcrs.insert(2usize, vec![0x6au8; 48]);
pcrs.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcrs,
vec![0u8; 64],
vec![vec![0u8; 64]],
None, Some(hh()),
None,
);
let mut payload = Vec::new();
ciborium::into_writer(&doc, &mut payload).unwrap();
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(payload),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut bytes = Vec::new();
ciborium::into_writer(&cose, &mut bytes).unwrap();
let pcrs =
verify_and_extract_pcrs(&bytes, &hh(), &[seed_pcrs(0x68)], true).expect("verify");
assert_eq!(pcrs.pcr0, vec![0x68u8; 48]);
}
#[test]
fn verify_against_accepts_matching_pcrs_in_debug_mode() {
let fake = test_utils::FakeAttestation::with_seed(0x22, hh());
let bytes = fake.encode();
let expected = Pcrs {
pcr0: fake.pcr0.clone(),
pcr1: fake.pcr1.clone(),
pcr2: fake.pcr2.clone(),
};
verify_against(&bytes, &hh(), &expected, true).expect("verify");
}
#[test]
fn verify_against_rejects_mismatched_pcrs() {
let fake = test_utils::FakeAttestation::with_seed(0x33, hh());
let bytes = fake.encode();
let expected = Pcrs {
pcr0: vec![0xff; 48],
pcr1: fake.pcr1.clone(),
pcr2: fake.pcr2.clone(),
};
let err = verify_against(&bytes, &hh(), &expected, true).unwrap_err();
assert!(
matches!(err, AttestationError::Validation(_)),
"expected Validation, got {err:?}"
);
}
#[test]
fn verify_rejects_wrong_handshake_hash() {
let fake = test_utils::FakeAttestation::with_seed(0x44, hh());
let bytes = fake.encode();
let wrong: Vec<u8> = vec![0xab; 32];
let err = verify_and_extract(&bytes, &wrong, true).unwrap_err();
assert!(
matches!(err, AttestationError::Validation(_)),
"expected Validation, got {err:?}"
);
}
#[test]
fn digest_is_sha256_of_concatenated_pcrs() {
let pcrs = Pcrs {
pcr0: vec![0x01; 48],
pcr1: vec![0x02; 48],
pcr2: vec![0x03; 48],
};
let mut hasher = Sha256::new();
hasher.update(&pcrs.pcr0);
hasher.update(&pcrs.pcr1);
hasher.update(&pcrs.pcr2);
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(pcrs.digest(), expected);
}
fn pcrs_from_seed(seed: u8) -> Pcrs {
Pcrs {
pcr0: vec![seed; 48],
pcr1: vec![seed.wrapping_add(1); 48],
pcr2: vec![seed.wrapping_add(2); 48],
}
}
#[test]
fn verify_chain_attestation_accepts_well_formed_link_in_debug_mode() {
let payload = b"chain-link-payload-canary".to_vec();
let fake = test_utils::FakeChainAttestation::for_payload(0x33, &payload);
let bytes = fake.encode();
let expected_pcrs = pcrs_from_seed(0x33);
verify_chain_attestation(&bytes, &payload, &expected_pcrs, true)
.expect("valid chain attestation must pass");
}
#[test]
fn verify_chain_attestation_rejects_mismatched_payload_binding() {
let payload = b"chain-link-payload-canary".to_vec();
let fake = test_utils::FakeChainAttestation::for_payload(0x44, &payload);
let bytes = fake.encode();
let expected_pcrs = pcrs_from_seed(0x44);
let err = verify_chain_attestation(&bytes, b"DIFFERENT", &expected_pcrs, true)
.expect_err("payload swap must fail the binding check");
assert!(
matches!(err, AttestationError::PayloadBindingMismatch),
"expected PayloadBindingMismatch, got {err:?}"
);
}
#[test]
fn verify_chain_attestation_rejects_pcr_mismatch() {
let payload = b"chain-link-payload-canary".to_vec();
let fake = test_utils::FakeChainAttestation::for_payload(0x55, &payload);
let bytes = fake.encode();
let mismatched_pcrs = pcrs_from_seed(0x99);
let err = verify_chain_attestation(&bytes, &payload, &mismatched_pcrs, true)
.expect_err("PCR mismatch must fail");
assert!(
matches!(err, AttestationError::Validation(_)),
"expected Validation error, got {err:?}"
);
}
#[test]
fn verify_chain_attestation_rejects_doc_without_user_data() {
use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
use ciborium::value::Value as CborValue;
use std::collections::BTreeMap;
let payload = b"any-payload".to_vec();
let pcrs = pcrs_from_seed(0x77);
let mut pcr_map = BTreeMap::new();
pcr_map.insert(0usize, pcrs.pcr0.clone());
pcr_map.insert(1usize, pcrs.pcr1.clone());
pcr_map.insert(2usize, pcrs.pcr2.clone());
pcr_map.insert(8usize, vec![0u8; 48]);
let doc = AttestationDoc::new(
"test-module".to_string(),
Digest::SHA384,
0,
pcr_map,
vec![0u8; 64],
vec![vec![0u8; 64]],
None, Some(vec![0u8; 32]),
None,
);
let mut doc_bytes = Vec::new();
ciborium::into_writer(&doc, &mut doc_bytes).unwrap();
let cose = CborValue::Array(vec![
CborValue::Bytes(vec![0xa0]),
CborValue::Map(Vec::new()),
CborValue::Bytes(doc_bytes),
CborValue::Bytes(vec![0u8; 96]),
]);
let mut bytes = Vec::new();
ciborium::into_writer(&cose, &mut bytes).unwrap();
let err = verify_chain_attestation(&bytes, &payload, &pcrs, true)
.expect_err("missing user_data must be rejected");
assert!(
matches!(err, AttestationError::PayloadBindingMismatch),
"expected PayloadBindingMismatch, got {err:?}"
);
}
}
#[cfg(test)]
mod non_upgradable_control_key_tests {
use super::{CONTROL_PUBKEY_LEN, NON_UPGRADABLE_CONTROL_KEY, NON_UPGRADABLE_CONTROL_KEY_DST};
use sha2::{Digest, Sha256};
fn derive_non_upgradable_control_key() -> [u8; CONTROL_PUBKEY_LEN] {
use p256::EncodedPoint;
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
for counter in 0u16..=255 {
let mut hasher = Sha256::new();
hasher.update(NON_UPGRADABLE_CONTROL_KEY_DST);
hasher.update([counter as u8]);
let x = hasher.finalize();
let mut compressed = [0u8; 33];
compressed[0] = 0x02; compressed[1..].copy_from_slice(&x);
let Ok(encoded) = EncodedPoint::from_bytes(compressed) else {
continue;
};
let maybe_point = p256::AffinePoint::from_encoded_point(&encoded);
if maybe_point.is_some().into() {
let uncompressed = maybe_point.unwrap().to_encoded_point(false);
let mut out = [0u8; CONTROL_PUBKEY_LEN];
out.copy_from_slice(uncompressed.as_bytes());
return out;
}
}
panic!("no valid P-256 point found deriving the non-upgradable control key");
}
#[test]
fn constant_matches_derivation() {
assert_eq!(
NON_UPGRADABLE_CONTROL_KEY,
derive_non_upgradable_control_key(),
"baked non-upgradable control key drifted from its DST derivation"
);
}
#[test]
fn is_uncompressed_sec1_shape() {
assert_eq!(NON_UPGRADABLE_CONTROL_KEY.len(), CONTROL_PUBKEY_LEN);
assert_eq!(NON_UPGRADABLE_CONTROL_KEY[0], 0x04);
}
#[test]
fn parses_as_a_valid_verifying_key() {
p256::ecdsa::VerifyingKey::from_sec1_bytes(NON_UPGRADABLE_CONTROL_KEY.as_slice())
.expect("canonical non-upgradable control key must be a valid P-256 point");
}
}