840 lines
30 KiB
Rust
840 lines
30 KiB
Rust
//! Confidential Core attestation module.
|
|
//!
|
|
//! Provides encoding/decoding of attestation records for WITNESS_SEG,
|
|
//! attestation-aware witness chain extensions, key-binding helpers for
|
|
//! CRYPTO_SEG, and a trait for pluggable platform-specific verification.
|
|
|
|
use alloc::vec::Vec;
|
|
use rvf_types::{AttestationHeader, AttestationWitnessType, ErrorCode, RvfError, TeePlatform};
|
|
|
|
use crate::hash::shake256_256;
|
|
use crate::witness::{create_witness_chain, verify_witness_chain, WitnessEntry};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. AttestationHeader Codec
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Size of a serialized `AttestationHeader` on the wire.
|
|
const ATTESTATION_HEADER_SIZE: usize = 112;
|
|
|
|
/// Size of one serialized witness entry (must match witness module).
|
|
const WITNESS_ENTRY_SIZE: usize = 73;
|
|
|
|
/// Encode an `AttestationHeader` to its 112-byte wire representation.
|
|
pub fn encode_attestation_header(header: &AttestationHeader) -> [u8; ATTESTATION_HEADER_SIZE] {
|
|
let mut buf = [0u8; ATTESTATION_HEADER_SIZE];
|
|
buf[0x00] = header.platform;
|
|
buf[0x01] = header.attestation_type;
|
|
buf[0x02..0x04].copy_from_slice(&header.quote_length.to_le_bytes());
|
|
buf[0x04..0x08].copy_from_slice(&header.reserved_0.to_le_bytes());
|
|
buf[0x08..0x28].copy_from_slice(&header.measurement);
|
|
buf[0x28..0x48].copy_from_slice(&header.signer_id);
|
|
buf[0x48..0x50].copy_from_slice(&header.timestamp_ns.to_le_bytes());
|
|
buf[0x50..0x60].copy_from_slice(&header.nonce);
|
|
buf[0x60..0x62].copy_from_slice(&header.svn.to_le_bytes());
|
|
buf[0x62..0x64].copy_from_slice(&header.sig_algo.to_le_bytes());
|
|
buf[0x64] = header.flags;
|
|
buf[0x65..0x68].copy_from_slice(&header.reserved_1);
|
|
buf[0x68..0x70].copy_from_slice(&header.report_data_len.to_le_bytes());
|
|
buf
|
|
}
|
|
|
|
/// Decode an `AttestationHeader` from wire bytes.
|
|
///
|
|
/// Returns `ErrorCode::TruncatedSegment` if `data.len() < 112`.
|
|
pub fn decode_attestation_header(data: &[u8]) -> Result<AttestationHeader, RvfError> {
|
|
if data.len() < ATTESTATION_HEADER_SIZE {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
|
|
let platform = data[0x00];
|
|
let attestation_type = data[0x01];
|
|
let quote_length = u16::from_le_bytes([data[0x02], data[0x03]]);
|
|
let reserved_0 = u32::from_le_bytes(data[0x04..0x08].try_into().unwrap());
|
|
|
|
let mut measurement = [0u8; 32];
|
|
measurement.copy_from_slice(&data[0x08..0x28]);
|
|
|
|
let mut signer_id = [0u8; 32];
|
|
signer_id.copy_from_slice(&data[0x28..0x48]);
|
|
|
|
let timestamp_ns = u64::from_le_bytes(data[0x48..0x50].try_into().unwrap());
|
|
|
|
let mut nonce = [0u8; 16];
|
|
nonce.copy_from_slice(&data[0x50..0x60]);
|
|
|
|
let svn = u16::from_le_bytes([data[0x60], data[0x61]]);
|
|
let sig_algo = u16::from_le_bytes([data[0x62], data[0x63]]);
|
|
let flags = data[0x64];
|
|
|
|
let mut reserved_1 = [0u8; 3];
|
|
reserved_1.copy_from_slice(&data[0x65..0x68]);
|
|
|
|
let report_data_len = u64::from_le_bytes(data[0x68..0x70].try_into().unwrap());
|
|
|
|
Ok(AttestationHeader {
|
|
platform,
|
|
attestation_type,
|
|
quote_length,
|
|
reserved_0,
|
|
measurement,
|
|
signer_id,
|
|
timestamp_ns,
|
|
nonce,
|
|
svn,
|
|
sig_algo,
|
|
flags,
|
|
reserved_1,
|
|
report_data_len,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Full Attestation Record Codec
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Encode a complete attestation record: header + report_data + quote.
|
|
pub fn encode_attestation_record(
|
|
header: &AttestationHeader,
|
|
report_data: &[u8],
|
|
quote: &[u8],
|
|
) -> Vec<u8> {
|
|
let hdr_bytes = encode_attestation_header(header);
|
|
let total = ATTESTATION_HEADER_SIZE + report_data.len() + quote.len();
|
|
let mut buf = Vec::with_capacity(total);
|
|
buf.extend_from_slice(&hdr_bytes);
|
|
buf.extend_from_slice(report_data);
|
|
buf.extend_from_slice(quote);
|
|
buf
|
|
}
|
|
|
|
/// Decode an attestation record, returning `(header, report_data, quote)`.
|
|
///
|
|
/// Returns `ErrorCode::TruncatedSegment` if data is too short for the
|
|
/// declared `report_data_len` and `quote_length`.
|
|
pub fn decode_attestation_record(
|
|
data: &[u8],
|
|
) -> Result<(AttestationHeader, Vec<u8>, Vec<u8>), RvfError> {
|
|
let header = decode_attestation_header(data)?;
|
|
|
|
let rd_len = header.report_data_len as usize;
|
|
let q_len = header.quote_length as usize;
|
|
let total_needed = ATTESTATION_HEADER_SIZE + rd_len + q_len;
|
|
|
|
if data.len() < total_needed {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
|
|
let rd_start = ATTESTATION_HEADER_SIZE;
|
|
let rd_end = rd_start + rd_len;
|
|
let report_data = data[rd_start..rd_end].to_vec();
|
|
|
|
let q_start = rd_end;
|
|
let q_end = q_start + q_len;
|
|
let quote = data[q_start..q_end].to_vec();
|
|
|
|
Ok((header, report_data, quote))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Witness Chain Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Create a witness chain entry for an attestation event.
|
|
///
|
|
/// The `action_hash` is SHAKE-256-256 of the full attestation record bytes.
|
|
pub fn attestation_witness_entry(
|
|
attestation_record: &[u8],
|
|
timestamp_ns: u64,
|
|
witness_type: AttestationWitnessType,
|
|
) -> WitnessEntry {
|
|
WitnessEntry {
|
|
prev_hash: [0u8; 32], // will be set by create_witness_chain
|
|
action_hash: shake256_256(attestation_record),
|
|
timestamp_ns,
|
|
witness_type: witness_type as u8,
|
|
}
|
|
}
|
|
|
|
/// Build a WITNESS_SEG payload for attestation records.
|
|
///
|
|
/// Wire layout:
|
|
/// `chain_entry_count`: u32 (LE)
|
|
/// `record_offsets`: [u64; count] (LE, byte offsets into records section)
|
|
/// `witness_chain`: [WitnessEntry; count] (73 bytes each, linked via SHAKE-256)
|
|
/// `records`: concatenated attestation record bytes
|
|
pub fn build_attestation_witness_payload(
|
|
records: &[Vec<u8>],
|
|
timestamps: &[u64],
|
|
witness_types: &[AttestationWitnessType],
|
|
) -> Result<Vec<u8>, RvfError> {
|
|
let count = records.len();
|
|
|
|
// 1. Create witness entries for each record.
|
|
let entries: Vec<WitnessEntry> = records
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, rec)| attestation_witness_entry(rec, timestamps[i], witness_types[i]))
|
|
.collect();
|
|
|
|
// 2. Run create_witness_chain to link entries via hashes.
|
|
let chain_bytes = create_witness_chain(&entries);
|
|
|
|
// 3. Compute record offsets (cumulative sums of record lengths).
|
|
let mut offsets = Vec::with_capacity(count);
|
|
let mut cumulative: u64 = 0;
|
|
for rec in records {
|
|
offsets.push(cumulative);
|
|
cumulative = cumulative
|
|
.checked_add(rec.len() as u64)
|
|
.ok_or(RvfError::Code(ErrorCode::SegmentTooLarge))?;
|
|
}
|
|
|
|
// 4. Concatenate: count(u32) + offsets([u64; n]) + chain_bytes + records.
|
|
let total = 4 + count * 8 + chain_bytes.len() + cumulative as usize;
|
|
let mut buf = Vec::with_capacity(total);
|
|
buf.extend_from_slice(&(count as u32).to_le_bytes());
|
|
for off in &offsets {
|
|
buf.extend_from_slice(&off.to_le_bytes());
|
|
}
|
|
buf.extend_from_slice(&chain_bytes);
|
|
for rec in records {
|
|
buf.extend_from_slice(rec);
|
|
}
|
|
Ok(buf)
|
|
}
|
|
|
|
/// A verified attestation entry: `(WitnessEntry, AttestationHeader, report_data, quote)`.
|
|
pub type VerifiedAttestationEntry = (WitnessEntry, AttestationHeader, Vec<u8>, Vec<u8>);
|
|
|
|
/// Verify an attestation witness payload.
|
|
///
|
|
/// Returns decoded entries paired with their attestation records.
|
|
pub fn verify_attestation_witness_payload(
|
|
data: &[u8],
|
|
) -> Result<Vec<VerifiedAttestationEntry>, RvfError> {
|
|
// 1. Read count from first 4 bytes.
|
|
if data.len() < 4 {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
let count = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
|
|
|
|
if count == 0 {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
// 2. Read offset table.
|
|
let offsets_end = 4 + count * 8;
|
|
if data.len() < offsets_end {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
let mut offsets = Vec::with_capacity(count);
|
|
for i in 0..count {
|
|
let o = 4 + i * 8;
|
|
let offset = u64::from_le_bytes(data[o..o + 8].try_into().unwrap());
|
|
offsets.push(offset as usize);
|
|
}
|
|
|
|
// 3. Extract witness chain bytes and verify.
|
|
let chain_start = offsets_end;
|
|
let chain_len = count * WITNESS_ENTRY_SIZE;
|
|
let chain_end = chain_start + chain_len;
|
|
if data.len() < chain_end {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
let chain_bytes = &data[chain_start..chain_end];
|
|
let entries = verify_witness_chain(chain_bytes)?;
|
|
|
|
// 4. Records start after the chain.
|
|
let records_base = chain_end;
|
|
let records_data = if records_base <= data.len() {
|
|
&data[records_base..]
|
|
} else {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
};
|
|
|
|
// 5. For each entry, decode the attestation record at the corresponding offset.
|
|
let mut results = Vec::with_capacity(count);
|
|
for (i, entry) in entries.iter().enumerate() {
|
|
let rec_start = offsets[i];
|
|
// Determine record end from the next offset, or from total records length.
|
|
let rec_end = if i + 1 < count {
|
|
offsets[i + 1]
|
|
} else {
|
|
records_data.len()
|
|
};
|
|
|
|
if rec_start > records_data.len() || rec_end > records_data.len() {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
|
|
let record_bytes = &records_data[rec_start..rec_end];
|
|
|
|
// Verify action_hash matches shake256_256(record_bytes).
|
|
let expected_hash = shake256_256(record_bytes);
|
|
if entry.action_hash != expected_hash {
|
|
return Err(RvfError::Code(ErrorCode::InvalidChecksum));
|
|
}
|
|
|
|
let (header, report_data, quote) = decode_attestation_record(record_bytes)?;
|
|
results.push((entry.clone(), header, report_data, quote));
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. TEE-Bound Key Record
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A TEE-bound key record for CRYPTO_SEG.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct TeeBoundKeyRecord {
|
|
/// Always `KEY_TYPE_TEE_BOUND` (4).
|
|
pub key_type: u8,
|
|
/// `SignatureAlgo` / KEM algo discriminant.
|
|
pub algorithm: u8,
|
|
/// Length of the sealed key material.
|
|
pub sealed_key_length: u16,
|
|
/// SHAKE-256-128 of the public key.
|
|
pub key_id: [u8; 16],
|
|
/// TEE measurement that seals this key.
|
|
pub measurement: [u8; 32],
|
|
/// `TeePlatform` discriminant.
|
|
pub platform: u8,
|
|
/// Reserved, must be zero.
|
|
pub reserved: [u8; 3],
|
|
/// Timestamp (nanoseconds) when key becomes valid.
|
|
pub valid_from: u64,
|
|
/// Timestamp (nanoseconds) when key expires. 0 = no expiry.
|
|
pub valid_until: u64,
|
|
/// Sealed key material.
|
|
pub sealed_key: Vec<u8>,
|
|
}
|
|
|
|
/// Size of the fixed header portion of a `TeeBoundKeyRecord`.
|
|
const TEE_KEY_HEADER_SIZE: usize = 72;
|
|
|
|
/// Encode a `TeeBoundKeyRecord` to wire format.
|
|
pub fn encode_tee_bound_key(record: &TeeBoundKeyRecord) -> Vec<u8> {
|
|
let total = TEE_KEY_HEADER_SIZE + record.sealed_key.len();
|
|
let mut buf = Vec::with_capacity(total);
|
|
|
|
buf.push(record.key_type); // 0x00
|
|
buf.push(record.algorithm); // 0x01
|
|
buf.extend_from_slice(&record.sealed_key_length.to_le_bytes()); // 0x02..0x04
|
|
buf.extend_from_slice(&record.key_id); // 0x04..0x14
|
|
buf.extend_from_slice(&record.measurement); // 0x14..0x34
|
|
buf.push(record.platform); // 0x34
|
|
buf.extend_from_slice(&record.reserved); // 0x35..0x38
|
|
buf.extend_from_slice(&record.valid_from.to_le_bytes()); // 0x38..0x40
|
|
buf.extend_from_slice(&record.valid_until.to_le_bytes()); // 0x40..0x48
|
|
buf.extend_from_slice(&record.sealed_key); // 0x48..
|
|
|
|
buf
|
|
}
|
|
|
|
/// Decode a `TeeBoundKeyRecord` from wire format.
|
|
///
|
|
/// Returns `ErrorCode::TruncatedSegment` if data is too short.
|
|
pub fn decode_tee_bound_key(data: &[u8]) -> Result<TeeBoundKeyRecord, RvfError> {
|
|
if data.len() < TEE_KEY_HEADER_SIZE {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
|
|
let key_type = data[0x00];
|
|
let algorithm = data[0x01];
|
|
let sealed_key_length = u16::from_le_bytes([data[0x02], data[0x03]]);
|
|
|
|
let mut key_id = [0u8; 16];
|
|
key_id.copy_from_slice(&data[0x04..0x14]);
|
|
|
|
let mut measurement = [0u8; 32];
|
|
measurement.copy_from_slice(&data[0x14..0x34]);
|
|
|
|
let platform = data[0x34];
|
|
|
|
let mut reserved = [0u8; 3];
|
|
reserved.copy_from_slice(&data[0x35..0x38]);
|
|
|
|
let valid_from = u64::from_le_bytes(data[0x38..0x40].try_into().unwrap());
|
|
let valid_until = u64::from_le_bytes(data[0x40..0x48].try_into().unwrap());
|
|
|
|
let sk_len = sealed_key_length as usize;
|
|
if data.len() < TEE_KEY_HEADER_SIZE + sk_len {
|
|
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
|
|
}
|
|
let sealed_key = data[0x48..0x48 + sk_len].to_vec();
|
|
|
|
Ok(TeeBoundKeyRecord {
|
|
key_type,
|
|
algorithm,
|
|
sealed_key_length,
|
|
key_id,
|
|
measurement,
|
|
platform,
|
|
reserved,
|
|
valid_from,
|
|
valid_until,
|
|
sealed_key,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. Key Binding Verification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Verify that a TEE-bound key is accessible in the current environment.
|
|
///
|
|
/// Checks platform, measurement, and expiry.
|
|
pub fn verify_key_binding(
|
|
key: &TeeBoundKeyRecord,
|
|
current_platform: TeePlatform,
|
|
current_measurement: &[u8; 32],
|
|
current_time_ns: u64,
|
|
) -> Result<(), RvfError> {
|
|
// Check platform matches.
|
|
if key.platform != current_platform as u8 {
|
|
return Err(RvfError::Code(ErrorCode::KeyNotBound));
|
|
}
|
|
|
|
// Check measurement matches.
|
|
if key.measurement != *current_measurement {
|
|
return Err(RvfError::Code(ErrorCode::KeyNotBound));
|
|
}
|
|
|
|
// Check not expired (valid_until == 0 means no expiry).
|
|
if key.valid_until != 0 && current_time_ns > key.valid_until {
|
|
return Err(RvfError::Code(ErrorCode::KeyExpired));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. QuoteVerifier Trait
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Platform-specific attestation quote verifier.
|
|
///
|
|
/// Object-safe for dynamic dispatch.
|
|
pub trait QuoteVerifier {
|
|
/// The TEE platform this verifier handles.
|
|
fn platform(&self) -> TeePlatform;
|
|
|
|
/// Verify a quote against its header and report data.
|
|
///
|
|
/// Returns `Ok(true)` if valid, `Ok(false)` if invalid, or an error
|
|
/// if verification could not be performed.
|
|
fn verify_quote(
|
|
&self,
|
|
header: &AttestationHeader,
|
|
report_data: &[u8],
|
|
quote: &[u8],
|
|
) -> Result<bool, RvfError>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::hash::shake256_128;
|
|
use alloc::vec;
|
|
use rvf_types::KEY_TYPE_TEE_BOUND;
|
|
|
|
/// Helper: build a fully-populated AttestationHeader.
|
|
fn make_test_header(report_data_len: u64, quote_length: u16) -> AttestationHeader {
|
|
let mut measurement = [0u8; 32];
|
|
measurement[0] = 0xAA;
|
|
measurement[31] = 0xBB;
|
|
|
|
let mut signer_id = [0u8; 32];
|
|
signer_id[0] = 0xCC;
|
|
signer_id[31] = 0xDD;
|
|
|
|
let mut nonce = [0u8; 16];
|
|
nonce[0] = 0x01;
|
|
nonce[15] = 0x0F;
|
|
|
|
AttestationHeader {
|
|
platform: TeePlatform::SevSnp as u8,
|
|
attestation_type: AttestationWitnessType::PlatformAttestation as u8,
|
|
quote_length,
|
|
reserved_0: 0,
|
|
measurement,
|
|
signer_id,
|
|
timestamp_ns: 1_700_000_000_000_000_000,
|
|
nonce,
|
|
svn: 42,
|
|
sig_algo: 1,
|
|
flags: AttestationHeader::FLAG_HAS_REPORT_DATA,
|
|
reserved_1: [0u8; 3],
|
|
report_data_len,
|
|
}
|
|
}
|
|
|
|
/// Helper: build a test record with given report_data and quote sizes.
|
|
fn make_test_record(rd_len: usize, q_len: usize) -> (AttestationHeader, Vec<u8>, Vec<u8>) {
|
|
let report_data: Vec<u8> = (0..rd_len).map(|i| (i & 0xFF) as u8).collect();
|
|
let quote: Vec<u8> = (0..q_len).map(|i| ((i + 0x80) & 0xFF) as u8).collect();
|
|
let header = make_test_header(rd_len as u64, q_len as u16);
|
|
(header, report_data, quote)
|
|
}
|
|
|
|
/// Helper: build a TeeBoundKeyRecord for testing.
|
|
fn make_test_key_record() -> TeeBoundKeyRecord {
|
|
let mut measurement = [0u8; 32];
|
|
measurement[0] = 0xAA;
|
|
measurement[31] = 0xBB;
|
|
|
|
let sealed_key = vec![0x10, 0x20, 0x30, 0x40, 0x50];
|
|
let public_key = b"test-public-key-material";
|
|
let key_id = shake256_128(public_key);
|
|
|
|
TeeBoundKeyRecord {
|
|
key_type: KEY_TYPE_TEE_BOUND,
|
|
algorithm: 1,
|
|
sealed_key_length: sealed_key.len() as u16,
|
|
key_id,
|
|
measurement,
|
|
platform: TeePlatform::SevSnp as u8,
|
|
reserved: [0u8; 3],
|
|
valid_from: 1_000_000_000,
|
|
valid_until: 2_000_000_000,
|
|
sealed_key,
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 1. header_codec_round_trip
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn header_codec_round_trip() {
|
|
let header = make_test_header(64, 256);
|
|
let encoded = encode_attestation_header(&header);
|
|
assert_eq!(encoded.len(), ATTESTATION_HEADER_SIZE);
|
|
|
|
let decoded = decode_attestation_header(&encoded).unwrap();
|
|
assert_eq!(decoded.platform, header.platform);
|
|
assert_eq!(decoded.attestation_type, header.attestation_type);
|
|
assert_eq!(decoded.quote_length, header.quote_length);
|
|
assert_eq!(decoded.reserved_0, header.reserved_0);
|
|
assert_eq!(decoded.measurement, header.measurement);
|
|
assert_eq!(decoded.signer_id, header.signer_id);
|
|
assert_eq!(decoded.timestamp_ns, header.timestamp_ns);
|
|
assert_eq!(decoded.nonce, header.nonce);
|
|
assert_eq!(decoded.svn, header.svn);
|
|
assert_eq!(decoded.sig_algo, header.sig_algo);
|
|
assert_eq!(decoded.flags, header.flags);
|
|
assert_eq!(decoded.reserved_1, header.reserved_1);
|
|
assert_eq!(decoded.report_data_len, header.report_data_len);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 2. header_decode_truncated
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn header_decode_truncated() {
|
|
let data = [0u8; 111]; // One byte short
|
|
let result = decode_attestation_header(&data);
|
|
assert!(matches!(
|
|
result,
|
|
Err(RvfError::Code(ErrorCode::TruncatedSegment))
|
|
));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 3. record_codec_round_trip
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn record_codec_round_trip() {
|
|
let (header, report_data, quote) = make_test_record(64, 128);
|
|
let encoded = encode_attestation_record(&header, &report_data, "e);
|
|
assert_eq!(encoded.len(), ATTESTATION_HEADER_SIZE + 64 + 128);
|
|
|
|
let (dec_hdr, dec_rd, dec_q) = decode_attestation_record(&encoded).unwrap();
|
|
assert_eq!(dec_hdr.platform, header.platform);
|
|
assert_eq!(dec_hdr.quote_length, header.quote_length);
|
|
assert_eq!(dec_hdr.report_data_len, header.report_data_len);
|
|
assert_eq!(dec_rd, report_data);
|
|
assert_eq!(dec_q, quote);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 4. record_empty_report_data
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn record_empty_report_data() {
|
|
let (header, report_data, quote) = make_test_record(0, 32);
|
|
let encoded = encode_attestation_record(&header, &report_data, "e);
|
|
|
|
let (dec_hdr, dec_rd, dec_q) = decode_attestation_record(&encoded).unwrap();
|
|
assert!(dec_rd.is_empty());
|
|
assert_eq!(dec_q, quote);
|
|
assert_eq!(dec_hdr.report_data_len, 0);
|
|
assert_eq!(dec_hdr.quote_length, 32);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 5. record_empty_quote
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn record_empty_quote() {
|
|
let (header, report_data, quote) = make_test_record(48, 0);
|
|
let encoded = encode_attestation_record(&header, &report_data, "e);
|
|
|
|
let (dec_hdr, dec_rd, dec_q) = decode_attestation_record(&encoded).unwrap();
|
|
assert_eq!(dec_rd, report_data);
|
|
assert!(dec_q.is_empty());
|
|
assert_eq!(dec_hdr.report_data_len, 48);
|
|
assert_eq!(dec_hdr.quote_length, 0);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 6. witness_entry_hash_binding
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn witness_entry_hash_binding() {
|
|
let (header, report_data, quote) = make_test_record(32, 64);
|
|
let record = encode_attestation_record(&header, &report_data, "e);
|
|
let expected_hash = shake256_256(&record);
|
|
|
|
let entry = attestation_witness_entry(
|
|
&record,
|
|
1_000_000_000,
|
|
AttestationWitnessType::PlatformAttestation,
|
|
);
|
|
assert_eq!(entry.action_hash, expected_hash);
|
|
assert_eq!(entry.timestamp_ns, 1_000_000_000);
|
|
assert_eq!(
|
|
entry.witness_type,
|
|
AttestationWitnessType::PlatformAttestation as u8
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 7. witness_payload_round_trip
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn witness_payload_round_trip() {
|
|
let records: Vec<Vec<u8>> = (0..3)
|
|
.map(|i| {
|
|
let (h, rd, q) = make_test_record(16 + i * 4, 32 + i * 8);
|
|
encode_attestation_record(&h, &rd, &q)
|
|
})
|
|
.collect();
|
|
|
|
let timestamps = vec![100, 200, 300];
|
|
let witness_types = vec![
|
|
AttestationWitnessType::PlatformAttestation,
|
|
AttestationWitnessType::KeyBinding,
|
|
AttestationWitnessType::ComputationProof,
|
|
];
|
|
|
|
let payload =
|
|
build_attestation_witness_payload(&records, ×tamps, &witness_types).unwrap();
|
|
let results = verify_attestation_witness_payload(&payload).unwrap();
|
|
|
|
assert_eq!(results.len(), 3);
|
|
for (i, (entry, header, rd, q)) in results.iter().enumerate() {
|
|
assert_eq!(entry.timestamp_ns, timestamps[i]);
|
|
assert_eq!(entry.witness_type, witness_types[i] as u8);
|
|
// Re-encode and compare the record bytes.
|
|
let re_encoded = encode_attestation_record(header, rd, q);
|
|
assert_eq!(re_encoded, records[i]);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 8. witness_payload_single_entry
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn witness_payload_single_entry() {
|
|
let (h, rd, q) = make_test_record(8, 16);
|
|
let record = encode_attestation_record(&h, &rd, &q);
|
|
let records = vec![record.clone()];
|
|
let timestamps = vec![42];
|
|
let witness_types = vec![AttestationWitnessType::DataProvenance];
|
|
|
|
let payload =
|
|
build_attestation_witness_payload(&records, ×tamps, &witness_types).unwrap();
|
|
let results = verify_attestation_witness_payload(&payload).unwrap();
|
|
|
|
assert_eq!(results.len(), 1);
|
|
let (entry, header, dec_rd, dec_q) = &results[0];
|
|
assert_eq!(entry.timestamp_ns, 42);
|
|
assert_eq!(
|
|
entry.witness_type,
|
|
AttestationWitnessType::DataProvenance as u8
|
|
);
|
|
assert_eq!(*dec_rd, rd);
|
|
assert_eq!(*dec_q, q);
|
|
assert_eq!(header.platform, h.platform);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 9. witness_payload_tamper_detected
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn witness_payload_tamper_detected() {
|
|
let (h, rd, q) = make_test_record(16, 32);
|
|
let record = encode_attestation_record(&h, &rd, &q);
|
|
let records = vec![record];
|
|
let timestamps = vec![999];
|
|
let witness_types = vec![AttestationWitnessType::PlatformAttestation];
|
|
|
|
let mut payload =
|
|
build_attestation_witness_payload(&records, ×tamps, &witness_types).unwrap();
|
|
|
|
// Flip a byte in the attestation record (after count + offsets + chain).
|
|
let records_offset = 4 + 8 + WITNESS_ENTRY_SIZE;
|
|
if records_offset + 50 < payload.len() {
|
|
payload[records_offset + 50] ^= 0xFF;
|
|
}
|
|
|
|
let result = verify_attestation_witness_payload(&payload);
|
|
assert!(matches!(
|
|
result,
|
|
Err(RvfError::Code(ErrorCode::InvalidChecksum))
|
|
));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 10. tee_key_codec_round_trip
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn tee_key_codec_round_trip() {
|
|
let record = make_test_key_record();
|
|
let encoded = encode_tee_bound_key(&record);
|
|
assert_eq!(encoded.len(), TEE_KEY_HEADER_SIZE + record.sealed_key.len());
|
|
|
|
let decoded = decode_tee_bound_key(&encoded).unwrap();
|
|
assert_eq!(decoded.key_type, record.key_type);
|
|
assert_eq!(decoded.algorithm, record.algorithm);
|
|
assert_eq!(decoded.sealed_key_length, record.sealed_key_length);
|
|
assert_eq!(decoded.key_id, record.key_id);
|
|
assert_eq!(decoded.measurement, record.measurement);
|
|
assert_eq!(decoded.platform, record.platform);
|
|
assert_eq!(decoded.reserved, record.reserved);
|
|
assert_eq!(decoded.valid_from, record.valid_from);
|
|
assert_eq!(decoded.valid_until, record.valid_until);
|
|
assert_eq!(decoded.sealed_key, record.sealed_key);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 11. tee_key_decode_truncated
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn tee_key_decode_truncated() {
|
|
// Header too short.
|
|
let data = [0u8; TEE_KEY_HEADER_SIZE - 1];
|
|
let result = decode_tee_bound_key(&data);
|
|
assert_eq!(result, Err(RvfError::Code(ErrorCode::TruncatedSegment)));
|
|
|
|
// Header present but sealed_key truncated.
|
|
let record = make_test_key_record();
|
|
let encoded = encode_tee_bound_key(&record);
|
|
let truncated = &encoded[..TEE_KEY_HEADER_SIZE + 2]; // 2 < sealed_key_length (5)
|
|
let result = decode_tee_bound_key(truncated);
|
|
assert_eq!(result, Err(RvfError::Code(ErrorCode::TruncatedSegment)));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 12. key_binding_valid
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn key_binding_valid() {
|
|
let record = make_test_key_record();
|
|
let mut measurement = [0u8; 32];
|
|
measurement[0] = 0xAA;
|
|
measurement[31] = 0xBB;
|
|
|
|
let result = verify_key_binding(
|
|
&record,
|
|
TeePlatform::SevSnp,
|
|
&measurement,
|
|
1_500_000_000, // between valid_from and valid_until
|
|
);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 13. key_binding_wrong_platform
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn key_binding_wrong_platform() {
|
|
let record = make_test_key_record();
|
|
let mut measurement = [0u8; 32];
|
|
measurement[0] = 0xAA;
|
|
measurement[31] = 0xBB;
|
|
|
|
let result = verify_key_binding(
|
|
&record,
|
|
TeePlatform::Sgx, // wrong platform
|
|
&measurement,
|
|
1_500_000_000,
|
|
);
|
|
assert_eq!(result, Err(RvfError::Code(ErrorCode::KeyNotBound)));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 14. key_binding_wrong_measurement
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn key_binding_wrong_measurement() {
|
|
let record = make_test_key_record();
|
|
let wrong_measurement = [0xFF; 32]; // does not match
|
|
|
|
let result = verify_key_binding(
|
|
&record,
|
|
TeePlatform::SevSnp,
|
|
&wrong_measurement,
|
|
1_500_000_000,
|
|
);
|
|
assert_eq!(result, Err(RvfError::Code(ErrorCode::KeyNotBound)));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 15. key_binding_expired
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn key_binding_expired() {
|
|
let record = make_test_key_record(); // valid_until = 2_000_000_000
|
|
let mut measurement = [0u8; 32];
|
|
measurement[0] = 0xAA;
|
|
measurement[31] = 0xBB;
|
|
|
|
let result = verify_key_binding(
|
|
&record,
|
|
TeePlatform::SevSnp,
|
|
&measurement,
|
|
3_000_000_000, // past valid_until
|
|
);
|
|
assert_eq!(result, Err(RvfError::Code(ErrorCode::KeyExpired)));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 16. key_binding_no_expiry
|
|
// -----------------------------------------------------------------------
|
|
#[test]
|
|
fn key_binding_no_expiry() {
|
|
let mut record = make_test_key_record();
|
|
record.valid_until = 0; // no expiry
|
|
|
|
let mut measurement = [0u8; 32];
|
|
measurement[0] = 0xAA;
|
|
measurement[31] = 0xBB;
|
|
|
|
let result = verify_key_binding(
|
|
&record,
|
|
TeePlatform::SevSnp,
|
|
&measurement,
|
|
u64::MAX, // far future -- should still pass
|
|
);
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|