1use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21
22use a3s_box_core::error::{BoxError, Result};
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct VersionedSealedData {
28 pub version: u64,
30 pub context: String,
32 pub policy: super::sealed::SealingPolicy,
34 #[serde(with = "base64_serde")]
36 pub blob: Vec<u8>,
37}
38
39pub struct VersionStore {
41 path: PathBuf,
42 versions: HashMap<String, u64>,
43}
44
45impl VersionStore {
46 pub fn load(path: &Path) -> Result<Self> {
48 if let Some(parent) = path.parent() {
49 std::fs::create_dir_all(parent).map_err(|e| {
50 BoxError::ConfigError(format!(
51 "Failed to create version store directory {}: {}",
52 parent.display(),
53 e
54 ))
55 })?;
56 }
57
58 let versions = if path.exists() {
59 let data = std::fs::read_to_string(path).map_err(|e| {
60 BoxError::ConfigError(format!(
61 "Failed to read version store {}: {}",
62 path.display(),
63 e
64 ))
65 })?;
66 serde_json::from_str(&data).unwrap_or_default()
67 } else {
68 HashMap::new()
69 };
70
71 Ok(Self {
72 path: path.to_path_buf(),
73 versions,
74 })
75 }
76
77 pub fn load_default() -> Result<Self> {
79 let home = a3s_box_core::dirs_home();
80 Self::load(&home.join("sealed-versions.json"))
81 }
82
83 pub fn current_version(&self, context: &str) -> u64 {
85 self.versions.get(context).copied().unwrap_or(0)
86 }
87
88 pub fn advance(&mut self, context: &str) -> Result<u64> {
91 let next = self.current_version(context) + 1;
92 self.versions.insert(context.to_string(), next);
93 self.save()?;
94 Ok(next)
95 }
96
97 pub fn check_version(&self, context: &str, blob_version: u64) -> Result<()> {
102 let stored = self.current_version(context);
103 if blob_version < stored {
104 return Err(BoxError::AttestationError(format!(
105 "Rollback detected for context '{}': blob version {} < stored version {}",
106 context, blob_version, stored
107 )));
108 }
109 Ok(())
110 }
111
112 pub fn update_version(&mut self, context: &str, blob_version: u64) -> Result<()> {
115 let stored = self.current_version(context);
116 if blob_version > stored {
117 self.versions.insert(context.to_string(), blob_version);
118 self.save()?;
119 }
120 Ok(())
121 }
122
123 pub fn list(&self) -> &HashMap<String, u64> {
125 &self.versions
126 }
127
128 pub fn remove(&mut self, context: &str) -> Result<bool> {
130 if self.versions.remove(context).is_some() {
131 self.save()?;
132 Ok(true)
133 } else {
134 Ok(false)
135 }
136 }
137
138 fn save(&self) -> Result<()> {
140 let data = serde_json::to_string_pretty(&self.versions).map_err(|e| {
141 BoxError::SerializationError(format!("Failed to serialize version store: {}", e))
142 })?;
143 let tmp = self.path.with_extension("json.tmp");
144 std::fs::write(&tmp, &data).map_err(BoxError::IoError)?;
145 std::fs::rename(&tmp, &self.path).map_err(BoxError::IoError)?;
146 Ok(())
147 }
148}
149
150fn versioned_seal_context(context: &str, version: u64) -> String {
161 format!("{context}\0v{version}")
162}
163
164pub fn seal_versioned(
170 report: &[u8],
171 plaintext: &[u8],
172 context: &str,
173 policy: super::sealed::SealingPolicy,
174 version_store: &mut VersionStore,
175) -> Result<VersionedSealedData> {
176 let version = version_store.advance(context)?;
177 let bound_context = versioned_seal_context(context, version);
178 let sealed = super::sealed::seal(report, plaintext, &bound_context, policy)?;
179
180 Ok(VersionedSealedData {
181 version,
182 context: context.to_string(),
185 policy: sealed.policy,
186 blob: sealed.blob,
187 })
188}
189
190pub fn unseal_versioned(
195 report: &[u8],
196 versioned: &VersionedSealedData,
197 version_store: &mut VersionStore,
198) -> Result<Vec<u8>> {
199 version_store.check_version(&versioned.context, versioned.version)?;
201
202 let sealed = super::sealed::SealedData {
207 policy: versioned.policy,
208 context: versioned_seal_context(&versioned.context, versioned.version),
209 blob: versioned.blob.clone(),
210 };
211
212 let plaintext = super::sealed::unseal(report, &sealed)?;
213
214 version_store.update_version(&versioned.context, versioned.version)?;
216
217 Ok(plaintext)
218}
219
220mod base64_serde {
222 use serde::{Deserialize, Deserializer, Serializer};
223
224 pub fn serialize<S: Serializer>(bytes: &Vec<u8>, s: S) -> std::result::Result<S::Ok, S::Error> {
225 use base64::Engine;
226 s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
227 }
228
229 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> std::result::Result<Vec<u8>, D::Error> {
230 use base64::Engine;
231 let s = String::deserialize(d)?;
232 base64::engine::general_purpose::STANDARD
233 .decode(&s)
234 .map_err(serde::de::Error::custom)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::super::sealed::SealingPolicy;
241 use super::*;
242 use tempfile::TempDir;
243
244 fn make_test_report() -> Vec<u8> {
245 let mut report = vec![0u8; 1184];
246 for i in 0..48 {
247 report[0x90 + i] = (i as u8).wrapping_mul(0xA3);
248 }
249 for b in &mut report[0x1A0..0x1E0] {
250 *b = 0xA3;
251 }
252 report
253 }
254
255 fn test_version_store(tmp: &TempDir) -> VersionStore {
256 VersionStore::load(&tmp.path().join("versions.json")).unwrap()
257 }
258
259 #[test]
260 fn test_version_store_new() {
261 let tmp = TempDir::new().unwrap();
262 let store = test_version_store(&tmp);
263 assert_eq!(store.current_version("test"), 0);
264 assert!(store.list().is_empty());
265 }
266
267 #[test]
268 fn test_version_store_advance() {
269 let tmp = TempDir::new().unwrap();
270 let mut store = test_version_store(&tmp);
271
272 assert_eq!(store.advance("ctx-a").unwrap(), 1);
273 assert_eq!(store.advance("ctx-a").unwrap(), 2);
274 assert_eq!(store.advance("ctx-b").unwrap(), 1);
275 assert_eq!(store.current_version("ctx-a"), 2);
276 assert_eq!(store.current_version("ctx-b"), 1);
277 }
278
279 #[test]
280 fn test_version_store_persistence() {
281 let tmp = TempDir::new().unwrap();
282 let path = tmp.path().join("versions.json");
283
284 {
285 let mut store = VersionStore::load(&path).unwrap();
286 store.advance("ctx-1").unwrap();
287 store.advance("ctx-1").unwrap();
288 store.advance("ctx-2").unwrap();
289 }
290
291 {
292 let store = VersionStore::load(&path).unwrap();
293 assert_eq!(store.current_version("ctx-1"), 2);
294 assert_eq!(store.current_version("ctx-2"), 1);
295 }
296 }
297
298 #[test]
299 fn test_version_store_check_version_ok() {
300 let tmp = TempDir::new().unwrap();
301 let mut store = test_version_store(&tmp);
302 store.advance("ctx").unwrap(); assert!(store.check_version("ctx", 1).is_ok());
306 assert!(store.check_version("ctx", 2).is_ok());
307 }
308
309 #[test]
310 fn test_version_store_check_version_rollback() {
311 let tmp = TempDir::new().unwrap();
312 let mut store = test_version_store(&tmp);
313 store.advance("ctx").unwrap(); store.advance("ctx").unwrap(); let result = store.check_version("ctx", 1);
318 assert!(result.is_err());
319 assert!(result
320 .unwrap_err()
321 .to_string()
322 .contains("Rollback detected"));
323 }
324
325 #[test]
326 fn test_version_store_update_version() {
327 let tmp = TempDir::new().unwrap();
328 let mut store = test_version_store(&tmp);
329
330 store.update_version("ctx", 5).unwrap();
331 assert_eq!(store.current_version("ctx"), 5);
332
333 store.update_version("ctx", 3).unwrap();
335 assert_eq!(store.current_version("ctx"), 5);
336 }
337
338 #[test]
339 fn test_version_store_remove() {
340 let tmp = TempDir::new().unwrap();
341 let mut store = test_version_store(&tmp);
342 store.advance("ctx").unwrap();
343
344 assert!(store.remove("ctx").unwrap());
345 assert_eq!(store.current_version("ctx"), 0);
346 assert!(!store.remove("ctx").unwrap());
347 }
348
349 #[test]
350 fn test_seal_versioned_roundtrip() {
351 let tmp = TempDir::new().unwrap();
352 let mut store = test_version_store(&tmp);
353 let report = make_test_report();
354
355 let sealed = seal_versioned(
356 &report,
357 b"secret data",
358 "my-context",
359 SealingPolicy::default(),
360 &mut store,
361 )
362 .unwrap();
363
364 assert_eq!(sealed.version, 1);
365 assert_eq!(sealed.context, "my-context");
366
367 let plaintext = unseal_versioned(&report, &sealed, &mut store).unwrap();
368 assert_eq!(plaintext, b"secret data");
369 }
370
371 #[test]
372 fn test_seal_versioned_increments() {
373 let tmp = TempDir::new().unwrap();
374 let mut store = test_version_store(&tmp);
375 let report = make_test_report();
376
377 let s1 =
378 seal_versioned(&report, b"v1", "ctx", SealingPolicy::default(), &mut store).unwrap();
379 let s2 =
380 seal_versioned(&report, b"v2", "ctx", SealingPolicy::default(), &mut store).unwrap();
381
382 assert_eq!(s1.version, 1);
383 assert_eq!(s2.version, 2);
384 }
385
386 #[test]
387 fn test_unseal_versioned_rollback_rejected() {
388 let tmp = TempDir::new().unwrap();
389 let mut store = test_version_store(&tmp);
390 let report = make_test_report();
391
392 let old =
393 seal_versioned(&report, b"old", "ctx", SealingPolicy::default(), &mut store).unwrap();
394 let _new =
395 seal_versioned(&report, b"new", "ctx", SealingPolicy::default(), &mut store).unwrap();
396
397 let result = unseal_versioned(&report, &old, &mut store);
399 assert!(result.is_err());
400 assert!(result.unwrap_err().to_string().contains("Rollback"));
401 }
402
403 #[test]
404 fn test_unseal_versioned_forged_version_rejected() {
405 let tmp = TempDir::new().unwrap();
406 let mut store = test_version_store(&tmp);
407 let report = make_test_report();
408
409 let mut sealed = seal_versioned(
411 &report,
412 b"secret-v1",
413 "ctx",
414 SealingPolicy::default(),
415 &mut store,
416 )
417 .unwrap();
418 assert_eq!(sealed.version, 1);
419
420 sealed.version = 2;
426 assert!(store.check_version("ctx", sealed.version).is_ok());
427
428 let result = unseal_versioned(&report, &sealed, &mut store);
429 assert!(result.is_err(), "forged version must not unseal");
430 let msg = result.unwrap_err().to_string();
431 assert!(
432 msg.contains("Unseal failed") || msg.contains("mismatch") || msg.contains("corrupted"),
433 "expected an authentication failure, got: {msg}"
434 );
435 }
436
437 #[test]
438 fn test_unseal_versioned_same_version_ok() {
439 let tmp = TempDir::new().unwrap();
440 let mut store = test_version_store(&tmp);
441 let report = make_test_report();
442
443 let sealed = seal_versioned(
444 &report,
445 b"data",
446 "ctx",
447 SealingPolicy::default(),
448 &mut store,
449 )
450 .unwrap();
451
452 let p1 = unseal_versioned(&report, &sealed, &mut store).unwrap();
454 let p2 = unseal_versioned(&report, &sealed, &mut store).unwrap();
455 assert_eq!(p1, b"data");
456 assert_eq!(p2, b"data");
457 }
458
459 #[test]
460 fn test_versioned_sealed_data_serde() {
461 let tmp = TempDir::new().unwrap();
462 let mut store = test_version_store(&tmp);
463 let report = make_test_report();
464
465 let sealed = seal_versioned(
466 &report,
467 b"serde-test",
468 "ctx",
469 SealingPolicy::default(),
470 &mut store,
471 )
472 .unwrap();
473 let json = serde_json::to_string(&sealed).unwrap();
474 let parsed: VersionedSealedData = serde_json::from_str(&json).unwrap();
475
476 assert_eq!(parsed.version, sealed.version);
477 assert_eq!(parsed.context, sealed.context);
478 assert_eq!(parsed.blob, sealed.blob);
479
480 let plaintext = unseal_versioned(&report, &parsed, &mut store).unwrap();
481 assert_eq!(plaintext, b"serde-test");
482 }
483
484 #[test]
485 fn test_independent_contexts() {
486 let tmp = TempDir::new().unwrap();
487 let mut store = test_version_store(&tmp);
488 let report = make_test_report();
489
490 let s1 =
491 seal_versioned(&report, b"a", "ctx-a", SealingPolicy::default(), &mut store).unwrap();
492 let s2 =
493 seal_versioned(&report, b"b", "ctx-b", SealingPolicy::default(), &mut store).unwrap();
494
495 assert_eq!(s1.version, 1);
497 assert_eq!(s2.version, 1);
498
499 assert_eq!(unseal_versioned(&report, &s1, &mut store).unwrap(), b"a");
501 assert_eq!(unseal_versioned(&report, &s2, &mut store).unwrap(), b"b");
502 }
503}