Skip to main content

ruvix_boot/
witness_log.rs

1//! Witness log implementation for boot attestation.
2//!
3//! The witness log is an append-only region that records:
4//! - Boot attestations (first entry)
5//! - Proof attestations during runtime
6//! - Capability mutations
7//! - Other security-relevant events
8//!
9//! # Integrity
10//!
11//! Entries are hash-chained for integrity verification.
12//! Each entry contains:
13//! - Previous entry hash (32 bytes)
14//! - Entry type (1 byte)
15//! - Timestamp (8 bytes)
16//! - Payload (variable)
17
18use crate::attestation::BootAttestation;
19use crate::manifest::WitnessLogPolicy;
20use ruvix_types::{KernelError, ProofAttestation};
21use sha2::{Sha256, Digest};
22
23#[cfg(feature = "alloc")]
24use alloc::vec::Vec;
25
26/// Witness log entry type.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[repr(u8)]
29pub enum WitnessLogEntryType {
30    /// Boot attestation (first entry).
31    BootAttestation = 0,
32
33    /// Proof attestation during runtime.
34    ProofAttestation = 1,
35
36    /// Capability grant.
37    CapabilityGrant = 2,
38
39    /// Capability revoke.
40    CapabilityRevoke = 3,
41
42    /// Component mount.
43    ComponentMount = 4,
44
45    /// Component unmount.
46    ComponentUnmount = 5,
47
48    /// Region creation.
49    RegionCreate = 6,
50
51    /// Region destruction.
52    RegionDestroy = 7,
53
54    /// Checkpoint marker.
55    Checkpoint = 8,
56
57    /// Custom application-defined entry.
58    Custom = 255,
59}
60
61/// Witness log entry header.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[repr(C)]
64pub struct WitnessLogEntryHeader {
65    /// Hash of the previous entry (chain link).
66    pub prev_hash: [u8; 32],
67
68    /// Entry type.
69    pub entry_type: WitnessLogEntryType,
70
71    /// Entry sequence number.
72    pub sequence: u64,
73
74    /// Timestamp in nanoseconds since epoch.
75    pub timestamp_ns: u64,
76
77    /// Payload size in bytes.
78    pub payload_size: u32,
79}
80
81impl WitnessLogEntryHeader {
82    /// Size of the header in bytes.
83    pub const SIZE: usize = 32 + 1 + 8 + 8 + 4; // 53 bytes
84
85    /// Creates a new entry header.
86    #[must_use]
87    pub fn new(
88        prev_hash: [u8; 32],
89        entry_type: WitnessLogEntryType,
90        sequence: u64,
91        timestamp_ns: u64,
92        payload_size: u32,
93    ) -> Self {
94        Self {
95            prev_hash,
96            entry_type,
97            sequence,
98            timestamp_ns,
99            payload_size,
100        }
101    }
102
103    /// Computes the hash of this header.
104    #[must_use]
105    pub fn hash(&self) -> [u8; 32] {
106        let mut hasher = Sha256::new();
107        hasher.update(&self.prev_hash);
108        hasher.update(&[self.entry_type as u8]);
109        hasher.update(&self.sequence.to_le_bytes());
110        hasher.update(&self.timestamp_ns.to_le_bytes());
111        hasher.update(&self.payload_size.to_le_bytes());
112
113        let result = hasher.finalize();
114        let mut hash = [0u8; 32];
115        hash.copy_from_slice(&result);
116        hash
117    }
118}
119
120/// Complete witness log entry (header + payload).
121#[derive(Debug, Clone)]
122pub struct WitnessLogEntry {
123    /// Entry header.
124    pub header: WitnessLogEntryHeader,
125
126    /// Entry payload.
127    #[cfg(feature = "alloc")]
128    pub payload: Vec<u8>,
129    /// Entry payload (fixed-size no_std variant).
130    #[cfg(not(feature = "alloc"))]
131    pub payload: [u8; 256],
132    /// Payload length in the fixed-size array.
133    #[cfg(not(feature = "alloc"))]
134    pub payload_len: usize,
135}
136
137impl WitnessLogEntry {
138    /// Creates a new witness log entry.
139    #[cfg(feature = "alloc")]
140    pub fn new(header: WitnessLogEntryHeader, payload: Vec<u8>) -> Self {
141        Self { header, payload }
142    }
143
144    /// Creates a new witness log entry (no_std).
145    #[cfg(not(feature = "alloc"))]
146    pub fn new(header: WitnessLogEntryHeader, payload_data: &[u8]) -> Self {
147        let mut payload = [0u8; 256];
148        let len = payload_data.len().min(256);
149        payload[..len].copy_from_slice(&payload_data[..len]);
150
151        Self {
152            header,
153            payload,
154            payload_len: len,
155        }
156    }
157
158    /// Computes the hash of the entire entry (header + payload).
159    #[must_use]
160    pub fn hash(&self) -> [u8; 32] {
161        let mut hasher = Sha256::new();
162        hasher.update(&self.header.prev_hash);
163        hasher.update(&[self.header.entry_type as u8]);
164        hasher.update(&self.header.sequence.to_le_bytes());
165        hasher.update(&self.header.timestamp_ns.to_le_bytes());
166        hasher.update(&self.header.payload_size.to_le_bytes());
167
168        #[cfg(feature = "alloc")]
169        hasher.update(&self.payload);
170        #[cfg(not(feature = "alloc"))]
171        hasher.update(&self.payload[..self.payload_len]);
172
173        let result = hasher.finalize();
174        let mut hash = [0u8; 32];
175        hash.copy_from_slice(&result);
176        hash
177    }
178
179    /// Returns the payload as a slice.
180    #[must_use]
181    pub fn payload(&self) -> &[u8] {
182        #[cfg(feature = "alloc")]
183        {
184            &self.payload
185        }
186        #[cfg(not(feature = "alloc"))]
187        {
188            &self.payload[..self.payload_len]
189        }
190    }
191}
192
193/// Witness log configuration.
194#[derive(Debug, Clone, Copy)]
195pub struct WitnessLogConfig {
196    /// Maximum entries before rotation.
197    pub max_entries: u64,
198
199    /// Maximum size in bytes.
200    pub max_size_bytes: u64,
201
202    /// Whether to hash-chain entries.
203    pub hash_chain: bool,
204
205    /// Retention period in seconds (0 = forever).
206    pub retention_seconds: u64,
207}
208
209impl WitnessLogConfig {
210    /// Default witness log configuration.
211    pub const DEFAULT: Self = Self {
212        max_entries: 1_000_000,
213        max_size_bytes: 100 * 1024 * 1024, // 100 MiB
214        hash_chain: true,
215        retention_seconds: 0,
216    };
217
218    /// Creates a configuration from a witness log policy.
219    #[must_use]
220    pub fn from_policy(policy: &WitnessLogPolicy) -> Self {
221        Self {
222            max_entries: policy.max_entries,
223            max_size_bytes: policy.max_size_bytes,
224            hash_chain: policy.hash_chain,
225            retention_seconds: policy.retention_seconds,
226        }
227    }
228}
229
230impl Default for WitnessLogConfig {
231    fn default() -> Self {
232        Self::DEFAULT
233    }
234}
235
236/// Witness log for boot and runtime attestation.
237///
238/// The witness log is append-only and hash-chained for integrity.
239#[derive(Debug)]
240pub struct WitnessLog {
241    /// Configuration.
242    config: WitnessLogConfig,
243
244    /// Current entry count.
245    entry_count: u64,
246
247    /// Current size in bytes.
248    size_bytes: u64,
249
250    /// Hash of the last entry (for chaining).
251    last_hash: [u8; 32],
252
253    /// Entries (in-memory for Phase A).
254    #[cfg(feature = "alloc")]
255    entries: Vec<WitnessLogEntry>,
256}
257
258impl WitnessLog {
259    /// Creates a new witness log.
260    #[must_use]
261    pub fn new(config: WitnessLogConfig) -> Self {
262        Self {
263            config,
264            entry_count: 0,
265            size_bytes: 0,
266            last_hash: [0u8; 32], // Genesis hash is all zeros
267            #[cfg(feature = "alloc")]
268            entries: Vec::new(),
269        }
270    }
271
272    /// Returns the current entry count.
273    #[inline]
274    #[must_use]
275    pub fn entry_count(&self) -> u64 {
276        self.entry_count
277    }
278
279    /// Returns the current size in bytes.
280    #[inline]
281    #[must_use]
282    pub fn size_bytes(&self) -> u64 {
283        self.size_bytes
284    }
285
286    /// Returns the hash of the last entry.
287    #[inline]
288    #[must_use]
289    pub fn last_hash(&self) -> [u8; 32] {
290        self.last_hash
291    }
292
293    /// Appends a boot attestation entry.
294    ///
295    /// This should be the first entry in the witness log.
296    pub fn append_boot_attestation(&mut self, attestation: &BootAttestation) -> Result<(), KernelError> {
297        if self.entry_count != 0 {
298            // Boot attestation must be first
299            return Err(KernelError::NotPermitted);
300        }
301
302        let payload = attestation.to_bytes();
303        self.append(WitnessLogEntryType::BootAttestation, &payload)
304    }
305
306    /// Appends a proof attestation entry.
307    pub fn append_proof_attestation(&mut self, attestation: &ProofAttestation) -> Result<(), KernelError> {
308        let payload = Self::serialize_proof_attestation(attestation);
309        self.append(WitnessLogEntryType::ProofAttestation, &payload)
310    }
311
312    /// Appends a generic entry.
313    pub fn append(&mut self, entry_type: WitnessLogEntryType, payload: &[u8]) -> Result<(), KernelError> {
314        // Check limits
315        if self.entry_count >= self.config.max_entries {
316            return Err(KernelError::RegionFull);
317        }
318
319        let entry_size = WitnessLogEntryHeader::SIZE + payload.len();
320        if self.size_bytes + entry_size as u64 > self.config.max_size_bytes {
321            return Err(KernelError::RegionFull);
322        }
323
324        // Create header
325        let header = WitnessLogEntryHeader::new(
326            self.last_hash,
327            entry_type,
328            self.entry_count,
329            Self::get_timestamp(),
330            payload.len() as u32,
331        );
332
333        // Create entry
334        #[cfg(feature = "alloc")]
335        let entry = WitnessLogEntry::new(header, payload.to_vec());
336        #[cfg(not(feature = "alloc"))]
337        let entry = WitnessLogEntry::new(header, payload);
338
339        // Update hash chain
340        if self.config.hash_chain {
341            self.last_hash = entry.hash();
342        }
343
344        // Store entry
345        #[cfg(feature = "alloc")]
346        self.entries.push(entry);
347
348        self.entry_count += 1;
349        self.size_bytes += entry_size as u64;
350
351        Ok(())
352    }
353
354    /// Gets an entry by sequence number.
355    #[cfg(feature = "alloc")]
356    #[must_use]
357    pub fn get_entry(&self, sequence: u64) -> Option<&WitnessLogEntry> {
358        self.entries.get(sequence as usize)
359    }
360
361    /// Verifies the hash chain integrity.
362    #[cfg(feature = "alloc")]
363    #[must_use]
364    pub fn verify_chain(&self) -> bool {
365        if !self.config.hash_chain {
366            return true;
367        }
368
369        let mut expected_prev = [0u8; 32]; // Genesis
370
371        for entry in &self.entries {
372            if entry.header.prev_hash != expected_prev {
373                return false;
374            }
375            expected_prev = entry.hash();
376        }
377
378        expected_prev == self.last_hash
379    }
380
381    fn serialize_proof_attestation(attestation: &ProofAttestation) -> [u8; 82] {
382        let mut bytes = [0u8; 82];
383
384        bytes[0..32].copy_from_slice(&attestation.proof_term_hash);
385        bytes[32..64].copy_from_slice(&attestation.environment_hash);
386        bytes[64..72].copy_from_slice(&attestation.verification_timestamp_ns.to_le_bytes());
387        bytes[72..76].copy_from_slice(&attestation.verifier_version.to_le_bytes());
388        bytes[76..80].copy_from_slice(&attestation.reduction_steps.to_le_bytes());
389        bytes[80..82].copy_from_slice(&attestation.cache_hit_rate_bps.to_le_bytes());
390
391        bytes
392    }
393
394    fn get_timestamp() -> u64 {
395        #[cfg(feature = "std")]
396        {
397            use std::time::{SystemTime, UNIX_EPOCH};
398            SystemTime::now()
399                .duration_since(UNIX_EPOCH)
400                .map(|d| d.as_nanos() as u64)
401                .unwrap_or(0)
402        }
403        #[cfg(not(feature = "std"))]
404        {
405            0
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use crate::attestation::BootAttestation;
414
415    #[test]
416    fn test_witness_log_creation() {
417        let config = WitnessLogConfig::default();
418        let log = WitnessLog::new(config);
419
420        assert_eq!(log.entry_count(), 0);
421        assert_eq!(log.size_bytes(), 0);
422        assert_eq!(log.last_hash(), [0u8; 32]);
423    }
424
425    #[test]
426    fn test_boot_attestation_append() {
427        let config = WitnessLogConfig::default();
428        let mut log = WitnessLog::new(config);
429
430        let attestation = BootAttestation::new(
431            [1u8; 32],
432            [2u8; 32],
433            [3u8; 32],
434            1234567890,
435        );
436
437        log.append_boot_attestation(&attestation).unwrap();
438
439        assert_eq!(log.entry_count(), 1);
440        assert!(log.size_bytes() > 0);
441        assert_ne!(log.last_hash(), [0u8; 32]); // Hash should have changed
442    }
443
444    #[test]
445    fn test_boot_attestation_must_be_first() {
446        let config = WitnessLogConfig::default();
447        let mut log = WitnessLog::new(config);
448
449        // Add a regular entry first
450        log.append(WitnessLogEntryType::Custom, b"test").unwrap();
451
452        // Boot attestation should fail
453        let attestation = BootAttestation::new([0u8; 32], [0u8; 32], [0u8; 32], 0);
454        let result = log.append_boot_attestation(&attestation);
455
456        assert_eq!(result, Err(KernelError::NotPermitted));
457    }
458
459    #[test]
460    fn test_witness_log_limit() {
461        let config = WitnessLogConfig {
462            max_entries: 2,
463            max_size_bytes: 1024 * 1024,
464            hash_chain: true,
465            retention_seconds: 0,
466        };
467        let mut log = WitnessLog::new(config);
468
469        log.append(WitnessLogEntryType::Custom, b"entry1").unwrap();
470        log.append(WitnessLogEntryType::Custom, b"entry2").unwrap();
471
472        // Third entry should fail
473        let result = log.append(WitnessLogEntryType::Custom, b"entry3");
474        assert_eq!(result, Err(KernelError::RegionFull));
475    }
476
477    #[cfg(feature = "alloc")]
478    #[test]
479    fn test_hash_chain_verification() {
480        let config = WitnessLogConfig::default();
481        let mut log = WitnessLog::new(config);
482
483        log.append(WitnessLogEntryType::Custom, b"entry1").unwrap();
484        log.append(WitnessLogEntryType::Custom, b"entry2").unwrap();
485        log.append(WitnessLogEntryType::Custom, b"entry3").unwrap();
486
487        assert!(log.verify_chain());
488    }
489
490    #[cfg(feature = "alloc")]
491    #[test]
492    fn test_get_entry() {
493        let config = WitnessLogConfig::default();
494        let mut log = WitnessLog::new(config);
495
496        log.append(WitnessLogEntryType::Custom, b"hello").unwrap();
497
498        let entry = log.get_entry(0).unwrap();
499        assert_eq!(entry.header.entry_type, WitnessLogEntryType::Custom);
500        assert_eq!(entry.header.sequence, 0);
501        assert_eq!(entry.payload(), b"hello");
502    }
503
504    #[test]
505    fn test_entry_header_hash() {
506        let header = WitnessLogEntryHeader::new(
507            [1u8; 32],
508            WitnessLogEntryType::Custom,
509            42,
510            1234567890,
511            100,
512        );
513
514        let hash1 = header.hash();
515        let hash2 = header.hash();
516
517        // Hash should be deterministic
518        assert_eq!(hash1, hash2);
519        assert_ne!(hash1, [0u8; 32]); // Non-zero
520    }
521}