1use crate::guard_state::{
26 GitCleanAttestation, GitHashAlgorithm, GuardPolicyIdentity, GuardRootRecord, GuardRootState,
27 GUARD_SCHEMA_VERSION,
28};
29use lru::LruCache;
30use parking_lot::Mutex;
31use redb::ReadableTable;
32use std::num::NonZeroUsize;
33
34pub const DEFAULT_HOT_INDEX_MEMORY: usize = 64 * 1024 * 1024;
36
37const ESTIMATED_BYTES_PER_ENTRY: usize = 320;
41
42fn max_entries_for_budget(budget: usize) -> NonZeroUsize {
44 let n = budget / ESTIMATED_BYTES_PER_ENTRY;
45 NonZeroUsize::new(n.max(1)).unwrap_or(NonZeroUsize::MIN)
47}
48
49pub struct HotAttestationIndex {
54 cache: Mutex<LruCache<HotKey, GitCleanAttestation>>,
55 budget: usize,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61struct HotKey {
62 hash_algorithm: GitHashAlgorithm,
63 blob_oid: String,
64 policy_short_digest: String,
65}
66
67impl HotAttestationIndex {
68 pub fn new() -> Self {
70 Self::with_budget(DEFAULT_HOT_INDEX_MEMORY)
71 }
72
73 pub fn with_budget(budget: usize) -> Self {
75 let cap = max_entries_for_budget(budget);
76 Self {
77 cache: Mutex::new(LruCache::new(cap)),
78 budget,
79 }
80 }
81
82 pub fn get(
84 &self,
85 hash_algorithm: GitHashAlgorithm,
86 blob_oid: &str,
87 policy_short_digest: &str,
88 ) -> Option<GitCleanAttestation> {
89 let key = HotKey {
90 hash_algorithm,
91 blob_oid: blob_oid.to_string(),
92 policy_short_digest: policy_short_digest.to_string(),
93 };
94 self.cache.lock().get(&key).cloned()
95 }
96
97 pub fn insert(&self, attestation: GitCleanAttestation) {
101 let key = HotKey {
102 hash_algorithm: attestation.hash_algorithm,
103 blob_oid: attestation.blob_oid.clone(),
104 policy_short_digest: attestation
105 .policy_identity
106 .short_digest()
107 .unwrap_or_default(),
108 };
109 self.cache.lock().put(key, attestation);
110 }
111
112 pub fn remove(
114 &self,
115 hash_algorithm: GitHashAlgorithm,
116 blob_oid: &str,
117 policy_short_digest: &str,
118 ) -> Option<GitCleanAttestation> {
119 let key = HotKey {
120 hash_algorithm,
121 blob_oid: blob_oid.to_string(),
122 policy_short_digest: policy_short_digest.to_string(),
123 };
124 self.cache.lock().pop(&key)
125 }
126
127 pub fn invalidate_for_policy(&self, current: &GuardPolicyIdentity) -> usize {
130 let current_short = current.short_digest().unwrap_or_default();
131 let mut removed = 0;
132 let mut to_remove = Vec::new();
133 {
134 let cache = self.cache.lock();
135 for (key, value) in cache.iter() {
136 let key_digest = &key.policy_short_digest;
137 let value_digest = value.policy_identity.short_digest().unwrap_or_default();
138 if key_digest != ¤t_short || value_digest != current_short {
139 to_remove.push(key.clone());
140 }
141 }
142 }
143 let mut cache = self.cache.lock();
144 for key in to_remove {
145 if cache.pop(&key).is_some() {
146 removed += 1;
147 }
148 }
149 removed
150 }
151
152 pub fn invalidate_policy_digest(&self, stale_digest: &str) -> usize {
155 if stale_digest.is_empty() {
156 return 0;
157 }
158 let mut removed = 0;
159 let mut to_remove = Vec::new();
160 {
161 let cache = self.cache.lock();
162 for (key, _) in cache.iter() {
163 if key.policy_short_digest == stale_digest {
164 to_remove.push(key.clone());
165 }
166 }
167 }
168 let mut cache = self.cache.lock();
169 for key in to_remove {
170 if cache.pop(&key).is_some() {
171 removed += 1;
172 }
173 }
174 removed
175 }
176
177 pub fn len(&self) -> usize {
179 self.cache.lock().len()
180 }
181
182 pub fn is_empty(&self) -> bool {
184 self.cache.lock().is_empty()
185 }
186
187 pub fn budget(&self) -> usize {
189 self.budget
190 }
191
192 pub fn clear(&self) {
194 self.cache.lock().clear();
195 }
196}
197
198impl Default for HotAttestationIndex {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct StoreMeta {
209 pub schema_version: u32,
211 pub store_uuid: [u8; 16],
213 pub created_version: String,
215 pub last_successful_migration: u32,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
221pub enum GuardStoreError {
222 #[error("guard store schema version {found} is newer than supported {supported}; upgrade keyhog or run `keyhog guard rebuild <root>`")]
224 SchemaTooNew {
225 found: u32,
227 supported: u32,
229 },
230 #[error("guard store schema version {found} is no longer supported; run `keyhog guard rebuild <root>` to recreate state")]
233 SchemaObsolete {
234 found: u32,
236 },
237 #[error("guard store is corrupt: {detail}; run `keyhog guard rebuild <root>`")]
239 Corrupt {
240 detail: String,
242 },
243 #[error("guard store path is unsafe: {detail}; run `keyhog guard repair <root>` or fix directory permissions")]
245 UnsafePath {
246 detail: String,
248 },
249 #[error("guard store I/O error: {0}; check disk space and permissions or run `keyhog guard repair <root>`")]
251 Io(String),
252 #[error("guard store was not closed cleanly; run `keyhog guard reconcile <root>`")]
255 UncleanShutdown,
256}
257
258pub fn check_schema_version(found: u32) -> Result<(), GuardStoreError> {
260 if found > GUARD_SCHEMA_VERSION {
261 return Err(GuardStoreError::SchemaTooNew {
262 found,
263 supported: GUARD_SCHEMA_VERSION,
264 });
265 }
266 if found < 1 {
267 return Err(GuardStoreError::SchemaObsolete { found });
268 }
269 Ok(())
271}
272
273#[derive(Debug, Default)]
278pub struct RootRegistry {
279 roots: std::collections::HashMap<Vec<u8>, GuardRootRecord>,
280}
281
282impl RootRegistry {
283 pub fn new() -> Self {
285 Self::default()
286 }
287
288 pub fn register(
290 &mut self,
291 canonical_path: Vec<u8>,
292 filesystem_identity: crate::guard_state::FilesystemIdentity,
293 filesystem_authority: crate::guard_state::FilesystemAuthority,
294 mode: crate::guard_state::GuardRootMode,
295 ) -> GuardRootRecord {
296 let record = GuardRootRecord {
297 canonical_path: canonical_path.clone(),
298 filesystem_identity,
299 filesystem_authority,
300 mode,
301 state: GuardRootState::Stopped,
302 terminal_sequence: 0,
303 accepted_event_sequence: 0,
304 completed_event_sequence: 0,
305 initial_reconciliation_time: None,
306 last_reconciliation_time: None,
307 backend_route_label: String::new(),
308 last_receipt: None,
309 recent_transitions: Vec::new(),
310 };
311 self.roots.insert(canonical_path, record.clone());
312 record
313 }
314
315 pub fn insert_record(&mut self, record: GuardRootRecord) {
319 self.roots.insert(record.canonical_path.clone(), record);
320 }
321
322 pub fn get(&self, canonical_path: &[u8]) -> Option<&GuardRootRecord> {
324 self.roots.get(canonical_path)
325 }
326
327 pub fn get_mut(&mut self, canonical_path: &[u8]) -> Option<&mut GuardRootRecord> {
329 self.roots.get_mut(canonical_path)
330 }
331
332 pub fn remove(&mut self, canonical_path: &[u8]) -> Option<GuardRootRecord> {
334 self.roots.remove(canonical_path)
335 }
336
337 pub fn list(&self) -> Vec<&GuardRootRecord> {
339 self.roots.values().collect()
340 }
341
342 pub fn len(&self) -> usize {
344 self.roots.len()
345 }
346
347 pub fn is_empty(&self) -> bool {
349 self.roots.is_empty()
350 }
351
352 pub fn count_by_state(&self, state: GuardRootState) -> usize {
354 self.roots.values().filter(|r| r.state == state).count()
355 }
356}
357
358const META_TABLE: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new("meta");
362
363const ROOTS_TABLE: redb::TableDefinition<&[u8], &[u8]> = redb::TableDefinition::new("roots");
365
366const ATTESTATIONS_TABLE: redb::TableDefinition<&[u8], &[u8]> =
369 redb::TableDefinition::new("git_clean_attestations");
370
371const ROOT_GAPS_TABLE: redb::TableDefinition<&[u8], &[u8]> =
374 redb::TableDefinition::new("root_gaps");
375
376const SERVICE_STATE_TABLE: redb::TableDefinition<&str, u8> =
379 redb::TableDefinition::new("service_state");
380
381pub struct DurableGuardStore {
387 db: redb::Database,
388 path: std::path::PathBuf,
389}
390
391impl DurableGuardStore {
392 pub fn open(path: &std::path::Path) -> Result<Self, GuardStoreError> {
398 if path.exists() {
401 let meta = std::fs::symlink_metadata(path)
402 .map_err(|e| GuardStoreError::Io(format!("stat guard store path: {e}")))?;
403 if meta.file_type().is_symlink() {
404 return Err(GuardStoreError::Io(
405 "guard store path is a symlink; refusing to open".to_string(),
406 ));
407 }
408 }
409 if let Some(parent) = path.parent() {
411 if !parent.exists() {
412 std::fs::create_dir_all(parent)
413 .map_err(|e| GuardStoreError::Io(format!("create guard store dir: {e}")))?;
414 #[cfg(unix)]
415 {
416 use std::os::unix::fs::PermissionsExt;
417 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
418 .map_err(|e| {
419 GuardStoreError::Io(format!("set guard store dir perms: {e}"))
420 })?;
421 }
422 }
423 }
424 let db = redb::Database::create(path)
425 .map_err(|e| GuardStoreError::Io(format!("open guard store: {e}")))?;
426 #[cfg(unix)]
428 {
429 use std::os::unix::fs::PermissionsExt;
430 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
431 .map_err(|e| GuardStoreError::Io(format!("set guard store perms: {e}")))?;
432 }
433 let store = Self {
434 db,
435 path: path.to_path_buf(),
436 };
437 store.ensure_schema()?;
438 Ok(store)
439 }
440 pub fn open_read_only(path: &std::path::Path) -> Result<Self, GuardStoreError> {
447 if !path.exists() {
448 return Err(GuardStoreError::Io(format!(
449 "guard store path '{}' does not exist",
450 path.display()
451 )));
452 }
453 let meta = std::fs::symlink_metadata(path)
454 .map_err(|e| GuardStoreError::Io(format!("stat guard store path: {e}")))?;
455 if meta.file_type().is_symlink() {
456 return Err(GuardStoreError::Io(
457 "guard store path is a symlink; refusing to open".to_string(),
458 ));
459 }
460 let db = redb::Database::open(path)
461 .map_err(|e| GuardStoreError::Io(format!("open guard store: {e}")))?;
462 let store = Self {
463 db,
464 path: path.to_path_buf(),
465 };
466 let txn = store
467 .db
468 .begin_read()
469 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
470 match txn.open_table(META_TABLE) {
471 Ok(meta_table) => {
472 let found_version: Option<u32> = meta_table
473 .get("schema_version")
474 .map_err(|e| GuardStoreError::Io(format!("read schema_version: {e}")))?
475 .map(|guard| {
476 let bytes: &[u8] = guard.value();
477 u32::from_le_bytes(bytes.try_into().unwrap_or([0, 0, 0, 0]))
478 });
479 match found_version {
480 Some(version) => check_schema_version(version)?,
481 None => {
482 return Err(GuardStoreError::Corrupt {
483 detail: "missing schema_version in meta table".to_string(),
484 });
485 }
486 }
487 }
488 Err(redb::TableError::TableDoesNotExist(_)) => {
489 return Err(GuardStoreError::Corrupt {
490 detail: "meta table does not exist".to_string(),
491 });
492 }
493 Err(e) => return Err(GuardStoreError::Io(format!("open meta table: {e}"))),
494 }
495 Ok(store)
496 }
497
498 pub fn path(&self) -> &std::path::Path {
500 &self.path
501 }
502
503 fn ensure_schema(&self) -> Result<(), GuardStoreError> {
505 let txn = self
506 .db
507 .begin_write()
508 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
509 {
510 let mut meta = txn
511 .open_table(META_TABLE)
512 .map_err(|e| GuardStoreError::Io(format!("open meta table: {e}")))?;
513 let found_version: Option<u32> = meta
514 .get("schema_version")
515 .map_err(|e| GuardStoreError::Io(format!("read schema_version: {e}")))?
516 .map(|guard| {
517 let bytes: &[u8] = guard.value();
518 u32::from_le_bytes(bytes.try_into().unwrap_or([0, 0, 0, 0]))
519 });
520 match found_version {
521 Some(version) => {
522 check_schema_version(version)?;
523 }
524 None => {
525 let version_bytes = GUARD_SCHEMA_VERSION.to_le_bytes();
527 meta.insert("schema_version", version_bytes.as_slice())
528 .map_err(|e| GuardStoreError::Io(format!("write schema_version: {e}")))?;
529 }
530 }
531 }
532 {
534 let _ = txn
535 .open_table(ROOTS_TABLE)
536 .map_err(|e| GuardStoreError::Io(format!("create roots table: {e}")))?;
537 let _ = txn
538 .open_table(ATTESTATIONS_TABLE)
539 .map_err(|e| GuardStoreError::Io(format!("create attestations table: {e}")))?;
540 let _ = txn
541 .open_table(ROOT_GAPS_TABLE)
542 .map_err(|e| GuardStoreError::Io(format!("create root_gaps table: {e}")))?;
543 let _ = txn
544 .open_table(SERVICE_STATE_TABLE)
545 .map_err(|e| GuardStoreError::Io(format!("create service_state table: {e}")))?;
546 }
547 txn.commit()
548 .map_err(|e| GuardStoreError::Io(format!("commit schema: {e}")))?;
549 Ok(())
550 }
551
552 pub fn load_roots(&self) -> Result<RootRegistry, GuardStoreError> {
554 let txn = self
555 .db
556 .begin_read()
557 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
558 let table = match txn.open_table(ROOTS_TABLE) {
559 Ok(t) => t,
560 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(RootRegistry::new()),
561 Err(e) => return Err(GuardStoreError::Io(format!("open roots table: {e}"))),
562 };
563 let mut registry = RootRegistry::new();
564 for entry in table
565 .range::<&[u8]>(..)
566 .map_err(|e| GuardStoreError::Io(format!("iterate roots: {e}")))?
567 {
568 let (key, value) =
569 entry.map_err(|e| GuardStoreError::Io(format!("read root entry: {e}")))?;
570 let record: GuardRootRecord =
571 serde_json::from_slice(value.value()).map_err(|e| GuardStoreError::Corrupt {
572 detail: format!("deserialize root record: {e}"),
573 })?;
574 registry.roots.insert(key.value().to_vec(), record);
575 }
576 Ok(registry)
577 }
578 pub fn get_root(
580 &self,
581 canonical_path: &[u8],
582 ) -> Result<Option<GuardRootRecord>, GuardStoreError> {
583 let txn = self
584 .db
585 .begin_read()
586 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
587 let table = match txn.open_table(ROOTS_TABLE) {
588 Ok(t) => t,
589 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
590 Err(e) => return Err(GuardStoreError::Io(format!("open roots table: {e}"))),
591 };
592 let entry = table
593 .get(canonical_path)
594 .map_err(|e| GuardStoreError::Io(format!("read root entry: {e}")))?;
595 match entry {
596 Some(value) => {
597 let record: GuardRootRecord =
598 serde_json::from_slice(value.value()).map_err(|e| {
599 GuardStoreError::Corrupt {
600 detail: format!("deserialize root record: {e}"),
601 }
602 })?;
603 Ok(Some(record))
604 }
605 None => Ok(None),
606 }
607 }
608
609 pub fn save_root(&self, record: &GuardRootRecord) -> Result<(), GuardStoreError> {
611 let txn = self
612 .db
613 .begin_write()
614 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
615 {
616 let mut table = txn
617 .open_table(ROOTS_TABLE)
618 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
619 let value = serde_json::to_vec(record)
620 .map_err(|e| GuardStoreError::Io(format!("serialize root record: {e}")))?;
621 table
622 .insert(record.canonical_path.as_slice(), value.as_slice())
623 .map_err(|e| GuardStoreError::Io(format!("insert root: {e}")))?;
624 }
625 txn.commit()
626 .map_err(|e| GuardStoreError::Io(format!("commit root: {e}")))?;
627 Ok(())
628 }
629
630 pub fn remove_root(&self, canonical_path: &[u8]) -> Result<(), GuardStoreError> {
632 let txn = self
633 .db
634 .begin_write()
635 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
636 {
637 let mut table = txn
638 .open_table(ROOTS_TABLE)
639 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
640 table
641 .remove(canonical_path)
642 .map_err(|e| GuardStoreError::Io(format!("remove root: {e}")))?;
643 }
644 txn.commit()
645 .map_err(|e| GuardStoreError::Io(format!("commit remove root: {e}")))?;
646 Ok(())
647 }
648
649 pub fn load_attestations(&self) -> Result<Vec<GitCleanAttestation>, GuardStoreError> {
651 let txn = self
652 .db
653 .begin_read()
654 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
655 let table = match txn.open_table(ATTESTATIONS_TABLE) {
656 Ok(t) => t,
657 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
658 Err(e) => return Err(GuardStoreError::Io(format!("open attestations table: {e}"))),
659 };
660 let mut attestations = Vec::new();
661 for entry in table
662 .range::<&[u8]>(..)
663 .map_err(|e| GuardStoreError::Io(format!("iterate attestations: {e}")))?
664 {
665 let (_, value) =
666 entry.map_err(|e| GuardStoreError::Io(format!("read attestation entry: {e}")))?;
667 let att: GitCleanAttestation =
668 serde_json::from_slice(value.value()).map_err(|e| GuardStoreError::Corrupt {
669 detail: format!("deserialize attestation: {e}"),
670 })?;
671 attestations.push(att);
672 }
673 Ok(attestations)
674 }
675
676 pub fn save_attestation(&self, att: &GitCleanAttestation) -> Result<(), GuardStoreError> {
678 let key = attestation_key(att);
679 let txn = self
680 .db
681 .begin_write()
682 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
683 {
684 let mut table = txn
685 .open_table(ATTESTATIONS_TABLE)
686 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
687 let value = serde_json::to_vec(att)
688 .map_err(|e| GuardStoreError::Io(format!("serialize attestation: {e}")))?;
689 table
690 .insert(key.as_slice(), value.as_slice())
691 .map_err(|e| GuardStoreError::Io(format!("insert attestation: {e}")))?;
692 }
693 txn.commit()
694 .map_err(|e| GuardStoreError::Io(format!("commit attestation: {e}")))?;
695 Ok(())
696 }
697
698 pub fn remove_attestation(&self, att: &GitCleanAttestation) -> Result<(), GuardStoreError> {
700 let key = attestation_key(att);
701 let txn = self
702 .db
703 .begin_write()
704 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
705 {
706 let mut table = txn
707 .open_table(ATTESTATIONS_TABLE)
708 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
709 table
710 .remove(key.as_slice())
711 .map_err(|e| GuardStoreError::Io(format!("remove attestation: {e}")))?;
712 }
713 txn.commit()
714 .map_err(|e| GuardStoreError::Io(format!("commit remove attestation: {e}")))?;
715 Ok(())
716 }
717
718 pub fn clear_attestations_for_policy(
720 &self,
721 policy_short: &str,
722 ) -> Result<usize, GuardStoreError> {
723 let txn = self
724 .db
725 .begin_write()
726 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
727 let removed = {
728 let mut table = txn
729 .open_table(ATTESTATIONS_TABLE)
730 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
731 let prefix = policy_short.as_bytes();
732 let mut count = 0usize;
733 let keys_to_remove: Vec<Vec<u8>> = table
734 .range::<&[u8]>(..)
735 .map_err(|e| GuardStoreError::Io(format!("iterate attestations: {e}")))?
736 .filter_map(|entry| {
737 let (key, _) = entry.ok()?;
738 let k = key.value();
739 if k.ends_with(prefix) {
742 Some(k.to_vec())
743 } else {
744 None
745 }
746 })
747 .collect();
748 for key in keys_to_remove {
749 table
750 .remove(key.as_slice())
751 .map_err(|e| GuardStoreError::Io(format!("remove attestation: {e}")))?;
752 count += 1;
753 }
754 count
755 };
756 txn.commit()
757 .map_err(|e| GuardStoreError::Io(format!("commit clear attestations: {e}")))?;
758 Ok(removed)
759 }
760
761 pub fn save_root_gap(
764 &self,
765 canonical_path: &[u8],
766 blob_oid: &str,
767 description: &str,
768 ) -> Result<(), GuardStoreError> {
769 let mut key = Vec::with_capacity(canonical_path.len() + 1 + blob_oid.len());
770 key.extend_from_slice(canonical_path);
771 key.push(0);
772 key.extend_from_slice(blob_oid.as_bytes());
773 let txn = self
774 .db
775 .begin_write()
776 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
777 {
778 let mut table = txn
779 .open_table(ROOT_GAPS_TABLE)
780 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
781 table
782 .insert(key.as_slice(), description.as_bytes())
783 .map_err(|e| GuardStoreError::Io(format!("insert root gap: {e}")))?;
784 }
785 txn.commit()
786 .map_err(|e| GuardStoreError::Io(format!("commit root gap: {e}")))?;
787 Ok(())
788 }
789
790 pub fn load_root_gaps(
792 &self,
793 canonical_path: &[u8],
794 ) -> Result<Vec<(String, String)>, GuardStoreError> {
795 let txn = self
796 .db
797 .begin_read()
798 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
799 let table = match txn.open_table(ROOT_GAPS_TABLE) {
800 Ok(t) => t,
801 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
802 Err(e) => return Err(GuardStoreError::Io(format!("open root_gaps table: {e}"))),
803 };
804 let prefix = canonical_path;
805 let mut gaps = Vec::new();
806 for entry in table
807 .range::<&[u8]>(..)
808 .map_err(|e| GuardStoreError::Io(format!("iterate root_gaps: {e}")))?
809 {
810 let (key, value) =
811 entry.map_err(|e| GuardStoreError::Io(format!("read root gap entry: {e}")))?;
812 let k = key.value();
813 if !k.starts_with(prefix) {
814 continue;
815 }
816 if k.len() <= prefix.len() || k[prefix.len()] != 0 {
820 continue;
821 }
822 let rest = &k[prefix.len() + 1..];
824 let blob_oid = String::from_utf8_lossy(rest).to_string();
825 let desc = String::from_utf8_lossy(value.value()).to_string();
826 gaps.push((blob_oid, desc));
827 }
828 Ok(gaps)
829 }
830
831 pub fn clear_root_gaps(&self, canonical_path: &[u8]) -> Result<usize, GuardStoreError> {
833 let txn = self
834 .db
835 .begin_write()
836 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
837 let removed = {
838 let mut table = txn
839 .open_table(ROOT_GAPS_TABLE)
840 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
841 let prefix = canonical_path;
842 let keys_to_remove: Vec<Vec<u8>> = table
843 .range::<&[u8]>(..)
844 .map_err(|e| GuardStoreError::Io(format!("iterate root_gaps: {e}")))?
845 .filter_map(|entry| {
846 let (key, _) = entry.ok()?;
847 let k = key.value();
848 if k.starts_with(prefix) && k.len() > prefix.len() && k[prefix.len()] == 0 {
849 Some(k.to_vec())
850 } else {
851 None
852 }
853 })
854 .collect();
855 let count = keys_to_remove.len();
856 for key in keys_to_remove {
857 table
858 .remove(key.as_slice())
859 .map_err(|e| GuardStoreError::Io(format!("remove root gap: {e}")))?;
860 }
861 count
862 };
863 txn.commit()
864 .map_err(|e| GuardStoreError::Io(format!("commit clear root gaps: {e}")))?;
865 Ok(removed)
866 }
867
868 pub fn mark_unclean_shutdown(&self) -> Result<(), GuardStoreError> {
872 let txn = self
873 .db
874 .begin_write()
875 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
876 {
877 let mut table = txn
878 .open_table(SERVICE_STATE_TABLE)
879 .map_err(|e| GuardStoreError::Io(format!("open service_state table: {e}")))?;
880 table
881 .insert("clean_shutdown", 0u8)
882 .map_err(|e| GuardStoreError::Io(format!("write clean_shutdown: {e}")))?;
883 }
884 txn.commit()
885 .map_err(|e| GuardStoreError::Io(format!("commit service state: {e}")))?;
886 Ok(())
887 }
888
889 pub fn mark_clean_shutdown(&self) -> Result<(), GuardStoreError> {
892 let txn = self
893 .db
894 .begin_write()
895 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
896 {
897 let mut table = txn
898 .open_table(SERVICE_STATE_TABLE)
899 .map_err(|e| GuardStoreError::Io(format!("open service_state table: {e}")))?;
900 table
901 .insert("clean_shutdown", 1u8)
902 .map_err(|e| GuardStoreError::Io(format!("write clean_shutdown: {e}")))?;
903 }
904 txn.commit()
905 .map_err(|e| GuardStoreError::Io(format!("commit service state: {e}")))?;
906 Ok(())
907 }
908
909 pub fn was_clean_shutdown(&self) -> Result<bool, GuardStoreError> {
913 let txn = self
914 .db
915 .begin_read()
916 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
917 let table = match txn.open_table(SERVICE_STATE_TABLE) {
918 Ok(t) => t,
919 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(false),
920 Err(e) => {
921 return Err(GuardStoreError::Io(format!(
922 "open service_state table: {e}"
923 )))
924 }
925 };
926 let value = table
927 .get("clean_shutdown")
928 .map_err(|e| GuardStoreError::Io(format!("read clean_shutdown: {e}")))?;
929 Ok(value.map(|v| v.value() == 1u8).unwrap_or(false))
930 }
931
932 pub fn save_root_with_gaps(
936 &self,
937 record: &GuardRootRecord,
938 gaps: &[(String, String)],
939 ) -> Result<(), GuardStoreError> {
940 let txn = self
941 .db
942 .begin_write()
943 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
944 {
945 let mut roots = txn
946 .open_table(ROOTS_TABLE)
947 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
948 let value = serde_json::to_vec(record)
949 .map_err(|e| GuardStoreError::Io(format!("serialize root record: {e}")))?;
950 roots
951 .insert(record.canonical_path.as_slice(), value.as_slice())
952 .map_err(|e| GuardStoreError::Io(format!("insert root: {e}")))?;
953
954 let mut gaps_table = txn
956 .open_table(ROOT_GAPS_TABLE)
957 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
958 let prefix = record.canonical_path.as_slice();
959 let keys_to_remove: Vec<Vec<u8>> = gaps_table
960 .range::<&[u8]>(..)
961 .map_err(|e| GuardStoreError::Io(format!("iterate root_gaps: {e}")))?
962 .filter_map(|entry| {
963 let (key, _) = entry.ok()?;
964 let k = key.value();
965 if k.starts_with(prefix) && k.len() > prefix.len() && k[prefix.len()] == 0 {
966 Some(k.to_vec())
967 } else {
968 None
969 }
970 })
971 .collect();
972 for key in keys_to_remove {
973 gaps_table
974 .remove(key.as_slice())
975 .map_err(|e| GuardStoreError::Io(format!("remove old root gap: {e}")))?;
976 }
977 for (blob_oid, desc) in gaps {
978 let mut key = Vec::with_capacity(prefix.len() + 1 + blob_oid.len());
979 key.extend_from_slice(prefix);
980 key.push(0);
981 key.extend_from_slice(blob_oid.as_bytes());
982 gaps_table
983 .insert(key.as_slice(), desc.as_bytes())
984 .map_err(|e| GuardStoreError::Io(format!("insert root gap: {e}")))?;
985 }
986 }
987 txn.commit()
988 .map_err(|e| GuardStoreError::Io(format!("commit root with gaps: {e}")))?;
989 Ok(())
990 }
991}
992
993fn attestation_key(att: &GitCleanAttestation) -> Vec<u8> {
996 let label = match att.hash_algorithm {
997 GitHashAlgorithm::Sha1 => "sha1",
998 GitHashAlgorithm::Sha256 => "sha256",
999 };
1000 let mut key = Vec::with_capacity(label.len() + 1 + att.blob_oid.len() + 1 + 64);
1001 key.extend_from_slice(label.as_bytes());
1002 key.push(0);
1003 key.extend_from_slice(att.blob_oid.as_bytes());
1004 key.push(0);
1005 key.extend_from_slice(att.policy_identity.detector_digest.as_bytes());
1006 key
1007}