use crate::crypto::{hash, SigningKey, VerifyingKey};
use crate::types::AuthorId;
use crate::{AionError, Result};
pub const HW_ATTESTATION_DOMAIN: &[u8] = b"AION_V2_KEY_ATTESTATION_V1";
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttestationKind {
Tpm2Quote = 1,
NvidiaNras = 2,
AmdSevSnp = 3,
IntelTdxReport = 4,
IntelSgxReport = 5,
AwsNitroEnclave = 6,
ArmCca = 7,
AzureAttestation = 8,
Custom = 0xFFFF,
}
impl AttestationKind {
pub fn from_u16(value: u16) -> Result<Self> {
match value {
1 => Ok(Self::Tpm2Quote),
2 => Ok(Self::NvidiaNras),
3 => Ok(Self::AmdSevSnp),
4 => Ok(Self::IntelTdxReport),
5 => Ok(Self::IntelSgxReport),
6 => Ok(Self::AwsNitroEnclave),
7 => Ok(Self::ArmCca),
8 => Ok(Self::AzureAttestation),
0xFFFF => Ok(Self::Custom),
other => Err(AionError::InvalidFormat {
reason: format!("Unknown attestation kind: {other}"),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttestationEvidence {
pub kind: AttestationKind,
pub nonce: [u8; 32],
pub evidence: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct KeyAttestationBinding {
pub author_id: AuthorId,
pub epoch: u32,
pub public_key: [u8; 32],
pub evidence: AttestationEvidence,
pub master_signature: [u8; 64],
}
#[must_use]
#[allow(clippy::arithmetic_side_effects)] pub fn canonical_binding_message(binding: &KeyAttestationBinding) -> Vec<u8> {
let evidence_hash = hash(&binding.evidence.evidence);
let mut msg = Vec::with_capacity(HW_ATTESTATION_DOMAIN.len() + 8 + 4 + 32 + 2 + 32 + 32);
msg.extend_from_slice(HW_ATTESTATION_DOMAIN);
msg.extend_from_slice(&binding.author_id.as_u64().to_le_bytes());
msg.extend_from_slice(&binding.epoch.to_le_bytes());
msg.extend_from_slice(&binding.public_key);
msg.extend_from_slice(&(binding.evidence.kind as u16).to_le_bytes());
msg.extend_from_slice(&binding.evidence.nonce);
msg.extend_from_slice(&evidence_hash);
msg
}
#[must_use]
pub fn sign_binding(
author: AuthorId,
epoch: u32,
public_key: [u8; 32],
evidence: AttestationEvidence,
master_key: &SigningKey,
) -> KeyAttestationBinding {
let mut binding = KeyAttestationBinding {
author_id: author,
epoch,
public_key,
evidence,
master_signature: [0u8; 64],
};
let message = canonical_binding_message(&binding);
binding.master_signature = master_key.sign(&message);
binding
}
pub fn verify_binding_signature(
binding: &KeyAttestationBinding,
master_verifying_key: &VerifyingKey,
) -> Result<()> {
let message = canonical_binding_message(binding);
master_verifying_key.verify(&message, &binding.master_signature)
}
pub trait EvidenceVerifier {
fn verify(&self, evidence: &AttestationEvidence, expected_pubkey: &[u8; 32]) -> Result<()>;
}
#[cfg(any(test, feature = "test-helpers"))]
#[derive(Debug, Clone, Copy, Default)]
pub struct AcceptAllEvidenceVerifier;
#[cfg(any(test, feature = "test-helpers"))]
impl EvidenceVerifier for AcceptAllEvidenceVerifier {
fn verify(&self, _evidence: &AttestationEvidence, _expected_pubkey: &[u8; 32]) -> Result<()> {
Ok(())
}
}
#[cfg(any(test, feature = "test-helpers"))]
#[derive(Debug, Clone, Copy, Default)]
pub struct RejectAllEvidenceVerifier;
#[cfg(any(test, feature = "test-helpers"))]
impl EvidenceVerifier for RejectAllEvidenceVerifier {
fn verify(&self, _evidence: &AttestationEvidence, _expected_pubkey: &[u8; 32]) -> Result<()> {
Err(AionError::InvalidFormat {
reason: "RejectAllEvidenceVerifier".to_string(),
})
}
}
#[cfg(any(test, feature = "test-helpers"))]
#[derive(Debug, Clone, Copy, Default)]
pub struct PubkeyPrefixEvidenceVerifier;
#[cfg(any(test, feature = "test-helpers"))]
impl EvidenceVerifier for PubkeyPrefixEvidenceVerifier {
fn verify(&self, evidence: &AttestationEvidence, expected_pubkey: &[u8; 32]) -> Result<()> {
let prefix = evidence
.evidence
.get(..32)
.ok_or_else(|| AionError::InvalidFormat {
reason: "evidence shorter than 32 bytes".to_string(),
})?;
if prefix == expected_pubkey.as_slice() {
Ok(())
} else {
Err(AionError::InvalidFormat {
reason: "evidence prefix does not match expected pubkey".to_string(),
})
}
}
}
pub fn verify_binding<V: EvidenceVerifier>(
binding: &KeyAttestationBinding,
registry: &crate::key_registry::KeyRegistry,
at_version: u64,
verifier: &V,
) -> Result<()> {
let signer = binding.author_id;
let master = registry
.master_key(signer)
.ok_or(AionError::SignatureVerificationFailed {
version: at_version,
author: signer,
})?;
let epoch = registry.active_epoch_at(signer, at_version).ok_or(
AionError::SignatureVerificationFailed {
version: at_version,
author: signer,
},
)?;
if binding.public_key != epoch.public_key || binding.epoch != epoch.epoch {
return Err(AionError::SignatureVerificationFailed {
version: at_version,
author: signer,
});
}
verify_binding_signature(binding, master)?;
verifier.verify(&binding.evidence, &binding.public_key)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
#[allow(deprecated)] mod tests {
use super::*;
fn sample_evidence() -> AttestationEvidence {
AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0x42u8; 32],
evidence: b"opaque-tpm-quote-bytes".to_vec(),
}
}
#[test]
fn signature_round_trip() {
let master = SigningKey::generate();
let binding = sign_binding(
AuthorId::new(1),
0,
[0xAAu8; 32],
sample_evidence(),
&master,
);
verify_binding_signature(&binding, &master.verifying_key()).unwrap();
}
#[test]
fn wrong_master_rejects() {
let master = SigningKey::generate();
let other = SigningKey::generate();
let binding = sign_binding(
AuthorId::new(1),
0,
[0xAAu8; 32],
sample_evidence(),
&master,
);
assert!(verify_binding_signature(&binding, &other.verifying_key()).is_err());
}
#[test]
fn tampered_evidence_rejects() {
let master = SigningKey::generate();
let mut binding = sign_binding(
AuthorId::new(1),
0,
[0xAAu8; 32],
sample_evidence(),
&master,
);
binding.evidence.evidence[0] ^= 0x01;
assert!(verify_binding_signature(&binding, &master.verifying_key()).is_err());
}
#[test]
fn tampered_pubkey_rejects() {
let master = SigningKey::generate();
let mut binding = sign_binding(
AuthorId::new(1),
0,
[0xAAu8; 32],
sample_evidence(),
&master,
);
binding.public_key[0] ^= 0x01;
assert!(verify_binding_signature(&binding, &master.verifying_key()).is_err());
}
use crate::key_registry::KeyRegistry;
fn reg_pinning(author: AuthorId, master: &SigningKey, op: &SigningKey) -> KeyRegistry {
let mut reg = KeyRegistry::new();
reg.register_author(author, master.verifying_key(), op.verifying_key(), 0)
.unwrap_or_else(|_| std::process::abort());
reg
}
#[test]
fn accept_all_verifier_accepts() {
let author = AuthorId::new(1);
let master = SigningKey::generate();
let op = SigningKey::generate();
let binding = sign_binding(
author,
0,
op.verifying_key().to_bytes(),
sample_evidence(),
&master,
);
let reg = reg_pinning(author, &master, &op);
assert!(verify_binding(&binding, ®, 1, &AcceptAllEvidenceVerifier).is_ok());
}
#[test]
fn reject_all_verifier_rejects_even_valid_signature() {
let author = AuthorId::new(1);
let master = SigningKey::generate();
let op = SigningKey::generate();
let binding = sign_binding(
author,
0,
op.verifying_key().to_bytes(),
sample_evidence(),
&master,
);
let reg = reg_pinning(author, &master, &op);
assert!(verify_binding(&binding, ®, 1, &RejectAllEvidenceVerifier).is_err());
}
#[test]
fn pubkey_prefix_verifier_matches_prefix_only() {
let author = AuthorId::new(1);
let master = SigningKey::generate();
let op = SigningKey::generate();
let pk = op.verifying_key().to_bytes();
let mut good_evidence = pk.to_vec();
good_evidence.extend_from_slice(b"tail");
let good = AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0u8; 32],
evidence: good_evidence,
};
let binding_good = sign_binding(author, 0, pk, good, &master);
let reg = reg_pinning(author, &master, &op);
assert!(verify_binding(&binding_good, ®, 1, &PubkeyPrefixEvidenceVerifier).is_ok());
let bad = AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0u8; 32],
evidence: vec![0u8; 64],
};
let binding_bad = sign_binding(author, 0, pk, bad, &master);
assert!(verify_binding(&binding_bad, ®, 1, &PubkeyPrefixEvidenceVerifier).is_err());
}
#[test]
fn attestation_kind_round_trips() {
for kind in [
AttestationKind::Tpm2Quote,
AttestationKind::NvidiaNras,
AttestationKind::AmdSevSnp,
AttestationKind::IntelTdxReport,
AttestationKind::IntelSgxReport,
AttestationKind::AwsNitroEnclave,
AttestationKind::ArmCca,
AttestationKind::AzureAttestation,
AttestationKind::Custom,
] {
let raw = kind as u16;
assert_eq!(AttestationKind::from_u16(raw).unwrap(), kind);
}
assert!(AttestationKind::from_u16(999).is_err());
}
mod properties {
use super::*;
use hegel::generators as gs;
fn draw_evidence(tc: &hegel::TestCase) -> AttestationEvidence {
let bytes = tc.draw(gs::binary().max_size(1024));
let nonce_vec = tc.draw(gs::binary().min_size(32).max_size(32));
let mut nonce = [0u8; 32];
nonce.copy_from_slice(&nonce_vec);
AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce,
evidence: bytes,
}
}
fn draw_pubkey(tc: &hegel::TestCase) -> [u8; 32] {
let v = tc.draw(gs::binary().min_size(32).max_size(32));
let mut pk = [0u8; 32];
pk.copy_from_slice(&v);
pk
}
#[hegel::test]
fn prop_binding_signature_roundtrip(tc: hegel::TestCase) {
let master = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let epoch = tc.draw(gs::integers::<u32>());
let pubkey = draw_pubkey(&tc);
let evidence = draw_evidence(&tc);
let binding = sign_binding(author, epoch, pubkey, evidence, &master);
verify_binding_signature(&binding, &master.verifying_key())
.unwrap_or_else(|_| std::process::abort());
}
#[hegel::test]
fn prop_binding_rejects_wrong_master(tc: hegel::TestCase) {
let master = SigningKey::generate();
let other = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let epoch = tc.draw(gs::integers::<u32>());
let pubkey = draw_pubkey(&tc);
let evidence = draw_evidence(&tc);
let binding = sign_binding(author, epoch, pubkey, evidence, &master);
assert!(verify_binding_signature(&binding, &other.verifying_key()).is_err());
}
#[hegel::test]
fn prop_binding_rejects_tampered_evidence(tc: hegel::TestCase) {
let master = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let epoch = tc.draw(gs::integers::<u32>());
let pubkey = draw_pubkey(&tc);
let mut evidence = draw_evidence(&tc);
if evidence.evidence.is_empty() {
evidence.evidence.push(0);
}
let mut binding = sign_binding(author, epoch, pubkey, evidence, &master);
let idx = tc.draw(
gs::integers::<usize>()
.max_value(binding.evidence.evidence.len().saturating_sub(1)),
);
if let Some(b) = binding.evidence.evidence.get_mut(idx) {
*b ^= 0x01;
}
assert!(verify_binding_signature(&binding, &master.verifying_key()).is_err());
}
#[hegel::test]
fn prop_binding_rejects_tampered_pubkey(tc: hegel::TestCase) {
let master = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let epoch = tc.draw(gs::integers::<u32>());
let pubkey = draw_pubkey(&tc);
let evidence = draw_evidence(&tc);
let mut binding = sign_binding(author, epoch, pubkey, evidence, &master);
binding.public_key[0] ^= 0x01;
assert!(verify_binding_signature(&binding, &master.verifying_key()).is_err());
}
#[hegel::test]
fn prop_binding_rejects_tampered_nonce(tc: hegel::TestCase) {
let master = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let epoch = tc.draw(gs::integers::<u32>());
let pubkey = draw_pubkey(&tc);
let evidence = draw_evidence(&tc);
let mut binding = sign_binding(author, epoch, pubkey, evidence, &master);
binding.evidence.nonce[0] ^= 0x01;
assert!(verify_binding_signature(&binding, &master.verifying_key()).is_err());
}
#[hegel::test]
fn prop_binding_rejects_tampered_author_or_epoch(tc: hegel::TestCase) {
let master = SigningKey::generate();
let author_raw = tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX / 2));
let epoch = tc.draw(gs::integers::<u32>().max_value(u32::MAX - 1));
let pubkey = draw_pubkey(&tc);
let evidence = draw_evidence(&tc);
let author = AuthorId::new(author_raw);
let binding = sign_binding(author, epoch, pubkey, evidence, &master);
let mut b1 = binding.clone();
b1.author_id = AuthorId::new(author_raw.saturating_add(1));
assert!(verify_binding_signature(&b1, &master.verifying_key()).is_err());
let mut b2 = binding;
b2.epoch = epoch.saturating_add(1);
assert!(verify_binding_signature(&b2, &master.verifying_key()).is_err());
}
fn prop_reg(author: AuthorId, master: &SigningKey, op: &SigningKey) -> KeyRegistry {
let mut reg = KeyRegistry::new();
reg.register_author(author, master.verifying_key(), op.verifying_key(), 0)
.unwrap_or_else(|_| std::process::abort());
reg
}
#[hegel::test]
fn prop_verify_binding_accept_all_ok(tc: hegel::TestCase) {
let master = SigningKey::generate();
let op = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let evidence = draw_evidence(&tc);
let binding = sign_binding(author, 0, op.verifying_key().to_bytes(), evidence, &master);
let reg = prop_reg(author, &master, &op);
verify_binding(&binding, ®, 1, &AcceptAllEvidenceVerifier)
.unwrap_or_else(|_| std::process::abort());
}
#[hegel::test]
fn prop_verify_binding_reject_all_err(tc: hegel::TestCase) {
let master = SigningKey::generate();
let op = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let evidence = draw_evidence(&tc);
let binding = sign_binding(author, 0, op.verifying_key().to_bytes(), evidence, &master);
let reg = prop_reg(author, &master, &op);
assert!(verify_binding(&binding, ®, 1, &RejectAllEvidenceVerifier).is_err());
}
#[hegel::test]
fn prop_pubkey_prefix_verifier_matches_prefix(tc: hegel::TestCase) {
let master = SigningKey::generate();
let op = SigningKey::generate();
let author = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let pubkey = op.verifying_key().to_bytes();
let tail = tc.draw(gs::binary().max_size(128));
let mut good_evidence_bytes = pubkey.to_vec();
good_evidence_bytes.extend_from_slice(&tail);
let good = AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0u8; 32],
evidence: good_evidence_bytes,
};
let binding_good = sign_binding(author, 0, pubkey, good, &master);
let reg = prop_reg(author, &master, &op);
verify_binding(&binding_good, ®, 1, &PubkeyPrefixEvidenceVerifier)
.unwrap_or_else(|_| std::process::abort());
let mut bad_prefix = pubkey;
bad_prefix[0] ^= 0x01;
let mut bad_bytes = bad_prefix.to_vec();
bad_bytes.extend_from_slice(&tail);
let bad = AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0u8; 32],
evidence: bad_bytes,
};
let binding_bad = sign_binding(author, 0, pubkey, bad, &master);
assert!(verify_binding(&binding_bad, ®, 1, &PubkeyPrefixEvidenceVerifier).is_err());
}
#[hegel::test]
fn prop_registry_verify_accepts_freshly_bound_key(tc: hegel::TestCase) {
use crate::key_registry::KeyRegistry;
let author =
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
let master = SigningKey::generate();
let op = SigningKey::generate();
let mut reg = KeyRegistry::new();
reg.register_author(author, master.verifying_key(), op.verifying_key(), 0)
.unwrap_or_else(|_| std::process::abort());
let evidence = AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0x42u8; 32],
evidence: tc.draw(gs::binary().max_size(128)),
};
let binding = sign_binding(author, 0, op.verifying_key().to_bytes(), evidence, &master);
let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
verify_binding(&binding, ®, at, &AcceptAllEvidenceVerifier)
.unwrap_or_else(|_| std::process::abort());
}
#[hegel::test]
fn prop_registry_verify_rejects_wrong_master_key(tc: hegel::TestCase) {
use crate::key_registry::KeyRegistry;
let author =
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
let real_master = SigningKey::generate();
let imposter_master = SigningKey::generate();
let op = SigningKey::generate();
let mut reg = KeyRegistry::new();
reg.register_author(author, real_master.verifying_key(), op.verifying_key(), 0)
.unwrap_or_else(|_| std::process::abort());
let evidence = AttestationEvidence {
kind: AttestationKind::Tpm2Quote,
nonce: [0x99u8; 32],
evidence: tc.draw(gs::binary().max_size(64)),
};
let binding = sign_binding(
author,
0,
op.verifying_key().to_bytes(),
evidence,
&imposter_master,
);
let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
assert!(verify_binding(&binding, ®, at, &AcceptAllEvidenceVerifier).is_err());
}
}
}