1use std::fs;
2use std::io;
3use std::path::Path;
4
5use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8
9use crate::{Reality, RealitySnapshot};
10
11pub const PERSISTENCE_VERSION: u8 = 2;
12
13const REALITY_DOMAIN: &[u8] = b"ARCHIMEDES-KERNEL-SIGNED-REALITY-V2";
14const SNAPSHOT_DOMAIN: &[u8] = b"ARCHIMEDES-KERNEL-SIGNED-SNAPSHOT-V2";
15
16#[derive(Debug)]
17pub enum PersistenceError {
18 Io(io::Error),
19 Serialization(postcard::Error),
20 IntegrityFailure(&'static str),
21 Signature(&'static str),
22 UnsupportedVersion { expected: u8, found: u8 },
23 PublicKeyMismatch,
24}
25
26impl std::fmt::Display for PersistenceError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 PersistenceError::Io(e) => write!(f, "IO error: {e}"),
30 PersistenceError::Serialization(e) => {
31 write!(f, "Serialization error: {e}")
32 }
33 PersistenceError::IntegrityFailure(msg) => {
34 write!(f, "Integrity failure: {msg}")
35 }
36 PersistenceError::Signature(msg) => {
37 write!(f, "Signature failure: {msg}")
38 }
39 PersistenceError::UnsupportedVersion { expected, found } => {
40 write!(
41 f,
42 "Unsupported persistence version: expected {expected}, found {found}"
43 )
44 }
45 PersistenceError::PublicKeyMismatch => {
46 write!(
47 f,
48 "Embedded public key does not match expected authority key"
49 )
50 }
51 }
52 }
53}
54
55impl std::error::Error for PersistenceError {
56 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57 match self {
58 PersistenceError::Io(e) => Some(e),
59 PersistenceError::Serialization(_)
60 | PersistenceError::IntegrityFailure(_)
61 | PersistenceError::Signature(_)
62 | PersistenceError::UnsupportedVersion { .. }
63 | PersistenceError::PublicKeyMismatch => None,
64 }
65 }
66}
67
68fn write_postcard<T: Serialize>(path: &Path, value: &T) -> Result<(), PersistenceError> {
69 let bytes = postcard::to_allocvec(value).map_err(PersistenceError::Serialization)?;
70 fs::write(path, bytes).map_err(PersistenceError::Io)
71}
72
73fn read_postcard<T: DeserializeOwned>(path: &Path) -> Result<T, PersistenceError> {
74 let bytes = fs::read(path).map_err(PersistenceError::Io)?;
75 postcard::from_bytes(&bytes).map_err(PersistenceError::Serialization)
76}
77
78fn check_version(found: u8) -> Result<(), PersistenceError> {
79 if found != PERSISTENCE_VERSION {
80 return Err(PersistenceError::UnsupportedVersion {
81 expected: PERSISTENCE_VERSION,
82 found,
83 });
84 }
85
86 Ok(())
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90struct PersistedReality {
91 persistence_version: u8,
92 reality: Reality,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96struct PersistedSnapshot {
97 persistence_version: u8,
98 snapshot: RealitySnapshot,
99}
100
101pub fn save_reality(reality: &Reality, path: &Path) -> Result<(), PersistenceError> {
102 let persisted = PersistedReality {
103 persistence_version: PERSISTENCE_VERSION,
104 reality: reality.clone(),
105 };
106
107 write_postcard(path, &persisted)
108}
109
110pub fn load_reality(path: &Path) -> Result<Reality, PersistenceError> {
111 let persisted: PersistedReality = read_postcard(path)?;
112
113 check_version(persisted.persistence_version)?;
114
115 let reality = persisted.reality;
116
117 if !reality.memory_integrity() {
118 return Err(PersistenceError::IntegrityFailure(
119 "movement memory integrity failed",
120 ));
121 }
122
123 if reality.drift_check().hidden_drift_required {
124 return Err(PersistenceError::IntegrityFailure("hidden drift detected"));
125 }
126
127 Ok(reality)
128}
129
130pub fn save_snapshot(snapshot: &RealitySnapshot, path: &Path) -> Result<(), PersistenceError> {
131 let persisted = PersistedSnapshot {
132 persistence_version: PERSISTENCE_VERSION,
133 snapshot: snapshot.clone(),
134 };
135
136 write_postcard(path, &persisted)
137}
138
139pub fn load_snapshot(path: &Path) -> Result<RealitySnapshot, PersistenceError> {
140 let persisted: PersistedSnapshot = read_postcard(path)?;
141
142 check_version(persisted.persistence_version)?;
143
144 Ok(persisted.snapshot)
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct SignedReality {
149 pub protocol_version: u8,
150 pub reality: Reality,
151 pub public_key: [u8; 32],
152 pub signature: Vec<u8>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct SignedSnapshot {
157 pub protocol_version: u8,
158 pub snapshot: RealitySnapshot,
159 pub public_key: [u8; 32],
160 pub signature: Vec<u8>,
161}
162
163#[derive(Serialize)]
164struct SignableReality<'a> {
165 domain: &'a [u8],
166 protocol_version: u8,
167 reality: &'a Reality,
168 public_key: [u8; 32],
169}
170
171#[derive(Serialize)]
172struct SignableSnapshot<'a> {
173 domain: &'a [u8],
174 protocol_version: u8,
175 snapshot: &'a RealitySnapshot,
176 public_key: [u8; 32],
177}
178
179fn sign_bytes(message: &[u8], signing_key: &SigningKey) -> ([u8; 32], [u8; 64]) {
180 let signature: Signature = signing_key.sign(message);
181
182 (signing_key.verifying_key().to_bytes(), signature.to_bytes())
183}
184
185fn verify_bytes(
186 message: &[u8],
187 public_key: &[u8; 32],
188 signature: &[u8],
189) -> Result<(), PersistenceError> {
190 let sig_bytes: [u8; 64] = signature
191 .try_into()
192 .map_err(|_| PersistenceError::Signature("invalid signature length"))?;
193
194 let verifying_key = VerifyingKey::from_bytes(public_key)
195 .map_err(|_| PersistenceError::Signature("invalid public key"))?;
196
197 let signature = Signature::from_bytes(&sig_bytes);
198
199 verifying_key
200 .verify_strict(message, &signature)
201 .map_err(|_| PersistenceError::Signature("invalid signature"))
202}
203
204pub fn sign_reality(
205 reality: &Reality,
206 signing_key: &SigningKey,
207) -> Result<SignedReality, PersistenceError> {
208 let public_key = signing_key.verifying_key().to_bytes();
209
210 let signable = SignableReality {
211 domain: REALITY_DOMAIN,
212 protocol_version: PERSISTENCE_VERSION,
213 reality,
214 public_key,
215 };
216
217 let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
218
219 let (_, signature) = sign_bytes(&message, signing_key);
220
221 Ok(SignedReality {
222 protocol_version: PERSISTENCE_VERSION,
223 reality: reality.clone(),
224 public_key,
225 signature: signature.to_vec(),
226 })
227}
228
229pub fn save_signed_reality(signed: &SignedReality, path: &Path) -> Result<(), PersistenceError> {
230 write_postcard(path, signed)
231}
232
233pub fn load_signed_reality(
234 path: &Path,
235 expected_public_key: &[u8; 32],
236) -> Result<SignedReality, PersistenceError> {
237 let signed: SignedReality = read_postcard(path)?;
238
239 check_version(signed.protocol_version)?;
240
241 if signed.public_key != *expected_public_key {
242 return Err(PersistenceError::PublicKeyMismatch);
243 }
244
245 let signable = SignableReality {
246 domain: REALITY_DOMAIN,
247 protocol_version: signed.protocol_version,
248 reality: &signed.reality,
249 public_key: signed.public_key,
250 };
251
252 let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
253
254 verify_bytes(&message, expected_public_key, &signed.signature)?;
255
256 if !signed.reality.memory_integrity() {
257 return Err(PersistenceError::IntegrityFailure(
258 "movement memory integrity failed",
259 ));
260 }
261
262 if signed.reality.drift_check().hidden_drift_required {
263 return Err(PersistenceError::IntegrityFailure("hidden drift detected"));
264 }
265
266 Ok(signed)
267}
268
269pub fn sign_snapshot(
270 snapshot: &RealitySnapshot,
271 signing_key: &SigningKey,
272) -> Result<SignedSnapshot, PersistenceError> {
273 let public_key = signing_key.verifying_key().to_bytes();
274
275 let signable = SignableSnapshot {
276 domain: SNAPSHOT_DOMAIN,
277 protocol_version: PERSISTENCE_VERSION,
278 snapshot,
279 public_key,
280 };
281
282 let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
283
284 let (_, signature) = sign_bytes(&message, signing_key);
285
286 Ok(SignedSnapshot {
287 protocol_version: PERSISTENCE_VERSION,
288 snapshot: snapshot.clone(),
289 public_key,
290 signature: signature.to_vec(),
291 })
292}
293
294pub fn save_signed_snapshot(signed: &SignedSnapshot, path: &Path) -> Result<(), PersistenceError> {
295 write_postcard(path, signed)
296}
297
298pub fn load_signed_snapshot(
299 path: &Path,
300 expected_public_key: &[u8; 32],
301) -> Result<SignedSnapshot, PersistenceError> {
302 let signed: SignedSnapshot = read_postcard(path)?;
303
304 check_version(signed.protocol_version)?;
305
306 if signed.public_key != *expected_public_key {
307 return Err(PersistenceError::PublicKeyMismatch);
308 }
309
310 let signable = SignableSnapshot {
311 domain: SNAPSHOT_DOMAIN,
312 protocol_version: signed.protocol_version,
313 snapshot: &signed.snapshot,
314 public_key: signed.public_key,
315 };
316
317 let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
318
319 verify_bytes(&message, expected_public_key, &signed.signature)?;
320
321 Ok(signed)
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::{perform_movement_sequence, Boundary, Event, Identity, Law, State};
328 use rand::rngs::OsRng;
329
330 fn test_reality() -> Reality {
331 Reality::new(
332 Identity("persist-test".to_string()),
333 Boundary {
334 allowed_values: vec![
335 "before".to_string(),
336 "after".to_string(),
337 "done".to_string(),
338 ],
339 },
340 Law {
341 allowed_transitions: vec![
342 ("before".to_string(), "after".to_string()),
343 ("after".to_string(), "done".to_string()),
344 ],
345 },
346 State {
347 field: "before".to_string(),
348 },
349 )
350 }
351
352 fn moved_reality() -> Reality {
353 let mut reality = test_reality();
354
355 let events = vec![
356 Event {
357 proposed_field: "after".to_string(),
358 },
359 Event {
360 proposed_field: "done".to_string(),
361 },
362 ];
363
364 perform_movement_sequence(&mut reality, events).unwrap();
365
366 reality
367 }
368
369 #[test]
370 fn reality_roundtrip_preserves_integrity() {
371 let reality = moved_reality();
372
373 let path = std::env::temp_dir().join("archimedes-v2-reality.bin");
374
375 save_reality(&reality, &path).unwrap();
376
377 let loaded = load_reality(&path).unwrap();
378
379 assert_eq!(reality, loaded);
380 assert!(loaded.memory_integrity());
381 assert!(!loaded.drift_check().hidden_drift_required);
382
383 let _ = std::fs::remove_file(&path);
384 }
385
386 #[test]
387 fn snapshot_roundtrip_matches_original() {
388 let reality = test_reality();
389 let snapshot = reality.snapshot();
390
391 let path = std::env::temp_dir().join("archimedes-v2-snapshot.bin");
392
393 save_snapshot(&snapshot, &path).unwrap();
394
395 let loaded = load_snapshot(&path).unwrap();
396
397 assert_eq!(snapshot, loaded);
398
399 let _ = std::fs::remove_file(&path);
400 }
401
402 #[test]
403 fn tampered_reality_file_is_rejected() {
404 let reality = moved_reality();
405
406 let path = std::env::temp_dir().join("archimedes-v2-tampered.bin");
407
408 save_reality(&reality, &path).unwrap();
409
410 let mut bytes = std::fs::read(&path).unwrap();
411 let mid = bytes.len() / 2;
412 bytes[mid] ^= 0xFF;
413 std::fs::write(&path, &bytes).unwrap();
414
415 assert!(load_reality(&path).is_err());
416
417 let _ = std::fs::remove_file(&path);
418 }
419
420 #[test]
421 fn unsupported_unsigned_version_is_rejected() {
422 let persisted = PersistedReality {
423 persistence_version: 99,
424 reality: test_reality(),
425 };
426
427 let path = std::env::temp_dir().join("archimedes-v2-version.bin");
428
429 write_postcard(&path, &persisted).unwrap();
430
431 let result = load_reality(&path);
432
433 assert!(matches!(
434 result,
435 Err(PersistenceError::UnsupportedVersion { .. })
436 ));
437
438 let _ = std::fs::remove_file(&path);
439 }
440
441 #[test]
442 fn signed_reality_roundtrip_works() {
443 let reality = moved_reality();
444
445 let mut csprng = OsRng;
446 let signing_key = SigningKey::generate(&mut csprng);
447 let public_key = signing_key.verifying_key().to_bytes();
448
449 let signed = sign_reality(&reality, &signing_key).unwrap();
450
451 let path = std::env::temp_dir().join("archimedes-v2-signed-reality.bin");
452
453 save_signed_reality(&signed, &path).unwrap();
454
455 let loaded = load_signed_reality(&path, &public_key).unwrap();
456
457 assert_eq!(loaded.protocol_version, PERSISTENCE_VERSION);
458 assert_eq!(loaded.reality, reality);
459 assert_eq!(loaded.public_key, public_key);
460
461 let _ = std::fs::remove_file(&path);
462 }
463
464 #[test]
465 fn signed_reality_rejects_wrong_key() {
466 let reality = moved_reality();
467
468 let mut csprng = OsRng;
469 let signing_key = SigningKey::generate(&mut csprng);
470 let wrong_key = SigningKey::generate(&mut csprng).verifying_key().to_bytes();
471
472 let signed = sign_reality(&reality, &signing_key).unwrap();
473
474 let path = std::env::temp_dir().join("archimedes-v2-wrong-key.bin");
475
476 save_signed_reality(&signed, &path).unwrap();
477
478 assert!(load_signed_reality(&path, &wrong_key).is_err());
479
480 let _ = std::fs::remove_file(&path);
481 }
482
483 #[test]
484 fn signed_reality_rejects_tampered_embedded_key() {
485 let reality = moved_reality();
486
487 let mut csprng = OsRng;
488 let signing_key = SigningKey::generate(&mut csprng);
489 let public_key = signing_key.verifying_key().to_bytes();
490
491 let mut signed = sign_reality(&reality, &signing_key).unwrap();
492 signed.public_key[0] ^= 0x01;
493
494 let path = std::env::temp_dir().join("archimedes-v2-key-tamper.bin");
495
496 save_signed_reality(&signed, &path).unwrap();
497
498 let result = load_signed_reality(&path, &public_key);
499
500 assert!(matches!(result, Err(PersistenceError::PublicKeyMismatch)));
501
502 let _ = std::fs::remove_file(&path);
503 }
504
505 #[test]
506 fn signed_reality_rejects_tampered_payload() {
507 let reality = moved_reality();
508
509 let mut csprng = OsRng;
510 let signing_key = SigningKey::generate(&mut csprng);
511 let public_key = signing_key.verifying_key().to_bytes();
512
513 let mut signed = sign_reality(&reality, &signing_key).unwrap();
514 signed.reality.state.field = "tampered".to_string();
515
516 let path = std::env::temp_dir().join("archimedes-v2-payload-tamper.bin");
517
518 save_signed_reality(&signed, &path).unwrap();
519
520 let result = load_signed_reality(&path, &public_key);
521
522 assert!(matches!(result, Err(PersistenceError::Signature(_))));
523
524 let _ = std::fs::remove_file(&path);
525 }
526
527 #[test]
528 fn unsupported_signed_version_is_rejected() {
529 let reality = moved_reality();
530
531 let mut csprng = OsRng;
532 let signing_key = SigningKey::generate(&mut csprng);
533 let public_key = signing_key.verifying_key().to_bytes();
534
535 let mut signed = sign_reality(&reality, &signing_key).unwrap();
536 signed.protocol_version = 99;
537
538 let path = std::env::temp_dir().join("archimedes-v2-protocol-version.bin");
539
540 save_signed_reality(&signed, &path).unwrap();
541
542 let result = load_signed_reality(&path, &public_key);
543
544 assert!(matches!(
545 result,
546 Err(PersistenceError::UnsupportedVersion { .. })
547 ));
548
549 let _ = std::fs::remove_file(&path);
550 }
551
552 #[test]
553 fn signed_snapshot_roundtrip_works() {
554 let reality = test_reality();
555 let snapshot = reality.snapshot();
556
557 let mut csprng = OsRng;
558 let signing_key = SigningKey::generate(&mut csprng);
559 let public_key = signing_key.verifying_key().to_bytes();
560
561 let signed = sign_snapshot(&snapshot, &signing_key).unwrap();
562
563 let path = std::env::temp_dir().join("archimedes-v2-signed-snapshot.bin");
564
565 save_signed_snapshot(&signed, &path).unwrap();
566
567 let loaded = load_signed_snapshot(&path, &public_key).unwrap();
568
569 assert_eq!(loaded.protocol_version, PERSISTENCE_VERSION);
570 assert_eq!(loaded.snapshot, snapshot);
571 assert_eq!(loaded.public_key, public_key);
572
573 let _ = std::fs::remove_file(&path);
574 }
575
576 #[test]
577 fn signed_snapshot_rejects_tampered_embedded_key() {
578 let reality = test_reality();
579 let snapshot = reality.snapshot();
580
581 let mut csprng = OsRng;
582 let signing_key = SigningKey::generate(&mut csprng);
583 let public_key = signing_key.verifying_key().to_bytes();
584
585 let mut signed = sign_snapshot(&snapshot, &signing_key).unwrap();
586 signed.public_key[0] ^= 0x01;
587
588 let path = std::env::temp_dir().join("archimedes-v2-snapshot-key-tamper.bin");
589
590 save_signed_snapshot(&signed, &path).unwrap();
591
592 let result = load_signed_snapshot(&path, &public_key);
593
594 assert!(matches!(result, Err(PersistenceError::PublicKeyMismatch)));
595
596 let _ = std::fs::remove_file(&path);
597 }
598
599 #[test]
600 fn signed_snapshot_rejects_tampered_payload() {
601 let reality = test_reality();
602 let snapshot = reality.snapshot();
603
604 let mut csprng = OsRng;
605 let signing_key = SigningKey::generate(&mut csprng);
606 let public_key = signing_key.verifying_key().to_bytes();
607
608 let mut signed = sign_snapshot(&snapshot, &signing_key).unwrap();
609 signed.snapshot.state.field = "tampered".to_string();
610
611 let path = std::env::temp_dir().join("archimedes-v2-snapshot-tamper.bin");
612
613 save_signed_snapshot(&signed, &path).unwrap();
614
615 let result = load_signed_snapshot(&path, &public_key);
616
617 assert!(matches!(result, Err(PersistenceError::Signature(_))));
618
619 let _ = std::fs::remove_file(&path);
620 }
621}