1use std::future::Future;
35use std::pin::Pin;
36use std::sync::{Arc, OnceLock};
37
38use hkdf::Hkdf;
39use hmac::digest::KeyInit;
40use hmac::{Hmac, Mac};
41use sha2::{Digest, Sha256};
42use tokio::sync::Mutex;
43use tracing::warn;
44
45use crate::error::AppError;
46use crate::store::KeyspaceHandle;
47
48type HmacSha256 = Hmac<Sha256>;
49type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
50
51pub trait AnchorCounter: Send + Sync {
61 fn read(&self) -> BoxFuture<'_, Result<Option<u64>, AppError>>;
64
65 fn init(&self, version: u64, digest: [u8; 32]) -> BoxFuture<'_, Result<(), AppError>>;
68
69 fn set(&self, expected: u64, new: u64, digest: [u8; 32])
72 -> BoxFuture<'_, Result<(), AppError>>;
73}
74
75pub const CARVEOUT_KEY: &str = "tee:bootstrap-carveout-closed";
79pub const JWT_FINGERPRINT_KEY: &str = "bootstrap:jwt_fingerprint";
82pub const MANIFEST_KEY: &str = "tee:integrity-manifest";
84
85const ACL_PREFIX: &str = "acl:";
86const PATH_COUNTER_PREFIX: &str = "path_counter:";
87const CTX_COUNTER_PREFIX: &str = "ctx_counter";
88
89const MAC_KEY_INFO: &[u8] = b"vti-integrity-manifest-mac/v1";
92const MAC_DOMAIN: &[u8] = b"vti-integrity-manifest/v1";
95
96const MANIFEST_LEN: usize = 8 + 1 + 16 + 32 + 32 + 32;
99
100#[derive(Clone, PartialEq, Eq, Debug)]
102struct CoveredState {
103 carveout_present: bool,
104 jwt_fp: [u8; 16],
105 acl_root: [u8; 32],
106 counters: [u8; 32],
107}
108
109impl CoveredState {
110 fn mac_input(&self, version: u64) -> Vec<u8> {
112 let mut buf = Vec::with_capacity(MAC_DOMAIN.len() + 8 + 1 + 16 + 32 + 32);
113 buf.extend_from_slice(MAC_DOMAIN);
114 buf.extend_from_slice(&version.to_le_bytes());
115 buf.push(self.carveout_present as u8);
116 buf.extend_from_slice(&self.jwt_fp);
117 buf.extend_from_slice(&self.acl_root);
118 buf.extend_from_slice(&self.counters);
119 buf
120 }
121}
122
123struct Manifest {
125 version: u64,
126 state: CoveredState,
127 mac: [u8; 32],
128}
129
130impl Manifest {
131 fn sealed(mac_key: &[u8; 32], version: u64, state: CoveredState) -> Self {
133 let mac = mac_over(mac_key, &state.mac_input(version));
134 Self {
135 version,
136 state,
137 mac,
138 }
139 }
140
141 fn to_bytes(&self) -> Vec<u8> {
142 let mut buf = Vec::with_capacity(MANIFEST_LEN);
143 buf.extend_from_slice(&self.version.to_le_bytes());
144 buf.push(self.state.carveout_present as u8);
145 buf.extend_from_slice(&self.state.jwt_fp);
146 buf.extend_from_slice(&self.state.acl_root);
147 buf.extend_from_slice(&self.state.counters);
148 buf.extend_from_slice(&self.mac);
149 buf
150 }
151
152 fn from_bytes(b: &[u8]) -> Option<Self> {
153 if b.len() != MANIFEST_LEN {
154 return None;
155 }
156 let version = u64::from_le_bytes(b[0..8].try_into().ok()?);
157 let carveout_present = match b[8] {
158 0 => false,
159 1 => true,
160 _ => return None,
161 };
162 let jwt_fp: [u8; 16] = b[9..25].try_into().ok()?;
163 let acl_root: [u8; 32] = b[25..57].try_into().ok()?;
164 let counters: [u8; 32] = b[57..89].try_into().ok()?;
165 let mac: [u8; 32] = b[89..121].try_into().ok()?;
166 Some(Self {
167 version,
168 state: CoveredState {
169 carveout_present,
170 jwt_fp,
171 acl_root,
172 counters,
173 },
174 mac,
175 })
176 }
177
178 fn mac_valid(&self, mac_key: &[u8; 32]) -> bool {
180 let mut h = HmacSha256::new_from_slice(mac_key).expect("hmac accepts 32-byte key");
181 h.update(&self.state.mac_input(self.version));
182 h.verify_slice(&self.mac).is_ok()
183 }
184}
185
186fn mac_over(mac_key: &[u8; 32], input: &[u8]) -> [u8; 32] {
187 let mut h = HmacSha256::new_from_slice(mac_key).expect("hmac accepts 32-byte key");
188 h.update(input);
189 h.finalize().into_bytes().into()
190}
191
192pub fn derive_mac_key(storage_key: &[u8; 32]) -> [u8; 32] {
195 let hk = Hkdf::<Sha256>::new(None, storage_key);
196 let mut out = [0u8; 32];
197 hk.expand(MAC_KEY_INFO, &mut out)
198 .expect("32-byte HKDF output is valid");
199 out
200}
201
202fn hash_rows(mut rows: Vec<(Vec<u8>, Vec<u8>)>) -> [u8; 32] {
205 rows.sort_by(|a, b| a.0.cmp(&b.0));
206 let mut hasher = Sha256::new();
207 for (k, v) in rows {
208 hasher.update((k.len() as u32).to_le_bytes());
209 hasher.update(&k);
210 hasher.update((v.len() as u32).to_le_bytes());
211 hasher.update(&v);
212 }
213 hasher.finalize().into()
214}
215
216struct ManifestSealer {
219 mac_key: [u8; 32],
220 keys_ks: KeyspaceHandle,
222 bootstrap_ks: KeyspaceHandle,
224 acl_ks: KeyspaceHandle,
226 contexts_ks: KeyspaceHandle,
228 anchor: Option<Arc<dyn AnchorCounter>>,
232}
233
234impl ManifestSealer {
235 async fn compute_state(&self) -> Result<CoveredState, AppError> {
236 let carveout_present = self.keys_ks.get_raw(CARVEOUT_KEY).await?.is_some();
237
238 let jwt_fp = match self.bootstrap_ks.get_raw(JWT_FINGERPRINT_KEY).await? {
239 Some(bytes) => {
240 let digest = Sha256::digest(&bytes);
241 let mut fp = [0u8; 16];
242 fp.copy_from_slice(&digest[..16]);
243 fp
244 }
245 None => [0u8; 16],
246 };
247
248 let acl_root = hash_rows(self.acl_ks.prefix_iter_raw(ACL_PREFIX).await?);
249
250 let mut counter_rows: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
254 for (k, v) in self.keys_ks.prefix_iter_raw(PATH_COUNTER_PREFIX).await? {
255 let mut tagged = b"k:".to_vec();
256 tagged.extend_from_slice(&k);
257 counter_rows.push((tagged, v));
258 }
259 for (k, v) in self.contexts_ks.prefix_iter_raw(CTX_COUNTER_PREFIX).await? {
260 let mut tagged = b"c:".to_vec();
261 tagged.extend_from_slice(&k);
262 counter_rows.push((tagged, v));
263 }
264 let counters = hash_rows(counter_rows);
265
266 Ok(CoveredState {
267 carveout_present,
268 jwt_fp,
269 acl_root,
270 counters,
271 })
272 }
273
274 async fn load_manifest(&self) -> Result<Option<Manifest>, AppError> {
275 Ok(self
276 .bootstrap_ks
277 .get_raw(MANIFEST_KEY)
278 .await?
279 .and_then(|b| Manifest::from_bytes(&b)))
280 }
281
282 async fn write_manifest(&self, m: &Manifest) -> Result<(), AppError> {
283 self.bootstrap_ks
284 .insert_raw(MANIFEST_KEY, m.to_bytes())
285 .await?;
286 self.bootstrap_ks.persist().await?;
287 Ok(())
288 }
289
290 async fn reseal(&self) -> Result<(), AppError> {
293 let cur_version = self.load_manifest().await?.map(|m| m.version);
294 let next_version = cur_version.map(|v| v + 1).unwrap_or(0);
295 let state = self.compute_state().await?;
296 let manifest = Manifest::sealed(&self.mac_key, next_version, state);
297
298 if let Some(anchor) = &self.anchor {
301 match cur_version {
302 Some(expected) => anchor.set(expected, next_version, manifest.mac).await?,
303 None => anchor.init(next_version, manifest.mac).await?,
304 }
305 }
306 self.write_manifest(&manifest).await
307 }
308
309 async fn verify_or_baseline(
313 &self,
314 allow_anchor_init: bool,
315 allow_unanchored: bool,
316 ) -> Result<(BootOutcome, bool), AppError> {
317 let current = self.compute_state().await?;
319 let (manifest, baselined) = match self.load_manifest().await? {
320 None => {
321 if !allow_anchor_init {
322 return Err(AppError::Internal(
323 "TEE integrity manifest is missing and allow_anchor_init is false — \
324 refusing to start. A missing manifest on a configured VTA is \
325 indistinguishable from a parent-deleted one; set \
326 tee.kms.allow_anchor_init = true for ONE boot to establish the \
327 baseline, then set it back to false."
328 .into(),
329 ));
330 }
331 let m = Manifest::sealed(&self.mac_key, 0, current);
332 self.write_manifest(&m).await?;
333 warn!(
334 "TEE integrity manifest established (version 0) under allow_anchor_init — \
335 set tee.kms.allow_anchor_init = false now that the baseline exists"
336 );
337 (m, true)
338 }
339 Some(stored) => {
340 if !stored.mac_valid(&self.mac_key) {
341 return Err(AppError::Internal(
342 "TEE integrity manifest MAC verification failed — the manifest was \
343 tampered with or the storage key changed. Refusing to start (P0.2). \
344 Restore a consistent backup or investigate parent-host compromise."
345 .into(),
346 ));
347 }
348 if stored.state != current {
349 return Err(AppError::Internal(format!(
350 "TEE integrity manifest mismatch — the on-disk state does not match the \
351 last sealed manifest (a covered singleton was deleted or the store was \
352 replayed to an inconsistent snapshot). Refusing to start (P0.2). [{}]",
353 describe_mismatch(&stored.state, ¤t),
354 )));
355 }
356 (stored, false)
357 }
358 };
359
360 let Some(anchor) = &self.anchor else {
362 return Ok((BootOutcome::manifest(baselined), false));
364 };
365 let m_version = manifest.version;
366 let digest = manifest.mac;
367
368 match anchor.read().await {
369 Err(e) => {
373 if !allow_unanchored {
374 return Err(AppError::Internal(format!(
375 "external anchor counter is unreachable ({e}) — cannot verify rollback \
376 freshness, refusing to start (P0.2b). This is a denial-of-service, not \
377 an integrity breach; set tee.kms.allow_unanchored = true to boot \
378 manifest-only for incident recovery."
379 )));
380 }
381 warn!(
382 error = %e,
383 "external anchor counter unreachable — booting UNANCHORED (manifest-only, \
384 P0.2a level) under allow_unanchored. Rollback protection is degraded until \
385 the counter is reachable again."
386 );
387 Ok((BootOutcome::Unanchored, true))
388 }
389 Ok(None) => {
392 if !allow_anchor_init {
393 return Err(AppError::Internal(
394 "external anchor counter does not exist and allow_anchor_init is false — \
395 refusing to start. Set tee.kms.allow_anchor_init = true for ONE boot to \
396 initialize it (first boot, or migration from a manifest-only VTA)."
397 .into(),
398 ));
399 }
400 anchor.init(m_version, digest).await?;
401 warn!(version = m_version, "external anchor counter initialized");
402 Ok((BootOutcome::manifest(baselined), false))
403 }
404 Ok(Some(n_ext)) if n_ext == m_version => Ok((BootOutcome::manifest(baselined), false)),
405 Ok(Some(n_ext)) => {
409 if !allow_unanchored {
410 let cause = if m_version < n_ext {
411 "the local store was rolled back to an older epoch"
412 } else {
413 "the external counter was rolled back, or a tightening op was torn \
414 mid-commit"
415 };
416 return Err(AppError::Internal(format!(
417 "external anchor version mismatch: manifest=v{m_version}, counter=v{n_ext} \
418 — {cause}. Refusing to start (P0.2b). Restore a consistent backup whose \
419 manifest matches the counter, or set tee.kms.allow_unanchored = true to \
420 re-anchor the counter to the local manifest."
421 )));
422 }
423 anchor.set(n_ext, m_version, digest).await?;
424 warn!(
425 from = n_ext,
426 to = m_version,
427 "external anchor counter RE-ANCHORED to the local manifest under \
428 allow_unanchored — set it back to false now that they agree"
429 );
430 Ok((BootOutcome::ReAnchored, false))
431 }
432 }
433 }
434}
435
436static SEALER: OnceLock<ManifestSealer> = OnceLock::new();
439static RESEAL_LOCK: Mutex<()> = Mutex::const_new(());
440
441pub async fn reseal_if_active() -> Result<(), AppError> {
453 let Some(sealer) = SEALER.get() else {
454 return Ok(());
455 };
456 let _guard = RESEAL_LOCK.lock().await;
457 sealer.reseal().await
458}
459
460#[derive(Debug, PartialEq, Eq)]
462pub enum BootOutcome {
463 Verified,
465 Baselined,
468 ReAnchored,
471 Unanchored,
474}
475
476impl BootOutcome {
477 fn manifest(baselined: bool) -> Self {
478 if baselined {
479 Self::Baselined
480 } else {
481 Self::Verified
482 }
483 }
484}
485
486#[allow(clippy::too_many_arguments)]
499pub async fn boot_verify_and_install(
500 mac_key: [u8; 32],
501 keys_ks: KeyspaceHandle,
502 bootstrap_ks: KeyspaceHandle,
503 acl_ks: KeyspaceHandle,
504 contexts_ks: KeyspaceHandle,
505 anchor: Option<Arc<dyn AnchorCounter>>,
506 allow_anchor_init: bool,
507 allow_unanchored: bool,
508) -> Result<BootOutcome, AppError> {
509 let mut sealer = ManifestSealer {
510 mac_key,
511 keys_ks,
512 bootstrap_ks,
513 acl_ks,
514 contexts_ks,
515 anchor,
516 };
517 let (outcome, drop_anchor) = sealer
518 .verify_or_baseline(allow_anchor_init, allow_unanchored)
519 .await?;
520 if drop_anchor {
523 sealer.anchor = None;
524 }
525 let _ = SEALER.set(sealer);
527 Ok(outcome)
528}
529
530fn describe_mismatch(stored: &CoveredState, current: &CoveredState) -> String {
533 let mut parts = Vec::new();
534 if stored.carveout_present != current.carveout_present {
535 parts.push(format!(
536 "carve-out sentinel (sealed present={}, now present={})",
537 stored.carveout_present, current.carveout_present
538 ));
539 }
540 if stored.jwt_fp != current.jwt_fp {
541 parts.push("JWT fingerprint".into());
542 }
543 if stored.acl_root != current.acl_root {
544 parts.push("ACL root".into());
545 }
546 if stored.counters != current.counters {
547 parts.push("path/context counters".into());
548 }
549 if parts.is_empty() {
550 "no field differs (internal error)".into()
551 } else {
552 parts.join("; ")
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::config::StoreConfig;
560 use crate::store::Store;
561
562 struct Ks {
563 keys: KeyspaceHandle,
564 bootstrap: KeyspaceHandle,
565 acl: KeyspaceHandle,
566 contexts: KeyspaceHandle,
567 _dir: tempfile::TempDir,
568 }
569
570 fn open() -> Ks {
571 let dir = tempfile::tempdir().unwrap();
572 let store = Store::open(&StoreConfig {
573 data_dir: dir.path().to_path_buf(),
574 })
575 .unwrap();
576 Ks {
577 keys: store.keyspace("keys").unwrap(),
578 bootstrap: store.keyspace("bootstrap").unwrap(),
579 acl: store.keyspace("acl").unwrap(),
580 contexts: store.keyspace("contexts").unwrap(),
581 _dir: dir,
582 }
583 }
584
585 fn sealer(ks: &Ks, mac_key: [u8; 32]) -> ManifestSealer {
586 sealer_with(ks, mac_key, None)
587 }
588
589 fn sealer_with(
590 ks: &Ks,
591 mac_key: [u8; 32],
592 anchor: Option<Arc<dyn AnchorCounter>>,
593 ) -> ManifestSealer {
594 ManifestSealer {
595 mac_key,
596 keys_ks: ks.keys.clone(),
597 bootstrap_ks: ks.bootstrap.clone(),
598 acl_ks: ks.acl.clone(),
599 contexts_ks: ks.contexts.clone(),
600 anchor,
601 }
602 }
603
604 struct MockCounter {
608 version: std::sync::Mutex<Option<u64>>,
609 unreachable: bool,
610 }
611 impl MockCounter {
612 fn empty() -> Arc<Self> {
613 Arc::new(Self {
614 version: std::sync::Mutex::new(None),
615 unreachable: false,
616 })
617 }
618 fn at(v: u64) -> Arc<Self> {
619 Arc::new(Self {
620 version: std::sync::Mutex::new(Some(v)),
621 unreachable: false,
622 })
623 }
624 fn unreachable() -> Arc<Self> {
625 Arc::new(Self {
626 version: std::sync::Mutex::new(None),
627 unreachable: true,
628 })
629 }
630 fn current(&self) -> Option<u64> {
631 *self.version.lock().unwrap()
632 }
633 }
634 impl AnchorCounter for MockCounter {
635 fn read(&self) -> BoxFuture<'_, Result<Option<u64>, AppError>> {
636 let r = if self.unreachable {
637 Err(AppError::Internal("egress denied".into()))
638 } else {
639 Ok(*self.version.lock().unwrap())
640 };
641 Box::pin(async move { r })
642 }
643 fn init(&self, version: u64, _digest: [u8; 32]) -> BoxFuture<'_, Result<(), AppError>> {
644 let r = if self.unreachable {
645 Err(AppError::Internal("egress denied".into()))
646 } else {
647 let mut g = self.version.lock().unwrap();
648 if g.is_some() {
649 Err(AppError::Conflict("counter already exists".into()))
650 } else {
651 *g = Some(version);
652 Ok(())
653 }
654 };
655 Box::pin(async move { r })
656 }
657 fn set(
658 &self,
659 expected: u64,
660 new: u64,
661 _digest: [u8; 32],
662 ) -> BoxFuture<'_, Result<(), AppError>> {
663 let r = if self.unreachable {
664 Err(AppError::Internal("egress denied".into()))
665 } else {
666 let mut g = self.version.lock().unwrap();
667 if *g == Some(expected) {
668 *g = Some(new);
669 Ok(())
670 } else {
671 Err(AppError::Conflict(format!(
672 "CAS failed: expected {expected}, found {:?}",
673 *g
674 )))
675 }
676 };
677 Box::pin(async move { r })
678 }
679 }
680
681 async fn vob(
684 s: &ManifestSealer,
685 allow_init: bool,
686 allow_unanchored: bool,
687 ) -> Result<BootOutcome, AppError> {
688 s.verify_or_baseline(allow_init, allow_unanchored)
689 .await
690 .map(|(o, _)| o)
691 }
692
693 #[test]
694 fn manifest_bytes_round_trip() {
695 let mac_key = [3u8; 32];
696 let state = CoveredState {
697 carveout_present: true,
698 jwt_fp: [7u8; 16],
699 acl_root: [9u8; 32],
700 counters: [11u8; 32],
701 };
702 let m = Manifest::sealed(&mac_key, 5, state.clone());
703 let bytes = m.to_bytes();
704 assert_eq!(bytes.len(), MANIFEST_LEN);
705 let back = Manifest::from_bytes(&bytes).expect("round trip");
706 assert_eq!(back.version, 5);
707 assert_eq!(back.state, state);
708 assert!(back.mac_valid(&mac_key));
709 assert!(!back.mac_valid(&[4u8; 32]));
711 }
712
713 #[test]
714 fn tampering_any_field_breaks_the_mac() {
715 let mac_key = [1u8; 32];
716 let state = CoveredState {
717 carveout_present: true,
718 jwt_fp: [0u8; 16],
719 acl_root: [0u8; 32],
720 counters: [0u8; 32],
721 };
722 let m = Manifest::sealed(&mac_key, 1, state);
723 let mut bytes = m.to_bytes();
724 bytes[8] ^= 1; let tampered = Manifest::from_bytes(&bytes).expect("decodes");
726 assert!(!tampered.mac_valid(&mac_key));
727 }
728
729 #[tokio::test]
735 async fn baseline_refused_without_allow_anchor_init() {
736 let ks = open();
737 let err = vob(&sealer(&ks, [2u8; 32]), false, false)
738 .await
739 .expect_err("missing manifest + flag false must fail closed");
740 assert!(format!("{err:?}").contains("allow_anchor_init"), "{err:?}");
741 }
742
743 #[tokio::test]
744 async fn baseline_then_verify_roundtrips() {
745 let ks = open();
746 let mac_key = [5u8; 32];
747 ks.keys
749 .insert_raw(CARVEOUT_KEY, b"closed".to_vec())
750 .await
751 .unwrap();
752 crate::store::counter::allocate_u32(&ks.keys, "path_counter:m/26'/0'")
753 .await
754 .unwrap();
755 crate::store::counter::allocate_u32(&ks.contexts, "ctx_counter")
756 .await
757 .unwrap();
758 let s = sealer(&ks, mac_key);
759
760 assert_eq!(vob(&s, true, false).await.unwrap(), BootOutcome::Baselined);
762 assert_eq!(vob(&s, false, false).await.unwrap(), BootOutcome::Verified);
764 }
765
766 #[tokio::test]
767 async fn deleting_a_covered_row_is_detected_at_boot() {
768 let ks = open();
769 let mac_key = [6u8; 32];
770 let s = sealer(&ks, mac_key);
771
772 ks.keys
774 .insert_raw(CARVEOUT_KEY, b"closed".to_vec())
775 .await
776 .unwrap();
777 vob(&s, true, false).await.unwrap();
778
779 ks.keys.remove(CARVEOUT_KEY).await.unwrap();
781
782 let err = vob(&s, false, false)
783 .await
784 .expect_err("deletion must be detected");
785 assert!(format!("{err:?}").contains("carve-out"), "{err:?}");
786 assert!(format!("{err:?}").contains("mismatch"), "{err:?}");
787 }
788
789 #[tokio::test]
790 async fn forged_manifest_fails_mac_at_boot() {
791 let ks = open();
792 let mac_key = [8u8; 32];
793 let s = sealer(&ks, mac_key);
794 vob(&s, true, false).await.unwrap();
795
796 let forged = Manifest::sealed(
799 &[0xFFu8; 32],
800 99,
801 CoveredState {
802 carveout_present: false,
803 jwt_fp: [0u8; 16],
804 acl_root: [0u8; 32],
805 counters: [0u8; 32],
806 },
807 );
808 ks.bootstrap
809 .insert_raw(MANIFEST_KEY, forged.to_bytes())
810 .await
811 .unwrap();
812
813 let err = vob(&s, false, false)
814 .await
815 .expect_err("forged manifest must fail the MAC");
816 assert!(
817 format!("{err:?}").contains("MAC verification failed"),
818 "{err:?}"
819 );
820 }
821
822 #[tokio::test]
823 async fn acl_change_reflected_after_reseal() {
824 let ks = open();
827 let mac_key = [10u8; 32];
828 let s = sealer(&ks, mac_key);
829 vob(&s, true, false).await.unwrap();
830
831 ks.acl
833 .insert_raw("acl:did:key:zAlice", b"{}".to_vec())
834 .await
835 .unwrap();
836 let next = s
837 .load_manifest()
838 .await
839 .unwrap()
840 .map(|m| m.version + 1)
841 .unwrap();
842 let state = s.compute_state().await.unwrap();
843 s.write_manifest(&Manifest::sealed(&mac_key, next, state))
844 .await
845 .unwrap();
846
847 assert_eq!(
848 vob(&s, false, false).await.unwrap(),
849 BootOutcome::Verified,
850 "post-reseal state must verify"
851 );
852 }
853
854 #[tokio::test]
855 async fn reseal_if_active_is_noop_without_installed_sealer() {
856 reseal_if_active().await.expect("no-op when not installed");
858 }
859
860 #[tokio::test]
863 async fn first_boot_initializes_counter_and_reseal_advances_it() {
864 let ks = open();
865 let counter = MockCounter::empty();
866 let s = sealer_with(&ks, [1u8; 32], Some(counter.clone()));
867
868 assert_eq!(vob(&s, true, false).await.unwrap(), BootOutcome::Baselined);
870 assert_eq!(counter.current(), Some(0));
871
872 ks.acl
874 .insert_raw("acl:did:key:zA", b"{}".to_vec())
875 .await
876 .unwrap();
877 s.reseal().await.unwrap();
878 assert_eq!(
879 counter.current(),
880 Some(1),
881 "reseal must CAS-bump the counter"
882 );
883
884 assert_eq!(vob(&s, false, false).await.unwrap(), BootOutcome::Verified);
886 }
887
888 #[tokio::test]
889 async fn local_rollback_behind_counter_is_detected() {
890 let ks = open();
893 let mac_key = [2u8; 32];
894 sealer(&ks, mac_key).reseal().await.unwrap(); let counter = MockCounter::at(1); let s = sealer_with(&ks, mac_key, Some(counter));
898
899 let err = vob(&s, false, false)
900 .await
901 .expect_err("manifest v0 < counter v1 must fail closed");
902 let msg = format!("{err:?}");
903 assert!(msg.contains("mismatch"), "{msg}");
904 assert!(msg.contains("rolled back"), "{msg}");
905 }
906
907 #[tokio::test]
908 async fn counter_rollback_ahead_of_manifest_is_detected() {
909 let ks = open();
912 let mac_key = [3u8; 32];
913 let plain = sealer(&ks, mac_key);
914 plain.reseal().await.unwrap(); plain.reseal().await.unwrap(); plain.reseal().await.unwrap(); let counter = MockCounter::at(0);
918 let s = sealer_with(&ks, mac_key, Some(counter));
919
920 let err = vob(&s, false, false)
921 .await
922 .expect_err("manifest v2 > counter v0 must fail closed");
923 assert!(format!("{err:?}").contains("mismatch"), "{err:?}");
924 }
925
926 #[tokio::test]
927 async fn allow_unanchored_reanchors_counter_to_manifest() {
928 let ks = open();
929 let mac_key = [4u8; 32];
930 let plain = sealer(&ks, mac_key);
931 plain.reseal().await.unwrap(); plain.reseal().await.unwrap(); let counter = MockCounter::at(5); let s = sealer_with(&ks, mac_key, Some(counter.clone()));
935
936 assert!(vob(&s, false, false).await.is_err());
938 assert_eq!(vob(&s, false, true).await.unwrap(), BootOutcome::ReAnchored);
940 assert_eq!(
941 counter.current(),
942 Some(1),
943 "counter re-anchored to manifest v1"
944 );
945 }
946
947 #[tokio::test]
948 async fn unreachable_counter_fails_closed_unless_allowed() {
949 let ks = open();
950 let mac_key = [7u8; 32];
951 sealer(&ks, mac_key).reseal().await.unwrap(); let counter = MockCounter::unreachable();
953 let s = sealer_with(&ks, mac_key, Some(counter));
954
955 let err = vob(&s, false, false)
957 .await
958 .expect_err("unreachable counter must fail closed");
959 assert!(format!("{err:?}").contains("unreachable"), "{err:?}");
960 assert_eq!(vob(&s, false, true).await.unwrap(), BootOutcome::Unanchored);
962 }
963}