use der::Decode;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use x509_cert::Certificate;
#[cfg(test)]
pub const MAX_ATTESTATION_AGE_SECS: u64 = 300;
#[cfg(not(test))]
const MAX_ATTESTATION_AGE_SECS: u64 = 300;
#[cfg(test)]
pub const MAX_ATTESTATION_FUTURE_SKEW_SECS: u64 = 30;
#[cfg(not(test))]
const MAX_ATTESTATION_FUTURE_SKEW_SECS: u64 = 30;
const TRUSTED_ROOTS: &[&str] = &[
"CONCLAVE_ROOT_CA_V1",
"CONCLAVE_CLOUD_ROOT_CA_V1",
"GOOGLE_STRONGBOX_ROOT_V1",
"AWS_NITRO_ROOT_V1",
];
fn unix_time_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AttestationLevel {
Software,
TEE,
StrongBox,
CloudTEE,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceIntegrityReport {
pub level: AttestationLevel,
pub challenge_nonce: Vec<u8>,
pub signature: Vec<u8>,
pub certificate_chain: Vec<String>, pub timestamp: u64,
pub extension_data: String,
}
impl DeviceIntegrityReport {
pub fn verify(&self, expected_nonce: &[u8]) -> bool {
self.verify_at_time_impl(expected_nonce, unix_time_secs())
}
#[cfg(test)]
pub fn verify_at_time(&self, expected_nonce: &[u8], now_secs: u64) -> bool {
self.verify_at_time_impl(expected_nonce, now_secs)
}
#[cfg_attr(test, allow(dead_code))]
fn verify_at_time_impl(&self, expected_nonce: &[u8], now_secs: u64) -> bool {
if self.signature.is_empty() || self.certificate_chain.len() < 2 {
return false;
}
if self.challenge_nonce != expected_nonce {
return false;
}
if self.timestamp > now_secs.saturating_add(MAX_ATTESTATION_FUTURE_SKEW_SECS) {
return false;
}
if now_secs > self.timestamp.saturating_add(MAX_ATTESTATION_AGE_SECS) {
return false;
}
if let Some(result) = self.verify_signature() {
if !result {
return false;
}
} else {
return false;
}
if !self.verify_certificate_chain() {
return false;
}
let is_hardened = match self.level {
AttestationLevel::StrongBox | AttestationLevel::CloudTEE => {
self.extension_data.contains("HARDWARE_BACKED")
&& self.extension_data.contains("SECURE_BOOT_ENABLED")
}
AttestationLevel::TEE => true,
AttestationLevel::Software => false, };
let has_valid_purpose = self.extension_data.contains("PURPOSE_SIGN")
&& (self.extension_data.contains("ALGORITHM_EC")
|| self.extension_data.contains("ALGORITHM_ED25519"));
is_hardened && has_valid_purpose
}
fn verify_signature(&self) -> Option<bool> {
let pubkey_entry = hex::decode(&self.certificate_chain[0]).ok()?;
let raw_pubkey = if let Ok(cert) = Certificate::from_der(&pubkey_entry) {
cert.tbs_certificate()
.subject_public_key_info()
.subject_public_key
.as_bytes()?
.to_vec()
} else {
pubkey_entry
};
if raw_pubkey.len() != 32 {
return None;
}
let bytes: [u8; 32] = raw_pubkey.try_into().ok()?;
let verifying_key = VerifyingKey::from_bytes(&bytes).ok()?;
let sig = Signature::from_slice(&self.signature).ok()?;
let mut data_to_verify = Vec::new();
data_to_verify.extend_from_slice(&self.challenge_nonce);
data_to_verify.extend_from_slice(self.extension_data.as_bytes());
data_to_verify.extend_from_slice(&self.timestamp.to_le_bytes());
Some(verifying_key.verify(&data_to_verify, &sig).is_ok())
}
fn verify_certificate_chain(&self) -> bool {
let last_entry = match self.certificate_chain.last() {
Some(entry) => entry,
None => return false,
};
if let Ok(cert_bytes) = hex::decode(last_entry) {
if let Ok(cert) = Certificate::from_der(&cert_bytes) {
let subject_str = format!("{:?}", cert.tbs_certificate().subject());
return TRUSTED_ROOTS.iter().any(|&root| subject_str.contains(root));
}
}
TRUSTED_ROOTS.iter().any(|&root| last_entry.contains(root))
}
pub fn get_device_fingerprint(&self) -> String {
let mut hasher = Sha256::new();
for cert in &self.certificate_chain {
hasher.update(cert.as_bytes());
}
hasher.update(self.extension_data.as_bytes());
hex::encode(hasher.finalize())
}
}
#[cfg(test)]
mod tests {
use super::{AttestationLevel, DeviceIntegrityReport, MAX_ATTESTATION_AGE_SECS};
use ed25519_dalek::{Signer, SigningKey};
use rand_core::Rng;
fn valid_report(timestamp: u64, level: AttestationLevel) -> DeviceIntegrityReport {
let mut seed = [0u8; 32];
rand::rng().fill_bytes(&mut seed);
let signing_key = SigningKey::from_bytes(&seed);
let pubkey_hex = hex::encode(signing_key.verifying_key().to_bytes());
let nonce = vec![1, 2, 3, 4];
let mut extension_data = "PURPOSE_SIGN|ALGORITHM_ED25519|OS_VERSION_14".to_string();
if level == AttestationLevel::StrongBox || level == AttestationLevel::CloudTEE {
extension_data.push_str("|HARDWARE_BACKED|SECURE_BOOT_ENABLED");
}
let mut data_to_verify = Vec::new();
data_to_verify.extend_from_slice(&nonce);
data_to_verify.extend_from_slice(extension_data.as_bytes());
data_to_verify.extend_from_slice(×tamp.to_le_bytes());
let signature = signing_key.sign(&data_to_verify).to_bytes().to_vec();
DeviceIntegrityReport {
level,
challenge_nonce: nonce,
signature,
certificate_chain: vec![pubkey_hex, "CONCLAVE_ROOT_CA_V1_01".to_string()],
timestamp,
extension_data,
}
}
#[test]
fn verify_accepts_report_within_freshness_window() {
let now_secs: u64 = 1_000_000;
let report = valid_report(now_secs.saturating_sub(60), AttestationLevel::TEE);
assert!(report.verify_at_time(&[1, 2, 3, 4], now_secs));
}
#[test]
fn verify_accepts_strongbox_report() {
let now_secs: u64 = 1_000_000;
let report = valid_report(now_secs.saturating_sub(60), AttestationLevel::StrongBox);
assert!(report.verify_at_time(&[1, 2, 3, 4], now_secs));
}
#[test]
fn verify_rejects_stale_report() {
let now_secs: u64 = 1_000_000;
let stale_timestamp = now_secs.saturating_sub(MAX_ATTESTATION_AGE_SECS + 1);
let report = valid_report(stale_timestamp, AttestationLevel::TEE);
assert!(!report.verify_at_time(&[1, 2, 3, 4], now_secs));
}
#[test]
fn verify_rejects_invalid_signature() {
let now_secs: u64 = 1_000_000;
let mut report = valid_report(now_secs.saturating_sub(60), AttestationLevel::TEE);
report.signature[0] ^= 0xFF;
assert!(!report.verify_at_time(&[1, 2, 3, 4], now_secs));
}
#[test]
fn verify_rejects_untrusted_root() {
let now_secs: u64 = 1_000_000;
let mut report = valid_report(now_secs.saturating_sub(60), AttestationLevel::TEE);
report.certificate_chain[1] = "UNKNOWN_ROOT".to_string();
assert!(!report.verify_at_time(&[1, 2, 3, 4], now_secs));
}
}