1use crate::{ManagedSecretReference, SecretMaterial};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::fs::{self, File, OpenOptions};
8use std::io::{Read, Write};
9use std::num::NonZeroU64;
10#[cfg(target_os = "linux")]
11use std::os::fd::AsRawFd;
12#[cfg(unix)]
13use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt};
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16use std::sync::Arc;
17use thiserror::Error;
18use uuid::Uuid;
19
20#[cfg(any(test, feature = "test-fixtures"))]
21mod absence_fixture;
22
23pub const MANAGED_SECRET_ROOT: &str = "/var/lib/ahri-tre/secrets";
25pub const MANAGED_SECRET_ROTATION_ROOT: &str = "/var/lib/ahri-tre/secrets-rotation";
27pub const ROOT_IDENTITY_PATH: &str = "/run/secrets/ahri-tre/root-identity/value";
29pub const NEXT_ROOT_IDENTITY_PATH: &str = "/run/secrets/ahri-tre/root-identity-next/value";
31
32const STORE_FORMAT_VERSION: u64 = 1;
33const DATASTORE_REFERENCE_INVENTORY_VERSION: u64 = 1;
34const ENVELOPE_FORMAT_VERSION: u8 = 1;
35const ENVELOPE_MAGIC: &[u8; 16] = b"AHRI-TRE-SECRET\0";
36const HEADER_FILE: &str = "header.json";
37const LOCK_FILE: &str = "store.lock";
38const MANIFEST_FILE: &str = "manifest.age";
39const OBJECTS_DIRECTORY: &str = "objects";
40const MAX_HEADER_BYTES: u64 = 16 * 1024;
41const MAX_MANIFEST_BYTES: u64 = 64 * 1024 * 1024;
42const MAX_CIPHERTEXT_BYTES: u64 = 256 * 1024;
43const ZERO_HASH: [u8; 32] = [0; 32];
44const LOCK_CONTENT: &[u8] = b"ahri-tre-managed-store-lock-v1\n";
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
48pub struct ManagedSecretVerification {
49 deployment_id: Uuid,
50 recipient_fingerprint: String,
51 active_secret_count: usize,
52 audit_record_count: usize,
53}
54
55impl ManagedSecretVerification {
56 pub fn deployment_id(&self) -> Uuid {
57 self.deployment_id
58 }
59
60 pub fn recipient_fingerprint(&self) -> &str {
61 &self.recipient_fingerprint
62 }
63
64 pub fn active_secret_count(&self) -> usize {
65 self.active_secret_count
66 }
67
68 pub fn audit_record_count(&self) -> usize {
69 self.audit_record_count
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
75pub struct RootRotationVerification {
76 deployment_id: Uuid,
77 current_recipient_fingerprint: String,
78 next_recipient_fingerprint: String,
79 active_secret_count: usize,
80 audit_record_count: usize,
81}
82
83impl RootRotationVerification {
84 pub fn deployment_id(&self) -> Uuid {
85 self.deployment_id
86 }
87
88 pub fn current_recipient_fingerprint(&self) -> &str {
89 &self.current_recipient_fingerprint
90 }
91
92 pub fn next_recipient_fingerprint(&self) -> &str {
93 &self.next_recipient_fingerprint
94 }
95
96 pub fn active_secret_count(&self) -> usize {
97 self.active_secret_count
98 }
99
100 pub fn audit_record_count(&self) -> usize {
101 self.audit_record_count
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
107#[serde(transparent)]
108pub struct ManagedSecretVersion(NonZeroU64);
109
110impl ManagedSecretVersion {
111 pub fn get(self) -> u64 {
112 self.0.get()
113 }
114
115 fn first() -> Self {
116 Self(NonZeroU64::MIN)
117 }
118
119 fn next(self) -> Result<Self, ManagedSecretError> {
120 self.get()
121 .checked_add(1)
122 .and_then(NonZeroU64::new)
123 .map(Self)
124 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::VersionConflict))
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
130pub struct ManagedSecretMetadata {
131 reference: ManagedSecretReference,
132 version: ManagedSecretVersion,
133 created_at: DateTime<Utc>,
134}
135
136impl ManagedSecretMetadata {
137 pub fn reference(&self) -> &ManagedSecretReference {
138 &self.reference
139 }
140
141 pub fn version(&self) -> ManagedSecretVersion {
142 self.version
143 }
144
145 pub fn created_at(&self) -> DateTime<Utc> {
146 self.created_at
147 }
148}
149
150pub struct ResolvedManagedSecret {
152 metadata: ManagedSecretMetadata,
153 material: SecretMaterial,
154}
155
156impl ResolvedManagedSecret {
157 pub fn metadata(&self) -> &ManagedSecretMetadata {
158 &self.metadata
159 }
160
161 pub fn material(&self) -> &SecretMaterial {
162 &self.material
163 }
164}
165
166impl fmt::Debug for ResolvedManagedSecret {
167 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168 formatter
169 .debug_struct("ResolvedManagedSecret")
170 .field("metadata", &self.metadata)
171 .field("material", &self.material)
172 .finish()
173 }
174}
175
176pub struct StagedManagedSecret {
179 stage_id: Uuid,
180 metadata: ManagedSecretMetadata,
181 material: SecretMaterial,
182}
183
184impl StagedManagedSecret {
185 pub fn metadata(&self) -> &ManagedSecretMetadata {
186 &self.metadata
187 }
188
189 pub fn material(&self) -> &SecretMaterial {
190 &self.material
191 }
192}
193
194impl fmt::Debug for StagedManagedSecret {
195 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196 formatter
197 .debug_struct("StagedManagedSecret")
198 .field("stage_id", &self.stage_id)
199 .field("metadata", &self.metadata)
200 .field("material", &self.material)
201 .finish()
202 }
203}
204pub trait ManagedSecretBackend: Send + Sync {
206 fn create(
207 &self,
208 reference: &ManagedSecretReference,
209 material: &SecretMaterial,
210 ) -> Result<ManagedSecretMetadata, ManagedSecretError>;
211
212 fn resolve(
213 &self,
214 reference: &ManagedSecretReference,
215 ) -> Result<ResolvedManagedSecret, ManagedSecretError>;
216
217 fn lookup_active_metadata(
220 &self,
221 reference: &ManagedSecretReference,
222 ) -> Result<ManagedSecretMetadata, ManagedSecretError>;
223}
224
225mod owner_backend_sealed {
226 pub trait Sealed {}
227}
228
229pub trait ManagedSecretOwnerBackend: ManagedSecretBackend + owner_backend_sealed::Sealed {
232 fn stage_owner_replacement(
233 &self,
234 reference: &ManagedSecretReference,
235 material: &SecretMaterial,
236 expected_active_version: ManagedSecretVersion,
237 ) -> Result<StagedManagedSecret, ManagedSecretError>;
238
239 fn resume_owner_replacement(
240 &self,
241 reference: &ManagedSecretReference,
242 ) -> Result<Option<StagedManagedSecret>, ManagedSecretError>;
243
244 fn activate_owner_replacement(
245 &self,
246 staged: &StagedManagedSecret,
247 ) -> Result<ManagedSecretMetadata, ManagedSecretError>;
248
249 fn activate_owner_replacements(
250 &self,
251 staged: &[&StagedManagedSecret],
252 ) -> Result<Vec<ManagedSecretMetadata>, ManagedSecretError>;
253
254 fn owner_replacement_is_active(
255 &self,
256 staged: &StagedManagedSecret,
257 ) -> Result<bool, ManagedSecretError>;
258
259 fn discard_owner_replacement(
260 &self,
261 staged: &StagedManagedSecret,
262 ) -> Result<(), ManagedSecretError>;
263}
264
265mod trusted_backend_sealed {
266 pub trait Sealed {}
267}
268
269#[doc(hidden)]
271pub trait ManagedSecretTrustedBackend:
272 ManagedSecretOwnerBackend + trusted_backend_sealed::Sealed
273{
274 fn replace_trusted_reference_holder(
275 &self,
276 holder: &str,
277 references: &[ManagedSecretReference],
278 ) -> Result<(), ManagedSecretError>;
279
280 fn remove_trusted_reference_holder(&self, holder: &str) -> Result<(), ManagedSecretError>;
281
282 fn trusted_held_reference_status(
283 &self,
284 reference: &ManagedSecretReference,
285 ) -> Result<ManagedSecretReferenceStatus, ManagedSecretError>;
286
287 fn trusted_reference_holders(
288 &self,
289 reference: &ManagedSecretReference,
290 ) -> Result<Vec<String>, ManagedSecretError>;
291
292 fn datastore_reference_inventory_is_complete(&self) -> Result<bool, ManagedSecretError>;
293
294 fn remove_with_repository_evidence(
295 &self,
296 reference: &ManagedSecretReference,
297 proof: ManagedSecretRemovalProof,
298 ) -> Result<ManagedSecretMetadata, ManagedSecretError>;
299}
300
301pub struct ManagedSecretIdentity {
303 identity: age::x25519::Identity,
304 recipient: age::x25519::Recipient,
305}
306
307impl ManagedSecretIdentity {
308 pub fn from_material(material: &SecretMaterial) -> Result<Self, ManagedSecretError> {
311 material.expose(|bytes| {
312 let encoded = std::str::from_utf8(bytes)
313 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::InvalidIdentity))?
314 .trim();
315 let identity = age::x25519::Identity::from_str(encoded)
316 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::InvalidIdentity))?;
317 let recipient = identity.to_public();
318 Ok(Self {
319 identity,
320 recipient,
321 })
322 })
323 }
324
325 pub fn load() -> Result<Self, ManagedSecretError> {
327 let material = crate::load_root_identity_material()
328 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::InvalidIdentity))?;
329 Self::from_material(&material)
330 }
331
332 pub fn load_next() -> Result<Self, ManagedSecretError> {
334 let material = crate::load_next_root_identity_material()
335 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::InvalidIdentity))?;
336 Self::from_material(&material)
337 }
338
339 #[cfg(any(test, feature = "test-fixtures"))]
342 #[doc(hidden)]
343 pub fn load_from_injected_root_for_test(root: &Path) -> Result<Self, ManagedSecretError> {
344 let material = crate::load_root_identity_material_from_root(root)
345 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::InvalidIdentity))?;
346 Self::from_material(&material)
347 }
348
349 #[cfg(any(test, feature = "test-fixtures"))]
351 #[doc(hidden)]
352 pub fn load_next_from_injected_root_for_test(root: &Path) -> Result<Self, ManagedSecretError> {
353 let material = crate::load_next_root_identity_material_from_root(root)
354 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::InvalidIdentity))?;
355 Self::from_material(&material)
356 }
357
358 fn recipient_string(&self) -> String {
359 self.recipient.to_string()
360 }
361}
362
363impl fmt::Debug for ManagedSecretIdentity {
364 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
365 formatter.write_str("ManagedSecretIdentity(<redacted>)")
366 }
367}
368
369pub struct LocalManagedSecretStore {
371 root: PathBuf,
372 anchor: Arc<StoreAnchor>,
373 deployment_id: Uuid,
374 identity: Arc<ManagedSecretIdentity>,
375 faults: Arc<dyn MutationFaultInjector>,
376}
377
378pub struct SelectedManagedSecretReader {
381 store: LocalManagedSecretStore,
382}
383
384impl SelectedManagedSecretReader {
385 pub fn resolve(
387 &self,
388 reference: &ManagedSecretReference,
389 ) -> Result<ResolvedManagedSecret, ManagedSecretError> {
390 self.store.resolve(reference)
391 }
392
393 pub fn inspect(
394 &self,
395 reference: &ManagedSecretReference,
396 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
397 self.store.inspect(reference)
398 }
399}
400
401impl fmt::Debug for SelectedManagedSecretReader {
402 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
403 formatter
404 .debug_struct("SelectedManagedSecretReader")
405 .field("store", &self.store)
406 .finish()
407 }
408}
409
410impl fmt::Debug for LocalManagedSecretStore {
411 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
412 formatter
413 .debug_struct("LocalManagedSecretStore")
414 .field("root", &ahri_tre_security::REDACTED_RESTRICTED_REFERENCE)
415 .field("deployment_id", &self.deployment_id)
416 .field("recipient", &self.identity.recipient_string())
417 .finish_non_exhaustive()
418 }
419}
420
421struct StoreAnchor {
422 root: File,
423 objects: File,
424 root_identity: DirectoryIdentity,
425 objects_identity: DirectoryIdentity,
426 require_write: bool,
427}
428
429impl StoreAnchor {
430 fn open(root_path: &Path, require_write: bool) -> Result<Self, ManagedSecretError> {
431 let root = open_directory(root_path)?;
432 let root_metadata = root
433 .metadata()
434 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
435 validate_directory_metadata(&root_metadata, require_write)?;
436 let objects = open_directory_at(&root, Path::new(OBJECTS_DIRECTORY))?;
437 let objects_metadata = objects
438 .metadata()
439 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
440 validate_directory_metadata(&objects_metadata, require_write)?;
441 Ok(Self {
442 root_identity: directory_identity(&root_metadata),
443 objects_identity: directory_identity(&objects_metadata),
444 root,
445 objects,
446 require_write,
447 })
448 }
449
450 fn validate_current(&self, root_path: &Path) -> Result<(), ManagedSecretError> {
451 let metadata = fs::symlink_metadata(root_path)
452 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
453 validate_directory_metadata(&metadata, self.require_write)?;
454 if directory_identity(&metadata) != self.root_identity {
455 return Err(ManagedSecretError::new(
456 ManagedSecretErrorKind::UnsafeFilesystem,
457 ));
458 }
459 let current_objects = open_directory_at(&self.root, Path::new(OBJECTS_DIRECTORY))?;
460 let metadata = current_objects
461 .metadata()
462 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
463 validate_directory_metadata(&metadata, self.require_write)?;
464 if directory_identity(&metadata) != self.objects_identity {
465 return Err(ManagedSecretError::new(
466 ManagedSecretErrorKind::UnsafeFilesystem,
467 ));
468 }
469 Ok(())
470 }
471}
472
473#[derive(Clone, Copy, PartialEq, Eq)]
474struct DirectoryIdentity {
475 device: u64,
476 inode: u64,
477}
478
479#[cfg(unix)]
480fn directory_identity(metadata: &fs::Metadata) -> DirectoryIdentity {
481 DirectoryIdentity {
482 device: metadata.dev(),
483 inode: metadata.ino(),
484 }
485}
486
487#[cfg(not(unix))]
488fn directory_identity(_metadata: &fs::Metadata) -> DirectoryIdentity {
489 DirectoryIdentity {
490 device: 0,
491 inode: 0,
492 }
493}
494
495impl LocalManagedSecretStore {
496 pub fn initialize(
497 deployment_id: Uuid,
498 identity: ManagedSecretIdentity,
499 ) -> Result<Self, ManagedSecretError> {
500 Self::initialize_at(Path::new(MANAGED_SECRET_ROOT), deployment_id, identity)
501 }
502
503 pub fn open(
504 deployment_id: Uuid,
505 identity: ManagedSecretIdentity,
506 ) -> Result<Self, ManagedSecretError> {
507 Self::open_at(Path::new(MANAGED_SECRET_ROOT), deployment_id, identity)
508 }
509
510 pub fn open_verified(
512 deployment_id: Uuid,
513 identity: ManagedSecretIdentity,
514 ) -> Result<Self, ManagedSecretError> {
515 Self::open_verified_at(Path::new(MANAGED_SECRET_ROOT), deployment_id, identity)
516 }
517
518 pub fn open_selected_read_only(
521 deployment_id: Uuid,
522 identity: ManagedSecretIdentity,
523 ) -> Result<SelectedManagedSecretReader, ManagedSecretError> {
524 Self::open_selected_read_only_at(Path::new(MANAGED_SECRET_ROOT), deployment_id, identity)
525 }
526
527 pub fn verify_all(
529 deployment_id: Uuid,
530 identity: ManagedSecretIdentity,
531 ) -> Result<ManagedSecretVerification, ManagedSecretError> {
532 Self::verify_all_at(Path::new(MANAGED_SECRET_ROOT), deployment_id, identity)
533 }
534
535 pub fn rotate_root(deployment_id: Uuid) -> Result<RootRotationVerification, RootRotationError> {
538 Self::rotate_root_at_with_identity_loader(
539 Path::new(MANAGED_SECRET_ROOT),
540 Path::new(MANAGED_SECRET_ROTATION_ROOT),
541 deployment_id,
542 true,
543 &NoRotationFaults,
544 || {
545 Ok((
546 ManagedSecretIdentity::load()?,
547 ManagedSecretIdentity::load_next()?,
548 ))
549 },
550 )
551 }
552
553 #[cfg(any(test, feature = "test-fixtures"))]
556 #[doc(hidden)]
557 pub fn verify_all_at_for_test(
558 root: &Path,
559 deployment_id: Uuid,
560 identity: ManagedSecretIdentity,
561 ) -> Result<ManagedSecretVerification, ManagedSecretError> {
562 Self::verify_all_at(root, deployment_id, identity)
563 }
564
565 fn verify_all_at(
566 root: &Path,
567 deployment_id: Uuid,
568 identity: ManagedSecretIdentity,
569 ) -> Result<ManagedSecretVerification, ManagedSecretError> {
570 ensure_supported_platform()?;
571 validate_directory(root, false)?;
572 let anchor = StoreAnchor::open(root, false)?;
573 let _lock = StoreLock::acquire(&anchor.root, LockMode::NonBlockingShared)?;
574 let verified = verify_anchored_store(&anchor, root, deployment_id, &identity)?;
575 Ok(verified.report())
576 }
577
578 #[cfg(any(test, feature = "test-fixtures"))]
581 #[doc(hidden)]
582 pub fn rotate_root_at_for_test(
583 active_root: &Path,
584 staging_root: &Path,
585 deployment_id: Uuid,
586 current_identity: ManagedSecretIdentity,
587 next_identity: ManagedSecretIdentity,
588 ) -> Result<RootRotationVerification, RootRotationError> {
589 Self::rotate_root_at(
590 active_root,
591 staging_root,
592 deployment_id,
593 current_identity,
594 next_identity,
595 false,
596 )
597 }
598
599 #[cfg(any(test, feature = "test-fixtures"))]
600 fn rotate_root_at(
601 active_root: &Path,
602 staging_root: &Path,
603 deployment_id: Uuid,
604 current_identity: ManagedSecretIdentity,
605 next_identity: ManagedSecretIdentity,
606 require_read_only_active_mount: bool,
607 ) -> Result<RootRotationVerification, RootRotationError> {
608 Self::rotate_root_at_with_faults(
609 active_root,
610 staging_root,
611 deployment_id,
612 current_identity,
613 next_identity,
614 require_read_only_active_mount,
615 &NoRotationFaults,
616 )
617 }
618
619 #[cfg(any(test, feature = "test-fixtures"))]
620 fn rotate_root_at_with_faults(
621 active_root: &Path,
622 staging_root: &Path,
623 deployment_id: Uuid,
624 current_identity: ManagedSecretIdentity,
625 next_identity: ManagedSecretIdentity,
626 require_read_only_active_mount: bool,
627 faults: &dyn RotationFaultInjector,
628 ) -> Result<RootRotationVerification, RootRotationError> {
629 Self::rotate_root_at_with_identity_loader(
630 active_root,
631 staging_root,
632 deployment_id,
633 require_read_only_active_mount,
634 faults,
635 move || Ok((current_identity, next_identity)),
636 )
637 }
638
639 fn rotate_root_at_with_identity_loader(
640 active_root: &Path,
641 staging_root: &Path,
642 deployment_id: Uuid,
643 require_read_only_active_mount: bool,
644 faults: &dyn RotationFaultInjector,
645 load_identities: impl FnOnce() -> Result<
646 (ManagedSecretIdentity, ManagedSecretIdentity),
647 ManagedSecretError,
648 >,
649 ) -> Result<RootRotationVerification, RootRotationError> {
650 ensure_supported_platform()
651 .and_then(|()| validate_directory(active_root, false))
652 .map_err(|error| RootRotationError::at(RootRotationPhase::Prerequisites, error))?;
653 let active = StoreAnchor::open(active_root, false)
654 .map_err(|error| RootRotationError::at(RootRotationPhase::Prerequisites, error))?;
655 faults
656 .check(RotationCheckpoint::Lock)
657 .map_err(|error| RootRotationError::at(RootRotationPhase::Lock, error))?;
658 let _lock = StoreLock::acquire(&active.root, LockMode::NonBlockingExclusive)
659 .map_err(|error| RootRotationError::at(RootRotationPhase::Lock, error))?;
660
661 let (current_identity, next_identity) = load_identities()
662 .map_err(|error| RootRotationError::at(RootRotationPhase::Prerequisites, error))?;
663
664 if require_read_only_active_mount
665 && !directory_mount_is_read_only(&active.root)
666 .map_err(|error| RootRotationError::at(RootRotationPhase::Prerequisites, error))?
667 {
668 return Err(RootRotationError::at(
669 RootRotationPhase::Prerequisites,
670 ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem),
671 ));
672 }
673 let staging = validate_empty_staging_directory(staging_root, &active)
674 .map_err(|error| RootRotationError::at(RootRotationPhase::Prerequisites, error))?;
675 if current_identity.recipient_string() == next_identity.recipient_string() {
676 return Err(RootRotationError::at(
677 RootRotationPhase::Prerequisites,
678 ManagedSecretError::new(ManagedSecretErrorKind::RecipientMismatch),
679 ));
680 }
681 faults
682 .check(RotationCheckpoint::SourceDecrypt)
683 .and_then(|()| faults.check(RotationCheckpoint::SourceAudit))
684 .map_err(|error| RootRotationError::at(RootRotationPhase::SourceVerification, error))?;
685 let verified =
686 verify_anchored_store(&active, active_root, deployment_id, ¤t_identity).map_err(
687 |error| RootRotationError::at(RootRotationPhase::SourceVerification, error),
688 )?;
689
690 let staged_anchor = populate_rotation_staging(&staging, &verified, &next_identity, faults)
691 .map_err(|error| RootRotationError::at(RootRotationPhase::StagingWrite, error))?;
692 faults
693 .check(RotationCheckpoint::DestinationVerification)
694 .map_err(|error| {
695 RootRotationError::at(RootRotationPhase::DestinationVerification, error)
696 })?;
697 let _staged_lock = StoreLock::acquire(&staged_anchor.root, LockMode::NonBlockingShared)
698 .map_err(|error| {
699 RootRotationError::at(RootRotationPhase::DestinationVerification, error)
700 })?;
701 let staged =
702 verify_anchored_store(&staged_anchor, &staging.path, deployment_id, &next_identity)
703 .map_err(|error| {
704 RootRotationError::at(RootRotationPhase::DestinationVerification, error)
705 })?
706 .report();
707 if staged.active_secret_count != verified.manifest.entries.len()
708 || staged.audit_record_count != verified.manifest.audit.len()
709 {
710 return Err(RootRotationError::at(
711 RootRotationPhase::DestinationVerification,
712 ManagedSecretError::new(ManagedSecretErrorKind::Integrity),
713 ));
714 }
715 Ok(RootRotationVerification {
716 deployment_id,
717 current_recipient_fingerprint: recipient_fingerprint(&verified.header.recipient),
718 next_recipient_fingerprint: staged.recipient_fingerprint,
719 active_secret_count: staged.active_secret_count,
720 audit_record_count: staged.audit_record_count,
721 })
722 }
723
724 pub fn list(&self) -> Result<Vec<ManagedSecretMetadata>, ManagedSecretError> {
727 self.anchor.validate_current(&self.root)?;
728 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
729 self.anchor.validate_current(&self.root)?;
730 let header = read_header(&self.anchor.root)?;
731 validate_header(&header, self.deployment_id, &self.identity)?;
732 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
733 validate_manifest(&manifest, &header)?;
734 validate_store_tree(&self.anchor, &manifest)?;
735 self.anchor.validate_current(&self.root)?;
736 Ok(manifest
737 .entries
738 .into_iter()
739 .map(|(reference, entry)| ManagedSecretMetadata {
740 reference: ManagedSecretReference::from_str(&reference)
741 .expect("validated manifest contains canonical references"),
742 version: entry.version,
743 created_at: entry.created_at,
744 })
745 .collect())
746 }
747
748 pub fn inspect(
750 &self,
751 reference: &ManagedSecretReference,
752 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
753 Ok(self.resolve(reference)?.metadata().clone())
754 }
755
756 fn lookup_active_metadata(
757 &self,
758 reference: &ManagedSecretReference,
759 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
760 self.anchor.validate_current(&self.root)?;
761 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
762 self.anchor.validate_current(&self.root)?;
763 let header = read_header(&self.anchor.root)?;
764 validate_header(&header, self.deployment_id, &self.identity)?;
765 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
766 validate_manifest(&manifest, &header)?;
767 validate_store_tree(&self.anchor, &manifest)?;
768 let entry = manifest
769 .entries
770 .get(reference.as_str())
771 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
772 self.anchor.validate_current(&self.root)?;
773 Ok(ManagedSecretMetadata {
774 reference: reference.clone(),
775 version: entry.version,
776 created_at: entry.created_at,
777 })
778 }
779
780 fn stage_replacement(
784 &self,
785 reference: &ManagedSecretReference,
786 material: &SecretMaterial,
787 expected_active_version: ManagedSecretVersion,
788 ) -> Result<StagedManagedSecret, ManagedSecretError> {
789 self.anchor.validate_current(&self.root)?;
790 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
791 self.anchor.validate_current(&self.root)?;
792 let header = read_header(&self.anchor.root)?;
793 validate_header(&header, self.deployment_id, &self.identity)?;
794 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
795 validate_manifest(&manifest, &header)?;
796 recover_uncommitted_files(&self.anchor, &manifest)?;
797 let active = manifest
798 .entries
799 .get(reference.as_str())
800 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
801 if active.version != expected_active_version {
802 return Err(ManagedSecretError::new(
803 ManagedSecretErrorKind::VersionConflict,
804 ));
805 }
806 if manifest.staged.contains_key(reference.as_str()) {
807 return Err(ManagedSecretError::new(
808 ManagedSecretErrorKind::AlreadyExists,
809 ));
810 }
811
812 let version = expected_active_version.next()?;
813 let created_at = Utc::now();
814 let metadata = ManagedSecretMetadata {
815 reference: reference.clone(),
816 version,
817 created_at,
818 };
819 let plaintext = encode_secret_envelope(
820 self.deployment_id,
821 &self.identity.recipient_string(),
822 &metadata,
823 material,
824 )?;
825 let ciphertext = encrypt(&self.identity.recipient, &plaintext)?;
826 let object_name = format!("object-{}.age", Uuid::new_v4());
827 atomic_write(
828 &self.anchor.objects,
829 &object_name,
830 &ciphertext,
831 self.faults.as_ref(),
832 MutationPhase::Object,
833 )
834 .map_err(AtomicWriteFailure::into_error)?;
835 if let Err(error) = verify_persisted_object(
836 &self.anchor.objects,
837 &object_name,
838 self.deployment_id,
839 &self.identity,
840 &metadata,
841 material,
842 ) {
843 remove_file_if_present(&self.anchor.objects, &object_name)?;
844 sync_directory(&self.anchor.objects)?;
845 return Err(error);
846 }
847
848 let stage_id = Uuid::new_v4();
849 manifest.staged.insert(
850 reference.as_str().to_string(),
851 StagedManifestEntry {
852 stage_id,
853 expected_active_version,
854 version,
855 object: object_name.clone(),
856 created_at,
857 },
858 );
859 if let Err(failure) = write_manifest(
860 &self.anchor.root,
861 &manifest,
862 &self.identity,
863 self.faults.as_ref(),
864 ) {
865 if failure.renamed {
866 return Err(ManagedSecretError::new(
867 ManagedSecretErrorKind::CommitUncertain,
868 ));
869 }
870 remove_file_if_present(&self.anchor.objects, &object_name)?;
871 sync_directory(&self.anchor.objects)?;
872 return Err(failure.into_error());
873 }
874 let material = material
875 .expose(|bytes| SecretMaterial::try_from(bytes.to_vec()))
876 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::SizeLimit))?;
877 Ok(StagedManagedSecret {
878 stage_id,
879 metadata,
880 material,
881 })
882 }
883
884 fn pending_replacement(
886 &self,
887 reference: &ManagedSecretReference,
888 ) -> Result<Option<StagedManagedSecret>, ManagedSecretError> {
889 self.anchor.validate_current(&self.root)?;
890 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
891 self.anchor.validate_current(&self.root)?;
892 let header = read_header(&self.anchor.root)?;
893 validate_header(&header, self.deployment_id, &self.identity)?;
894 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
895 validate_manifest(&manifest, &header)?;
896 validate_store_tree(&self.anchor, &manifest)?;
897 let Some(staged) = manifest.staged.get(reference.as_str()) else {
898 return Ok(None);
899 };
900 let entry = ManifestEntry {
901 version: staged.version,
902 object: staged.object.clone(),
903 created_at: staged.created_at,
904 };
905 let ciphertext =
906 read_store_file(&self.anchor.objects, &entry.object, MAX_CIPHERTEXT_BYTES)?;
907 let plaintext = decrypt(&self.identity.identity, &ciphertext)?;
908 let resolved = decode_secret_envelope(
909 &plaintext,
910 self.deployment_id,
911 &self.identity.recipient_string(),
912 reference,
913 &entry,
914 )?;
915 Ok(Some(StagedManagedSecret {
916 stage_id: staged.stage_id,
917 metadata: resolved.metadata,
918 material: resolved.material,
919 }))
920 }
921
922 fn activate_replacement(
925 &self,
926 staged: &StagedManagedSecret,
927 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
928 self.anchor.validate_current(&self.root)?;
929 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
930 self.anchor.validate_current(&self.root)?;
931 let header = read_header(&self.anchor.root)?;
932 validate_header(&header, self.deployment_id, &self.identity)?;
933 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
934 validate_manifest(&manifest, &header)?;
935 let pending = manifest
936 .staged
937 .get(staged.metadata.reference().as_str())
938 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
939 if pending.stage_id != staged.stage_id
940 || pending.version != staged.metadata.version()
941 || pending.created_at != staged.metadata.created_at()
942 {
943 return Err(ManagedSecretError::new(
944 ManagedSecretErrorKind::VersionConflict,
945 ));
946 }
947 verify_persisted_object(
948 &self.anchor.objects,
949 &pending.object,
950 self.deployment_id,
951 &self.identity,
952 &staged.metadata,
953 &staged.material,
954 )?;
955 let active = manifest
956 .entries
957 .get(staged.metadata.reference().as_str())
958 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
959 if active.version != pending.expected_active_version {
960 return Err(ManagedSecretError::new(
961 ManagedSecretErrorKind::VersionConflict,
962 ));
963 }
964 let prior_object = active.object.clone();
965 let pending = manifest
966 .staged
967 .remove(staged.metadata.reference().as_str())
968 .expect("validated staged replacement remains present");
969 manifest.entries.insert(
970 staged.metadata.reference().as_str().to_string(),
971 ManifestEntry {
972 version: pending.version,
973 object: pending.object,
974 created_at: pending.created_at,
975 },
976 );
977 manifest.generation = manifest
978 .generation
979 .checked_add(1)
980 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::VersionConflict))?;
981 manifest.audit.push(AuditRecord::mutation(
982 manifest.audit.last(),
983 AuditOperation::Replace,
984 staged.metadata.reference(),
985 staged.metadata.version(),
986 staged.metadata.created_at(),
987 self.deployment_id,
988 )?);
989 if let Err(failure) = write_manifest(
990 &self.anchor.root,
991 &manifest,
992 &self.identity,
993 self.faults.as_ref(),
994 ) {
995 return Err(if failure.renamed {
996 ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain)
997 } else {
998 failure.into_error()
999 });
1000 }
1001 remove_file_if_present(&self.anchor.objects, &prior_object)
1002 .and_then(|()| sync_directory(&self.anchor.objects))
1003 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))?;
1004 self.anchor
1005 .validate_current(&self.root)
1006 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))?;
1007 Ok(staged.metadata.clone())
1008 }
1009
1010 fn activate_replacements(
1012 &self,
1013 staged: &[&StagedManagedSecret],
1014 ) -> Result<Vec<ManagedSecretMetadata>, ManagedSecretError> {
1015 if staged.is_empty() {
1016 return Ok(Vec::new());
1017 }
1018 self.anchor.validate_current(&self.root)?;
1019 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
1020 self.anchor.validate_current(&self.root)?;
1021 let header = read_header(&self.anchor.root)?;
1022 validate_header(&header, self.deployment_id, &self.identity)?;
1023 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
1024 validate_manifest(&manifest, &header)?;
1025 let mut references = BTreeSet::new();
1026 let mut prior_objects = Vec::with_capacity(staged.len());
1027 for candidate in staged {
1028 let reference = candidate.metadata.reference();
1029 if !references.insert(reference.as_str()) {
1030 return Err(ManagedSecretError::new(
1031 ManagedSecretErrorKind::VersionConflict,
1032 ));
1033 }
1034 let pending = manifest
1035 .staged
1036 .get(reference.as_str())
1037 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
1038 if pending.stage_id != candidate.stage_id
1039 || pending.version != candidate.metadata.version()
1040 || pending.created_at != candidate.metadata.created_at()
1041 {
1042 return Err(ManagedSecretError::new(
1043 ManagedSecretErrorKind::VersionConflict,
1044 ));
1045 }
1046 verify_persisted_object(
1047 &self.anchor.objects,
1048 &pending.object,
1049 self.deployment_id,
1050 &self.identity,
1051 &candidate.metadata,
1052 &candidate.material,
1053 )?;
1054 let active = manifest
1055 .entries
1056 .get(reference.as_str())
1057 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
1058 if active.version != pending.expected_active_version {
1059 return Err(ManagedSecretError::new(
1060 ManagedSecretErrorKind::VersionConflict,
1061 ));
1062 }
1063 prior_objects.push(active.object.clone());
1064 }
1065 for candidate in staged {
1066 let reference = candidate.metadata.reference();
1067 let pending = manifest
1068 .staged
1069 .remove(reference.as_str())
1070 .expect("validated staged replacement remains present");
1071 manifest.entries.insert(
1072 reference.as_str().to_string(),
1073 ManifestEntry {
1074 version: pending.version,
1075 object: pending.object,
1076 created_at: pending.created_at,
1077 },
1078 );
1079 manifest.audit.push(AuditRecord::mutation(
1080 manifest.audit.last(),
1081 AuditOperation::Replace,
1082 reference,
1083 candidate.metadata.version(),
1084 candidate.metadata.created_at(),
1085 self.deployment_id,
1086 )?);
1087 }
1088 manifest.generation =
1089 manifest
1090 .generation
1091 .checked_add(u64::try_from(staged.len()).map_err(|_| {
1092 ManagedSecretError::new(ManagedSecretErrorKind::VersionConflict)
1093 })?)
1094 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::VersionConflict))?;
1095 if let Err(failure) = write_manifest(
1096 &self.anchor.root,
1097 &manifest,
1098 &self.identity,
1099 self.faults.as_ref(),
1100 ) {
1101 return Err(if failure.renamed {
1102 ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain)
1103 } else {
1104 failure.into_error()
1105 });
1106 }
1107 for object in prior_objects {
1108 remove_file_if_present(&self.anchor.objects, &object)
1109 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))?;
1110 }
1111 sync_directory(&self.anchor.objects)
1112 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))?;
1113 self.anchor
1114 .validate_current(&self.root)
1115 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))?;
1116 Ok(staged
1117 .iter()
1118 .map(|candidate| candidate.metadata.clone())
1119 .collect())
1120 }
1121
1122 fn replacement_is_active(
1124 &self,
1125 staged: &StagedManagedSecret,
1126 ) -> Result<bool, ManagedSecretError> {
1127 self.anchor.validate_current(&self.root)?;
1128 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
1129 self.anchor.validate_current(&self.root)?;
1130 let header = read_header(&self.anchor.root)?;
1131 validate_header(&header, self.deployment_id, &self.identity)?;
1132 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
1133 validate_manifest(&manifest, &header)?;
1134 Ok(!manifest
1135 .staged
1136 .contains_key(staged.metadata.reference().as_str())
1137 && manifest
1138 .entries
1139 .get(staged.metadata.reference().as_str())
1140 .is_some_and(|entry| {
1141 entry.version == staged.metadata.version()
1142 && entry.created_at == staged.metadata.created_at()
1143 }))
1144 }
1145
1146 fn discard_replacement(&self, staged: &StagedManagedSecret) -> Result<(), ManagedSecretError> {
1148 self.anchor.validate_current(&self.root)?;
1149 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
1150 self.anchor.validate_current(&self.root)?;
1151 let header = read_header(&self.anchor.root)?;
1152 validate_header(&header, self.deployment_id, &self.identity)?;
1153 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
1154 validate_manifest(&manifest, &header)?;
1155 let pending = manifest
1156 .staged
1157 .get(staged.metadata.reference().as_str())
1158 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
1159 if pending.stage_id != staged.stage_id
1160 || pending.version != staged.metadata.version()
1161 || pending.created_at != staged.metadata.created_at()
1162 {
1163 return Err(ManagedSecretError::new(
1164 ManagedSecretErrorKind::VersionConflict,
1165 ));
1166 }
1167 let object = pending.object.clone();
1168 manifest
1169 .staged
1170 .remove(staged.metadata.reference().as_str())
1171 .expect("validated staged replacement remains present");
1172 if let Err(failure) = write_manifest(
1173 &self.anchor.root,
1174 &manifest,
1175 &self.identity,
1176 self.faults.as_ref(),
1177 ) && !failure.renamed
1178 {
1179 return Err(failure.into_error());
1180 }
1181 remove_file_if_present(&self.anchor.objects, &object)?;
1182 sync_directory(&self.anchor.objects)?;
1183 self.anchor.validate_current(&self.root)
1184 }
1185
1186 fn replace_reference_holder(
1188 &self,
1189 holder: &str,
1190 references: &[ManagedSecretReference],
1191 ) -> Result<(), ManagedSecretError> {
1192 if !valid_reference_holder(holder) || references.is_empty() {
1193 return Err(ManagedSecretError::new(
1194 ManagedSecretErrorKind::VersionConflict,
1195 ));
1196 }
1197 self.anchor.validate_current(&self.root)?;
1198 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
1199 self.anchor.validate_current(&self.root)?;
1200 let header = read_header(&self.anchor.root)?;
1201 validate_header(&header, self.deployment_id, &self.identity)?;
1202 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
1203 validate_manifest(&manifest, &header)?;
1204 recover_uncommitted_files(&self.anchor, &manifest)?;
1205 let references = references
1206 .iter()
1207 .map(|reference| {
1208 if manifest.entries.contains_key(reference.as_str()) {
1209 Ok(reference.as_str().to_string())
1210 } else {
1211 Err(ManagedSecretError::new(ManagedSecretErrorKind::Missing))
1212 }
1213 })
1214 .collect::<Result<BTreeSet<_>, _>>()?;
1215 if manifest.reference_holders.get(holder) == Some(&references) {
1216 return Ok(());
1217 }
1218 manifest
1219 .reference_holders
1220 .insert(holder.to_string(), references);
1221 if let Err(failure) = write_manifest(
1222 &self.anchor.root,
1223 &manifest,
1224 &self.identity,
1225 self.faults.as_ref(),
1226 ) {
1227 return Err(if failure.renamed {
1228 ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain)
1229 } else {
1230 failure.into_error()
1231 });
1232 }
1233 self.anchor.validate_current(&self.root)
1234 }
1235
1236 fn remove_reference_holder(&self, holder: &str) -> Result<(), ManagedSecretError> {
1238 if !valid_reference_holder(holder) {
1239 return Err(ManagedSecretError::new(
1240 ManagedSecretErrorKind::VersionConflict,
1241 ));
1242 }
1243 self.anchor.validate_current(&self.root)?;
1244 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
1245 self.anchor.validate_current(&self.root)?;
1246 let header = read_header(&self.anchor.root)?;
1247 validate_header(&header, self.deployment_id, &self.identity)?;
1248 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
1249 validate_manifest(&manifest, &header)?;
1250 if manifest.reference_holders.remove(holder).is_none() {
1251 return Ok(());
1252 }
1253 if let Err(failure) = write_manifest(
1254 &self.anchor.root,
1255 &manifest,
1256 &self.identity,
1257 self.faults.as_ref(),
1258 ) {
1259 return Err(if failure.renamed {
1260 ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain)
1261 } else {
1262 failure.into_error()
1263 });
1264 }
1265 self.anchor.validate_current(&self.root)
1266 }
1267
1268 fn held_reference_status(
1270 &self,
1271 reference: &ManagedSecretReference,
1272 ) -> Result<ManagedSecretReferenceStatus, ManagedSecretError> {
1273 self.anchor.validate_current(&self.root)?;
1274 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
1275 self.anchor.validate_current(&self.root)?;
1276 let header = read_header(&self.anchor.root)?;
1277 validate_header(&header, self.deployment_id, &self.identity)?;
1278 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
1279 validate_manifest(&manifest, &header)?;
1280 Ok(
1281 if manifest
1282 .reference_holders
1283 .values()
1284 .any(|references| references.contains(reference.as_str()))
1285 {
1286 ManagedSecretReferenceStatus::Referenced
1287 } else {
1288 ManagedSecretReferenceStatus::Unreferenced
1289 },
1290 )
1291 }
1292
1293 fn reference_holders(
1294 &self,
1295 reference: &ManagedSecretReference,
1296 ) -> Result<Vec<String>, ManagedSecretError> {
1297 self.anchor.validate_current(&self.root)?;
1298 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
1299 self.anchor.validate_current(&self.root)?;
1300 let header = read_header(&self.anchor.root)?;
1301 validate_header(&header, self.deployment_id, &self.identity)?;
1302 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
1303 validate_manifest(&manifest, &header)?;
1304 Ok(manifest
1305 .reference_holders
1306 .iter()
1307 .filter(|(_, references)| references.contains(reference.as_str()))
1308 .map(|(holder, _)| holder.clone())
1309 .collect())
1310 }
1311
1312 fn datastore_reference_inventory_is_complete(&self) -> Result<bool, ManagedSecretError> {
1313 self.anchor.validate_current(&self.root)?;
1314 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
1315 self.anchor.validate_current(&self.root)?;
1316 let header = read_header(&self.anchor.root)?;
1317 validate_header(&header, self.deployment_id, &self.identity)?;
1318 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
1319 validate_manifest(&manifest, &header)?;
1320 Ok(manifest.datastore_reference_inventory_version == DATASTORE_REFERENCE_INVENTORY_VERSION)
1321 }
1322
1323 fn remove_proven_unreferenced(
1327 &self,
1328 reference: &ManagedSecretReference,
1329 proof: ManagedSecretRemovalProof,
1330 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1331 if proof.reference != *reference {
1332 return Err(ManagedSecretError::new(
1333 ManagedSecretErrorKind::VersionConflict,
1334 ));
1335 }
1336 self.anchor.validate_current(&self.root)?;
1337 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
1338 self.anchor.validate_current(&self.root)?;
1339 let header = read_header(&self.anchor.root)?;
1340 validate_header(&header, self.deployment_id, &self.identity)?;
1341 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
1342 validate_manifest(&manifest, &header)?;
1343 recover_uncommitted_files(&self.anchor, &manifest)?;
1344 if manifest
1345 .reference_holders
1346 .values()
1347 .any(|references| references.contains(reference.as_str()))
1348 {
1349 return Err(ManagedSecretError::new(
1350 ManagedSecretErrorKind::VersionConflict,
1351 ));
1352 }
1353 if manifest.staged.contains_key(reference.as_str()) {
1354 return Err(ManagedSecretError::new(
1355 ManagedSecretErrorKind::VersionConflict,
1356 ));
1357 }
1358 let active = manifest
1359 .entries
1360 .remove(reference.as_str())
1361 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
1362 let metadata = ManagedSecretMetadata {
1363 reference: reference.clone(),
1364 version: active.version,
1365 created_at: active.created_at,
1366 };
1367 manifest.generation = manifest
1368 .generation
1369 .checked_add(1)
1370 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::VersionConflict))?;
1371 manifest.audit.push(AuditRecord::mutation(
1372 manifest.audit.last(),
1373 AuditOperation::Remove,
1374 reference,
1375 active.version,
1376 Utc::now(),
1377 self.deployment_id,
1378 )?);
1379 if let Err(failure) = write_manifest(
1380 &self.anchor.root,
1381 &manifest,
1382 &self.identity,
1383 self.faults.as_ref(),
1384 ) {
1385 return Err(if failure.renamed {
1386 ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain)
1387 } else {
1388 failure.into_error()
1389 });
1390 }
1391 remove_file_if_present(&self.anchor.objects, &active.object)
1392 .and_then(|()| sync_directory(&self.anchor.objects))
1393 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))?;
1394 self.anchor.validate_current(&self.root)?;
1395 Ok(metadata)
1396 }
1397
1398 #[cfg(any(test, feature = "test-fixtures"))]
1401 #[doc(hidden)]
1402 pub fn initialize_at_for_test(
1403 root: &Path,
1404 deployment_id: Uuid,
1405 identity: ManagedSecretIdentity,
1406 ) -> Result<Self, ManagedSecretError> {
1407 Self::initialize_at(root, deployment_id, identity)
1408 }
1409
1410 fn initialize_at(
1411 root: &Path,
1412 deployment_id: Uuid,
1413 identity: ManagedSecretIdentity,
1414 ) -> Result<Self, ManagedSecretError> {
1415 Self::initialize_at_with_faults(root, deployment_id, identity, Arc::new(NoMutationFaults))
1416 }
1417
1418 fn initialize_at_with_faults(
1419 root: &Path,
1420 deployment_id: Uuid,
1421 identity: ManagedSecretIdentity,
1422 faults: Arc<dyn MutationFaultInjector>,
1423 ) -> Result<Self, ManagedSecretError> {
1424 ensure_supported_platform()?;
1425 if deployment_id.is_nil() {
1426 return Err(ManagedSecretError::new(
1427 ManagedSecretErrorKind::DeploymentMismatch,
1428 ));
1429 }
1430 if fs::symlink_metadata(root).is_ok() {
1431 return Err(ManagedSecretError::new(
1432 ManagedSecretErrorKind::AlreadyInitialized,
1433 ));
1434 }
1435
1436 let identity = Arc::new(identity);
1437 let created_at = Utc::now();
1438 let header = StoreHeader {
1439 format_version: STORE_FORMAT_VERSION,
1440 deployment_id,
1441 recipient: identity.recipient_string(),
1442 created_at,
1443 };
1444 let audit = AuditRecord::bootstrap(deployment_id, created_at)?;
1445 let manifest = StoreManifest {
1446 format_version: STORE_FORMAT_VERSION,
1447 deployment_id,
1448 recipient: header.recipient.clone(),
1449 generation: 0,
1450 entries: BTreeMap::new(),
1451 staged: BTreeMap::new(),
1452 reference_holders: BTreeMap::new(),
1453 datastore_reference_inventory_version: DATASTORE_REFERENCE_INVENTORY_VERSION,
1454 audit: vec![audit],
1455 };
1456
1457 let parent = root
1458 .parent()
1459 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
1460 let file_name = root
1461 .file_name()
1462 .and_then(|value| value.to_str())
1463 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
1464 let staging = parent.join(format!(".{file_name}.init-{}", Uuid::new_v4()));
1465 if let Err(error) = create_initial_store(&staging, &header, &manifest, &identity) {
1466 let _ = fs::remove_dir_all(&staging);
1467 return Err(error);
1468 }
1469
1470 if let Err(error) = rename_directory_create_only(&staging, root) {
1471 let _ = fs::remove_dir_all(&staging);
1472 return Err(error);
1473 }
1474 if faults
1475 .check(MutationCheckpoint::DirectorySync(
1476 MutationPhase::Initialization,
1477 ))
1478 .and_then(|()| sync_directory_path(parent))
1479 .is_err()
1480 {
1481 return Err(ManagedSecretError::new(
1482 ManagedSecretErrorKind::CommitUncertain,
1483 ));
1484 }
1485
1486 Self::open_at_with_faults(root, deployment_id, identity, faults)
1487 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::CommitUncertain))
1488 }
1489
1490 #[cfg(any(test, feature = "test-fixtures"))]
1493 #[doc(hidden)]
1494 pub fn open_at_for_test(
1495 root: &Path,
1496 deployment_id: Uuid,
1497 identity: ManagedSecretIdentity,
1498 ) -> Result<Self, ManagedSecretError> {
1499 Self::open_at(root, deployment_id, identity)
1500 }
1501
1502 #[cfg(any(test, feature = "test-fixtures"))]
1504 #[doc(hidden)]
1505 pub fn open_verified_at_for_test(
1506 root: &Path,
1507 deployment_id: Uuid,
1508 identity: ManagedSecretIdentity,
1509 ) -> Result<Self, ManagedSecretError> {
1510 Self::open_verified_at(root, deployment_id, identity)
1511 }
1512
1513 #[cfg(any(test, feature = "test-fixtures"))]
1515 #[doc(hidden)]
1516 pub fn open_selected_read_only_at_for_test(
1517 root: &Path,
1518 deployment_id: Uuid,
1519 identity: ManagedSecretIdentity,
1520 ) -> Result<SelectedManagedSecretReader, ManagedSecretError> {
1521 Self::open_selected_read_only_at(root, deployment_id, identity)
1522 }
1523
1524 fn open_verified_at(
1525 root: &Path,
1526 deployment_id: Uuid,
1527 identity: ManagedSecretIdentity,
1528 ) -> Result<Self, ManagedSecretError> {
1529 ensure_supported_platform()?;
1530 validate_directory(root, false)?;
1531 let anchor = Arc::new(StoreAnchor::open(root, false)?);
1532 let identity = Arc::new(identity);
1533 let lock = StoreLock::acquire(&anchor.root, LockMode::Shared)?;
1534 verify_anchored_store(&anchor, root, deployment_id, &identity)?;
1535 drop(lock);
1536 Ok(Self {
1537 root: root.to_path_buf(),
1538 anchor,
1539 deployment_id,
1540 identity,
1541 faults: Arc::new(NoMutationFaults),
1542 })
1543 }
1544
1545 fn open_selected_read_only_at(
1546 root: &Path,
1547 deployment_id: Uuid,
1548 identity: ManagedSecretIdentity,
1549 ) -> Result<SelectedManagedSecretReader, ManagedSecretError> {
1550 ensure_supported_platform()?;
1551 validate_directory(root, false)?;
1552 let anchor = Arc::new(StoreAnchor::open(root, false)?);
1553 let identity = Arc::new(identity);
1554 let _lock = StoreLock::acquire(&anchor.root, LockMode::Shared)?;
1555 anchor.validate_current(root)?;
1556 let header = read_header(&anchor.root)?;
1557 validate_header(&header, deployment_id, &identity)?;
1558 if read_store_file(&anchor.root, LOCK_FILE, MAX_HEADER_BYTES)? != LOCK_CONTENT {
1559 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
1560 }
1561 let manifest = read_manifest(&anchor.root, &identity)?;
1562 validate_manifest(&manifest, &header)?;
1563 validate_store_control_plane(&anchor)?;
1564 anchor.validate_current(root)?;
1565 Ok(SelectedManagedSecretReader {
1566 store: Self {
1567 root: root.to_path_buf(),
1568 anchor,
1569 deployment_id,
1570 identity,
1571 faults: Arc::new(NoMutationFaults),
1572 },
1573 })
1574 }
1575
1576 fn open_at(
1577 root: &Path,
1578 deployment_id: Uuid,
1579 identity: ManagedSecretIdentity,
1580 ) -> Result<Self, ManagedSecretError> {
1581 Self::open_at_with_faults(
1582 root,
1583 deployment_id,
1584 Arc::new(identity),
1585 Arc::new(NoMutationFaults),
1586 )
1587 }
1588
1589 fn open_at_with_faults(
1590 root: &Path,
1591 deployment_id: Uuid,
1592 identity: Arc<ManagedSecretIdentity>,
1593 faults: Arc<dyn MutationFaultInjector>,
1594 ) -> Result<Self, ManagedSecretError> {
1595 ensure_supported_platform()?;
1596 validate_directory(root, true)?;
1597 let anchor = Arc::new(StoreAnchor::open(root, true)?);
1598 let lock = StoreLock::acquire(&anchor.root, LockMode::Exclusive)?;
1599 let header = read_header(&anchor.root)?;
1600 validate_header(&header, deployment_id, &identity)?;
1601 let manifest = read_manifest(&anchor.root, &identity)?;
1602 validate_manifest(&manifest, &header)?;
1603 recover_uncommitted_files(&anchor, &manifest)?;
1604 validate_store_tree(&anchor, &manifest)?;
1605 anchor.validate_current(root)?;
1606 drop(lock);
1607 Ok(Self {
1608 root: root.to_path_buf(),
1609 anchor,
1610 deployment_id,
1611 identity,
1612 faults,
1613 })
1614 }
1615
1616 fn write_version(
1617 &self,
1618 reference: &ManagedSecretReference,
1619 material: &SecretMaterial,
1620 expected_version: Option<ManagedSecretVersion>,
1621 operation: AuditOperation,
1622 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1623 self.anchor.validate_current(&self.root)?;
1624 if self.faults.check(MutationCheckpoint::Lock).is_err() {
1625 return Err(ManagedSecretError::new(
1626 ManagedSecretErrorKind::Coordination,
1627 ));
1628 }
1629 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Exclusive)?;
1630 self.anchor.validate_current(&self.root)?;
1631 let header = read_header(&self.anchor.root)?;
1632 validate_header(&header, self.deployment_id, &self.identity)?;
1633 if self.faults.check(MutationCheckpoint::Decrypt).is_err() {
1634 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
1635 }
1636 let mut manifest = read_manifest(&self.anchor.root, &self.identity)?;
1637 validate_manifest(&manifest, &header)?;
1638 recover_uncommitted_files(&self.anchor, &manifest)?;
1639
1640 let current = manifest.entries.get(reference.as_str());
1641 let version = match (current, expected_version) {
1642 (None, None)
1643 if !manifest
1644 .audit
1645 .iter()
1646 .any(|record| record.reference.as_deref() == Some(reference.as_str())) =>
1647 {
1648 ManagedSecretVersion::first()
1649 }
1650 (Some(_), None) => {
1651 return Err(ManagedSecretError::new(
1652 ManagedSecretErrorKind::AlreadyExists,
1653 ));
1654 }
1655 (Some(entry), Some(expected)) if entry.version == expected => expected.next()?,
1656 _ => {
1657 return Err(ManagedSecretError::new(
1658 ManagedSecretErrorKind::VersionConflict,
1659 ));
1660 }
1661 };
1662 let created_at = Utc::now();
1663 let metadata = ManagedSecretMetadata {
1664 reference: reference.clone(),
1665 version,
1666 created_at,
1667 };
1668 self.faults.check(MutationCheckpoint::Audit)?;
1669 let plaintext = encode_secret_envelope(
1670 self.deployment_id,
1671 &self.identity.recipient_string(),
1672 &metadata,
1673 material,
1674 )?;
1675 let ciphertext = encrypt(&self.identity.recipient, &plaintext)?;
1676 let object_name = format!("object-{}.age", Uuid::new_v4());
1677 atomic_write(
1678 &self.anchor.objects,
1679 &object_name,
1680 &ciphertext,
1681 self.faults.as_ref(),
1682 MutationPhase::Object,
1683 )
1684 .map_err(AtomicWriteFailure::into_error)?;
1685 let verification = self
1686 .faults
1687 .check(MutationCheckpoint::Verification)
1688 .and_then(|()| {
1689 verify_persisted_object(
1690 &self.anchor.objects,
1691 &object_name,
1692 self.deployment_id,
1693 &self.identity,
1694 &metadata,
1695 material,
1696 )
1697 });
1698 if let Err(error) = verification {
1699 remove_file_if_present(&self.anchor.objects, &object_name)?;
1700 sync_directory(&self.anchor.objects)?;
1701 return Err(error);
1702 }
1703
1704 let prior_object = current.map(|entry| entry.object.clone());
1705 manifest.generation = manifest
1706 .generation
1707 .checked_add(1)
1708 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::VersionConflict))?;
1709 manifest.entries.insert(
1710 reference.as_str().to_string(),
1711 ManifestEntry {
1712 version,
1713 object: object_name.clone(),
1714 created_at,
1715 },
1716 );
1717 let audit = AuditRecord::mutation(
1718 manifest.audit.last(),
1719 operation,
1720 reference,
1721 version,
1722 created_at,
1723 self.deployment_id,
1724 )?;
1725 manifest.audit.push(audit);
1726
1727 let manifest_result = write_manifest(
1728 &self.anchor.root,
1729 &manifest,
1730 &self.identity,
1731 self.faults.as_ref(),
1732 );
1733 if let Err(failure) = manifest_result {
1734 if failure.renamed {
1735 return Err(ManagedSecretError::new(
1736 ManagedSecretErrorKind::CommitUncertain,
1737 ));
1738 }
1739 remove_file_if_present(&self.anchor.objects, &object_name)?;
1740 sync_directory(&self.anchor.objects)?;
1741 return Err(failure.into_error());
1742 }
1743
1744 if let Some(prior_object) = prior_object
1745 && self
1746 .faults
1747 .check(MutationCheckpoint::CleanupRemove)
1748 .and_then(|()| remove_file_if_present(&self.anchor.objects, &prior_object))
1749 .and_then(|()| self.faults.check(MutationCheckpoint::CleanupSync))
1750 .and_then(|()| sync_directory(&self.anchor.objects))
1751 .is_err()
1752 {
1753 return Err(ManagedSecretError::new(
1754 ManagedSecretErrorKind::CommitUncertain,
1755 ));
1756 }
1757 self.anchor.validate_current(&self.root)?;
1758 Ok(metadata)
1759 }
1760
1761 #[cfg(any(test, feature = "test-fixtures"))]
1762 #[doc(hidden)]
1763 pub fn replace_for_test(
1764 &self,
1765 reference: &ManagedSecretReference,
1766 material: &SecretMaterial,
1767 expected_version: ManagedSecretVersion,
1768 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1769 self.write_version(
1770 reference,
1771 material,
1772 Some(expected_version),
1773 AuditOperation::Replace,
1774 )
1775 }
1776}
1777
1778impl ManagedSecretBackend for LocalManagedSecretStore {
1779 fn create(
1780 &self,
1781 reference: &ManagedSecretReference,
1782 material: &SecretMaterial,
1783 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1784 self.write_version(reference, material, None, AuditOperation::Create)
1785 }
1786
1787 fn resolve(
1788 &self,
1789 reference: &ManagedSecretReference,
1790 ) -> Result<ResolvedManagedSecret, ManagedSecretError> {
1791 self.anchor.validate_current(&self.root)?;
1792 let _lock = StoreLock::acquire(&self.anchor.root, LockMode::Shared)?;
1793 self.anchor.validate_current(&self.root)?;
1794 let header = read_header(&self.anchor.root)?;
1795 validate_header(&header, self.deployment_id, &self.identity)?;
1796 let manifest = read_manifest(&self.anchor.root, &self.identity)?;
1797 validate_manifest(&manifest, &header)?;
1798 let entry = manifest
1799 .entries
1800 .get(reference.as_str())
1801 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Missing))?;
1802 let ciphertext =
1803 read_store_file(&self.anchor.objects, &entry.object, MAX_CIPHERTEXT_BYTES)?;
1804 let plaintext = decrypt(&self.identity.identity, &ciphertext)?;
1805 let resolved = decode_secret_envelope(
1806 &plaintext,
1807 self.deployment_id,
1808 &self.identity.recipient_string(),
1809 reference,
1810 entry,
1811 )?;
1812 self.anchor.validate_current(&self.root)?;
1813 Ok(resolved)
1814 }
1815
1816 fn lookup_active_metadata(
1817 &self,
1818 reference: &ManagedSecretReference,
1819 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1820 LocalManagedSecretStore::lookup_active_metadata(self, reference)
1821 }
1822}
1823
1824impl owner_backend_sealed::Sealed for LocalManagedSecretStore {}
1825impl trusted_backend_sealed::Sealed for LocalManagedSecretStore {}
1826
1827impl ManagedSecretOwnerBackend for LocalManagedSecretStore {
1828 fn stage_owner_replacement(
1829 &self,
1830 reference: &ManagedSecretReference,
1831 material: &SecretMaterial,
1832 expected_active_version: ManagedSecretVersion,
1833 ) -> Result<StagedManagedSecret, ManagedSecretError> {
1834 self.stage_replacement(reference, material, expected_active_version)
1835 }
1836
1837 fn resume_owner_replacement(
1838 &self,
1839 reference: &ManagedSecretReference,
1840 ) -> Result<Option<StagedManagedSecret>, ManagedSecretError> {
1841 self.pending_replacement(reference)
1842 }
1843
1844 fn activate_owner_replacement(
1845 &self,
1846 staged: &StagedManagedSecret,
1847 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1848 self.activate_replacement(staged)
1849 }
1850
1851 fn activate_owner_replacements(
1852 &self,
1853 staged: &[&StagedManagedSecret],
1854 ) -> Result<Vec<ManagedSecretMetadata>, ManagedSecretError> {
1855 self.activate_replacements(staged)
1856 }
1857
1858 fn owner_replacement_is_active(
1859 &self,
1860 staged: &StagedManagedSecret,
1861 ) -> Result<bool, ManagedSecretError> {
1862 self.replacement_is_active(staged)
1863 }
1864
1865 fn discard_owner_replacement(
1866 &self,
1867 staged: &StagedManagedSecret,
1868 ) -> Result<(), ManagedSecretError> {
1869 self.discard_replacement(staged)
1870 }
1871}
1872
1873impl ManagedSecretTrustedBackend for LocalManagedSecretStore {
1874 fn replace_trusted_reference_holder(
1875 &self,
1876 holder: &str,
1877 references: &[ManagedSecretReference],
1878 ) -> Result<(), ManagedSecretError> {
1879 self.replace_reference_holder(holder, references)
1880 }
1881
1882 fn remove_trusted_reference_holder(&self, holder: &str) -> Result<(), ManagedSecretError> {
1883 self.remove_reference_holder(holder)
1884 }
1885
1886 fn trusted_held_reference_status(
1887 &self,
1888 reference: &ManagedSecretReference,
1889 ) -> Result<ManagedSecretReferenceStatus, ManagedSecretError> {
1890 self.held_reference_status(reference)
1891 }
1892
1893 fn trusted_reference_holders(
1894 &self,
1895 reference: &ManagedSecretReference,
1896 ) -> Result<Vec<String>, ManagedSecretError> {
1897 self.reference_holders(reference)
1898 }
1899
1900 fn datastore_reference_inventory_is_complete(&self) -> Result<bool, ManagedSecretError> {
1901 LocalManagedSecretStore::datastore_reference_inventory_is_complete(self)
1902 }
1903
1904 fn remove_with_repository_evidence(
1905 &self,
1906 reference: &ManagedSecretReference,
1907 proof: ManagedSecretRemovalProof,
1908 ) -> Result<ManagedSecretMetadata, ManagedSecretError> {
1909 self.remove_proven_unreferenced(reference, proof)
1910 }
1911}
1912
1913#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1915pub enum ManagedSecretErrorKind {
1916 AlreadyInitialized,
1917 NotInitialized,
1918 AlreadyExists,
1919 Missing,
1920 InvalidIdentity,
1921 DeploymentMismatch,
1922 RecipientMismatch,
1923 UnsafeFilesystem,
1924 Coordination,
1925 Integrity,
1926 VersionConflict,
1927 SizeLimit,
1928 CommitUncertain,
1929 Mutation,
1930 UnsupportedPlatform,
1931}
1932
1933#[derive(Debug, Clone, PartialEq, Eq, Error)]
1935#[error("Managed-secret operation failed: {kind:?}")]
1936pub struct ManagedSecretError {
1937 kind: ManagedSecretErrorKind,
1938}
1939
1940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1942pub enum RootRotationPhase {
1943 Lock,
1944 Prerequisites,
1945 SourceVerification,
1946 StagingWrite,
1947 DestinationVerification,
1948}
1949
1950#[derive(Debug, Clone, PartialEq, Eq, Error)]
1952#[error("Managed-secret root rotation failed during {phase:?}: {source}")]
1953pub struct RootRotationError {
1954 phase: RootRotationPhase,
1955 source: ManagedSecretError,
1956}
1957
1958impl RootRotationError {
1959 fn at(phase: RootRotationPhase, source: ManagedSecretError) -> Self {
1960 Self { phase, source }
1961 }
1962
1963 pub fn phase(&self) -> RootRotationPhase {
1964 self.phase
1965 }
1966
1967 pub fn kind(&self) -> ManagedSecretErrorKind {
1968 self.source.kind()
1969 }
1970}
1971
1972impl ManagedSecretError {
1973 fn new(kind: ManagedSecretErrorKind) -> Self {
1974 Self { kind }
1975 }
1976
1977 pub fn kind(&self) -> ManagedSecretErrorKind {
1978 self.kind
1979 }
1980}
1981
1982#[derive(Debug, Clone, Serialize, Deserialize)]
1983#[serde(deny_unknown_fields)]
1984struct StoreHeader {
1985 format_version: u64,
1986 deployment_id: Uuid,
1987 recipient: String,
1988 created_at: DateTime<Utc>,
1989}
1990
1991#[derive(Debug, Clone, Serialize, Deserialize)]
1992#[serde(deny_unknown_fields)]
1993struct StoreManifest {
1994 format_version: u64,
1995 deployment_id: Uuid,
1996 recipient: String,
1997 generation: u64,
1998 entries: BTreeMap<String, ManifestEntry>,
1999 #[serde(default)]
2000 staged: BTreeMap<String, StagedManifestEntry>,
2001 #[serde(default)]
2002 reference_holders: BTreeMap<String, BTreeSet<String>>,
2003 #[serde(default)]
2004 datastore_reference_inventory_version: u64,
2005 audit: Vec<AuditRecord>,
2006}
2007
2008#[derive(Debug, Clone, Serialize, Deserialize)]
2009#[serde(deny_unknown_fields)]
2010struct ManifestEntry {
2011 version: ManagedSecretVersion,
2012 object: String,
2013 created_at: DateTime<Utc>,
2014}
2015
2016#[derive(Debug, Clone, Serialize, Deserialize)]
2017#[serde(deny_unknown_fields)]
2018struct StagedManifestEntry {
2019 stage_id: Uuid,
2020 expected_active_version: ManagedSecretVersion,
2021 version: ManagedSecretVersion,
2022 object: String,
2023 created_at: DateTime<Utc>,
2024}
2025
2026#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2027#[serde(rename_all = "snake_case")]
2028enum AuditOperation {
2029 Bootstrap,
2030 Create,
2031 Replace,
2032 Remove,
2033}
2034
2035#[derive(Debug, Clone, Serialize, Deserialize)]
2036#[serde(deny_unknown_fields)]
2037struct AuditActor {
2038 effective_uid: u32,
2039 effective_gid: u32,
2040}
2041
2042#[derive(Debug, Clone, Serialize, Deserialize)]
2043#[serde(deny_unknown_fields)]
2044struct AuditRecord {
2045 sequence: u64,
2046 operation: AuditOperation,
2047 reference: Option<String>,
2048 version: Option<ManagedSecretVersion>,
2049 occurred_at: DateTime<Utc>,
2050 deployment_id: Uuid,
2051 actor: AuditActor,
2052 previous_hash: [u8; 32],
2053 record_hash: [u8; 32],
2054}
2055
2056#[derive(Serialize)]
2057struct AuditHashInput<'a> {
2058 sequence: u64,
2059 operation: AuditOperation,
2060 reference: &'a Option<String>,
2061 version: Option<ManagedSecretVersion>,
2062 occurred_at: DateTime<Utc>,
2063 deployment_id: Uuid,
2064 actor: &'a AuditActor,
2065 previous_hash: [u8; 32],
2066}
2067
2068impl AuditRecord {
2069 fn bootstrap(
2070 deployment_id: Uuid,
2071 occurred_at: DateTime<Utc>,
2072 ) -> Result<Self, ManagedSecretError> {
2073 Self::new(
2074 1,
2075 AuditOperation::Bootstrap,
2076 None,
2077 None,
2078 occurred_at,
2079 deployment_id,
2080 ZERO_HASH,
2081 )
2082 }
2083
2084 fn mutation(
2085 previous: Option<&Self>,
2086 operation: AuditOperation,
2087 reference: &ManagedSecretReference,
2088 version: ManagedSecretVersion,
2089 occurred_at: DateTime<Utc>,
2090 deployment_id: Uuid,
2091 ) -> Result<Self, ManagedSecretError> {
2092 let previous =
2093 previous.ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2094 Self::new(
2095 previous
2096 .sequence
2097 .checked_add(1)
2098 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?,
2099 operation,
2100 Some(reference.as_str().to_string()),
2101 Some(version),
2102 occurred_at,
2103 deployment_id,
2104 previous.record_hash,
2105 )
2106 }
2107
2108 fn new(
2109 sequence: u64,
2110 operation: AuditOperation,
2111 reference: Option<String>,
2112 version: Option<ManagedSecretVersion>,
2113 occurred_at: DateTime<Utc>,
2114 deployment_id: Uuid,
2115 previous_hash: [u8; 32],
2116 ) -> Result<Self, ManagedSecretError> {
2117 let actor = current_actor();
2118 let mut record = Self {
2119 sequence,
2120 operation,
2121 reference,
2122 version,
2123 occurred_at,
2124 deployment_id,
2125 actor,
2126 previous_hash,
2127 record_hash: ZERO_HASH,
2128 };
2129 record.record_hash = record.calculate_hash()?;
2130 Ok(record)
2131 }
2132
2133 fn calculate_hash(&self) -> Result<[u8; 32], ManagedSecretError> {
2134 let input = AuditHashInput {
2135 sequence: self.sequence,
2136 operation: self.operation,
2137 reference: &self.reference,
2138 version: self.version,
2139 occurred_at: self.occurred_at,
2140 deployment_id: self.deployment_id,
2141 actor: &self.actor,
2142 previous_hash: self.previous_hash,
2143 };
2144 let encoded = serde_json::to_vec(&input)
2145 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2146 Ok(Sha256::digest(encoded).into())
2147 }
2148}
2149
2150fn current_actor() -> AuditActor {
2151 #[cfg(unix)]
2152 {
2153 let effective_uid = unsafe { libc::geteuid() };
2155 let effective_gid = unsafe { libc::getegid() };
2157 AuditActor {
2158 effective_uid,
2159 effective_gid,
2160 }
2161 }
2162 #[cfg(not(unix))]
2163 {
2164 AuditActor {
2165 effective_uid: 0,
2166 effective_gid: 0,
2167 }
2168 }
2169}
2170
2171fn create_initial_store(
2172 staging: &Path,
2173 header: &StoreHeader,
2174 manifest: &StoreManifest,
2175 identity: &ManagedSecretIdentity,
2176) -> Result<(), ManagedSecretError> {
2177 create_private_directory(staging)?;
2178 let objects = staging.join(OBJECTS_DIRECTORY);
2179 create_private_directory(&objects)?;
2180 write_new_file(&staging.join(LOCK_FILE), LOCK_CONTENT)?;
2181 let header_bytes = serde_json::to_vec(header)
2182 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2183 write_new_file(&staging.join(HEADER_FILE), &header_bytes)?;
2184 let manifest_bytes = serialize_and_encrypt_manifest(manifest, identity)?;
2185 write_new_file(&staging.join(MANIFEST_FILE), &manifest_bytes)?;
2186 sync_directory_path(&objects)?;
2187 sync_directory_path(staging)
2188}
2189
2190fn write_new_file(path: &Path, bytes: &[u8]) -> Result<(), ManagedSecretError> {
2191 let mut options = OpenOptions::new();
2192 options.write(true).create_new(true);
2193 #[cfg(unix)]
2194 options
2195 .mode(0o600)
2196 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
2197 let mut file = options
2198 .open(path)
2199 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2200 file.write_all(bytes)
2201 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2202 file.sync_all()
2203 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
2204}
2205
2206fn read_header(root: &File) -> Result<StoreHeader, ManagedSecretError> {
2207 let bytes = read_store_file(root, HEADER_FILE, MAX_HEADER_BYTES)?;
2208 serde_json::from_slice(&bytes)
2209 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))
2210}
2211
2212fn validate_header(
2213 header: &StoreHeader,
2214 deployment_id: Uuid,
2215 identity: &ManagedSecretIdentity,
2216) -> Result<(), ManagedSecretError> {
2217 if header.format_version != STORE_FORMAT_VERSION {
2218 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2219 }
2220 if header.deployment_id != deployment_id {
2221 return Err(ManagedSecretError::new(
2222 ManagedSecretErrorKind::DeploymentMismatch,
2223 ));
2224 }
2225 if header.recipient != identity.recipient_string() {
2226 return Err(ManagedSecretError::new(
2227 ManagedSecretErrorKind::RecipientMismatch,
2228 ));
2229 }
2230 age::x25519::Recipient::from_str(&header.recipient)
2231 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2232 Ok(())
2233}
2234
2235fn read_manifest(
2236 root: &File,
2237 identity: &ManagedSecretIdentity,
2238) -> Result<StoreManifest, ManagedSecretError> {
2239 let ciphertext = read_store_file(root, MANIFEST_FILE, MAX_MANIFEST_BYTES)?;
2240 let plaintext = decrypt(&identity.identity, &ciphertext)?;
2241 serde_json::from_slice(&plaintext)
2242 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))
2243}
2244
2245fn write_manifest(
2246 root: &File,
2247 manifest: &StoreManifest,
2248 identity: &ManagedSecretIdentity,
2249 faults: &dyn MutationFaultInjector,
2250) -> Result<(), AtomicWriteFailure> {
2251 let ciphertext =
2252 serialize_and_encrypt_manifest(manifest, identity).map_err(|error| AtomicWriteFailure {
2253 error,
2254 renamed: false,
2255 })?;
2256 if ciphertext.len() as u64 > MAX_MANIFEST_BYTES {
2257 return Err(AtomicWriteFailure {
2258 error: ManagedSecretError::new(ManagedSecretErrorKind::SizeLimit),
2259 renamed: false,
2260 });
2261 }
2262 atomic_write(
2263 root,
2264 MANIFEST_FILE,
2265 &ciphertext,
2266 faults,
2267 MutationPhase::Manifest,
2268 )
2269}
2270
2271fn serialize_and_encrypt_manifest(
2272 manifest: &StoreManifest,
2273 identity: &ManagedSecretIdentity,
2274) -> Result<Vec<u8>, ManagedSecretError> {
2275 let plaintext = serde_json::to_vec(manifest)
2276 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2277 encrypt(&identity.recipient, &plaintext)
2278}
2279
2280fn valid_reference_holder(value: &str) -> bool {
2281 !value.is_empty()
2282 && value.len() <= 128
2283 && value
2284 .bytes()
2285 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b':'))
2286}
2287
2288fn validate_manifest(
2289 manifest: &StoreManifest,
2290 header: &StoreHeader,
2291) -> Result<(), ManagedSecretError> {
2292 if manifest.format_version != STORE_FORMAT_VERSION
2293 || manifest.deployment_id != header.deployment_id
2294 || manifest.recipient != header.recipient
2295 || manifest.datastore_reference_inventory_version > DATASTORE_REFERENCE_INVENTORY_VERSION
2296 || manifest.audit.is_empty()
2297 {
2298 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2299 }
2300
2301 for (holder, references) in &manifest.reference_holders {
2302 if !valid_reference_holder(holder) || references.is_empty() {
2303 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2304 }
2305 for reference in references {
2306 let parsed = ManagedSecretReference::from_str(reference)
2307 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2308 if parsed.as_str() != reference || !manifest.entries.contains_key(reference) {
2309 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2310 }
2311 }
2312 }
2313
2314 let mut previous_hash = ZERO_HASH;
2315 let mut previous_time = None;
2316 for (index, record) in manifest.audit.iter().enumerate() {
2317 let expected_sequence = u64::try_from(index)
2318 .ok()
2319 .and_then(|value| value.checked_add(1))
2320 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2321 if record.sequence != expected_sequence
2322 || record.deployment_id != header.deployment_id
2323 || record.previous_hash != previous_hash
2324 || record.calculate_hash()? != record.record_hash
2325 || previous_time.is_some_and(|previous| record.occurred_at < previous)
2326 {
2327 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2328 }
2329 if index == 0
2330 && (!matches!(record.operation, AuditOperation::Bootstrap)
2331 || record.reference.is_some()
2332 || record.version.is_some()
2333 || record.occurred_at != header.created_at)
2334 {
2335 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2336 }
2337 previous_hash = record.record_hash;
2338 previous_time = Some(record.occurred_at);
2339 }
2340
2341 let mutation_count = manifest
2342 .audit
2343 .len()
2344 .checked_sub(1)
2345 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2346 if manifest.generation != mutation_count as u64 {
2347 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2348 }
2349
2350 let mut audited_versions =
2351 BTreeMap::<String, (ManagedSecretVersion, DateTime<Utc>, bool)>::new();
2352 for record in manifest.audit.iter().skip(1) {
2353 let reference = record
2354 .reference
2355 .as_deref()
2356 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2357 let parsed = ManagedSecretReference::from_str(reference)
2358 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2359 let version = record
2360 .version
2361 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2362 if parsed.as_str() != reference {
2363 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2364 }
2365 let next_state = match (record.operation, audited_versions.get(reference).copied()) {
2366 (AuditOperation::Create, None) if version == ManagedSecretVersion::first() => {
2367 (version, record.occurred_at, true)
2368 }
2369 (AuditOperation::Replace, Some((previous, _, true))) if previous.next()? == version => {
2370 (version, record.occurred_at, true)
2371 }
2372 (AuditOperation::Remove, Some((current, activated_at, true))) if current == version => {
2373 (current, activated_at, false)
2374 }
2375 _ => {
2376 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2377 }
2378 };
2379 audited_versions.insert(reference.to_string(), next_state);
2380 }
2381
2382 for (reference, entry) in &manifest.entries {
2383 let parsed = ManagedSecretReference::from_str(reference)
2384 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2385 if parsed.as_str() != reference
2386 || audited_versions.get(reference).copied()
2387 != Some((entry.version, entry.created_at, true))
2388 || !valid_object_name(&entry.object)
2389 {
2390 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2391 }
2392 }
2393 let mut object_names = manifest
2394 .entries
2395 .values()
2396 .map(|entry| entry.object.as_str())
2397 .collect::<BTreeSet<_>>();
2398 for (reference, staged) in &manifest.staged {
2399 let parsed = ManagedSecretReference::from_str(reference)
2400 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2401 let active = manifest
2402 .entries
2403 .get(reference)
2404 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2405 if parsed.as_str() != reference
2406 || active.version != staged.expected_active_version
2407 || staged.version != staged.expected_active_version.next()?
2408 || !valid_object_name(&staged.object)
2409 || !object_names.insert(&staged.object)
2410 {
2411 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2412 }
2413 }
2414 if audited_versions
2415 .values()
2416 .filter(|(_, _, active)| *active)
2417 .count()
2418 != manifest.entries.len()
2419 {
2420 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2421 }
2422 Ok(())
2423}
2424
2425#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
2428struct VerifiedStore {
2429 header: StoreHeader,
2430 manifest: StoreManifest,
2431 material: BTreeMap<String, SecretMaterial>,
2432}
2433
2434impl VerifiedStore {
2435 fn report(&self) -> ManagedSecretVerification {
2436 ManagedSecretVerification {
2437 deployment_id: self.header.deployment_id,
2438 recipient_fingerprint: recipient_fingerprint(&self.header.recipient),
2439 active_secret_count: self.manifest.entries.len(),
2440 audit_record_count: self.manifest.audit.len(),
2441 }
2442 }
2443}
2444
2445fn verify_anchored_store(
2446 anchor: &StoreAnchor,
2447 root: &Path,
2448 deployment_id: Uuid,
2449 identity: &ManagedSecretIdentity,
2450) -> Result<VerifiedStore, ManagedSecretError> {
2451 anchor.validate_current(root)?;
2452 let header = read_header(&anchor.root)?;
2453 validate_header(&header, deployment_id, identity)?;
2454 if read_store_file(&anchor.root, LOCK_FILE, MAX_HEADER_BYTES)? != LOCK_CONTENT {
2455 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2456 }
2457 let manifest = read_manifest(&anchor.root, identity)?;
2458 validate_manifest(&manifest, &header)?;
2459 validate_store_tree(anchor, &manifest)?;
2460
2461 let mut material = BTreeMap::new();
2462 for (reference, entry) in &manifest.entries {
2463 let reference = ManagedSecretReference::from_str(reference)
2464 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2465 let ciphertext = read_store_file(&anchor.objects, &entry.object, MAX_CIPHERTEXT_BYTES)?;
2466 let plaintext = decrypt(&identity.identity, &ciphertext)?;
2467 let resolved = decode_secret_envelope(
2468 &plaintext,
2469 deployment_id,
2470 &identity.recipient_string(),
2471 &reference,
2472 entry,
2473 )?;
2474 material.insert(reference.as_str().to_string(), resolved.material);
2475 }
2476 for (reference, staged) in &manifest.staged {
2477 let reference = ManagedSecretReference::from_str(reference)
2478 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2479 let entry = ManifestEntry {
2480 version: staged.version,
2481 object: staged.object.clone(),
2482 created_at: staged.created_at,
2483 };
2484 let ciphertext = read_store_file(&anchor.objects, &entry.object, MAX_CIPHERTEXT_BYTES)?;
2485 let plaintext = decrypt(&identity.identity, &ciphertext)?;
2486 decode_secret_envelope(
2487 &plaintext,
2488 deployment_id,
2489 &identity.recipient_string(),
2490 &reference,
2491 &entry,
2492 )?;
2493 }
2494 anchor.validate_current(root)?;
2495 Ok(VerifiedStore {
2496 header,
2497 manifest,
2498 material,
2499 })
2500}
2501
2502fn recipient_fingerprint(recipient: &str) -> String {
2503 let digest = Sha256::digest(recipient.as_bytes());
2504 let mut encoded = String::with_capacity(digest.len() * 2);
2505 const HEX: &[u8; 16] = b"0123456789abcdef";
2506 for byte in digest {
2507 encoded.push(char::from(HEX[usize::from(byte >> 4)]));
2508 encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
2509 }
2510 encoded
2511}
2512
2513#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
2516struct EmptyRotationStaging {
2517 path: PathBuf,
2518 root: File,
2519 identity: DirectoryIdentity,
2520}
2521
2522#[cfg(target_os = "linux")]
2523impl EmptyRotationStaging {
2524 fn validate_current(&self) -> Result<(), ManagedSecretError> {
2525 let metadata = fs::symlink_metadata(&self.path)
2526 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
2527 validate_directory_metadata(&metadata, true)?;
2528 if directory_identity(&metadata) != self.identity {
2529 return Err(ManagedSecretError::new(
2530 ManagedSecretErrorKind::UnsafeFilesystem,
2531 ));
2532 }
2533 Ok(())
2534 }
2535
2536 fn populated_anchor(&self, objects: File) -> Result<StoreAnchor, ManagedSecretError> {
2537 self.validate_current()?;
2538 let root = self
2539 .root
2540 .try_clone()
2541 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
2542 let objects_metadata = objects
2543 .metadata()
2544 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
2545 validate_directory_metadata(&objects_metadata, true)?;
2546 Ok(StoreAnchor {
2547 root,
2548 objects,
2549 root_identity: self.identity,
2550 objects_identity: directory_identity(&objects_metadata),
2551 require_write: true,
2552 })
2553 }
2554}
2555
2556fn validate_empty_staging_directory(
2557 staging_root: &Path,
2558 active: &StoreAnchor,
2559) -> Result<EmptyRotationStaging, ManagedSecretError> {
2560 validate_directory(staging_root, true)?;
2561 let staging = open_directory(staging_root)?;
2562 let metadata = staging
2563 .metadata()
2564 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
2565 validate_directory_metadata(&metadata, true)?;
2566 let identity = directory_identity(&metadata);
2567 if identity == active.root_identity
2568 || directory_mount_is_read_only(&staging)?
2569 || read_directory(&staging)?.next().is_some()
2570 {
2571 return Err(ManagedSecretError::new(
2572 ManagedSecretErrorKind::UnsafeFilesystem,
2573 ));
2574 }
2575 Ok(EmptyRotationStaging {
2576 path: staging_root.to_path_buf(),
2577 root: staging,
2578 identity,
2579 })
2580}
2581
2582#[cfg(target_os = "linux")]
2583fn populate_rotation_staging(
2584 staging: &EmptyRotationStaging,
2585 source: &VerifiedStore,
2586 next_identity: &ManagedSecretIdentity,
2587 faults: &dyn RotationFaultInjector,
2588) -> Result<StoreAnchor, ManagedSecretError> {
2589 staging.validate_current()?;
2590 faults.check(RotationCheckpoint::CreateObjectsDirectory)?;
2591 rustix::fs::mkdirat(
2592 &staging.root,
2593 OBJECTS_DIRECTORY,
2594 rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR | rustix::fs::Mode::XUSR,
2595 )
2596 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2597 let objects = open_directory_at(&staging.root, Path::new(OBJECTS_DIRECTORY))?;
2598 for (reference, entry) in &source.manifest.entries {
2599 let reference = ManagedSecretReference::from_str(reference)
2600 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2601 let metadata = ManagedSecretMetadata {
2602 reference,
2603 version: entry.version,
2604 created_at: entry.created_at,
2605 };
2606 let material = source
2607 .material
2608 .get(metadata.reference().as_str())
2609 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2610 let plaintext = encode_secret_envelope(
2611 source.header.deployment_id,
2612 &next_identity.recipient_string(),
2613 &metadata,
2614 material,
2615 )?;
2616 let ciphertext = encrypt(&next_identity.recipient, &plaintext)?;
2617 write_rotation_file(
2618 &objects,
2619 &entry.object,
2620 &ciphertext,
2621 RotationFile::Object,
2622 faults,
2623 )?;
2624 }
2625
2626 let mut header = source.header.clone();
2627 header.recipient = next_identity.recipient_string();
2628 let mut manifest = source.manifest.clone();
2629 manifest.recipient.clone_from(&header.recipient);
2630 write_rotation_file(
2631 &staging.root,
2632 LOCK_FILE,
2633 LOCK_CONTENT,
2634 RotationFile::Lock,
2635 faults,
2636 )?;
2637 let header_bytes = serde_json::to_vec(&header)
2638 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2639 write_rotation_file(
2640 &staging.root,
2641 HEADER_FILE,
2642 &header_bytes,
2643 RotationFile::Header,
2644 faults,
2645 )?;
2646 let manifest_bytes = serialize_and_encrypt_manifest(&manifest, next_identity)?;
2647 write_rotation_file(
2648 &staging.root,
2649 MANIFEST_FILE,
2650 &manifest_bytes,
2651 RotationFile::Manifest,
2652 faults,
2653 )?;
2654 faults.check(RotationCheckpoint::SyncDirectory(
2655 RotationDirectory::Objects,
2656 ))?;
2657 sync_directory(&objects)?;
2658 faults.check(RotationCheckpoint::SyncDirectory(RotationDirectory::Root))?;
2659 sync_directory(&staging.root)?;
2660 staging.populated_anchor(objects)
2661}
2662
2663#[cfg(not(target_os = "linux"))]
2664fn populate_rotation_staging(
2665 _staging: &EmptyRotationStaging,
2666 _source: &VerifiedStore,
2667 _next_identity: &ManagedSecretIdentity,
2668 _faults: &dyn RotationFaultInjector,
2669) -> Result<StoreAnchor, ManagedSecretError> {
2670 Err(ManagedSecretError::new(
2671 ManagedSecretErrorKind::UnsupportedPlatform,
2672 ))
2673}
2674
2675#[cfg(target_os = "linux")]
2676fn write_rotation_file(
2677 directory: &File,
2678 name: &str,
2679 bytes: &[u8],
2680 file_kind: RotationFile,
2681 faults: &dyn RotationFaultInjector,
2682) -> Result<(), ManagedSecretError> {
2683 faults.check(RotationCheckpoint::Write(file_kind))?;
2684 let descriptor = rustix::fs::openat2(
2685 directory,
2686 name,
2687 rustix::fs::OFlags::WRONLY
2688 | rustix::fs::OFlags::CREATE
2689 | rustix::fs::OFlags::EXCL
2690 | rustix::fs::OFlags::CLOEXEC
2691 | rustix::fs::OFlags::NOFOLLOW,
2692 rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR,
2693 rustix::fs::ResolveFlags::BENEATH
2694 | rustix::fs::ResolveFlags::NO_MAGICLINKS
2695 | rustix::fs::ResolveFlags::NO_SYMLINKS,
2696 )
2697 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2698 let mut file = File::from(descriptor);
2699 file.write_all(bytes)
2700 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2701 faults.check(RotationCheckpoint::SyncFile(file_kind))?;
2702 file.sync_all()
2703 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
2704}
2705
2706#[cfg(target_os = "linux")]
2707fn directory_mount_is_read_only(directory: &File) -> Result<bool, ManagedSecretError> {
2708 let status = rustix::fs::fstatvfs(directory)
2709 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
2710 Ok(status
2711 .f_flag
2712 .contains(rustix::fs::StatVfsMountFlags::RDONLY))
2713}
2714
2715#[cfg(not(target_os = "linux"))]
2716fn directory_mount_is_read_only(_directory: &File) -> Result<bool, ManagedSecretError> {
2717 Err(ManagedSecretError::new(
2718 ManagedSecretErrorKind::UnsupportedPlatform,
2719 ))
2720}
2721
2722fn encrypt(
2723 recipient: &age::x25519::Recipient,
2724 plaintext: &[u8],
2725) -> Result<Vec<u8>, ManagedSecretError> {
2726 age::encrypt(recipient, plaintext)
2727 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
2728}
2729
2730fn decrypt(
2731 identity: &age::x25519::Identity,
2732 ciphertext: &[u8],
2733) -> Result<Vec<u8>, ManagedSecretError> {
2734 age::decrypt(identity, ciphertext)
2735 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))
2736}
2737
2738fn encode_secret_envelope(
2739 deployment_id: Uuid,
2740 recipient: &str,
2741 metadata: &ManagedSecretMetadata,
2742 material: &SecretMaterial,
2743) -> Result<Vec<u8>, ManagedSecretError> {
2744 let reference = metadata.reference.as_str().as_bytes();
2745 let recipient = recipient.as_bytes();
2746 let reference_len = u16::try_from(reference.len())
2747 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2748 let recipient_len = u16::try_from(recipient.len())
2749 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2750 material.expose(|secret| {
2751 let material_len = u32::try_from(secret.len())
2752 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2753 let mut encoded = Vec::with_capacity(
2754 ENVELOPE_MAGIC.len() + 55 + recipient.len() + reference.len() + secret.len(),
2755 );
2756 encoded.extend_from_slice(ENVELOPE_MAGIC);
2757 encoded.push(ENVELOPE_FORMAT_VERSION);
2758 encoded.extend_from_slice(deployment_id.as_bytes());
2759 encoded.extend_from_slice(&metadata.version.get().to_be_bytes());
2760 encoded.extend_from_slice(&metadata.created_at.timestamp().to_be_bytes());
2761 encoded.extend_from_slice(&metadata.created_at.timestamp_subsec_nanos().to_be_bytes());
2762 encoded.extend_from_slice(&recipient_len.to_be_bytes());
2763 encoded.extend_from_slice(recipient);
2764 encoded.extend_from_slice(&reference_len.to_be_bytes());
2765 encoded.extend_from_slice(reference);
2766 encoded.extend_from_slice(&material_len.to_be_bytes());
2767 encoded.extend_from_slice(secret);
2768 Ok(encoded)
2769 })
2770}
2771
2772fn decode_secret_envelope(
2773 plaintext: &[u8],
2774 deployment_id: Uuid,
2775 recipient: &str,
2776 reference: &ManagedSecretReference,
2777 entry: &ManifestEntry,
2778) -> Result<ResolvedManagedSecret, ManagedSecretError> {
2779 let mut input = EnvelopeReader::new(plaintext);
2780 if input.take(ENVELOPE_MAGIC.len())? != ENVELOPE_MAGIC
2781 || input.u8()? != ENVELOPE_FORMAT_VERSION
2782 || input.take(16)? != deployment_id.as_bytes()
2783 {
2784 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2785 }
2786 let version = ManagedSecretVersion(
2787 NonZeroU64::new(input.u64()?)
2788 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?,
2789 );
2790 let seconds = input.i64()?;
2791 let nanos = input.u32()?;
2792 let created_at = DateTime::<Utc>::from_timestamp(seconds, nanos)
2793 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2794 let envelope_recipient = input.length_prefixed_u16_string()?;
2795 let envelope_reference = input.length_prefixed_u16_string()?;
2796 let material_len = input.u32()? as usize;
2797 if material_len > crate::MAX_SECRET_MATERIAL_BYTES {
2798 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2799 }
2800 let material_bytes = input.take(material_len)?.to_vec();
2801 if !input.is_empty()
2802 || envelope_recipient != recipient
2803 || envelope_reference != reference.as_str()
2804 || version != entry.version
2805 || created_at != entry.created_at
2806 {
2807 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2808 }
2809 let material = SecretMaterial::try_from(material_bytes)
2810 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2811 Ok(ResolvedManagedSecret {
2812 metadata: ManagedSecretMetadata {
2813 reference: reference.clone(),
2814 version,
2815 created_at,
2816 },
2817 material,
2818 })
2819}
2820
2821struct EnvelopeReader<'a> {
2822 remaining: &'a [u8],
2823}
2824
2825impl<'a> EnvelopeReader<'a> {
2826 fn new(input: &'a [u8]) -> Self {
2827 Self { remaining: input }
2828 }
2829
2830 fn take(&mut self, length: usize) -> Result<&'a [u8], ManagedSecretError> {
2831 if self.remaining.len() < length {
2832 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
2833 }
2834 let (value, remaining) = self.remaining.split_at(length);
2835 self.remaining = remaining;
2836 Ok(value)
2837 }
2838
2839 fn u8(&mut self) -> Result<u8, ManagedSecretError> {
2840 Ok(self.take(1)?[0])
2841 }
2842
2843 fn u16(&mut self) -> Result<u16, ManagedSecretError> {
2844 let bytes: [u8; 2] = self
2845 .take(2)?
2846 .try_into()
2847 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2848 Ok(u16::from_be_bytes(bytes))
2849 }
2850
2851 fn u32(&mut self) -> Result<u32, ManagedSecretError> {
2852 let bytes: [u8; 4] = self
2853 .take(4)?
2854 .try_into()
2855 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2856 Ok(u32::from_be_bytes(bytes))
2857 }
2858
2859 fn u64(&mut self) -> Result<u64, ManagedSecretError> {
2860 let bytes: [u8; 8] = self
2861 .take(8)?
2862 .try_into()
2863 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2864 Ok(u64::from_be_bytes(bytes))
2865 }
2866
2867 fn i64(&mut self) -> Result<i64, ManagedSecretError> {
2868 let bytes: [u8; 8] = self
2869 .take(8)?
2870 .try_into()
2871 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
2872 Ok(i64::from_be_bytes(bytes))
2873 }
2874
2875 fn length_prefixed_u16_string(&mut self) -> Result<&'a str, ManagedSecretError> {
2876 let length = self.u16()? as usize;
2877 std::str::from_utf8(self.take(length)?)
2878 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))
2879 }
2880
2881 fn is_empty(&self) -> bool {
2882 self.remaining.is_empty()
2883 }
2884}
2885
2886#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2887enum MutationPhase {
2888 Initialization,
2889 Object,
2890 Manifest,
2891}
2892
2893#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
2895#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2896enum MutationCheckpoint {
2897 Lock,
2898 Decrypt,
2899 Audit,
2900 Verification,
2901 Write(MutationPhase),
2902 Sync(MutationPhase),
2903 Rename(MutationPhase),
2904 DirectorySync(MutationPhase),
2905 CleanupRemove,
2906 CleanupSync,
2907}
2908
2909trait MutationFaultInjector: Send + Sync {
2910 fn check(&self, checkpoint: MutationCheckpoint) -> Result<(), ManagedSecretError>;
2911}
2912
2913struct NoMutationFaults;
2914
2915impl MutationFaultInjector for NoMutationFaults {
2916 fn check(&self, _checkpoint: MutationCheckpoint) -> Result<(), ManagedSecretError> {
2917 Ok(())
2918 }
2919}
2920
2921#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
2923#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2924enum RotationCheckpoint {
2925 Lock,
2926 SourceDecrypt,
2927 SourceAudit,
2928 CreateObjectsDirectory,
2929 Write(RotationFile),
2930 SyncFile(RotationFile),
2931 SyncDirectory(RotationDirectory),
2932 DestinationVerification,
2933}
2934
2935#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
2936#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2937enum RotationFile {
2938 Object,
2939 Lock,
2940 Header,
2941 Manifest,
2942}
2943
2944#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
2945#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2946enum RotationDirectory {
2947 Objects,
2948 Root,
2949}
2950
2951trait RotationFaultInjector {
2952 fn check(&self, checkpoint: RotationCheckpoint) -> Result<(), ManagedSecretError>;
2953}
2954
2955struct NoRotationFaults;
2956
2957impl RotationFaultInjector for NoRotationFaults {
2958 fn check(&self, _checkpoint: RotationCheckpoint) -> Result<(), ManagedSecretError> {
2959 Ok(())
2960 }
2961}
2962
2963#[cfg(target_os = "linux")]
2964fn atomic_write(
2965 directory: &File,
2966 destination: &str,
2967 bytes: &[u8],
2968 faults: &dyn MutationFaultInjector,
2969 phase: MutationPhase,
2970) -> Result<(), AtomicWriteFailure> {
2971 let temp = format!(".mutation-{}.tmp", Uuid::new_v4());
2972 let mut renamed = false;
2973 let result = (|| {
2974 faults.check(MutationCheckpoint::Write(phase))?;
2975 let descriptor = rustix::fs::openat2(
2976 directory,
2977 temp.as_str(),
2978 rustix::fs::OFlags::WRONLY
2979 | rustix::fs::OFlags::CREATE
2980 | rustix::fs::OFlags::EXCL
2981 | rustix::fs::OFlags::CLOEXEC
2982 | rustix::fs::OFlags::NOFOLLOW,
2983 rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR,
2984 rustix::fs::ResolveFlags::BENEATH
2985 | rustix::fs::ResolveFlags::NO_MAGICLINKS
2986 | rustix::fs::ResolveFlags::NO_SYMLINKS,
2987 )
2988 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2989 let mut file = File::from(descriptor);
2990 file.write_all(bytes)
2991 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2992 faults.check(MutationCheckpoint::Sync(phase))?;
2993 file.sync_all()
2994 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2995 drop(file);
2996 faults.check(MutationCheckpoint::Rename(phase))?;
2997 rustix::fs::renameat(directory, temp.as_str(), directory, destination)
2998 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
2999 renamed = true;
3000 faults.check(MutationCheckpoint::DirectorySync(phase))?;
3001 sync_directory(directory)
3002 })();
3003 if result.is_err() {
3004 let _ = rustix::fs::unlinkat(directory, temp.as_str(), rustix::fs::AtFlags::empty());
3005 }
3006 result.map_err(|error| AtomicWriteFailure { error, renamed })
3007}
3008
3009#[cfg(not(target_os = "linux"))]
3010fn atomic_write(
3011 _directory: &File,
3012 _destination: &str,
3013 _bytes: &[u8],
3014 _faults: &dyn MutationFaultInjector,
3015 _phase: MutationPhase,
3016) -> Result<(), AtomicWriteFailure> {
3017 Err(AtomicWriteFailure {
3018 error: ManagedSecretError::new(ManagedSecretErrorKind::UnsupportedPlatform),
3019 renamed: false,
3020 })
3021}
3022
3023#[derive(Debug)]
3024struct AtomicWriteFailure {
3025 error: ManagedSecretError,
3026 renamed: bool,
3027}
3028
3029impl AtomicWriteFailure {
3030 fn into_error(self) -> ManagedSecretError {
3031 self.error
3032 }
3033}
3034
3035#[cfg(target_os = "linux")]
3036fn recover_uncommitted_files(
3037 anchor: &StoreAnchor,
3038 manifest: &StoreManifest,
3039) -> Result<(), ManagedSecretError> {
3040 let referenced: BTreeSet<&str> = manifest
3041 .entries
3042 .values()
3043 .map(|entry| entry.object.as_str())
3044 .chain(manifest.staged.values().map(|entry| entry.object.as_str()))
3045 .collect();
3046 for entry in read_directory(&anchor.objects)? {
3047 let entry =
3048 entry.map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3049 let name = entry.file_name();
3050 let name = name
3051 .to_str()
3052 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3053 if !referenced.contains(name) {
3054 let metadata = entry
3055 .metadata()
3056 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3057 validate_regular_metadata(
3058 &metadata,
3059 OwnershipPolicy::EffectiveUser,
3060 FileModePolicy::Store,
3061 )?;
3062 if valid_object_name(name) || valid_mutation_temp_name(name) {
3063 rustix::fs::unlinkat(&anchor.objects, name, rustix::fs::AtFlags::empty())
3064 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
3065 } else {
3066 return Err(ManagedSecretError::new(
3067 ManagedSecretErrorKind::UnsafeFilesystem,
3068 ));
3069 }
3070 }
3071 }
3072 for entry in read_directory(&anchor.root)? {
3073 let entry =
3074 entry.map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3075 let name = entry.file_name();
3076 let name = name
3077 .to_str()
3078 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3079 if valid_mutation_temp_name(name) {
3080 let metadata = entry
3081 .metadata()
3082 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3083 validate_regular_metadata(
3084 &metadata,
3085 OwnershipPolicy::EffectiveUser,
3086 FileModePolicy::Store,
3087 )?;
3088 rustix::fs::unlinkat(&anchor.root, name, rustix::fs::AtFlags::empty())
3089 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
3090 }
3091 }
3092 sync_directory(&anchor.objects)?;
3093 sync_directory(&anchor.root)
3094}
3095
3096#[cfg(not(target_os = "linux"))]
3097fn recover_uncommitted_files(
3098 _anchor: &StoreAnchor,
3099 _manifest: &StoreManifest,
3100) -> Result<(), ManagedSecretError> {
3101 Err(ManagedSecretError::new(
3102 ManagedSecretErrorKind::UnsupportedPlatform,
3103 ))
3104}
3105
3106fn validate_store_tree(
3107 anchor: &StoreAnchor,
3108 manifest: &StoreManifest,
3109) -> Result<(), ManagedSecretError> {
3110 validate_store_control_plane(anchor)?;
3111
3112 let allowed_objects: BTreeSet<&str> = manifest
3113 .entries
3114 .values()
3115 .map(|entry| entry.object.as_str())
3116 .chain(manifest.staged.values().map(|entry| entry.object.as_str()))
3117 .collect();
3118 for entry in read_directory(&anchor.objects)? {
3119 let entry =
3120 entry.map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3121 let name = entry.file_name();
3122 let name = name
3123 .to_str()
3124 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3125 if !allowed_objects.contains(name) {
3126 return Err(ManagedSecretError::new(
3127 ManagedSecretErrorKind::UnsafeFilesystem,
3128 ));
3129 }
3130 let _ = open_regular_file(
3131 &anchor.objects,
3132 name,
3133 OwnershipPolicy::EffectiveUser,
3134 FileModePolicy::Store,
3135 MAX_CIPHERTEXT_BYTES,
3136 )?;
3137 }
3138 Ok(())
3139}
3140
3141fn validate_store_control_plane(anchor: &StoreAnchor) -> Result<(), ManagedSecretError> {
3142 validate_directory_metadata(
3143 &anchor
3144 .root
3145 .metadata()
3146 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?,
3147 anchor.require_write,
3148 )?;
3149 validate_directory_metadata(
3150 &anchor
3151 .objects
3152 .metadata()
3153 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?,
3154 anchor.require_write,
3155 )?;
3156 for name in [HEADER_FILE, LOCK_FILE, MANIFEST_FILE] {
3157 let _ = open_regular_file(
3158 &anchor.root,
3159 name,
3160 OwnershipPolicy::EffectiveUser,
3161 FileModePolicy::Store,
3162 if name == MANIFEST_FILE {
3163 MAX_MANIFEST_BYTES
3164 } else {
3165 MAX_HEADER_BYTES
3166 },
3167 )?;
3168 }
3169 let allowed_root: BTreeSet<&str> = [HEADER_FILE, LOCK_FILE, MANIFEST_FILE, OBJECTS_DIRECTORY]
3170 .into_iter()
3171 .collect();
3172 for entry in read_directory(&anchor.root)? {
3173 let entry =
3174 entry.map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3175 let name = entry.file_name();
3176 let name = name
3177 .to_str()
3178 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3179 if !allowed_root.contains(name) {
3180 return Err(ManagedSecretError::new(
3181 ManagedSecretErrorKind::UnsafeFilesystem,
3182 ));
3183 }
3184 }
3185 Ok(())
3186}
3187
3188fn valid_object_name(name: &str) -> bool {
3189 name.strip_prefix("object-")
3190 .and_then(|value| value.strip_suffix(".age"))
3191 .and_then(|value| Uuid::parse_str(value).ok())
3192 .is_some()
3193}
3194
3195#[cfg(target_os = "linux")]
3196fn valid_mutation_temp_name(name: &str) -> bool {
3197 name.strip_prefix(".mutation-")
3198 .and_then(|value| value.strip_suffix(".tmp"))
3199 .and_then(|value| Uuid::parse_str(value).ok())
3200 .is_some()
3201}
3202
3203fn read_store_file(
3204 directory: &File,
3205 name: &str,
3206 maximum: u64,
3207) -> Result<Vec<u8>, ManagedSecretError> {
3208 let file = open_regular_file(
3209 directory,
3210 name,
3211 OwnershipPolicy::EffectiveUser,
3212 FileModePolicy::Store,
3213 maximum,
3214 )?;
3215 read_bounded(file, maximum)
3216}
3217
3218fn verify_persisted_object(
3219 objects: &File,
3220 object_name: &str,
3221 deployment_id: Uuid,
3222 identity: &ManagedSecretIdentity,
3223 metadata: &ManagedSecretMetadata,
3224 expected_material: &SecretMaterial,
3225) -> Result<(), ManagedSecretError> {
3226 let ciphertext = read_store_file(objects, object_name, MAX_CIPHERTEXT_BYTES)?;
3227 let plaintext = decrypt(&identity.identity, &ciphertext)?;
3228 let entry = ManifestEntry {
3229 version: metadata.version,
3230 object: object_name.to_string(),
3231 created_at: metadata.created_at,
3232 };
3233 let resolved = decode_secret_envelope(
3234 &plaintext,
3235 deployment_id,
3236 &identity.recipient_string(),
3237 &metadata.reference,
3238 &entry,
3239 )?;
3240 let material_matches = resolved
3241 .material
3242 .expose(|actual| expected_material.expose(|expected| actual == expected));
3243 if resolved.metadata != *metadata || !material_matches {
3244 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
3245 }
3246 Ok(())
3247}
3248
3249fn read_bounded(file: File, maximum: u64) -> Result<Vec<u8>, ManagedSecretError> {
3250 let limit = maximum
3251 .checked_add(1)
3252 .ok_or_else(|| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
3253 let mut bytes = Vec::new();
3254 file.take(limit)
3255 .read_to_end(&mut bytes)
3256 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Integrity))?;
3257 if bytes.len() as u64 > maximum {
3258 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
3259 }
3260 Ok(bytes)
3261}
3262
3263#[derive(Clone, Copy)]
3264enum OwnershipPolicy {
3265 EffectiveUser,
3266}
3267
3268#[derive(Clone, Copy)]
3269enum FileModePolicy {
3270 Store,
3271}
3272
3273#[cfg(target_os = "linux")]
3274fn open_regular_file(
3275 directory: &File,
3276 name: &str,
3277 ownership: OwnershipPolicy,
3278 mode: FileModePolicy,
3279 maximum: u64,
3280) -> Result<File, ManagedSecretError> {
3281 let descriptor = rustix::fs::openat2(
3282 directory,
3283 name,
3284 rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC | rustix::fs::OFlags::NOFOLLOW,
3285 rustix::fs::Mode::empty(),
3286 rustix::fs::ResolveFlags::BENEATH
3287 | rustix::fs::ResolveFlags::NO_MAGICLINKS
3288 | rustix::fs::ResolveFlags::NO_SYMLINKS,
3289 )
3290 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3291 let file = File::from(descriptor);
3292 let metadata = file
3293 .metadata()
3294 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3295 validate_regular_metadata(&metadata, ownership, mode)?;
3296 if metadata.len() > maximum {
3297 return Err(ManagedSecretError::new(ManagedSecretErrorKind::Integrity));
3298 }
3299 Ok(file)
3300}
3301
3302#[cfg(not(target_os = "linux"))]
3303fn open_regular_file(
3304 _directory: &File,
3305 _name: &str,
3306 _ownership: OwnershipPolicy,
3307 _mode: FileModePolicy,
3308 _maximum: u64,
3309) -> Result<File, ManagedSecretError> {
3310 Err(ManagedSecretError::new(
3311 ManagedSecretErrorKind::UnsupportedPlatform,
3312 ))
3313}
3314
3315#[cfg(target_os = "linux")]
3316fn validate_regular_metadata(
3317 metadata: &fs::Metadata,
3318 ownership: OwnershipPolicy,
3319 mode_policy: FileModePolicy,
3320) -> Result<(), ManagedSecretError> {
3321 if !metadata.is_file() {
3322 return Err(ManagedSecretError::new(
3323 ManagedSecretErrorKind::UnsafeFilesystem,
3324 ));
3325 }
3326 validate_regular_unix_attributes(
3327 metadata.uid(),
3328 metadata.gid(),
3329 metadata.nlink(),
3330 metadata.mode() & 0o777,
3331 ownership,
3332 mode_policy,
3333 )
3334}
3335
3336#[cfg(any(target_os = "linux", test))]
3337fn validate_regular_unix_attributes(
3338 uid: u32,
3339 gid: u32,
3340 link_count: u64,
3341 mode: u32,
3342 ownership: OwnershipPolicy,
3343 mode_policy: FileModePolicy,
3344) -> Result<(), ManagedSecretError> {
3345 let owner_matches = match ownership {
3346 OwnershipPolicy::EffectiveUser => uid == effective_uid() && gid == effective_gid(),
3347 };
3348 let unsafe_bits = match mode_policy {
3349 FileModePolicy::Store => mode & !0o640,
3350 };
3351 let readable = match mode_policy {
3352 FileModePolicy::Store => mode & 0o400 != 0,
3353 };
3354 if link_count != 1 || !owner_matches || unsafe_bits != 0 || !readable {
3355 return Err(ManagedSecretError::new(
3356 ManagedSecretErrorKind::UnsafeFilesystem,
3357 ));
3358 }
3359 Ok(())
3360}
3361
3362fn validate_directory(path: &Path, require_write: bool) -> Result<(), ManagedSecretError> {
3363 let metadata = fs::symlink_metadata(path)
3364 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::NotInitialized))?;
3365 validate_directory_metadata(&metadata, require_write)
3366}
3367
3368fn validate_directory_metadata(
3369 metadata: &fs::Metadata,
3370 require_write: bool,
3371) -> Result<(), ManagedSecretError> {
3372 if !metadata.is_dir() {
3373 return Err(ManagedSecretError::new(
3374 ManagedSecretErrorKind::UnsafeFilesystem,
3375 ));
3376 }
3377 #[cfg(unix)]
3378 {
3379 let mode = metadata.mode() & 0o777;
3380 if metadata.uid() != effective_uid()
3381 || metadata.gid() != effective_gid()
3382 || mode & !0o750 != 0
3383 || mode & 0o500 != 0o500
3384 || (require_write && mode & 0o200 == 0)
3385 {
3386 return Err(ManagedSecretError::new(
3387 ManagedSecretErrorKind::UnsafeFilesystem,
3388 ));
3389 }
3390 }
3391 Ok(())
3392}
3393
3394#[cfg(target_os = "linux")]
3395fn open_directory(path: &Path) -> Result<File, ManagedSecretError> {
3396 let descriptor = rustix::fs::open(
3397 path,
3398 rustix::fs::OFlags::RDONLY
3399 | rustix::fs::OFlags::DIRECTORY
3400 | rustix::fs::OFlags::CLOEXEC
3401 | rustix::fs::OFlags::NOFOLLOW,
3402 rustix::fs::Mode::empty(),
3403 )
3404 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3405 Ok(File::from(descriptor))
3406}
3407
3408#[cfg(not(target_os = "linux"))]
3409fn open_directory(_path: &Path) -> Result<File, ManagedSecretError> {
3410 Err(ManagedSecretError::new(
3411 ManagedSecretErrorKind::UnsupportedPlatform,
3412 ))
3413}
3414
3415#[cfg(target_os = "linux")]
3416fn open_directory_at(directory: &File, path: &Path) -> Result<File, ManagedSecretError> {
3417 let descriptor = rustix::fs::openat2(
3418 directory,
3419 path,
3420 rustix::fs::OFlags::RDONLY
3421 | rustix::fs::OFlags::DIRECTORY
3422 | rustix::fs::OFlags::CLOEXEC
3423 | rustix::fs::OFlags::NOFOLLOW,
3424 rustix::fs::Mode::empty(),
3425 rustix::fs::ResolveFlags::BENEATH
3426 | rustix::fs::ResolveFlags::NO_MAGICLINKS
3427 | rustix::fs::ResolveFlags::NO_SYMLINKS,
3428 )
3429 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))?;
3430 Ok(File::from(descriptor))
3431}
3432
3433#[cfg(not(target_os = "linux"))]
3434fn open_directory_at(_directory: &File, _path: &Path) -> Result<File, ManagedSecretError> {
3435 Err(ManagedSecretError::new(
3436 ManagedSecretErrorKind::UnsupportedPlatform,
3437 ))
3438}
3439
3440#[cfg(target_os = "linux")]
3441fn read_directory(directory: &File) -> Result<fs::ReadDir, ManagedSecretError> {
3442 fs::read_dir(format!("/proc/self/fd/{}", directory.as_raw_fd()))
3443 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::UnsafeFilesystem))
3444}
3445
3446#[cfg(not(target_os = "linux"))]
3447fn read_directory(_directory: &File) -> Result<fs::ReadDir, ManagedSecretError> {
3448 Err(ManagedSecretError::new(
3449 ManagedSecretErrorKind::UnsupportedPlatform,
3450 ))
3451}
3452
3453fn create_private_directory(path: &Path) -> Result<(), ManagedSecretError> {
3454 #[cfg(unix)]
3455 {
3456 let mut builder = fs::DirBuilder::new();
3457 builder.mode(0o700);
3458 builder
3459 .create(path)
3460 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
3461 }
3462 #[cfg(not(unix))]
3463 {
3464 let _ = path;
3465 Err(ManagedSecretError::new(
3466 ManagedSecretErrorKind::UnsupportedPlatform,
3467 ))
3468 }
3469}
3470
3471fn effective_uid() -> u32 {
3472 #[cfg(unix)]
3473 {
3474 unsafe { libc::geteuid() }
3476 }
3477 #[cfg(not(unix))]
3478 {
3479 0
3480 }
3481}
3482
3483fn effective_gid() -> u32 {
3484 #[cfg(unix)]
3485 {
3486 unsafe { libc::getegid() }
3488 }
3489 #[cfg(not(unix))]
3490 {
3491 0
3492 }
3493}
3494
3495struct StoreLock {
3496 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
3498 file: File,
3499}
3500
3501enum LockMode {
3502 Shared,
3503 Exclusive,
3504 NonBlockingShared,
3505 NonBlockingExclusive,
3506}
3507
3508impl StoreLock {
3509 fn acquire(root: &File, mode: LockMode) -> Result<Self, ManagedSecretError> {
3510 let file = open_regular_file(
3511 root,
3512 LOCK_FILE,
3513 OwnershipPolicy::EffectiveUser,
3514 FileModePolicy::Store,
3515 MAX_HEADER_BYTES,
3516 )?;
3517 #[cfg(target_os = "linux")]
3518 {
3519 let operation = match mode {
3520 LockMode::Shared => rustix::fs::FlockOperation::LockShared,
3521 LockMode::Exclusive => rustix::fs::FlockOperation::LockExclusive,
3522 LockMode::NonBlockingShared => rustix::fs::FlockOperation::NonBlockingLockShared,
3523 LockMode::NonBlockingExclusive => {
3524 rustix::fs::FlockOperation::NonBlockingLockExclusive
3525 }
3526 };
3527 rustix::fs::flock(&file, operation)
3528 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Coordination))?;
3529 Ok(Self { file })
3530 }
3531 #[cfg(not(target_os = "linux"))]
3532 {
3533 let _ = (file, mode);
3534 Err(ManagedSecretError::new(
3535 ManagedSecretErrorKind::UnsupportedPlatform,
3536 ))
3537 }
3538 }
3539}
3540
3541impl Drop for StoreLock {
3542 fn drop(&mut self) {
3543 #[cfg(target_os = "linux")]
3544 {
3545 let _ = rustix::fs::flock(&self.file, rustix::fs::FlockOperation::Unlock);
3546 }
3547 }
3548}
3549
3550fn sync_directory(directory: &File) -> Result<(), ManagedSecretError> {
3551 directory
3552 .sync_all()
3553 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
3554}
3555
3556fn sync_directory_path(path: &Path) -> Result<(), ManagedSecretError> {
3557 let directory = open_directory(path)
3558 .map_err(|_| ManagedSecretError::new(ManagedSecretErrorKind::Mutation))?;
3559 sync_directory(&directory)
3560}
3561
3562#[cfg(target_os = "linux")]
3563fn remove_file_if_present(directory: &File, name: &str) -> Result<(), ManagedSecretError> {
3564 match rustix::fs::unlinkat(directory, name, rustix::fs::AtFlags::empty()) {
3565 Ok(()) => Ok(()),
3566 Err(error) if error == rustix::io::Errno::NOENT => Ok(()),
3567 Err(_) => Err(ManagedSecretError::new(ManagedSecretErrorKind::Mutation)),
3568 }
3569}
3570
3571#[cfg(not(target_os = "linux"))]
3572fn remove_file_if_present(_directory: &File, _name: &str) -> Result<(), ManagedSecretError> {
3573 Err(ManagedSecretError::new(
3574 ManagedSecretErrorKind::UnsupportedPlatform,
3575 ))
3576}
3577
3578#[cfg(target_os = "linux")]
3579fn rename_directory_create_only(
3580 source: &Path,
3581 destination: &Path,
3582) -> Result<(), ManagedSecretError> {
3583 rustix::fs::renameat_with(
3584 rustix::fs::CWD,
3585 source,
3586 rustix::fs::CWD,
3587 destination,
3588 rustix::fs::RenameFlags::NOREPLACE,
3589 )
3590 .map_err(|error| {
3591 if error == rustix::io::Errno::EXIST {
3592 ManagedSecretError::new(ManagedSecretErrorKind::AlreadyInitialized)
3593 } else {
3594 ManagedSecretError::new(ManagedSecretErrorKind::Mutation)
3595 }
3596 })
3597}
3598
3599#[cfg(not(target_os = "linux"))]
3600fn rename_directory_create_only(
3601 _source: &Path,
3602 _destination: &Path,
3603) -> Result<(), ManagedSecretError> {
3604 Err(ManagedSecretError::new(
3605 ManagedSecretErrorKind::UnsupportedPlatform,
3606 ))
3607}
3608
3609fn ensure_supported_platform() -> Result<(), ManagedSecretError> {
3610 if cfg!(target_os = "linux") {
3611 Ok(())
3612 } else {
3613 Err(ManagedSecretError::new(
3614 ManagedSecretErrorKind::UnsupportedPlatform,
3615 ))
3616 }
3617}
3618
3619#[cfg(test)]
3620mod tests {
3621 use super::*;
3622 use crate::SecretMaterial;
3623 use age::secrecy::ExposeSecret;
3624 use std::os::unix::fs::PermissionsExt;
3625 use std::str::FromStr;
3626 use std::sync::{Barrier, Mutex};
3627 use std::thread;
3628
3629 struct FixedReferenceAuthority(Option<ManagedSecretReferenceStatus>);
3630
3631 impl ManagedSecretReferenceAuthority for FixedReferenceAuthority {
3632 fn reference_status(
3633 &self,
3634 _reference: &ManagedSecretReference,
3635 ) -> Result<ManagedSecretReferenceStatus, ManagedSecretReferenceInspectionError> {
3636 self.0
3637 .ok_or_else(ManagedSecretReferenceInspectionError::unavailable)
3638 }
3639 }
3640
3641 #[test]
3642 fn backend_creates_and_resolves_authenticated_material() {
3643 let fixture = StoreFixture::new();
3644 let deployment_id = Uuid::new_v4();
3645 let (identity, _) = test_identity_pair();
3646 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3647 .unwrap();
3648 let reference = ManagedSecretReference::from_str("managed://postgres/admin").unwrap();
3649 let material = SecretMaterial::try_from(b"credential-canary".to_vec()).unwrap();
3650
3651 let metadata = store.create(&reference, &material).unwrap();
3652 assert_eq!(metadata.reference(), &reference);
3653 assert_eq!(metadata.version().get(), 1);
3654
3655 let resolved = store.resolve(&reference).unwrap();
3656 assert_eq!(resolved.metadata(), &metadata);
3657 resolved
3658 .material()
3659 .expose(|value| assert_eq!(value, b"credential-canary"));
3660 }
3661
3662 #[test]
3663 fn owner_replacement_is_durable_but_inactive_until_atomic_activation() {
3664 let fixture = StoreFixture::new();
3665 let deployment_id = Uuid::new_v4();
3666 let (identity, encoded_identity) = test_identity_pair();
3667 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3668 .unwrap();
3669 let reference = managed_reference("managed://datastore/owner/credential");
3670 let prior = store
3671 .create(&reference, &secret(b"prior-credential"))
3672 .unwrap();
3673
3674 let staged = store
3675 .stage_replacement(&reference, &secret(b"staged-credential"), prior.version())
3676 .unwrap();
3677 assert_eq!(staged.metadata().version().get(), 2);
3678 store
3679 .resolve(&reference)
3680 .unwrap()
3681 .material()
3682 .expose(|value| assert_eq!(value, b"prior-credential"));
3683 drop(store);
3684
3685 let reopened = LocalManagedSecretStore::open_at(
3686 fixture.root(),
3687 deployment_id,
3688 identity_from_text(&encoded_identity),
3689 )
3690 .unwrap();
3691 let resumed = reopened.pending_replacement(&reference).unwrap().unwrap();
3692 resumed
3693 .material()
3694 .expose(|value| assert_eq!(value, b"staged-credential"));
3695 let activated = reopened.activate_replacement(&resumed).unwrap();
3696 assert_eq!(activated.version().get(), 2);
3697 reopened
3698 .resolve(&reference)
3699 .unwrap()
3700 .material()
3701 .expose(|value| assert_eq!(value, b"staged-credential"));
3702 assert_eq!(read_directory(&reopened.anchor.objects).unwrap().count(), 1);
3703 }
3704 #[test]
3705 fn owner_replacement_batch_activates_all_artifacts_in_one_generation() {
3706 let fixture = StoreFixture::new();
3707 let deployment_id = Uuid::new_v4();
3708 let (identity, encoded_identity) = test_identity_pair();
3709 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3710 .unwrap();
3711 let oauth = managed_reference("managed://authentication/oauth/client-secret");
3712 let runtime = managed_reference("managed://authentication/runtime/credential");
3713 let oauth_prior = store.create(&oauth, &secret(b"oauth-v1")).unwrap();
3714 let runtime_prior = store.create(&runtime, &secret(b"runtime-v1")).unwrap();
3715 let oauth_staged = store
3716 .stage_replacement(&oauth, &secret(b"oauth-v2"), oauth_prior.version())
3717 .unwrap();
3718 let runtime_staged = store
3719 .stage_replacement(&runtime, &secret(b"runtime-v2"), runtime_prior.version())
3720 .unwrap();
3721 let prior_generation = read_manifest(&store.anchor.root, &store.identity)
3722 .unwrap()
3723 .generation;
3724
3725 let activated = store
3726 .activate_replacements(&[&oauth_staged, &runtime_staged])
3727 .unwrap();
3728 assert_eq!(activated.len(), 2);
3729 drop(store);
3730
3731 let reopened = LocalManagedSecretStore::open_at(
3732 fixture.root(),
3733 deployment_id,
3734 identity_from_text(&encoded_identity),
3735 )
3736 .unwrap();
3737 reopened
3738 .resolve(&oauth)
3739 .unwrap()
3740 .material()
3741 .expose(|value| assert_eq!(value, b"oauth-v2"));
3742 reopened
3743 .resolve(&runtime)
3744 .unwrap()
3745 .material()
3746 .expose(|value| assert_eq!(value, b"runtime-v2"));
3747 let manifest = read_manifest(&reopened.anchor.root, &reopened.identity).unwrap();
3748 assert_eq!(manifest.generation, prior_generation + 2);
3749 assert!(manifest.staged.is_empty());
3750 }
3751
3752 #[test]
3753 fn failed_owner_workflow_discards_stage_and_preserves_prior_active_credential() {
3754 let fixture = StoreFixture::new();
3755 let deployment_id = Uuid::new_v4();
3756 let (identity, _) = test_identity_pair();
3757 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3758 .unwrap();
3759 let reference = managed_reference("managed://authentication/owner/artifact");
3760 let prior = store
3761 .create(&reference, &secret(b"working-credential"))
3762 .unwrap();
3763 let staged = store
3764 .stage_replacement(&reference, &secret(b"rejected-credential"), prior.version())
3765 .unwrap();
3766
3767 store.discard_replacement(&staged).unwrap();
3768
3769 assert!(store.pending_replacement(&reference).unwrap().is_none());
3770 store
3771 .resolve(&reference)
3772 .unwrap()
3773 .material()
3774 .expose(|value| assert_eq!(value, b"working-credential"));
3775 assert_eq!(read_directory(&store.anchor.objects).unwrap().count(), 1);
3776 }
3777
3778 #[test]
3779 fn removal_requires_absence_proof_from_every_reference_authority() {
3780 let fixture = StoreFixture::new();
3781 let deployment_id = Uuid::new_v4();
3782 let (identity, encoded_identity) = test_identity_pair();
3783 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3784 .unwrap();
3785 let reference = managed_reference("managed://datastore/owner/credential");
3786 let metadata = store
3787 .create(&reference, &secret(b"removal-canary"))
3788 .unwrap();
3789 let clear = FixedReferenceAuthority(Some(ManagedSecretReferenceStatus::Unreferenced));
3790 let referenced = FixedReferenceAuthority(Some(ManagedSecretReferenceStatus::Referenced));
3791 let unavailable = FixedReferenceAuthority(None);
3792
3793 let blocked = ManagedSecretReferenceAuthorities::new(&clear, &referenced, &clear, &clear)
3794 .prove_unreferenced(&reference)
3795 .unwrap_err();
3796 assert_eq!(
3797 blocked.authority(),
3798 ManagedSecretReferenceAuthorityKind::DatastoreBindings
3799 );
3800 assert_eq!(
3801 blocked.kind(),
3802 ManagedSecretRemovalProofErrorKind::Referenced
3803 );
3804 assert_eq!(store.inspect(&reference).unwrap(), metadata);
3805
3806 let unproven = ManagedSecretReferenceAuthorities::new(&clear, &clear, &unavailable, &clear)
3807 .prove_unreferenced(&reference)
3808 .unwrap_err();
3809 assert_eq!(
3810 unproven.authority(),
3811 ManagedSecretReferenceAuthorityKind::SessionRecords
3812 );
3813 assert_eq!(
3814 unproven.kind(),
3815 ManagedSecretRemovalProofErrorKind::InspectionUnavailable
3816 );
3817 assert_eq!(store.inspect(&reference).unwrap(), metadata);
3818
3819 let proof = ManagedSecretReferenceAuthorities::new(&clear, &clear, &clear, &clear)
3820 .prove_unreferenced(&reference)
3821 .unwrap();
3822 let removed = store.remove_proven_unreferenced(&reference, proof).unwrap();
3823 assert_eq!(removed, metadata);
3824 assert_eq!(
3825 store.resolve(&reference).unwrap_err().kind(),
3826 ManagedSecretErrorKind::Missing
3827 );
3828 drop(store);
3829
3830 let verification = LocalManagedSecretStore::verify_all_at(
3831 fixture.root(),
3832 deployment_id,
3833 identity_from_text(&encoded_identity),
3834 )
3835 .unwrap();
3836 assert_eq!(verification.active_secret_count(), 0);
3837 assert_eq!(verification.audit_record_count(), 3);
3838 }
3839 #[test]
3840 fn datastore_reference_inventory_epoch_is_authenticated_and_legacy_stores_fail_closed() {
3841 let fixture = StoreFixture::new();
3842 let deployment_id = Uuid::new_v4();
3843 let (identity, _) = test_identity_pair();
3844 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3845 .unwrap();
3846 assert!(store.datastore_reference_inventory_is_complete().unwrap());
3847
3848 let mut manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
3849 manifest.datastore_reference_inventory_version = 0;
3850 write_manifest(
3851 &store.anchor.root,
3852 &manifest,
3853 &store.identity,
3854 store.faults.as_ref(),
3855 )
3856 .unwrap();
3857 assert!(!store.datastore_reference_inventory_is_complete().unwrap());
3858
3859 manifest.datastore_reference_inventory_version = DATASTORE_REFERENCE_INVENTORY_VERSION + 1;
3860 write_manifest(
3861 &store.anchor.root,
3862 &manifest,
3863 &store.identity,
3864 store.faults.as_ref(),
3865 )
3866 .unwrap();
3867 assert_eq!(
3868 store
3869 .datastore_reference_inventory_is_complete()
3870 .unwrap_err()
3871 .kind(),
3872 ManagedSecretErrorKind::Integrity
3873 );
3874 }
3875
3876 #[test]
3877 fn durable_reference_holder_survives_restart_and_invalidates_stale_absence_proof() {
3878 let fixture = StoreFixture::new();
3879 let deployment_id = Uuid::new_v4();
3880 let (identity, encoded_identity) = test_identity_pair();
3881 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3882 .unwrap();
3883 let reference = managed_reference("managed://datastore/owner/credential");
3884 store.create(&reference, &secret(b"credential-v1")).unwrap();
3885 let clear = FixedReferenceAuthority(Some(ManagedSecretReferenceStatus::Unreferenced));
3886 let stale_proof = ManagedSecretReferenceAuthorities::new(&clear, &clear, &clear, &clear)
3887 .prove_unreferenced(&reference)
3888 .unwrap();
3889 store
3890 .replace_reference_holder("session:1a", std::slice::from_ref(&reference))
3891 .unwrap();
3892 drop(store);
3893
3894 let reopened = LocalManagedSecretStore::open_at(
3895 fixture.root(),
3896 deployment_id,
3897 identity_from_text(&encoded_identity),
3898 )
3899 .unwrap();
3900 assert_eq!(
3901 reopened.held_reference_status(&reference).unwrap(),
3902 ManagedSecretReferenceStatus::Referenced
3903 );
3904 assert_eq!(
3905 reopened
3906 .remove_proven_unreferenced(&reference, stale_proof)
3907 .unwrap_err()
3908 .kind(),
3909 ManagedSecretErrorKind::VersionConflict
3910 );
3911
3912 reopened.remove_reference_holder("session:1a").unwrap();
3913 let fresh_proof = ManagedSecretReferenceAuthorities::new(&clear, &clear, &clear, &clear)
3914 .prove_unreferenced(&reference)
3915 .unwrap();
3916 reopened
3917 .remove_proven_unreferenced(&reference, fresh_proof)
3918 .unwrap();
3919 }
3920
3921 #[test]
3922 fn list_and_inspect_return_only_safe_active_metadata() {
3923 let fixture = StoreFixture::new();
3924 let deployment_id = Uuid::new_v4();
3925 let (identity, _) = test_identity_pair();
3926 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
3927 .unwrap();
3928 let first = managed_reference("managed://oauth/client-token");
3929 let second = managed_reference("managed://postgres/admin");
3930 store.create(&second, &secret(b"postgres-canary")).unwrap();
3931 store.create(&first, &secret(b"oauth-canary")).unwrap();
3932
3933 let listed = store.list().unwrap();
3934 assert_eq!(listed.len(), 2);
3935 assert_eq!(listed[0].reference(), &first);
3936 assert_eq!(listed[1].reference(), &second);
3937 assert_eq!(listed[0].version().get(), 1);
3938
3939 let inspected = store.inspect(&first).unwrap();
3940 assert_eq!(inspected, listed[0]);
3941 let rendered = serde_json::to_string(&(listed, inspected)).unwrap();
3942 assert!(!rendered.contains("oauth-canary"));
3943 assert!(!rendered.contains("postgres-canary"));
3944 assert!(!rendered.contains(fixture.root().to_string_lossy().as_ref()));
3945 }
3946
3947 #[test]
3948 fn initialization_is_create_only_and_identity_debug_is_redacted() {
3949 let fixture = StoreFixture::new();
3950 let deployment_id = Uuid::new_v4();
3951 let (identity, encoded) = test_identity_pair();
3952 let debug = format!("{identity:?}");
3953 assert!(!debug.contains(&encoded));
3954 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity).unwrap();
3955
3956 let (different_identity, _) = test_identity_pair();
3957 let error = LocalManagedSecretStore::initialize_at(
3958 fixture.root(),
3959 deployment_id,
3960 different_identity,
3961 )
3962 .unwrap_err();
3963 assert_eq!(error.kind(), ManagedSecretErrorKind::AlreadyInitialized);
3964 }
3965
3966 #[test]
3967 fn initialization_reports_commit_uncertain_after_the_store_rename() {
3968 let fixture = StoreFixture::new();
3969 let deployment_id = Uuid::new_v4();
3970 let (identity, encoded) = test_identity_pair();
3971 let error = LocalManagedSecretStore::initialize_at_with_faults(
3972 fixture.root(),
3973 deployment_id,
3974 identity,
3975 Arc::new(FailAt(MutationCheckpoint::DirectorySync(
3976 MutationPhase::Initialization,
3977 ))),
3978 )
3979 .unwrap_err();
3980 assert_eq!(error.kind(), ManagedSecretErrorKind::CommitUncertain);
3981
3982 let reopened = LocalManagedSecretStore::open_at(
3983 fixture.root(),
3984 deployment_id,
3985 identity_from_text(&encoded),
3986 )
3987 .unwrap();
3988 assert_eq!(
3989 read_manifest(&reopened.anchor.root, &reopened.identity)
3990 .unwrap()
3991 .generation,
3992 0
3993 );
3994 drop(reopened);
3995
3996 let (different_identity, _) = test_identity_pair();
3997 let retry = LocalManagedSecretStore::initialize_at(
3998 fixture.root(),
3999 deployment_id,
4000 different_identity,
4001 )
4002 .unwrap_err();
4003 assert_eq!(retry.kind(), ManagedSecretErrorKind::AlreadyInitialized);
4004 }
4005
4006 #[test]
4007 fn backend_round_trips_the_maximum_secret_material_size() {
4008 let fixture = StoreFixture::new();
4009 let deployment_id = Uuid::new_v4();
4010 let (identity, _) = test_identity_pair();
4011 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4012 .unwrap();
4013 let reference = managed_reference("managed://bounds/maximum");
4014 let material = secret(&vec![b'x'; crate::MAX_SECRET_MATERIAL_BYTES]);
4015
4016 store.create(&reference, &material).unwrap();
4017 store
4018 .resolve(&reference)
4019 .unwrap()
4020 .material()
4021 .expose(|value| assert_eq!(value.len(), crate::MAX_SECRET_MATERIAL_BYTES));
4022 assert!(SecretMaterial::try_from(vec![0; crate::MAX_SECRET_MATERIAL_BYTES + 1]).is_err());
4023 }
4024
4025 #[test]
4026 fn active_versions_increase_monotonically_and_create_never_overwrites() {
4027 let fixture = StoreFixture::new();
4028 let deployment_id = Uuid::new_v4();
4029 let (identity, _) = test_identity_pair();
4030 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4031 .unwrap();
4032 let reference = managed_reference("managed://oauth/token");
4033 let first = secret(b"first-version");
4034 let second = secret(b"second-version");
4035 let third = secret(b"third-version");
4036
4037 let first_metadata = store.create(&reference, &first).unwrap();
4038 let duplicate = store.create(&reference, &second).unwrap_err();
4039 assert_eq!(duplicate.kind(), ManagedSecretErrorKind::AlreadyExists);
4040
4041 let second_metadata = store
4042 .replace_for_test(&reference, &second, first_metadata.version())
4043 .unwrap();
4044 let stale = store
4045 .replace_for_test(&reference, &third, first_metadata.version())
4046 .unwrap_err();
4047 assert_eq!(stale.kind(), ManagedSecretErrorKind::VersionConflict);
4048 let third_metadata = store
4049 .replace_for_test(&reference, &third, second_metadata.version())
4050 .unwrap();
4051 assert_eq!(second_metadata.version().get(), 2);
4052 assert_eq!(third_metadata.version().get(), 3);
4053
4054 let resolved = store.resolve(&reference).unwrap();
4055 assert_eq!(resolved.metadata().version().get(), 3);
4056 resolved
4057 .material()
4058 .expose(|value| assert_eq!(value, b"third-version"));
4059 assert_eq!(
4060 fs::read_dir(fixture.root().join(OBJECTS_DIRECTORY))
4061 .unwrap()
4062 .count(),
4063 1
4064 );
4065 }
4066
4067 #[test]
4068 fn opening_rejects_deployment_and_recipient_mismatch() {
4069 let fixture = StoreFixture::new();
4070 let deployment_id = Uuid::new_v4();
4071 let (identity, encoded) = test_identity_pair();
4072 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity).unwrap();
4073
4074 let wrong_deployment = LocalManagedSecretStore::open_at(
4075 fixture.root(),
4076 Uuid::new_v4(),
4077 identity_from_text(&encoded),
4078 )
4079 .unwrap_err();
4080 assert_eq!(
4081 wrong_deployment.kind(),
4082 ManagedSecretErrorKind::DeploymentMismatch
4083 );
4084
4085 let (wrong_identity, _) = test_identity_pair();
4086 let wrong_recipient =
4087 LocalManagedSecretStore::open_at(fixture.root(), deployment_id, wrong_identity)
4088 .unwrap_err();
4089 assert_eq!(
4090 wrong_recipient.kind(),
4091 ManagedSecretErrorKind::RecipientMismatch
4092 );
4093 }
4094
4095 #[test]
4096 fn ciphertext_relocation_and_corruption_fail_authentication() {
4097 let fixture = StoreFixture::new();
4098 let deployment_id = Uuid::new_v4();
4099 let (identity, _) = test_identity_pair();
4100 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4101 .unwrap();
4102 let first_reference = managed_reference("managed://datastore/first");
4103 let second_reference = managed_reference("managed://datastore/second");
4104 store
4105 .create(&first_reference, &secret(b"first-secret"))
4106 .unwrap();
4107 store
4108 .create(&second_reference, &secret(b"second-secret"))
4109 .unwrap();
4110 let manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4111 let first_path = fixture
4112 .root()
4113 .join(OBJECTS_DIRECTORY)
4114 .join(&manifest.entries[first_reference.as_str()].object);
4115 let second_path = fixture
4116 .root()
4117 .join(OBJECTS_DIRECTORY)
4118 .join(&manifest.entries[second_reference.as_str()].object);
4119 let swap_path = fixture
4120 .root()
4121 .join(OBJECTS_DIRECTORY)
4122 .join(format!(".mutation-{}.tmp", Uuid::new_v4()));
4123 fs::rename(&first_path, &swap_path).unwrap();
4124 fs::rename(&second_path, &first_path).unwrap();
4125 fs::rename(&swap_path, &second_path).unwrap();
4126
4127 let relocated = store.resolve(&first_reference).unwrap_err();
4128 assert_eq!(relocated.kind(), ManagedSecretErrorKind::Integrity);
4129
4130 let mut ciphertext = fs::read(&first_path).unwrap();
4131 let middle = ciphertext.len() / 2;
4132 ciphertext[middle] ^= 0x80;
4133 fs::write(&first_path, ciphertext).unwrap();
4134 let corrupted = store.resolve(&first_reference).unwrap_err();
4135 assert_eq!(corrupted.kind(), ManagedSecretErrorKind::Integrity);
4136 }
4137
4138 #[test]
4139 fn unsafe_store_permissions_fail_closed() {
4140 let fixture = StoreFixture::new();
4141 let deployment_id = Uuid::new_v4();
4142 let (identity, _) = test_identity_pair();
4143 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4144 .unwrap();
4145 let reference = managed_reference("managed://runtime/key");
4146 store.create(&reference, &secret(b"runtime-key")).unwrap();
4147
4148 fs::set_permissions(fixture.root(), fs::Permissions::from_mode(0o770)).unwrap();
4149 let error = store.resolve(&reference).unwrap_err();
4150 assert_eq!(error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4151 }
4152
4153 #[test]
4154 fn unsafe_modes_on_every_store_file_fail_closed() {
4155 let fixture = StoreFixture::new();
4156 let deployment_id = Uuid::new_v4();
4157 let (identity, _) = test_identity_pair();
4158 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4159 .unwrap();
4160 let reference = managed_reference("managed://filesystem/file-modes");
4161 store.create(&reference, &secret(b"mode-canary")).unwrap();
4162 let manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4163 let object = fixture
4164 .root()
4165 .join(OBJECTS_DIRECTORY)
4166 .join(&manifest.entries[reference.as_str()].object);
4167
4168 for path in [
4169 fixture.root().join(HEADER_FILE),
4170 fixture.root().join(LOCK_FILE),
4171 fixture.root().join(MANIFEST_FILE),
4172 object,
4173 ] {
4174 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
4175 let error = store.resolve(&reference).unwrap_err();
4176 assert_eq!(error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4177 fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap();
4178 store.resolve(&reference).unwrap();
4179 }
4180 }
4181
4182 #[test]
4183 fn store_file_security_attributes_require_effective_uid_gid_and_one_link() {
4184 let uid = effective_uid();
4185 let gid = effective_gid();
4186 assert!(
4187 validate_regular_unix_attributes(
4188 uid,
4189 gid,
4190 1,
4191 0o600,
4192 OwnershipPolicy::EffectiveUser,
4193 FileModePolicy::Store,
4194 )
4195 .is_ok()
4196 );
4197 for (candidate_uid, candidate_gid, links, mode) in [
4198 (uid.wrapping_add(1), gid, 1, 0o600),
4199 (uid, gid.wrapping_add(1), 1, 0o600),
4200 (uid, gid, 2, 0o600),
4201 (uid, gid, 1, 0o644),
4202 ] {
4203 let error = validate_regular_unix_attributes(
4204 candidate_uid,
4205 candidate_gid,
4206 links,
4207 mode,
4208 OwnershipPolicy::EffectiveUser,
4209 FileModePolicy::Store,
4210 )
4211 .unwrap_err();
4212 assert_eq!(error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4213 }
4214 }
4215
4216 #[test]
4217 fn replaced_objects_directory_cannot_redirect_reads_or_writes() {
4218 use std::os::unix::fs::symlink;
4219
4220 let fixture = StoreFixture::new();
4221 let outside = StoreFixture::new();
4222 fs::create_dir(outside.root()).unwrap();
4223 fs::set_permissions(outside.root(), fs::Permissions::from_mode(0o700)).unwrap();
4224 let deployment_id = Uuid::new_v4();
4225 let (identity, _) = test_identity_pair();
4226 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4227 .unwrap();
4228 let reference = managed_reference("managed://filesystem/anchored");
4229 store
4230 .create(&reference, &secret(b"anchored-value"))
4231 .unwrap();
4232
4233 let objects = fixture.root().join(OBJECTS_DIRECTORY);
4234 fs::rename(&objects, fixture.root().join("objects-original")).unwrap();
4235 symlink(outside.root(), &objects).unwrap();
4236
4237 let read_error = store.resolve(&reference).unwrap_err();
4238 assert_eq!(read_error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4239 let write_error = store
4240 .create(
4241 &managed_reference("managed://filesystem/redirected"),
4242 &secret(b"must-not-escape"),
4243 )
4244 .unwrap_err();
4245 assert_eq!(write_error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4246 assert_eq!(fs::read_dir(outside.root()).unwrap().count(), 0);
4247 }
4248
4249 #[test]
4250 fn concurrent_writers_preserve_every_committed_reference() {
4251 let fixture = StoreFixture::new();
4252 let deployment_id = Uuid::new_v4();
4253 let (identity, _) = test_identity_pair();
4254 let store = Arc::new(
4255 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4256 .unwrap(),
4257 );
4258 let barrier = Arc::new(Barrier::new(9));
4259 let mut writers = Vec::new();
4260 for index in 0..8 {
4261 let store = Arc::clone(&store);
4262 let barrier = Arc::clone(&barrier);
4263 writers.push(thread::spawn(move || {
4264 let reference = managed_reference(&format!("managed://concurrent/item-{index}"));
4265 let value = secret(format!("value-{index}").as_bytes());
4266 barrier.wait();
4267 store.create(&reference, &value).unwrap();
4268 }));
4269 }
4270 barrier.wait();
4271 for writer in writers {
4272 writer.join().unwrap();
4273 }
4274 for index in 0..8 {
4275 let reference = managed_reference(&format!("managed://concurrent/item-{index}"));
4276 let expected = format!("value-{index}");
4277 store
4278 .resolve(&reference)
4279 .unwrap()
4280 .material()
4281 .expose(|value| assert_eq!(value, expected.as_bytes()));
4282 }
4283 }
4284
4285 #[test]
4286 fn concurrent_readers_observe_only_complete_active_versions() {
4287 let fixture = StoreFixture::new();
4288 let deployment_id = Uuid::new_v4();
4289 let (identity, _) = test_identity_pair();
4290 let store = Arc::new(
4291 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4292 .unwrap(),
4293 );
4294 let reference = managed_reference("managed://concurrent/active");
4295 let first = store.create(&reference, &secret(b"version-one")).unwrap();
4296 let barrier = Arc::new(Barrier::new(9));
4297 let mut readers = Vec::new();
4298 for _ in 0..8 {
4299 let store = Arc::clone(&store);
4300 let reference = reference.clone();
4301 let barrier = Arc::clone(&barrier);
4302 readers.push(thread::spawn(move || {
4303 barrier.wait();
4304 for _ in 0..16 {
4305 let resolved = store.resolve(&reference).unwrap();
4306 let version = resolved.metadata().version().get();
4307 resolved.material().expose(|value| match version {
4308 1 => assert_eq!(value, b"version-one"),
4309 2 => assert_eq!(value, b"version-two"),
4310 _ => panic!("unexpected active version"),
4311 });
4312 }
4313 }));
4314 }
4315 barrier.wait();
4316 let second = store
4317 .replace_for_test(&reference, &secret(b"version-two"), first.version())
4318 .unwrap();
4319 assert_eq!(second.version().get(), 2);
4320 for reader in readers {
4321 reader.join().unwrap();
4322 }
4323 }
4324
4325 #[test]
4326 fn injected_mutation_failures_preserve_the_prior_active_version() {
4327 let checkpoints = [
4328 MutationCheckpoint::Write(MutationPhase::Object),
4329 MutationCheckpoint::Sync(MutationPhase::Object),
4330 MutationCheckpoint::Rename(MutationPhase::Object),
4331 MutationCheckpoint::DirectorySync(MutationPhase::Object),
4332 MutationCheckpoint::Write(MutationPhase::Manifest),
4333 MutationCheckpoint::Sync(MutationPhase::Manifest),
4334 MutationCheckpoint::Rename(MutationPhase::Manifest),
4335 ];
4336 for checkpoint in checkpoints {
4337 let fixture = StoreFixture::new();
4338 let deployment_id = Uuid::new_v4();
4339 let (identity, encoded) = test_identity_pair();
4340 let initial =
4341 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4342 .unwrap();
4343 let reference = managed_reference("managed://failure/active");
4344 let first = initial
4345 .create(&reference, &secret(b"stable-value"))
4346 .unwrap();
4347 drop(initial);
4348
4349 let store = LocalManagedSecretStore::open_at_with_faults(
4350 fixture.root(),
4351 deployment_id,
4352 Arc::new(identity_from_text(&encoded)),
4353 Arc::new(FailAt(checkpoint)),
4354 )
4355 .unwrap();
4356 let failure = store
4357 .replace_for_test(&reference, &secret(b"failed-value"), first.version())
4358 .unwrap_err();
4359 assert_eq!(failure.kind(), ManagedSecretErrorKind::Mutation);
4360 store
4361 .resolve(&reference)
4362 .unwrap()
4363 .material()
4364 .expose(|value| assert_eq!(value, b"stable-value"));
4365 drop(store);
4366
4367 let reopened = LocalManagedSecretStore::open_at(
4368 fixture.root(),
4369 deployment_id,
4370 identity_from_text(&encoded),
4371 )
4372 .unwrap();
4373 reopened
4374 .resolve(&reference)
4375 .unwrap()
4376 .material()
4377 .expose(|value| assert_eq!(value, b"stable-value"));
4378 }
4379 }
4380
4381 #[test]
4382 fn injected_coordination_decrypt_audit_and_verification_fail_closed() {
4383 for (checkpoint, expected_kind) in [
4384 (
4385 MutationCheckpoint::Lock,
4386 ManagedSecretErrorKind::Coordination,
4387 ),
4388 (
4389 MutationCheckpoint::Decrypt,
4390 ManagedSecretErrorKind::Integrity,
4391 ),
4392 (MutationCheckpoint::Audit, ManagedSecretErrorKind::Mutation),
4393 (
4394 MutationCheckpoint::Verification,
4395 ManagedSecretErrorKind::Mutation,
4396 ),
4397 ] {
4398 let fixture = StoreFixture::new();
4399 let deployment_id = Uuid::new_v4();
4400 let (identity, encoded) = test_identity_pair();
4401 let initial =
4402 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4403 .unwrap();
4404 let reference = managed_reference("managed://failure/non-write-phase");
4405 let first = initial
4406 .create(&reference, &secret(b"stable-value"))
4407 .unwrap();
4408 drop(initial);
4409
4410 let store = LocalManagedSecretStore::open_at_with_faults(
4411 fixture.root(),
4412 deployment_id,
4413 Arc::new(identity_from_text(&encoded)),
4414 Arc::new(FailAt(checkpoint)),
4415 )
4416 .unwrap();
4417 let error = store
4418 .replace_for_test(&reference, &secret(b"failed-value"), first.version())
4419 .unwrap_err();
4420 assert_eq!(error.kind(), expected_kind);
4421 store
4422 .resolve(&reference)
4423 .unwrap()
4424 .material()
4425 .expose(|value| assert_eq!(value, b"stable-value"));
4426 assert_eq!(
4427 fs::read_dir(fixture.root().join(OBJECTS_DIRECTORY))
4428 .unwrap()
4429 .count(),
4430 1
4431 );
4432 }
4433 }
4434
4435 #[test]
4436 fn post_activation_sync_failure_preserves_a_recoverable_committed_state() {
4437 for checkpoint in [
4438 MutationCheckpoint::DirectorySync(MutationPhase::Manifest),
4439 MutationCheckpoint::CleanupRemove,
4440 MutationCheckpoint::CleanupSync,
4441 ] {
4442 let fixture = StoreFixture::new();
4443 let deployment_id = Uuid::new_v4();
4444 let (identity, encoded) = test_identity_pair();
4445 let initial =
4446 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4447 .unwrap();
4448 let reference = managed_reference("managed://failure/commit-uncertain");
4449 let first = initial.create(&reference, &secret(b"first-value")).unwrap();
4450 drop(initial);
4451
4452 let store = LocalManagedSecretStore::open_at_with_faults(
4453 fixture.root(),
4454 deployment_id,
4455 Arc::new(identity_from_text(&encoded)),
4456 Arc::new(FailAt(checkpoint)),
4457 )
4458 .unwrap();
4459 let error = store
4460 .replace_for_test(&reference, &secret(b"second-value"), first.version())
4461 .unwrap_err();
4462 assert_eq!(error.kind(), ManagedSecretErrorKind::CommitUncertain);
4463 store
4464 .resolve(&reference)
4465 .unwrap()
4466 .material()
4467 .expose(|value| assert_eq!(value, b"second-value"));
4468 drop(store);
4469
4470 let reopened = LocalManagedSecretStore::open_at(
4471 fixture.root(),
4472 deployment_id,
4473 identity_from_text(&encoded),
4474 )
4475 .unwrap();
4476 reopened
4477 .resolve(&reference)
4478 .unwrap()
4479 .material()
4480 .expose(|value| assert_eq!(value, b"second-value"));
4481 assert_eq!(
4482 fs::read_dir(fixture.root().join(OBJECTS_DIRECTORY))
4483 .unwrap()
4484 .count(),
4485 1
4486 );
4487 }
4488 }
4489
4490 #[test]
4491 fn audit_history_is_secret_free_at_rest_and_chain_corruption_is_detected() {
4492 let fixture = StoreFixture::new();
4493 let deployment_id = Uuid::new_v4();
4494 let (identity, _) = test_identity_pair();
4495 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4496 .unwrap();
4497 let reference = managed_reference("managed://audit/canary");
4498 let canary = b"audit-must-not-contain-this-secret-canary";
4499 store.create(&reference, &secret(canary)).unwrap();
4500
4501 let mut manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4502 let plaintext_manifest = serde_json::to_vec(&manifest).unwrap();
4503 assert!(!contains_bytes(&plaintext_manifest, canary));
4504 assert_eq!(manifest.audit.len(), 2);
4505 assert_eq!(manifest.audit[1].actor.effective_uid, effective_uid());
4506 assert_eq!(manifest.audit[1].deployment_id, deployment_id);
4507 assert_eq!(
4508 manifest.audit[1].reference.as_deref(),
4509 Some(reference.as_str())
4510 );
4511
4512 for path in [
4513 fixture.root().join(HEADER_FILE),
4514 fixture.root().join(MANIFEST_FILE),
4515 ] {
4516 assert!(!contains_bytes(&fs::read(path).unwrap(), canary));
4517 }
4518 let object = fixture
4519 .root()
4520 .join(OBJECTS_DIRECTORY)
4521 .join(&manifest.entries[reference.as_str()].object);
4522 assert!(!contains_bytes(&fs::read(object).unwrap(), canary));
4523
4524 manifest.audit[1].previous_hash[0] ^= 1;
4525 let ciphertext = serialize_and_encrypt_manifest(&manifest, &store.identity).unwrap();
4526 atomic_write(
4527 &store.anchor.root,
4528 MANIFEST_FILE,
4529 &ciphertext,
4530 &NoMutationFaults,
4531 MutationPhase::Manifest,
4532 )
4533 .unwrap();
4534 let error = store.resolve(&reference).unwrap_err();
4535 assert_eq!(error.kind(), ManagedSecretErrorKind::Integrity);
4536 }
4537
4538 #[test]
4539 fn uncommitted_ciphertext_is_recovered_without_changing_active_state() {
4540 let fixture = StoreFixture::new();
4541 let deployment_id = Uuid::new_v4();
4542 let (identity, encoded) = test_identity_pair();
4543 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4544 .unwrap();
4545 let reference = managed_reference("managed://recovery/active");
4546 store.create(&reference, &secret(b"active-value")).unwrap();
4547 let manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4548 let active = fixture
4549 .root()
4550 .join(OBJECTS_DIRECTORY)
4551 .join(&manifest.entries[reference.as_str()].object);
4552 let orphan = fixture
4553 .root()
4554 .join(OBJECTS_DIRECTORY)
4555 .join(format!("object-{}.age", Uuid::new_v4()));
4556 fs::copy(active, &orphan).unwrap();
4557 fs::set_permissions(&orphan, fs::Permissions::from_mode(0o600)).unwrap();
4558 drop(store);
4559
4560 let reopened = LocalManagedSecretStore::open_at(
4561 fixture.root(),
4562 deployment_id,
4563 identity_from_text(&encoded),
4564 )
4565 .unwrap();
4566 assert!(!orphan.exists());
4567 reopened
4568 .resolve(&reference)
4569 .unwrap()
4570 .material()
4571 .expose(|value| assert_eq!(value, b"active-value"));
4572 }
4573
4574 #[test]
4575 fn complete_verification_authenticates_every_active_secret_without_repair() {
4576 let fixture = StoreFixture::new();
4577 let deployment_id = Uuid::new_v4();
4578 let (identity, encoded) = test_identity_pair();
4579 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4580 .unwrap();
4581 store
4582 .create(
4583 &managed_reference("managed://recovery/first"),
4584 &secret(b"first-recovery-canary"),
4585 )
4586 .unwrap();
4587 store
4588 .create(
4589 &managed_reference("managed://recovery/second"),
4590 &secret(b"second-recovery-canary"),
4591 )
4592 .unwrap();
4593 drop(store);
4594 let before = StoreSnapshot::capture(fixture.root());
4595
4596 let report = LocalManagedSecretStore::verify_all_at_for_test(
4597 fixture.root(),
4598 deployment_id,
4599 identity_from_text(&encoded),
4600 )
4601 .unwrap();
4602
4603 assert_eq!(report.deployment_id(), deployment_id);
4604 assert_eq!(report.active_secret_count(), 2);
4605 assert_eq!(report.audit_record_count(), 3);
4606 assert_eq!(report.recipient_fingerprint().len(), 64);
4607 assert_eq!(StoreSnapshot::capture(fixture.root()), before);
4608 }
4609
4610 #[test]
4611 fn complete_verification_rejects_exact_recovery_mismatches_and_extra_state() {
4612 let fixture = StoreFixture::new();
4613 let deployment_id = Uuid::new_v4();
4614 let (identity, encoded) = test_identity_pair();
4615 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4616 .unwrap();
4617 store
4618 .create(
4619 &managed_reference("managed://recovery/exact"),
4620 &secret(b"exact-recovery-canary"),
4621 )
4622 .unwrap();
4623 drop(store);
4624
4625 let deployment_error = LocalManagedSecretStore::verify_all_at_for_test(
4626 fixture.root(),
4627 Uuid::new_v4(),
4628 identity_from_text(&encoded),
4629 )
4630 .unwrap_err();
4631 assert_eq!(
4632 deployment_error.kind(),
4633 ManagedSecretErrorKind::DeploymentMismatch
4634 );
4635 let (other_identity, _) = test_identity_pair();
4636 let identity_error = LocalManagedSecretStore::verify_all_at_for_test(
4637 fixture.root(),
4638 deployment_id,
4639 other_identity,
4640 )
4641 .unwrap_err();
4642 assert_eq!(
4643 identity_error.kind(),
4644 ManagedSecretErrorKind::RecipientMismatch
4645 );
4646
4647 let extra = fixture.root().join(OBJECTS_DIRECTORY).join("unexpected");
4648 fs::write(&extra, b"extra").unwrap();
4649 fs::set_permissions(&extra, fs::Permissions::from_mode(0o600)).unwrap();
4650 let extra_error = LocalManagedSecretStore::verify_all_at_for_test(
4651 fixture.root(),
4652 deployment_id,
4653 identity_from_text(&encoded),
4654 )
4655 .unwrap_err();
4656 assert_eq!(extra_error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4657 }
4658
4659 #[test]
4660 fn complete_verification_rejects_missing_corrupt_relocated_and_broken_history() {
4661 let deployment_id = Uuid::new_v4();
4662
4663 let missing = StoreFixture::new();
4664 let (identity, encoded) = test_identity_pair();
4665 let store = LocalManagedSecretStore::initialize_at(missing.root(), deployment_id, identity)
4666 .unwrap();
4667 let reference = managed_reference("managed://recovery/missing");
4668 store
4669 .create(&reference, &secret(b"missing-canary"))
4670 .unwrap();
4671 let manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4672 fs::remove_file(
4673 missing
4674 .root()
4675 .join(OBJECTS_DIRECTORY)
4676 .join(&manifest.entries[reference.as_str()].object),
4677 )
4678 .unwrap();
4679 drop(store);
4680 let error = LocalManagedSecretStore::verify_all_at_for_test(
4681 missing.root(),
4682 deployment_id,
4683 identity_from_text(&encoded),
4684 )
4685 .unwrap_err();
4686 assert_eq!(error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
4687
4688 let corrupt = StoreFixture::new();
4689 let (identity, encoded) = test_identity_pair();
4690 let store = LocalManagedSecretStore::initialize_at(corrupt.root(), deployment_id, identity)
4691 .unwrap();
4692 let reference = managed_reference("managed://recovery/corrupt");
4693 store
4694 .create(&reference, &secret(b"corrupt-canary"))
4695 .unwrap();
4696 let manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4697 let path = corrupt
4698 .root()
4699 .join(OBJECTS_DIRECTORY)
4700 .join(&manifest.entries[reference.as_str()].object);
4701 let mut ciphertext = fs::read(&path).unwrap();
4702 let middle = ciphertext.len() / 2;
4703 ciphertext[middle] ^= 1;
4704 fs::write(path, ciphertext).unwrap();
4705 drop(store);
4706 let error = LocalManagedSecretStore::verify_all_at_for_test(
4707 corrupt.root(),
4708 deployment_id,
4709 identity_from_text(&encoded),
4710 )
4711 .unwrap_err();
4712 assert_eq!(error.kind(), ManagedSecretErrorKind::Integrity);
4713
4714 let relocated = StoreFixture::new();
4715 let (identity, encoded) = test_identity_pair();
4716 let store =
4717 LocalManagedSecretStore::initialize_at(relocated.root(), deployment_id, identity)
4718 .unwrap();
4719 let first = managed_reference("managed://recovery/relocated-first");
4720 let second = managed_reference("managed://recovery/relocated-second");
4721 store.create(&first, &secret(b"first-canary")).unwrap();
4722 store.create(&second, &secret(b"second-canary")).unwrap();
4723 let manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4724 let first_path = relocated
4725 .root()
4726 .join(OBJECTS_DIRECTORY)
4727 .join(&manifest.entries[first.as_str()].object);
4728 let second_path = relocated
4729 .root()
4730 .join(OBJECTS_DIRECTORY)
4731 .join(&manifest.entries[second.as_str()].object);
4732 let swap = relocated.root().join(OBJECTS_DIRECTORY).join("swap");
4733 fs::rename(&first_path, &swap).unwrap();
4734 fs::rename(&second_path, &first_path).unwrap();
4735 fs::rename(&swap, &second_path).unwrap();
4736 drop(store);
4737 let error = LocalManagedSecretStore::verify_all_at_for_test(
4738 relocated.root(),
4739 deployment_id,
4740 identity_from_text(&encoded),
4741 )
4742 .unwrap_err();
4743 assert_eq!(error.kind(), ManagedSecretErrorKind::Integrity);
4744
4745 let history = StoreFixture::new();
4746 let (identity, encoded) = test_identity_pair();
4747 let store = LocalManagedSecretStore::initialize_at(history.root(), deployment_id, identity)
4748 .unwrap();
4749 let reference = managed_reference("managed://recovery/history");
4750 store
4751 .create(&reference, &secret(b"history-canary"))
4752 .unwrap();
4753 let mut manifest = read_manifest(&store.anchor.root, &store.identity).unwrap();
4754 manifest.audit[1].previous_hash[0] ^= 1;
4755 let ciphertext = serialize_and_encrypt_manifest(&manifest, &store.identity).unwrap();
4756 atomic_write(
4757 &store.anchor.root,
4758 MANIFEST_FILE,
4759 &ciphertext,
4760 &NoMutationFaults,
4761 MutationPhase::Manifest,
4762 )
4763 .unwrap();
4764 drop(store);
4765 let error = LocalManagedSecretStore::verify_all_at_for_test(
4766 history.root(),
4767 deployment_id,
4768 identity_from_text(&encoded),
4769 )
4770 .unwrap_err();
4771 assert_eq!(error.kind(), ManagedSecretErrorKind::Integrity);
4772 }
4773
4774 #[test]
4775 fn offline_root_rotation_stages_a_fully_verified_store_without_mutating_active_state() {
4776 let fixture = StoreFixture::new();
4777 let staging = StoreFixture::new();
4778 fs::create_dir(staging.root()).unwrap();
4779 fs::set_permissions(staging.root(), fs::Permissions::from_mode(0o700)).unwrap();
4780 let deployment_id = Uuid::new_v4();
4781 let (identity, encoded) = test_identity_pair();
4782 let (next_identity, next_encoded) = test_identity_pair();
4783 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4784 .unwrap();
4785 let reference = managed_reference("managed://rotation/canary");
4786 store
4787 .create(&reference, &secret(b"root-rotation-canary"))
4788 .unwrap();
4789 drop(store);
4790 let active_before = StoreSnapshot::capture(fixture.root());
4791
4792 let report = LocalManagedSecretStore::rotate_root_at_for_test(
4793 fixture.root(),
4794 staging.root(),
4795 deployment_id,
4796 identity_from_text(&encoded),
4797 next_identity,
4798 )
4799 .unwrap();
4800
4801 assert_eq!(report.deployment_id(), deployment_id);
4802 assert_eq!(report.active_secret_count(), 1);
4803 assert_eq!(report.audit_record_count(), 2);
4804 assert_ne!(
4805 report.current_recipient_fingerprint(),
4806 report.next_recipient_fingerprint()
4807 );
4808 assert_eq!(StoreSnapshot::capture(fixture.root()), active_before);
4809 LocalManagedSecretStore::verify_all_at_for_test(
4810 fixture.root(),
4811 deployment_id,
4812 identity_from_text(&encoded),
4813 )
4814 .unwrap();
4815 LocalManagedSecretStore::verify_all_at_for_test(
4816 staging.root(),
4817 deployment_id,
4818 identity_from_text(&next_encoded),
4819 )
4820 .unwrap();
4821 let staged = LocalManagedSecretStore::open_at(
4822 staging.root(),
4823 deployment_id,
4824 identity_from_text(&next_encoded),
4825 )
4826 .unwrap();
4827 staged
4828 .resolve(&reference)
4829 .unwrap()
4830 .material()
4831 .expose(|value| assert_eq!(value, b"root-rotation-canary"));
4832 }
4833
4834 #[test]
4835 fn offline_root_rotation_refuses_prerequisites_before_staging_writes() {
4836 let fixture = StoreFixture::new();
4837 let staging = StoreFixture::new();
4838 fs::create_dir(staging.root()).unwrap();
4839 fs::set_permissions(staging.root(), fs::Permissions::from_mode(0o700)).unwrap();
4840 let deployment_id = Uuid::new_v4();
4841 let (identity, encoded) = test_identity_pair();
4842 let store = LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4843 .unwrap();
4844 let active = StoreAnchor::open(fixture.root(), true).unwrap();
4845 let held_lock = StoreLock::acquire(&active.root, LockMode::Exclusive).unwrap();
4846 let (next_identity, _) = test_identity_pair();
4847
4848 let lock_error = LocalManagedSecretStore::rotate_root_at_for_test(
4849 fixture.root(),
4850 staging.root(),
4851 deployment_id,
4852 identity_from_text(&encoded),
4853 next_identity,
4854 )
4855 .unwrap_err();
4856 assert_eq!(lock_error.kind(), ManagedSecretErrorKind::Coordination);
4857 assert_eq!(fs::read_dir(staging.root()).unwrap().count(), 0);
4858 drop(held_lock);
4859 drop(active);
4860 drop(store);
4861
4862 let same_identity_error = LocalManagedSecretStore::rotate_root_at_for_test(
4863 fixture.root(),
4864 staging.root(),
4865 deployment_id,
4866 identity_from_text(&encoded),
4867 identity_from_text(&encoded),
4868 )
4869 .unwrap_err();
4870 assert_eq!(
4871 same_identity_error.kind(),
4872 ManagedSecretErrorKind::RecipientMismatch
4873 );
4874 assert_eq!(fs::read_dir(staging.root()).unwrap().count(), 0);
4875
4876 fs::write(staging.root().join("operator-evidence"), b"retain").unwrap();
4877 fs::set_permissions(
4878 staging.root().join("operator-evidence"),
4879 fs::Permissions::from_mode(0o600),
4880 )
4881 .unwrap();
4882 let (next_identity, _) = test_identity_pair();
4883 let nonempty_error = LocalManagedSecretStore::rotate_root_at_for_test(
4884 fixture.root(),
4885 staging.root(),
4886 deployment_id,
4887 identity_from_text(&encoded),
4888 next_identity,
4889 )
4890 .unwrap_err();
4891 assert_eq!(
4892 nonempty_error.kind(),
4893 ManagedSecretErrorKind::UnsafeFilesystem
4894 );
4895 assert_eq!(
4896 fs::read(staging.root().join("operator-evidence")).unwrap(),
4897 b"retain"
4898 );
4899 }
4900
4901 #[test]
4902 fn failed_root_rotation_preserves_active_state_and_staging_evidence() {
4903 let checkpoints = [
4904 RotationCheckpoint::Lock,
4905 RotationCheckpoint::SourceDecrypt,
4906 RotationCheckpoint::SourceAudit,
4907 RotationCheckpoint::CreateObjectsDirectory,
4908 RotationCheckpoint::Write(RotationFile::Object),
4909 RotationCheckpoint::SyncFile(RotationFile::Object),
4910 RotationCheckpoint::Write(RotationFile::Lock),
4911 RotationCheckpoint::SyncFile(RotationFile::Lock),
4912 RotationCheckpoint::Write(RotationFile::Header),
4913 RotationCheckpoint::SyncFile(RotationFile::Header),
4914 RotationCheckpoint::Write(RotationFile::Manifest),
4915 RotationCheckpoint::SyncFile(RotationFile::Manifest),
4916 RotationCheckpoint::SyncDirectory(RotationDirectory::Objects),
4917 RotationCheckpoint::SyncDirectory(RotationDirectory::Root),
4918 RotationCheckpoint::DestinationVerification,
4919 ];
4920 for checkpoint in checkpoints {
4921 let fixture = StoreFixture::new();
4922 let staging = StoreFixture::new();
4923 fs::create_dir(staging.root()).unwrap();
4924 fs::set_permissions(staging.root(), fs::Permissions::from_mode(0o700)).unwrap();
4925 let deployment_id = Uuid::new_v4();
4926 let (identity, encoded) = test_identity_pair();
4927 let (next_identity, _) = test_identity_pair();
4928 let store =
4929 LocalManagedSecretStore::initialize_at(fixture.root(), deployment_id, identity)
4930 .unwrap();
4931 store
4932 .create(
4933 &managed_reference("managed://rotation/failure-evidence"),
4934 &secret(b"rotation-failure-canary"),
4935 )
4936 .unwrap();
4937 drop(store);
4938 let active_before = StoreSnapshot::capture(fixture.root());
4939
4940 let injected_failure = RecordingRotationFailure::new(checkpoint, staging.root());
4941 let error = LocalManagedSecretStore::rotate_root_at_with_faults(
4942 fixture.root(),
4943 staging.root(),
4944 deployment_id,
4945 identity_from_text(&encoded),
4946 next_identity,
4947 false,
4948 &injected_failure,
4949 )
4950 .unwrap_err();
4951
4952 assert_eq!(error.kind(), ManagedSecretErrorKind::Mutation);
4953 let expected_phase = match checkpoint {
4954 RotationCheckpoint::Lock => RootRotationPhase::Lock,
4955 RotationCheckpoint::SourceDecrypt | RotationCheckpoint::SourceAudit => {
4956 RootRotationPhase::SourceVerification
4957 }
4958 RotationCheckpoint::DestinationVerification => {
4959 RootRotationPhase::DestinationVerification
4960 }
4961 _ => RootRotationPhase::StagingWrite,
4962 };
4963 assert_eq!(error.phase(), expected_phase);
4964 assert_eq!(StoreSnapshot::capture(fixture.root()), active_before);
4965 assert_eq!(
4966 StoreSnapshot::capture(staging.root()),
4967 injected_failure.snapshot_at_failure()
4968 );
4969 }
4970 }
4971
4972 #[test]
4973 fn rotation_staging_anchor_rejects_path_substitution_without_redirecting_writes() {
4974 use std::os::unix::fs::symlink;
4975
4976 let active_fixture = StoreFixture::new();
4977 let staging_fixture = StoreFixture::new();
4978 let outside = StoreFixture::new();
4979 fs::create_dir(staging_fixture.root()).unwrap();
4980 fs::create_dir(outside.root()).unwrap();
4981 fs::set_permissions(staging_fixture.root(), fs::Permissions::from_mode(0o700)).unwrap();
4982 fs::set_permissions(outside.root(), fs::Permissions::from_mode(0o700)).unwrap();
4983 let deployment_id = Uuid::new_v4();
4984 let (identity, _) = test_identity_pair();
4985 let store =
4986 LocalManagedSecretStore::initialize_at(active_fixture.root(), deployment_id, identity)
4987 .unwrap();
4988 let active = StoreAnchor::open(active_fixture.root(), true).unwrap();
4989 let staging = validate_empty_staging_directory(staging_fixture.root(), &active).unwrap();
4990 let moved = staging_fixture.root().with_extension("moved");
4991 fs::rename(staging_fixture.root(), &moved).unwrap();
4992 symlink(outside.root(), staging_fixture.root()).unwrap();
4993 let (_, next_encoded) = test_identity_pair();
4994 let verified = verify_anchored_store(
4995 &store.anchor,
4996 active_fixture.root(),
4997 deployment_id,
4998 &store.identity,
4999 )
5000 .unwrap();
5001
5002 let error = match populate_rotation_staging(
5003 &staging,
5004 &verified,
5005 &identity_from_text(&next_encoded),
5006 &NoRotationFaults,
5007 ) {
5008 Ok(_) => panic!("substituted staging path must fail"),
5009 Err(error) => error,
5010 };
5011
5012 assert_eq!(error.kind(), ManagedSecretErrorKind::UnsafeFilesystem);
5013 assert_eq!(fs::read_dir(outside.root()).unwrap().count(), 0);
5014 assert_eq!(fs::read_dir(&moved).unwrap().count(), 0);
5015 fs::remove_file(staging_fixture.root()).unwrap();
5016 fs::rename(moved, staging_fixture.root()).unwrap();
5017 }
5018
5019 #[derive(Debug, PartialEq, Eq)]
5020 struct StoreSnapshot(Vec<(PathBuf, Vec<u8>, u32)>);
5021
5022 impl StoreSnapshot {
5023 fn capture(root: &Path) -> Self {
5024 fn visit(base: &Path, current: &Path, entries: &mut Vec<(PathBuf, Vec<u8>, u32)>) {
5025 let mut children = fs::read_dir(current)
5026 .unwrap()
5027 .map(|entry| entry.unwrap())
5028 .collect::<Vec<_>>();
5029 children.sort_by_key(|entry| entry.file_name());
5030 for child in children {
5031 let path = child.path();
5032 let metadata = child.metadata().unwrap();
5033 let relative = path.strip_prefix(base).unwrap().to_path_buf();
5034 if metadata.is_dir() {
5035 entries.push((
5036 relative.clone(),
5037 Vec::new(),
5038 metadata.permissions().mode() & 0o777,
5039 ));
5040 visit(base, &path, entries);
5041 } else {
5042 entries.push((
5043 relative,
5044 fs::read(path).unwrap(),
5045 metadata.permissions().mode() & 0o777,
5046 ));
5047 }
5048 }
5049 }
5050 let mut entries = Vec::new();
5051 visit(root, root, &mut entries);
5052 Self(entries)
5053 }
5054 }
5055
5056 struct FailAt(MutationCheckpoint);
5057
5058 impl MutationFaultInjector for FailAt {
5059 fn check(&self, checkpoint: MutationCheckpoint) -> Result<(), ManagedSecretError> {
5060 if checkpoint == self.0 {
5061 Err(ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
5062 } else {
5063 Ok(())
5064 }
5065 }
5066 }
5067
5068 struct RecordingRotationFailure {
5069 checkpoint: RotationCheckpoint,
5070 staging_root: PathBuf,
5071 snapshot: Mutex<Option<StoreSnapshot>>,
5072 }
5073
5074 impl RecordingRotationFailure {
5075 fn new(checkpoint: RotationCheckpoint, staging_root: &Path) -> Self {
5076 Self {
5077 checkpoint,
5078 staging_root: staging_root.to_path_buf(),
5079 snapshot: Mutex::new(None),
5080 }
5081 }
5082
5083 fn snapshot_at_failure(&self) -> StoreSnapshot {
5084 self.snapshot
5085 .lock()
5086 .unwrap()
5087 .take()
5088 .expect("injected checkpoint must be reached")
5089 }
5090 }
5091
5092 impl RotationFaultInjector for RecordingRotationFailure {
5093 fn check(&self, checkpoint: RotationCheckpoint) -> Result<(), ManagedSecretError> {
5094 if checkpoint == self.checkpoint {
5095 *self.snapshot.lock().unwrap() = Some(StoreSnapshot::capture(&self.staging_root));
5096 Err(ManagedSecretError::new(ManagedSecretErrorKind::Mutation))
5097 } else {
5098 Ok(())
5099 }
5100 }
5101 }
5102
5103 fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
5104 haystack
5105 .windows(needle.len())
5106 .any(|window| window == needle)
5107 }
5108
5109 fn managed_reference(value: &str) -> ManagedSecretReference {
5110 ManagedSecretReference::from_str(value).unwrap()
5111 }
5112
5113 fn secret(value: &[u8]) -> SecretMaterial {
5114 SecretMaterial::try_from(value.to_vec()).unwrap()
5115 }
5116
5117 fn test_identity_pair() -> (ManagedSecretIdentity, String) {
5118 let identity = age::x25519::Identity::generate();
5119 let encoded = identity.to_string().expose_secret().to_string();
5120 let recipient = identity.to_public();
5121 (
5122 ManagedSecretIdentity {
5123 identity,
5124 recipient,
5125 },
5126 encoded,
5127 )
5128 }
5129
5130 fn identity_from_text(encoded: &str) -> ManagedSecretIdentity {
5131 ManagedSecretIdentity::from_material(&secret(encoded.as_bytes())).unwrap()
5132 }
5133
5134 struct StoreFixture {
5135 root: PathBuf,
5136 }
5137
5138 impl StoreFixture {
5139 fn new() -> Self {
5140 let root = std::env::temp_dir()
5141 .join(format!("ahri-tre-managed-secret-test-{}", Uuid::new_v4()));
5142 Self { root }
5143 }
5144
5145 fn root(&self) -> &Path {
5146 &self.root
5147 }
5148 }
5149
5150 impl Drop for StoreFixture {
5151 fn drop(&mut self) {
5152 let _ = std::fs::remove_dir_all(&self.root);
5153 }
5154 }
5155}
5156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5159pub enum ManagedSecretReferenceStatus {
5160 Referenced,
5161 Unreferenced,
5162}
5163
5164#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
5166#[error("Managed-secret reference inspection is unavailable")]
5167pub struct ManagedSecretReferenceInspectionError;
5168
5169impl ManagedSecretReferenceInspectionError {
5170 pub fn unavailable() -> Self {
5171 Self
5172 }
5173}
5174
5175pub trait ManagedSecretReferenceAuthority: Send + Sync {
5177 fn reference_status(
5178 &self,
5179 reference: &ManagedSecretReference,
5180 ) -> Result<ManagedSecretReferenceStatus, ManagedSecretReferenceInspectionError>;
5181}
5182
5183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5185pub enum ManagedSecretReferenceAuthorityKind {
5186 ApplicationConfiguration,
5187 DatastoreBindings,
5188 SessionRecords,
5189 AuthenticationArtifacts,
5190}
5191
5192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5194pub enum ManagedSecretRemovalProofErrorKind {
5195 Referenced,
5196 InspectionUnavailable,
5197}
5198
5199#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
5201#[error("Managed-secret removal proof failed for {authority:?}: {kind:?}")]
5202pub struct ManagedSecretRemovalProofError {
5203 authority: ManagedSecretReferenceAuthorityKind,
5204 kind: ManagedSecretRemovalProofErrorKind,
5205}
5206
5207impl ManagedSecretRemovalProofError {
5208 pub fn authority(&self) -> ManagedSecretReferenceAuthorityKind {
5209 self.authority
5210 }
5211
5212 pub fn kind(&self) -> ManagedSecretRemovalProofErrorKind {
5213 self.kind
5214 }
5215}
5216
5217pub struct ManagedSecretReferenceAuthorities<'a> {
5219 application_configuration: &'a dyn ManagedSecretReferenceAuthority,
5220 datastore_bindings: &'a dyn ManagedSecretReferenceAuthority,
5221 session_records: &'a dyn ManagedSecretReferenceAuthority,
5222 authentication_artifacts: &'a dyn ManagedSecretReferenceAuthority,
5223}
5224
5225impl<'a> ManagedSecretReferenceAuthorities<'a> {
5226 pub fn new(
5227 application_configuration: &'a dyn ManagedSecretReferenceAuthority,
5228 datastore_bindings: &'a dyn ManagedSecretReferenceAuthority,
5229 session_records: &'a dyn ManagedSecretReferenceAuthority,
5230 authentication_artifacts: &'a dyn ManagedSecretReferenceAuthority,
5231 ) -> Self {
5232 Self {
5233 application_configuration,
5234 datastore_bindings,
5235 session_records,
5236 authentication_artifacts,
5237 }
5238 }
5239
5240 pub fn prove_unreferenced(
5241 &self,
5242 reference: &ManagedSecretReference,
5243 ) -> Result<ManagedSecretRemovalProof, ManagedSecretRemovalProofError> {
5244 let authorities = [
5245 (
5246 ManagedSecretReferenceAuthorityKind::ApplicationConfiguration,
5247 self.application_configuration,
5248 ),
5249 (
5250 ManagedSecretReferenceAuthorityKind::DatastoreBindings,
5251 self.datastore_bindings,
5252 ),
5253 (
5254 ManagedSecretReferenceAuthorityKind::SessionRecords,
5255 self.session_records,
5256 ),
5257 (
5258 ManagedSecretReferenceAuthorityKind::AuthenticationArtifacts,
5259 self.authentication_artifacts,
5260 ),
5261 ];
5262 for (authority, inspector) in authorities {
5263 let status = inspector.reference_status(reference).map_err(|_| {
5264 ManagedSecretRemovalProofError {
5265 authority,
5266 kind: ManagedSecretRemovalProofErrorKind::InspectionUnavailable,
5267 }
5268 })?;
5269 if status == ManagedSecretReferenceStatus::Referenced {
5270 return Err(ManagedSecretRemovalProofError {
5271 authority,
5272 kind: ManagedSecretRemovalProofErrorKind::Referenced,
5273 });
5274 }
5275 }
5276 Ok(ManagedSecretRemovalProof {
5277 reference: reference.clone(),
5278 })
5279 }
5280}
5281
5282#[derive(Debug)]
5285pub struct ManagedSecretRemovalProof {
5286 reference: ManagedSecretReference,
5287}