Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,839 @@
//! 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, &quote);
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, &quote);
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, &quote);
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, &quote);
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, &timestamps, &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, &timestamps, &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, &timestamps, &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());
}
}

View File

@@ -0,0 +1,113 @@
//! Signature footer codec for RVF segments.
//!
//! Encodes/decodes `rvf_types::SignatureFooter` to/from wire-format bytes.
//! Wire layout:
//! [0..2] sig_algo (u16 LE)
//! [2..4] sig_length (u16 LE)
//! [4..4+sig_length] signature bytes
//! [4+sig_length..4+sig_length+4] footer_length (u32 LE)
use alloc::vec::Vec;
use rvf_types::{ErrorCode, RvfError, SignatureFooter};
/// Minimum footer wire size: 2 (algo) + 2 (sig_len) + 4 (footer_len) = 8 bytes.
const FOOTER_MIN_SIZE: usize = 8;
/// Encode a `SignatureFooter` into wire-format bytes.
pub fn encode_signature_footer(footer: &SignatureFooter) -> Vec<u8> {
let sig_len = footer.sig_length as usize;
let total = 2 + 2 + sig_len + 4;
let mut buf = Vec::with_capacity(total);
buf.extend_from_slice(&footer.sig_algo.to_le_bytes());
buf.extend_from_slice(&footer.sig_length.to_le_bytes());
buf.extend_from_slice(&footer.signature[..sig_len]);
buf.extend_from_slice(&footer.footer_length.to_le_bytes());
buf
}
/// Decode a `SignatureFooter` from wire-format bytes.
pub fn decode_signature_footer(data: &[u8]) -> Result<SignatureFooter, RvfError> {
if data.len() < FOOTER_MIN_SIZE {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let sig_algo = u16::from_le_bytes([data[0], data[1]]);
let sig_length = u16::from_le_bytes([data[2], data[3]]);
let sig_len = sig_length as usize;
if sig_len > SignatureFooter::MAX_SIG_LEN {
return Err(RvfError::Code(ErrorCode::InvalidSignature));
}
if data.len() < 4 + sig_len + 4 {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let mut signature = [0u8; SignatureFooter::MAX_SIG_LEN];
signature[..sig_len].copy_from_slice(&data[4..4 + sig_len]);
let fl_offset = 4 + sig_len;
let footer_length = u32::from_le_bytes([
data[fl_offset],
data[fl_offset + 1],
data[fl_offset + 2],
data[fl_offset + 3],
]);
Ok(SignatureFooter {
sig_algo,
sig_length,
signature,
footer_length,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_footer(algo: u16, sig_len: u16, fill: u8) -> SignatureFooter {
let mut signature = [0u8; SignatureFooter::MAX_SIG_LEN];
signature[..sig_len as usize].fill(fill);
SignatureFooter {
sig_algo: algo,
sig_length: sig_len,
signature,
footer_length: SignatureFooter::compute_footer_length(sig_len),
}
}
#[test]
fn round_trip_ed25519() {
let footer = make_footer(0, 64, 0xAB);
let encoded = encode_signature_footer(&footer);
assert_eq!(encoded.len(), 2 + 2 + 64 + 4);
let decoded = decode_signature_footer(&encoded).unwrap();
assert_eq!(decoded.sig_algo, footer.sig_algo);
assert_eq!(decoded.sig_length, footer.sig_length);
assert_eq!(&decoded.signature[..64], &footer.signature[..64]);
assert_eq!(decoded.footer_length, footer.footer_length);
}
#[test]
fn decode_truncated_header() {
let result = decode_signature_footer(&[0u8; 5]);
assert!(result.is_err());
}
#[test]
fn decode_truncated_signature() {
let footer = make_footer(0, 64, 0xCC);
let encoded = encode_signature_footer(&footer);
let result = decode_signature_footer(&encoded[..10]);
assert!(result.is_err());
}
#[test]
fn empty_signature() {
let footer = make_footer(1, 0, 0);
let encoded = encode_signature_footer(&footer);
assert_eq!(encoded.len(), FOOTER_MIN_SIZE);
let decoded = decode_signature_footer(&encoded).unwrap();
assert_eq!(decoded.sig_algo, 1);
assert_eq!(decoded.sig_length, 0);
}
}

View File

@@ -0,0 +1,97 @@
//! SHAKE-256 hashing for cryptographic witness and content hashing.
use sha3::{
digest::{ExtendableOutput, Update, XofReader},
Shake256,
};
use alloc::vec;
use alloc::vec::Vec;
/// Compute SHAKE-256 hash of `data` with arbitrary `output_len`.
pub fn shake256_hash(data: &[u8], output_len: usize) -> Vec<u8> {
let mut hasher = Shake256::default();
hasher.update(data);
let mut reader = hasher.finalize_xof();
let mut output = vec![0u8; output_len];
reader.read(&mut output);
output
}
/// Compute 128-bit (16-byte) SHAKE-256 hash.
pub fn shake256_128(data: &[u8]) -> [u8; 16] {
let mut hasher = Shake256::default();
hasher.update(data);
let mut reader = hasher.finalize_xof();
let mut output = [0u8; 16];
reader.read(&mut output);
output
}
/// Compute 256-bit (32-byte) SHAKE-256 hash.
pub fn shake256_256(data: &[u8]) -> [u8; 32] {
let mut hasher = Shake256::default();
hasher.update(data);
let mut reader = hasher.finalize_xof();
let mut output = [0u8; 32];
reader.read(&mut output);
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shake256_empty_input() {
let h128 = shake256_128(b"");
let h256 = shake256_256(b"");
// Non-zero output for empty input (SHAKE-256 is a sponge)
assert_ne!(h128, [0u8; 16]);
assert_ne!(h256, [0u8; 32]);
}
#[test]
fn shake256_deterministic() {
let a = shake256_256(b"test data");
let b = shake256_256(b"test data");
assert_eq!(a, b);
}
#[test]
fn shake256_different_inputs() {
let a = shake256_256(b"input A");
let b = shake256_256(b"input B");
assert_ne!(a, b);
}
#[test]
fn shake256_arbitrary_output_len() {
let h = shake256_hash(b"hello", 64);
assert_eq!(h.len(), 64);
// Prefix should match the 32-byte version
let h32 = shake256_hash(b"hello", 32);
assert_eq!(&h[..32], &h32[..]);
}
#[test]
fn shake256_128_is_prefix_of_256() {
let h128 = shake256_128(b"consistency check");
let h256 = shake256_256(b"consistency check");
assert_eq!(&h128[..], &h256[..16]);
}
#[test]
fn shake256_known_vector() {
// NIST test: SHAKE256("") first 32 bytes
let h = shake256_hash(b"", 32);
assert_eq!(
h,
[
0x46, 0xb9, 0xdd, 0x2b, 0x0b, 0xa8, 0x8d, 0x13, 0x23, 0x3b, 0x3f, 0xeb, 0x74, 0x3e,
0xeb, 0x24, 0x3f, 0xcd, 0x52, 0xea, 0x62, 0xb8, 0x1b, 0x82, 0xb5, 0x0c, 0x27, 0x64,
0x6e, 0xd5, 0x76, 0x2f,
]
);
}
}

View File

@@ -0,0 +1,32 @@
//! Cryptographic primitives for the RuVector Format (RVF).
//!
//! Provides SHAKE-256 hashing, Ed25519 segment signing/verification,
//! signature footer codec, and WITNESS_SEG audit-trail support.
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
pub mod attestation;
pub mod footer;
pub mod hash;
pub mod lineage;
#[cfg(feature = "ed25519")]
pub mod sign;
pub mod witness;
pub use attestation::{
attestation_witness_entry, build_attestation_witness_payload, decode_attestation_header,
decode_attestation_record, decode_tee_bound_key, encode_attestation_header,
encode_attestation_record, encode_tee_bound_key, verify_attestation_witness_payload,
verify_key_binding, QuoteVerifier, TeeBoundKeyRecord, VerifiedAttestationEntry,
};
pub use footer::{decode_signature_footer, encode_signature_footer};
pub use hash::{shake256_128, shake256_256, shake256_hash};
pub use lineage::{
compute_manifest_hash, lineage_record_from_bytes, lineage_record_to_bytes,
lineage_witness_entry, verify_lineage_chain,
};
#[cfg(feature = "ed25519")]
pub use sign::{sign_segment, verify_segment};
pub use witness::{create_witness_chain, verify_witness_chain, WitnessEntry};

View File

@@ -0,0 +1,272 @@
//! Lineage witness functions for DNA-style provenance chains.
//!
//! Provides serialization, hashing, and verification for lineage records
//! that track file derivation history through witness chain entries.
use rvf_types::{
DerivationType, ErrorCode, FileIdentity, LineageRecord, RvfError, LINEAGE_RECORD_SIZE,
WITNESS_DERIVATION,
};
use crate::hash::shake256_256;
use crate::witness::WitnessEntry;
/// Serialize a `LineageRecord` to a fixed 128-byte array.
pub fn lineage_record_to_bytes(record: &LineageRecord) -> [u8; LINEAGE_RECORD_SIZE] {
let mut buf = [0u8; LINEAGE_RECORD_SIZE];
buf[0x00..0x10].copy_from_slice(&record.file_id);
buf[0x10..0x20].copy_from_slice(&record.parent_id);
buf[0x20..0x40].copy_from_slice(&record.parent_hash);
buf[0x40] = record.derivation_type as u8;
// 3 bytes padding at 0x41..0x44
buf[0x44..0x48].copy_from_slice(&record.mutation_count.to_le_bytes());
buf[0x48..0x50].copy_from_slice(&record.timestamp_ns.to_le_bytes());
buf[0x50] = record.description_len;
let desc_len = (record.description_len as usize).min(47);
buf[0x51..0x51 + desc_len].copy_from_slice(&record.description[..desc_len]);
buf
}
/// Deserialize a `LineageRecord` from a 128-byte slice.
pub fn lineage_record_from_bytes(
data: &[u8; LINEAGE_RECORD_SIZE],
) -> Result<LineageRecord, RvfError> {
let mut file_id = [0u8; 16];
file_id.copy_from_slice(&data[0x00..0x10]);
let mut parent_id = [0u8; 16];
parent_id.copy_from_slice(&data[0x10..0x20]);
let mut parent_hash = [0u8; 32];
parent_hash.copy_from_slice(&data[0x20..0x40]);
let derivation_type =
DerivationType::try_from(data[0x40]).map_err(|v| RvfError::InvalidEnumValue {
type_name: "DerivationType",
value: v as u64,
})?;
let mutation_count = u32::from_le_bytes(data[0x44..0x48].try_into().unwrap());
let timestamp_ns = u64::from_le_bytes(data[0x48..0x50].try_into().unwrap());
let description_len = data[0x50].min(47);
let mut description = [0u8; 47];
description[..description_len as usize]
.copy_from_slice(&data[0x51..0x51 + description_len as usize]);
Ok(LineageRecord {
file_id,
parent_id,
parent_hash,
derivation_type,
mutation_count,
timestamp_ns,
description_len,
description,
})
}
/// Create a witness entry for a lineage derivation event.
///
/// The `action_hash` is SHAKE-256-256 of the serialized record bytes.
/// Uses witness type `WITNESS_DERIVATION` (0x09).
pub fn lineage_witness_entry(record: &LineageRecord, prev_hash: [u8; 32]) -> WitnessEntry {
let record_bytes = lineage_record_to_bytes(record);
let action_hash = shake256_256(&record_bytes);
WitnessEntry {
prev_hash,
action_hash,
timestamp_ns: record.timestamp_ns,
witness_type: WITNESS_DERIVATION,
}
}
/// Compute the SHAKE-256-256 hash of a 4096-byte manifest for use as parent_hash.
pub fn compute_manifest_hash(manifest: &[u8; 4096]) -> [u8; 32] {
shake256_256(manifest)
}
/// Verify a lineage chain: each child's parent_hash must match the
/// hash of the corresponding parent's manifest bytes.
///
/// Takes pairs of (FileIdentity, manifest_hash) in order from root to leaf.
pub fn verify_lineage_chain(entries: &[(FileIdentity, [u8; 32])]) -> Result<(), RvfError> {
if entries.is_empty() {
return Ok(());
}
// First entry must be root
if !entries[0].0.is_root() {
return Err(RvfError::Code(ErrorCode::LineageBroken));
}
for i in 1..entries.len() {
let child = &entries[i].0;
let parent = &entries[i - 1].0;
let parent_manifest_hash = &entries[i - 1].1;
// Child's parent_id must match parent's file_id
if child.parent_id != parent.file_id {
return Err(RvfError::Code(ErrorCode::LineageBroken));
}
// Child's parent_hash must match parent's manifest hash
if child.parent_hash != *parent_manifest_hash {
return Err(RvfError::Code(ErrorCode::ParentHashMismatch));
}
// Depth must increment by 1
if child.lineage_depth != parent.lineage_depth + 1 {
return Err(RvfError::Code(ErrorCode::LineageBroken));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_record() -> LineageRecord {
LineageRecord::new(
[1u8; 16],
[2u8; 16],
[3u8; 32],
DerivationType::Filter,
5,
1_700_000_000_000_000_000,
"test derivation",
)
}
#[test]
fn lineage_record_round_trip() {
let record = sample_record();
let bytes = lineage_record_to_bytes(&record);
assert_eq!(bytes.len(), LINEAGE_RECORD_SIZE);
let decoded = lineage_record_from_bytes(&bytes).unwrap();
assert_eq!(decoded.file_id, record.file_id);
assert_eq!(decoded.parent_id, record.parent_id);
assert_eq!(decoded.parent_hash, record.parent_hash);
assert_eq!(decoded.derivation_type, record.derivation_type);
assert_eq!(decoded.mutation_count, record.mutation_count);
assert_eq!(decoded.timestamp_ns, record.timestamp_ns);
assert_eq!(decoded.description_str(), record.description_str());
}
#[test]
fn lineage_record_invalid_derivation_type() {
let record = sample_record();
let mut bytes = lineage_record_to_bytes(&record);
bytes[0x40] = 0xFE; // invalid derivation type
let result = lineage_record_from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn lineage_witness_entry_creates_valid_entry() {
let record = sample_record();
let prev_hash = [0u8; 32];
let entry = lineage_witness_entry(&record, prev_hash);
assert_eq!(entry.witness_type, WITNESS_DERIVATION);
assert_eq!(entry.prev_hash, prev_hash);
assert_eq!(entry.timestamp_ns, record.timestamp_ns);
assert_ne!(entry.action_hash, [0u8; 32]);
}
#[test]
fn compute_manifest_hash_deterministic() {
let manifest = [0xABu8; 4096];
let h1 = compute_manifest_hash(&manifest);
let h2 = compute_manifest_hash(&manifest);
assert_eq!(h1, h2);
assert_ne!(h1, [0u8; 32]);
}
#[test]
fn verify_empty_chain() {
assert!(verify_lineage_chain(&[]).is_ok());
}
#[test]
fn verify_single_root() {
let root = FileIdentity::new_root([1u8; 16]);
let hash = [0xAAu8; 32];
assert!(verify_lineage_chain(&[(root, hash)]).is_ok());
}
#[test]
fn verify_parent_child_chain() {
let root_id = [1u8; 16];
let child_id = [2u8; 16];
let root_hash = [0xAAu8; 32];
let child_hash = [0xBBu8; 32];
let root = FileIdentity::new_root(root_id);
let child = FileIdentity {
file_id: child_id,
parent_id: root_id,
parent_hash: root_hash,
lineage_depth: 1,
};
assert!(verify_lineage_chain(&[(root, root_hash), (child, child_hash)]).is_ok());
}
#[test]
fn verify_broken_parent_id() {
let root = FileIdentity::new_root([1u8; 16]);
let root_hash = [0xAAu8; 32];
let child = FileIdentity {
file_id: [2u8; 16],
parent_id: [3u8; 16], // wrong parent_id
parent_hash: root_hash,
lineage_depth: 1,
};
let result = verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]);
assert!(result.is_err());
}
#[test]
fn verify_hash_mismatch() {
let root_id = [1u8; 16];
let root = FileIdentity::new_root(root_id);
let root_hash = [0xAAu8; 32];
let child = FileIdentity {
file_id: [2u8; 16],
parent_id: root_id,
parent_hash: [0xCCu8; 32], // wrong hash
lineage_depth: 1,
};
let result = verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]);
assert!(matches!(
result,
Err(RvfError::Code(ErrorCode::ParentHashMismatch))
));
}
#[test]
fn verify_non_root_first() {
let non_root = FileIdentity {
file_id: [1u8; 16],
parent_id: [2u8; 16],
parent_hash: [3u8; 32],
lineage_depth: 1,
};
let result = verify_lineage_chain(&[(non_root, [0u8; 32])]);
assert!(result.is_err());
}
#[test]
fn verify_depth_mismatch() {
let root_id = [1u8; 16];
let root = FileIdentity::new_root(root_id);
let root_hash = [0xAAu8; 32];
let child = FileIdentity {
file_id: [2u8; 16],
parent_id: root_id,
parent_hash: root_hash,
lineage_depth: 5, // should be 1
};
let result = verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,188 @@
//! Ed25519 segment signing and verification.
//!
//! Signs the canonical representation: header bytes || content_hash || context.
//! ML-DSA-65 is a future TODO behind a feature flag.
use alloc::vec::Vec;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rvf_types::{SegmentHeader, SignatureFooter};
use crate::hash::shake256_128;
/// Ed25519 algorithm identifier (matches `SignatureAlgo::Ed25519`).
const SIG_ALGO_ED25519: u16 = 0;
/// Build the canonical message to sign for a segment.
///
/// signed_data = segment_header_bytes[0..40] || content_hash || context_string || segment_id
fn build_signed_data(header: &SegmentHeader, payload: &[u8]) -> Vec<u8> {
// Safe serialization of header fields to bytes, matching the wire format
// layout (see write_path.rs header_to_bytes). Avoids unsafe transmute which
// relies on compiler-specific struct layout guarantees.
let header_bytes = header_to_sign_bytes(header);
let mut msg = Vec::with_capacity(40 + 16 + 32);
// First 40 bytes of header (up to but not including content_hash at offset 0x28)
msg.extend_from_slice(&header_bytes[..40]);
// Content hash from header
msg.extend_from_slice(&header.content_hash);
// Context string for domain separation
msg.extend_from_slice(b"RVF-v1-segment");
// Segment ID bytes for replay prevention
msg.extend_from_slice(&header.segment_id.to_le_bytes());
// Include payload hash for binding
let payload_hash = shake256_128(payload);
msg.extend_from_slice(&payload_hash);
msg
}
/// Safely serialize a `SegmentHeader` into its 64-byte wire representation.
///
/// This mirrors the layout in `write_path::header_to_bytes` but lives here to
/// avoid an unsafe `transmute` / pointer cast whose correctness depends on
/// padding and alignment guarantees that are not enforced by the language.
fn header_to_sign_bytes(h: &SegmentHeader) -> [u8; 64] {
let mut buf = [0u8; 64];
buf[0x00..0x04].copy_from_slice(&h.magic.to_le_bytes());
buf[0x04] = h.version;
buf[0x05] = h.seg_type;
buf[0x06..0x08].copy_from_slice(&h.flags.to_le_bytes());
buf[0x08..0x10].copy_from_slice(&h.segment_id.to_le_bytes());
buf[0x10..0x18].copy_from_slice(&h.payload_length.to_le_bytes());
buf[0x18..0x20].copy_from_slice(&h.timestamp_ns.to_le_bytes());
buf[0x20] = h.checksum_algo;
buf[0x21] = h.compression;
buf[0x22..0x24].copy_from_slice(&h.reserved_0.to_le_bytes());
buf[0x24..0x28].copy_from_slice(&h.reserved_1.to_le_bytes());
buf[0x28..0x38].copy_from_slice(&h.content_hash);
buf[0x38..0x3C].copy_from_slice(&h.uncompressed_len.to_le_bytes());
buf[0x3C..0x40].copy_from_slice(&h.alignment_pad.to_le_bytes());
buf
}
/// Sign a segment with Ed25519, producing a `SignatureFooter`.
pub fn sign_segment(header: &SegmentHeader, payload: &[u8], key: &SigningKey) -> SignatureFooter {
let msg = build_signed_data(header, payload);
let sig: Signature = key.sign(&msg);
let sig_bytes = sig.to_bytes();
let mut signature = [0u8; SignatureFooter::MAX_SIG_LEN];
signature[..64].copy_from_slice(&sig_bytes);
SignatureFooter {
sig_algo: SIG_ALGO_ED25519,
sig_length: 64,
signature,
footer_length: SignatureFooter::compute_footer_length(64),
}
}
/// Verify a segment signature using Ed25519.
///
/// Returns `true` if the signature is valid, `false` otherwise.
pub fn verify_segment(
header: &SegmentHeader,
payload: &[u8],
footer: &SignatureFooter,
pubkey: &VerifyingKey,
) -> bool {
if footer.sig_algo != SIG_ALGO_ED25519 {
return false;
}
if footer.sig_length != 64 {
return false;
}
let msg = build_signed_data(header, payload);
let sig_bytes: [u8; 64] = match footer.signature[..64].try_into() {
Ok(b) => b,
Err(_) => return false,
};
let sig = Signature::from_bytes(&sig_bytes);
pubkey.verify(&msg, &sig).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
fn make_test_header() -> SegmentHeader {
let mut h = SegmentHeader::new(0x01, 42);
h.timestamp_ns = 1_000_000_000;
h.payload_length = 100;
h
}
#[test]
fn sign_verify_round_trip() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"test payload data for signing";
let footer = sign_segment(&header, payload, &key);
let pubkey = key.verifying_key();
assert!(verify_segment(&header, payload, &footer, &pubkey));
}
#[test]
fn tampered_payload_fails() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"original payload";
let footer = sign_segment(&header, payload, &key);
let pubkey = key.verifying_key();
let tampered = b"tampered payload";
assert!(!verify_segment(&header, tampered, &footer, &pubkey));
}
#[test]
fn tampered_header_fails() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"payload";
let footer = sign_segment(&header, payload, &key);
let pubkey = key.verifying_key();
let mut bad_header = header;
bad_header.segment_id = 999;
assert!(!verify_segment(&bad_header, payload, &footer, &pubkey));
}
#[test]
fn wrong_key_fails() {
let key1 = SigningKey::generate(&mut OsRng);
let key2 = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"payload";
let footer = sign_segment(&header, payload, &key1);
let wrong_pubkey = key2.verifying_key();
assert!(!verify_segment(&header, payload, &footer, &wrong_pubkey));
}
#[test]
fn sig_algo_is_ed25519() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let footer = sign_segment(&header, b"x", &key);
assert_eq!(footer.sig_algo, 0);
assert_eq!(footer.sig_length, 64);
}
#[test]
fn footer_length_correct() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let footer = sign_segment(&header, b"data", &key);
assert_eq!(
footer.footer_length,
SignatureFooter::compute_footer_length(64)
);
}
}

