Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
839
vendor/ruvector/crates/rvf/rvf-crypto/src/attestation.rs
vendored
Normal file
839
vendor/ruvector/crates/rvf/rvf-crypto/src/attestation.rs
vendored
Normal 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, "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());
|
||||
}
|
||||
}
|
||||
113
vendor/ruvector/crates/rvf/rvf-crypto/src/footer.rs
vendored
Normal file
113
vendor/ruvector/crates/rvf/rvf-crypto/src/footer.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
97
vendor/ruvector/crates/rvf/rvf-crypto/src/hash.rs
vendored
Normal file
97
vendor/ruvector/crates/rvf/rvf-crypto/src/hash.rs
vendored
Normal 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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
32
vendor/ruvector/crates/rvf/rvf-crypto/src/lib.rs
vendored
Normal file
32
vendor/ruvector/crates/rvf/rvf-crypto/src/lib.rs
vendored
Normal 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};
|
||||
272
vendor/ruvector/crates/rvf/rvf-crypto/src/lineage.rs
vendored
Normal file
272
vendor/ruvector/crates/rvf/rvf-crypto/src/lineage.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
188
vendor/ruvector/crates/rvf/rvf-crypto/src/sign.rs
vendored
Normal file
188
vendor/ruvector/crates/rvf/rvf-crypto/src/sign.rs
vendored
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
189
vendor/ruvector/crates/rvf/rvf-crypto/src/witness.rs
vendored
Normal file
189
vendor/ruvector/crates/rvf/rvf-crypto/src/witness.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user