1use hmac::{Hmac, Mac};
13use nklave_core::state::integrity::DecisionRecord;
14use sha2::Sha256;
15use std::fs::{File, OpenOptions};
16use std::io::{BufRead, BufReader, BufWriter, Write};
17use std::path::{Path, PathBuf};
18use thiserror::Error;
19use tracing::debug;
20
21type HmacSha256 = Hmac<Sha256>;
22
23#[derive(Debug, Clone)]
25pub struct SecureLogConfig {
26 pub path: PathBuf,
28
29 pub encryption_key: Option<[u8; 32]>,
31
32 pub hmac_key: [u8; 32],
34}
35
36impl SecureLogConfig {
37 pub fn with_encryption(path: impl AsRef<Path>, encryption_key: [u8; 32], hmac_key: [u8; 32]) -> Self {
39 Self {
40 path: path.as_ref().to_path_buf(),
41 encryption_key: Some(encryption_key),
42 hmac_key,
43 }
44 }
45
46 pub fn integrity_only(path: impl AsRef<Path>, hmac_key: [u8; 32]) -> Self {
48 Self {
49 path: path.as_ref().to_path_buf(),
50 encryption_key: None,
51 hmac_key,
52 }
53 }
54}
55
56pub struct SecureDecisionLog {
58 config: SecureLogConfig,
59 writer: BufWriter<File>,
60 last_sequence: u64,
61 last_hmac: [u8; 32],
62}
63
64impl SecureDecisionLog {
65 pub fn open(config: SecureLogConfig) -> Result<Self, SecureLogError> {
67 let (last_sequence, last_hmac) = if config.path.exists() {
69 Self::read_last_entry(&config)?
70 } else {
71 let mut mac = HmacSha256::new_from_slice(&config.hmac_key)
73 .map_err(|e| SecureLogError::Crypto(e.to_string()))?;
74 mac.update(b"nklave-secure-log-genesis");
75 let genesis_hmac: [u8; 32] = mac.finalize().into_bytes().into();
76 (0, genesis_hmac)
77 };
78
79 let file = OpenOptions::new()
81 .create(true)
82 .append(true)
83 .open(&config.path)
84 .map_err(|e| SecureLogError::Io(e.to_string()))?;
85
86 let writer = BufWriter::new(file);
87
88 Ok(Self {
89 config,
90 writer,
91 last_sequence,
92 last_hmac,
93 })
94 }
95
96 fn read_last_entry(config: &SecureLogConfig) -> Result<(u64, [u8; 32]), SecureLogError> {
98 let file = File::open(&config.path).map_err(|e| SecureLogError::Io(e.to_string()))?;
99 let reader = BufReader::new(file);
100
101 let mut last_sequence = 0u64;
102 let mut last_hmac: [u8; 32] = {
103 let mut mac = HmacSha256::new_from_slice(&config.hmac_key)
104 .map_err(|e| SecureLogError::Crypto(e.to_string()))?;
105 mac.update(b"nklave-secure-log-genesis");
106 mac.finalize().into_bytes().into()
107 };
108
109 for line in reader.lines() {
110 let line = line.map_err(|e| SecureLogError::Io(e.to_string()))?;
111 if line.is_empty() {
112 continue;
113 }
114
115 let parts: Vec<&str> = line.splitn(3, '|').collect();
116 if parts.len() != 3 {
117 return Err(SecureLogError::Parse("Invalid line format".to_string()));
118 }
119
120 let seq: u64 = parts[0]
121 .parse()
122 .map_err(|_| SecureLogError::Parse("Invalid sequence number".to_string()))?;
123
124 let hmac_hex = parts[1];
125 let hmac_bytes = hex::decode(hmac_hex)
126 .map_err(|e| SecureLogError::Parse(format!("Invalid HMAC hex: {}", e)))?;
127
128 if hmac_bytes.len() != 32 {
129 return Err(SecureLogError::Parse("Invalid HMAC length".to_string()));
130 }
131
132 last_sequence = seq;
133 last_hmac.copy_from_slice(&hmac_bytes);
134 }
135
136 Ok((last_sequence, last_hmac))
137 }
138
139 pub fn append(&mut self, record: &DecisionRecord) -> Result<(), SecureLogError> {
141 if record.sequence != self.last_sequence + 1 {
143 return Err(SecureLogError::SequenceGap {
144 expected: self.last_sequence + 1,
145 actual: record.sequence,
146 });
147 }
148
149 let json =
151 serde_json::to_string(record).map_err(|e| SecureLogError::Serialize(e.to_string()))?;
152
153 let data = if let Some(ref key) = self.config.encryption_key {
155 Self::encrypt_data(key, record.sequence, json.as_bytes())?
156 } else {
157 json
158 };
159
160 let mut mac = HmacSha256::new_from_slice(&self.config.hmac_key)
162 .map_err(|e| SecureLogError::Crypto(e.to_string()))?;
163 mac.update(&self.last_hmac);
164 mac.update(&record.sequence.to_be_bytes());
165 mac.update(data.as_bytes());
166 let new_hmac: [u8; 32] = mac.finalize().into_bytes().into();
167
168 let line = format!(
170 "{}|{}|{}",
171 record.sequence,
172 hex::encode(new_hmac),
173 data
174 );
175 writeln!(self.writer, "{}", line).map_err(|e| SecureLogError::Io(e.to_string()))?;
176
177 self.writer
179 .flush()
180 .map_err(|e| SecureLogError::Io(e.to_string()))?;
181
182 self.last_sequence = record.sequence;
183 self.last_hmac = new_hmac;
184
185 Ok(())
186 }
187
188 fn encrypt_data(key: &[u8; 32], nonce_base: u64, plaintext: &[u8]) -> Result<String, SecureLogError> {
190 use aes::Aes256;
191 use aes::cipher::{KeyIvInit, StreamCipher};
192 use ctr::Ctr64BE;
193
194 let mut nonce = [0u8; 16];
196 nonce[8..16].copy_from_slice(&nonce_base.to_be_bytes());
197
198 let mut cipher = Ctr64BE::<Aes256>::new(key.into(), &nonce.into());
199
200 let mut ciphertext = plaintext.to_vec();
201 cipher.apply_keystream(&mut ciphertext);
202
203 Ok(hex::encode(ciphertext))
204 }
205
206 fn decrypt_data(key: &[u8; 32], nonce_base: u64, ciphertext_hex: &str) -> Result<String, SecureLogError> {
208 use aes::Aes256;
209 use aes::cipher::{KeyIvInit, StreamCipher};
210 use ctr::Ctr64BE;
211
212 let ciphertext = hex::decode(ciphertext_hex)
213 .map_err(|e| SecureLogError::Parse(format!("Invalid ciphertext hex: {}", e)))?;
214
215 let mut nonce = [0u8; 16];
217 nonce[8..16].copy_from_slice(&nonce_base.to_be_bytes());
218
219 let mut cipher = Ctr64BE::<Aes256>::new(key.into(), &nonce.into());
220
221 let mut plaintext = ciphertext;
222 cipher.apply_keystream(&mut plaintext);
223
224 String::from_utf8(plaintext)
225 .map_err(|e| SecureLogError::Parse(format!("Invalid UTF-8 after decryption: {}", e)))
226 }
227
228 pub fn last_sequence(&self) -> u64 {
230 self.last_sequence
231 }
232
233 pub fn replay_and_verify(&self) -> Result<Vec<DecisionRecord>, SecureLogError> {
238 let file = File::open(&self.config.path).map_err(|e| SecureLogError::Io(e.to_string()))?;
239 let reader = BufReader::new(file);
240
241 let mut records = Vec::new();
242 let mut expected_hmac = {
243 let mut mac = HmacSha256::new_from_slice(&self.config.hmac_key)
244 .map_err(|e| SecureLogError::Crypto(e.to_string()))?;
245 mac.update(b"nklave-secure-log-genesis");
246 mac.finalize().into_bytes()
247 };
248
249 for (line_num, line) in reader.lines().enumerate() {
250 let line = line.map_err(|e| SecureLogError::Io(e.to_string()))?;
251 if line.is_empty() {
252 continue;
253 }
254
255 let parts: Vec<&str> = line.splitn(3, '|').collect();
256 if parts.len() != 3 {
257 return Err(SecureLogError::Parse(format!(
258 "Invalid line format at line {}",
259 line_num + 1
260 )));
261 }
262
263 let seq: u64 = parts[0]
264 .parse()
265 .map_err(|_| SecureLogError::Parse("Invalid sequence number".to_string()))?;
266
267 let hmac_hex = parts[1];
268 let stored_hmac = hex::decode(hmac_hex)
269 .map_err(|e| SecureLogError::Parse(format!("Invalid HMAC hex: {}", e)))?;
270
271 let data = parts[2];
272
273 let mut mac = HmacSha256::new_from_slice(&self.config.hmac_key)
275 .map_err(|e| SecureLogError::Crypto(e.to_string()))?;
276 mac.update(&expected_hmac);
277 mac.update(&seq.to_be_bytes());
278 mac.update(data.as_bytes());
279 let computed_hmac = mac.finalize().into_bytes();
280
281 if computed_hmac.as_slice() != stored_hmac.as_slice() {
282 return Err(SecureLogError::IntegrityViolation {
283 sequence: seq,
284 expected: hex::encode(computed_hmac),
285 actual: hex::encode(&stored_hmac),
286 });
287 }
288
289 let json = if self.config.encryption_key.is_some() {
291 Self::decrypt_data(self.config.encryption_key.as_ref().unwrap(), seq, data)?
292 } else {
293 data.to_string()
294 };
295
296 let record: DecisionRecord =
298 serde_json::from_str(&json).map_err(|e| SecureLogError::Parse(e.to_string()))?;
299
300 if record.sequence != seq {
301 return Err(SecureLogError::Parse(format!(
302 "Sequence mismatch: header says {}, record says {}",
303 seq, record.sequence
304 )));
305 }
306
307 records.push(record);
308 expected_hmac = computed_hmac;
309 }
310
311 debug!(
312 record_count = records.len(),
313 "Verified and replayed secure log"
314 );
315
316 Ok(records)
317 }
318
319 pub fn replay_from(&self, start_sequence: u64) -> Result<Vec<DecisionRecord>, SecureLogError> {
321 let records = self.replay_and_verify()?;
322 Ok(records
323 .into_iter()
324 .filter(|r| r.sequence >= start_sequence)
325 .collect())
326 }
327
328 pub fn sync(&mut self) -> Result<(), SecureLogError> {
330 self.writer
331 .flush()
332 .map_err(|e| SecureLogError::Io(e.to_string()))?;
333 self.writer
334 .get_ref()
335 .sync_all()
336 .map_err(|e| SecureLogError::Io(e.to_string()))?;
337 Ok(())
338 }
339
340 pub fn is_encrypted(&self) -> bool {
342 self.config.encryption_key.is_some()
343 }
344}
345
346#[derive(Debug, Error)]
348pub enum SecureLogError {
349 #[error("I/O error: {0}")]
350 Io(String),
351
352 #[error("Parse error: {0}")]
353 Parse(String),
354
355 #[error("Serialization error: {0}")]
356 Serialize(String),
357
358 #[error("Crypto error: {0}")]
359 Crypto(String),
360
361 #[error("Sequence gap: expected {expected}, got {actual}")]
362 SequenceGap { expected: u64, actual: u64 },
363
364 #[error("Integrity violation at sequence {sequence}: expected HMAC {expected}, got {actual}")]
365 IntegrityViolation {
366 sequence: u64,
367 expected: String,
368 actual: String,
369 },
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use nklave_core::policy::types::{PolicyDecision, SigningType};
376 use tempfile::TempDir;
377
378 fn make_record(seq: u64) -> DecisionRecord {
379 DecisionRecord {
380 sequence: seq,
381 timestamp: 1234567890,
382 validator_pubkey: [0u8; 48],
383 request_type: SigningType::BlockProposal,
384 decision: PolicyDecision::Allow,
385 signing_root: [seq as u8; 32],
386 prev_state_hash: [0u8; 32],
387 signing_context: None,
388 }
389 }
390
391 #[test]
392 fn test_secure_log_integrity_only() {
393 let dir = TempDir::new().unwrap();
394 let log_path = dir.path().join("secure.log");
395 let hmac_key = [1u8; 32];
396
397 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
398 let mut log = SecureDecisionLog::open(config).unwrap();
399
400 assert_eq!(log.last_sequence(), 0);
401 assert!(!log.is_encrypted());
402
403 log.append(&make_record(1)).unwrap();
404 log.append(&make_record(2)).unwrap();
405 log.append(&make_record(3)).unwrap();
406
407 assert_eq!(log.last_sequence(), 3);
408 }
409
410 #[test]
411 fn test_secure_log_with_encryption() {
412 let dir = TempDir::new().unwrap();
413 let log_path = dir.path().join("encrypted.log");
414 let encryption_key = [2u8; 32];
415 let hmac_key = [3u8; 32];
416
417 let config = SecureLogConfig::with_encryption(&log_path, encryption_key, hmac_key);
418 let mut log = SecureDecisionLog::open(config).unwrap();
419
420 assert!(log.is_encrypted());
421
422 log.append(&make_record(1)).unwrap();
423 log.append(&make_record(2)).unwrap();
424
425 assert_eq!(log.last_sequence(), 2);
426 }
427
428 #[test]
429 fn test_secure_log_replay_and_verify() {
430 let dir = TempDir::new().unwrap();
431 let log_path = dir.path().join("verify.log");
432 let hmac_key = [4u8; 32];
433
434 {
435 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
436 let mut log = SecureDecisionLog::open(config).unwrap();
437 log.append(&make_record(1)).unwrap();
438 log.append(&make_record(2)).unwrap();
439 log.append(&make_record(3)).unwrap();
440 }
441
442 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
444 let log = SecureDecisionLog::open(config).unwrap();
445
446 let records = log.replay_and_verify().unwrap();
447 assert_eq!(records.len(), 3);
448 assert_eq!(records[0].sequence, 1);
449 assert_eq!(records[1].sequence, 2);
450 assert_eq!(records[2].sequence, 3);
451 }
452
453 #[test]
454 fn test_encrypted_log_replay() {
455 let dir = TempDir::new().unwrap();
456 let log_path = dir.path().join("encrypted_replay.log");
457 let encryption_key = [5u8; 32];
458 let hmac_key = [6u8; 32];
459
460 {
461 let config = SecureLogConfig::with_encryption(&log_path, encryption_key, hmac_key);
462 let mut log = SecureDecisionLog::open(config).unwrap();
463 log.append(&make_record(1)).unwrap();
464 log.append(&make_record(2)).unwrap();
465 }
466
467 let config = SecureLogConfig::with_encryption(&log_path, encryption_key, hmac_key);
469 let log = SecureDecisionLog::open(config).unwrap();
470
471 let records = log.replay_and_verify().unwrap();
472 assert_eq!(records.len(), 2);
473 assert_eq!(records[0].signing_root, [1u8; 32]);
474 assert_eq!(records[1].signing_root, [2u8; 32]);
475 }
476
477 #[test]
478 fn test_tamper_detection() {
479 let dir = TempDir::new().unwrap();
480 let log_path = dir.path().join("tamper.log");
481 let hmac_key = [7u8; 32];
482
483 {
484 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
485 let mut log = SecureDecisionLog::open(config).unwrap();
486 log.append(&make_record(1)).unwrap();
487 log.append(&make_record(2)).unwrap();
488 }
489
490 let contents = std::fs::read_to_string(&log_path).unwrap();
492 let tampered = contents.replace("\"sequence\":1", "\"sequence\":9");
494 std::fs::write(&log_path, tampered).unwrap();
495
496 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
498 let log = SecureDecisionLog::open(config).unwrap();
499 let result = log.replay_and_verify();
500
501 assert!(matches!(result, Err(SecureLogError::IntegrityViolation { .. })));
502 }
503
504 #[test]
505 fn test_wrong_hmac_key() {
506 let dir = TempDir::new().unwrap();
507 let log_path = dir.path().join("wrong_key.log");
508 let hmac_key1 = [8u8; 32];
509 let hmac_key2 = [9u8; 32]; {
512 let config = SecureLogConfig::integrity_only(&log_path, hmac_key1);
513 let mut log = SecureDecisionLog::open(config).unwrap();
514 log.append(&make_record(1)).unwrap();
515 }
516
517 let config = SecureLogConfig::integrity_only(&log_path, hmac_key2);
519 let log = SecureDecisionLog::open(config).unwrap();
520 let result = log.replay_and_verify();
521
522 assert!(matches!(result, Err(SecureLogError::IntegrityViolation { .. })));
523 }
524
525 #[test]
526 fn test_sequence_gap_rejected() {
527 let dir = TempDir::new().unwrap();
528 let log_path = dir.path().join("gap.log");
529 let hmac_key = [10u8; 32];
530
531 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
532 let mut log = SecureDecisionLog::open(config).unwrap();
533
534 log.append(&make_record(1)).unwrap();
535 let result = log.append(&make_record(5)); assert!(matches!(result, Err(SecureLogError::SequenceGap { .. })));
538 }
539
540 #[test]
541 fn test_replay_from_sequence() {
542 let dir = TempDir::new().unwrap();
543 let log_path = dir.path().join("replay_from.log");
544 let hmac_key = [11u8; 32];
545
546 {
547 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
548 let mut log = SecureDecisionLog::open(config).unwrap();
549 for i in 1..=5 {
550 log.append(&make_record(i)).unwrap();
551 }
552 }
553
554 let config = SecureLogConfig::integrity_only(&log_path, hmac_key);
555 let log = SecureDecisionLog::open(config).unwrap();
556
557 let records = log.replay_from(3).unwrap();
558 assert_eq!(records.len(), 3);
559 assert_eq!(records[0].sequence, 3);
560 assert_eq!(records[2].sequence, 5);
561 }
562}