View File

@@ -0,0 +1,189 @@
//! WITNESS_SEG support for cryptographic audit trails.
//!
//! Each witness entry chains to the previous via hashes, forming a
//! tamper-evident log. The chain uses SHAKE-256 for hash binding.
use alloc::vec::Vec;
use rvf_types::{ErrorCode, RvfError};
use crate::hash::shake256_256;
/// A single entry in a witness chain.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WitnessEntry {
/// Hash of the previous entry (zero for the first entry).
pub prev_hash: [u8; 32],
/// Hash of the action being witnessed.
pub action_hash: [u8; 32],
/// Nanosecond UNIX timestamp.
pub timestamp_ns: u64,
/// Witness type: 0x01=PROVENANCE, 0x02=COMPUTATION, etc.
pub witness_type: u8,
}
/// Size of one serialized witness entry: 32 + 32 + 8 + 1 = 73 bytes.
const ENTRY_SIZE: usize = 73;
/// Serialize a `WitnessEntry` into bytes.
fn encode_entry(entry: &WitnessEntry) -> [u8; ENTRY_SIZE] {
let mut buf = [0u8; ENTRY_SIZE];
buf[0..32].copy_from_slice(&entry.prev_hash);
buf[32..64].copy_from_slice(&entry.action_hash);
buf[64..72].copy_from_slice(&entry.timestamp_ns.to_le_bytes());
buf[72] = entry.witness_type;
buf
}
/// Deserialize a `WitnessEntry` from bytes.
///
/// # Errors
///
/// Returns `TruncatedSegment` if `data` is shorter than `ENTRY_SIZE` (73) bytes.
fn decode_entry(data: &[u8]) -> Result<WitnessEntry, RvfError> {
if data.len() < ENTRY_SIZE {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let mut prev_hash = [0u8; 32];
prev_hash.copy_from_slice(&data[0..32]);
let mut action_hash = [0u8; 32];
action_hash.copy_from_slice(&data[32..64]);
let timestamp_ns = u64::from_le_bytes(data[64..72].try_into().unwrap());
let witness_type = data[72];
Ok(WitnessEntry {
prev_hash,
action_hash,
timestamp_ns,
witness_type,
})
}
/// Create a witness chain from entries, linking each to the previous via hashes.
///
/// The first entry's `prev_hash` is set to all zeros (genesis).
/// Subsequent entries have `prev_hash` = SHAKE-256(previous entry bytes).
///
/// Returns the serialized chain as a byte vector.
pub fn create_witness_chain(entries: &[WitnessEntry]) -> Vec<u8> {
let mut chain = Vec::with_capacity(entries.len() * ENTRY_SIZE);
let mut prev_hash = [0u8; 32];
for entry in entries {
let mut linked = entry.clone();
linked.prev_hash = prev_hash;
let encoded = encode_entry(&linked);
prev_hash = shake256_256(&encoded);
chain.extend_from_slice(&encoded);
}
chain
}
/// Verify a witness chain's integrity.
///
/// Checks that each entry's `prev_hash` matches the SHAKE-256 hash of the
/// preceding entry. Returns the decoded entries if valid.
pub fn verify_witness_chain(data: &[u8]) -> Result<Vec<WitnessEntry>, RvfError> {
if data.is_empty() {
return Ok(Vec::new());
}
if !data.len().is_multiple_of(ENTRY_SIZE) {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let count = data.len() / ENTRY_SIZE;
let mut entries = Vec::with_capacity(count);
let mut expected_prev = [0u8; 32];
for i in 0..count {
let offset = i * ENTRY_SIZE;
let entry_bytes = &data[offset..offset + ENTRY_SIZE];
let entry = decode_entry(entry_bytes)?;
if entry.prev_hash != expected_prev {
return Err(RvfError::Code(ErrorCode::InvalidChecksum));
}
expected_prev = shake256_256(entry_bytes);
entries.push(entry);
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entries(n: usize) -> Vec<WitnessEntry> {
(0..n)
.map(|i| WitnessEntry {
prev_hash: [0u8; 32], // will be overwritten by create_witness_chain
action_hash: shake256_256(&[i as u8]),
timestamp_ns: 1_000_000_000 + i as u64,
witness_type: 0x01,
})
.collect()
}
#[test]
fn empty_chain() {
let chain = create_witness_chain(&[]);
assert!(chain.is_empty());
let result = verify_witness_chain(&chain).unwrap();
assert!(result.is_empty());
}
#[test]
fn single_entry_chain() {
let entries = make_entries(1);
let chain = create_witness_chain(&entries);
assert_eq!(chain.len(), ENTRY_SIZE);
let verified = verify_witness_chain(&chain).unwrap();
assert_eq!(verified.len(), 1);
assert_eq!(verified[0].prev_hash, [0u8; 32]);
}
#[test]
fn multi_entry_chain() {
let entries = make_entries(5);
let chain = create_witness_chain(&entries);
assert_eq!(chain.len(), 5 * ENTRY_SIZE);
let verified = verify_witness_chain(&chain).unwrap();
assert_eq!(verified.len(), 5);
for (i, entry) in verified.iter().enumerate() {
assert_eq!(entry.action_hash, entries[i].action_hash);
assert_eq!(entry.timestamp_ns, entries[i].timestamp_ns);
}
}
#[test]
fn tampered_chain_detected() {
let entries = make_entries(3);
let mut chain = create_witness_chain(&entries);
// Tamper with the second entry's action_hash byte
chain[ENTRY_SIZE + 32] ^= 0xFF;
let result = verify_witness_chain(&chain);
assert!(result.is_err());
}
#[test]
fn truncated_chain_detected() {
let entries = make_entries(2);
let chain = create_witness_chain(&entries);
let result = verify_witness_chain(&chain[..ENTRY_SIZE + 10]);
assert!(result.is_err());
}
#[test]
fn chain_links_are_correct() {
let entries = make_entries(3);
let chain = create_witness_chain(&entries);
let verified = verify_witness_chain(&chain).unwrap();
// First entry has zero prev_hash
assert_eq!(verified[0].prev_hash, [0u8; 32]);
// Second entry's prev_hash should equal hash of first entry's bytes
let first_bytes = &chain[0..ENTRY_SIZE];
let expected = shake256_256(first_bytes);
assert_eq!(verified[1].prev_hash, expected);
}
}