1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[repr(u8)]
29pub enum WitnessLogEntryType {
30 BootAttestation = 0,
32
33 ProofAttestation = 1,
35
36 CapabilityGrant = 2,
38
39 CapabilityRevoke = 3,
41
42 ComponentMount = 4,
44
45 ComponentUnmount = 5,
47
48 RegionCreate = 6,
50
51 RegionDestroy = 7,
53
54 Checkpoint = 8,
56
57 Custom = 255,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[repr(C)]
64pub struct WitnessLogEntryHeader {
65 pub prev_hash: [u8; 32],
67
68 pub entry_type: WitnessLogEntryType,
70
71 pub sequence: u64,
73
74 pub timestamp_ns: u64,
76
77 pub payload_size: u32,
79}
80
81impl WitnessLogEntryHeader {
82 pub const SIZE: usize = 32 + 1 + 8 + 8 + 4; #[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 #[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#[derive(Debug, Clone)]
122pub struct WitnessLogEntry {
123 pub header: WitnessLogEntryHeader,
125
126 #[cfg(feature = "alloc")]
128 pub payload: Vec<u8>,
129 #[cfg(not(feature = "alloc"))]
131 pub payload: [u8; 256],
132 #[cfg(not(feature = "alloc"))]
134 pub payload_len: usize,
135}
136
137impl WitnessLogEntry {
138 #[cfg(feature = "alloc")]
140 pub fn new(header: WitnessLogEntryHeader, payload: Vec<u8>) -> Self {
141 Self { header, payload }
142 }
143
144 #[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 #[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 #[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#[derive(Debug, Clone, Copy)]
195pub struct WitnessLogConfig {
196 pub max_entries: u64,
198
199 pub max_size_bytes: u64,
201
202 pub hash_chain: bool,
204
205 pub retention_seconds: u64,
207}
208
209impl WitnessLogConfig {
210 pub const DEFAULT: Self = Self {
212 max_entries: 1_000_000,
213 max_size_bytes: 100 * 1024 * 1024, hash_chain: true,
215 retention_seconds: 0,
216 };
217
218 #[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#[derive(Debug)]
240pub struct WitnessLog {
241 config: WitnessLogConfig,
243
244 entry_count: u64,
246
247 size_bytes: u64,
249
250 last_hash: [u8; 32],
252
253 #[cfg(feature = "alloc")]
255 entries: Vec<WitnessLogEntry>,
256}
257
258impl WitnessLog {
259 #[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], #[cfg(feature = "alloc")]
268 entries: Vec::new(),
269 }
270 }
271
272 #[inline]
274 #[must_use]
275 pub fn entry_count(&self) -> u64 {
276 self.entry_count
277 }
278
279 #[inline]
281 #[must_use]
282 pub fn size_bytes(&self) -> u64 {
283 self.size_bytes
284 }
285
286 #[inline]
288 #[must_use]
289 pub fn last_hash(&self) -> [u8; 32] {
290 self.last_hash
291 }
292
293 pub fn append_boot_attestation(&mut self, attestation: &BootAttestation) -> Result<(), KernelError> {
297 if self.entry_count != 0 {
298 return Err(KernelError::NotPermitted);
300 }
301
302 let payload = attestation.to_bytes();
303 self.append(WitnessLogEntryType::BootAttestation, &payload)
304 }
305
306 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 pub fn append(&mut self, entry_type: WitnessLogEntryType, payload: &[u8]) -> Result<(), KernelError> {
314 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 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 #[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 if self.config.hash_chain {
341 self.last_hash = entry.hash();
342 }
343
344 #[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 #[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 #[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]; 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]); }
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 log.append(WitnessLogEntryType::Custom, b"test").unwrap();
451
452 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 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 assert_eq!(hash1, hash2);
519 assert_ne!(hash1, [0u8; 32]); }
521}