Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
[package]
name = "rvf-crypto"
version = "0.2.0"
edition = "2021"
description = "RuVector Format cryptographic primitives -- SHA-3 hashing and Ed25519 signing"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/ruvector"
homepage = "https://github.com/ruvnet/ruvector"
readme = "README.md"
categories = ["cryptography", "authentication"]
keywords = ["vector", "crypto", "sha3", "ed25519", "rvf"]
rust-version = "1.87"
[features]
default = ["std", "ed25519"]
std = ["sha3/std"]
ed25519 = ["dep:ed25519-dalek"]
[dependencies]
rvf-types = { version = "0.2.0", path = "../rvf-types" }
sha3 = { version = "0.10", default-features = false }
ed25519-dalek = { version = "2", features = ["rand_core"], optional = true }
[dev-dependencies]
rand = "0.8"

View File

@@ -0,0 +1,90 @@
# rvf-crypto
[![Crates.io](https://img.shields.io/crates/v/rvf-crypto.svg)](https://crates.io/crates/rvf-crypto)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue.svg)](https://opensource.org/licenses/MIT)
**Tamper-proof hashing and signing for every RVF segment -- SHA-3 digests, Ed25519 signatures, and lineage witness chains.**
```toml
rvf-crypto = "0.1"
```
Every operation on an RVF file gets recorded in a cryptographic witness chain. `rvf-crypto` provides the primitives that make this possible: SHA-3 (SHAKE-256) content hashing for segment identity, Ed25519 digital signatures for provenance, and lineage verification functions that ensure no record in the chain has been altered. If you are building tools that read, write, or transform `.rvf` files, this crate handles all the cryptography so you do not have to.
| | rvf-crypto | Manual hashing + signing | No integrity checks |
|---|---|---|---|
| **Segment identity** | SHAKE-256-256 content-addressable IDs | Roll your own digest scheme | Rely on filenames |
| **Provenance** | Ed25519 signatures on every segment | Integrate a signing library yourself | Trust the source blindly |
| **Lineage verification** | One function call validates an entire chain | Write chain-walking logic from scratch | No verification possible |
| **no_std / WASM** | Hashing works without std; signing is feature-gated | Varies by library | N/A |
## Quick Start
```rust
use rvf_crypto::lineage::{lineage_record_to_bytes, lineage_record_from_bytes, verify_lineage_chain};
use rvf_types::{LineageRecord, DerivationType, FileIdentity};
// Serialize a lineage record to a fixed 128-byte array
let record = LineageRecord::new(
[1u8; 16], [2u8; 16], [3u8; 32],
DerivationType::Filter, 5, 1_700_000_000_000_000_000,
"filtered by category",
);
let bytes = lineage_record_to_bytes(&record);
let decoded = lineage_record_from_bytes(&bytes).unwrap();
assert_eq!(decoded.description_str(), "filtered by category");
// Verify a parent-child lineage chain
let root = FileIdentity::new_root([1u8; 16]);
let root_hash = [0xAAu8; 32];
let child = FileIdentity {
file_id: [2u8; 16],
parent_id: [1u8; 16],
parent_hash: root_hash,
lineage_depth: 1,
};
verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]).unwrap();
```
## Key Features
| Feature | What It Does | Why It Matters |
|---|---|---|
| **SHA-3 (SHAKE-256)** | Content-addressable hashing for segment identifiers | Every segment gets a unique, collision-resistant ID |
| **Ed25519 signing** | Segment-level digital signatures via `ed25519-dalek` | Proves who created or modified a segment |
| **Lineage witness chains** | Cryptographic chain linking parent and child segments | Detects tampering anywhere in the derivation history |
| **Record serialization** | Fixed 128-byte binary codec for `LineageRecord` | Compact, deterministic encoding for witness entries |
| **Manifest hashing** | SHAKE-256-256 over 4096-byte manifests | Anchors `FileIdentity` parent references to real data |
| **Chain verification** | `verify_lineage_chain()` validates root-to-leaf integrity | One call proves the entire history is intact |
## Feature Flags
| Flag | Default | What It Enables |
|---|---|---|
| `std` | Yes | Standard library support |
| `ed25519` | Yes | Ed25519 signing via `ed25519-dalek` |
For `no_std` or WASM targets that only need hashing and witness chains (no signing), disable defaults:
```toml
[dependencies]
rvf-crypto = { version = "0.1", default-features = false }
```
## API Reference
| Function | Description |
|---|---|
| `lineage_record_to_bytes(record)` | Serialize a `LineageRecord` to a fixed 128-byte array |
| `lineage_record_from_bytes(bytes)` | Deserialize a `LineageRecord` from 128 bytes |
| `lineage_witness_entry(record, prev_hash)` | Create a `WitnessEntry` (type `0x09`) for a derivation event |
| `compute_manifest_hash(manifest)` | SHAKE-256-256 digest over a 4096-byte manifest |
| `verify_lineage_chain(chain)` | Validate parent-child integrity from root to leaf |
## License
MIT OR Apache-2.0
---
Part of [RuVector](https://github.com/ruvnet/ruvector) -- the self-learning vector database.

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);
}
}