Skip to main content

ahri_tre_secrets/
managed.rs

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
23/// Fixed production root for the encrypted Managed-secret store.
24pub const MANAGED_SECRET_ROOT: &str = "/var/lib/ahri-tre/secrets";
25/// Fixed production destination for an offline root-identity rotation.
26pub const MANAGED_SECRET_ROTATION_ROOT: &str = "/var/lib/ahri-tre/secrets-rotation";
27/// Fixed production projection for the deployment X25519 identity.
28pub const ROOT_IDENTITY_PATH: &str = "/run/secrets/ahri-tre/root-identity/value";
29/// Fixed production projection for the next deployment X25519 identity.
30pub 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/// Aggregate, disclosure-safe evidence from complete offline verification.
47#[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/// Aggregate, disclosure-safe evidence from a staged offline root rotation.
74#[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/// A monotonically increasing Managed-secret version.
106#[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/// Safe metadata returned after a Managed-secret mutation or resolution.
129#[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
150/// Protected Managed-secret material with its safe resolution metadata.
151pub 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
176/// A durable, owner-specific replacement that is not active until its owning
177/// authority has applied and verified the staged material.
178pub 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}
204/// Narrow backend contract consumed by trusted bootstrap and workflows.
205pub 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    /// Authenticates the store manifest and looks up safe active metadata
218    /// without opening the selected Secret envelope.
219    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
229/// Internal mutation primitives available only to explicit credential-owner
230/// workflows. There is intentionally no generic operator overwrite command.
231pub 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/// Repository-internal capability for Trusted-runtime reference retention and removal.
270#[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
301/// Parsed deployment root identity. Its private key has no formatting surface.
302pub struct ManagedSecretIdentity {
303    identity: age::x25519::Identity,
304    recipient: age::x25519::Recipient,
305}
306
307impl ManagedSecretIdentity {
308    /// Parses a protected age X25519 identity without exposing it through an
309    /// ordinary string-returning interface.
310    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    /// Loads the identity from its fixed production projection.
326    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    /// Loads the next identity from its fixed production projection.
333    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    /// Internal acceptance-harness seam. Production callers must use
340    /// [`Self::load`], which retains the fixed injected-secret contract.
341    #[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    /// Internal acceptance-harness seam for the fixed next-identity projection.
350    #[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
369/// Version-1 encrypted local Managed-secret backend.
370pub struct LocalManagedSecretStore {
371    root: PathBuf,
372    anchor: Arc<StoreAnchor>,
373    deployment_id: Uuid,
374    identity: Arc<ManagedSecretIdentity>,
375    faults: Arc<dyn MutationFaultInjector>,
376}
377
378/// Read-only capability that authenticates only explicitly selected Managed
379/// Secret envelopes and exposes safe metadata for them.
380pub struct SelectedManagedSecretReader {
381    store: LocalManagedSecretStore,
382}
383
384impl SelectedManagedSecretReader {
385    /// Resolves only the selected reference under the read-only store anchor.
386    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    /// Opens a completely verified store without recovery or another mutation.
511    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    /// Opens the fixed store for selected, read-only resolution without
519    /// recovering state or authenticating unrelated Secret envelopes.
520    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    /// Completely verifies the fixed active store without repair or mutation.
528    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    /// Stages a complete root-identity rotation at the fixed production paths.
536    /// The active store and both identity projections are never replaced.
537    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    /// Internal acceptance-harness seam. Production verification uses the
554    /// fixed active store root.
555    #[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    /// Internal acceptance-harness seam. Production rotation also proves that
579    /// the active store is mounted read-only.
580    #[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, &current_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    /// Returns safe metadata for every active Managed secret in canonical
725    /// reference order. No ciphertext or Secret material crosses this seam.
726    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    /// Authenticates one active envelope and returns only its safe metadata.
749    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    /// Persists a new encrypted version without making it active. Only the
781    /// credential's owning authority may carry this capability through apply
782    /// and verification to activation.
783    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    /// Reopens a durable pending replacement after process restart.
885    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    /// Atomically makes an owner-verified staged version active and then
923    /// destroys the previous active ciphertext.
924    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    /// Atomically activates a complete owner transaction in one manifest commit.
1011    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    /// Authenticates the manifest and reports whether an uncertain activation committed.
1123    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    /// Destroys a rejected staged value without changing the active version.
1147    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    /// Durably replaces the safe Managed-reference snapshot retained by one Session.
1187    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    /// Durably releases the reference snapshot for one closed Session.
1237    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    /// Reads the authenticated durable Session-reference index.
1269    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    /// Removes a Managed Secret only when the caller supplies proof issued by
1324    /// every authoritative reference owner. The audit tombstone is durable and
1325    /// prevents the canonical reference from being silently recreated.
1326    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    /// Internal acceptance-harness seam. Production callers must use
1399    /// [`Self::initialize`], which retains the fixed store root.
1400    #[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    /// Internal acceptance-harness seam. Production callers must use
1491    /// [`Self::open`], which retains the fixed store root.
1492    #[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    /// Internal bootstrap-harness seam for non-mutating verified open.
1503    #[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    /// Internal acceptance-harness seam for selected, read-only resolution.
1514    #[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/// Stable, disclosure-safe Managed-secret failure categories.
1914#[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/// A disclosure-safe Managed-secret backend error.
1934#[derive(Debug, Clone, PartialEq, Eq, Error)]
1935#[error("Managed-secret operation failed: {kind:?}")]
1936pub struct ManagedSecretError {
1937    kind: ManagedSecretErrorKind,
1938}
1939
1940/// Disclosure-safe phase of an offline root-identity rotation.
1941#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1942pub enum RootRotationPhase {
1943    Lock,
1944    Prerequisites,
1945    SourceVerification,
1946    StagingWrite,
1947    DestinationVerification,
1948}
1949
1950/// A disclosure-safe root-rotation failure with aggregate phase context.
1951#[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        // SAFETY: These libc calls read process credentials and have no preconditions.
2154        let effective_uid = unsafe { libc::geteuid() };
2155        // SAFETY: These libc calls read process credentials and have no preconditions.
2156        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// `material` is only read by the Linux rotation path; verification still
2426// decrypts it everywhere so the store is proven readable before reporting.
2427#[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// Only the Linux rotation path populates staging, so the descriptor and
2514// identity are unused elsewhere.
2515#[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// Fault-injection checkpoints are only reached by the Linux mutation path.
2894#[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// Fault-injection checkpoints are only reached by the Linux rotation path.
2922#[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        // SAFETY: This libc call reads the process credential and has no preconditions.
3475        unsafe { libc::geteuid() }
3476    }
3477    #[cfg(not(unix))]
3478    {
3479        0
3480    }
3481}
3482
3483fn effective_gid() -> u32 {
3484    #[cfg(unix)]
3485    {
3486        // SAFETY: This libc call reads the process credential and has no preconditions.
3487        unsafe { libc::getegid() }
3488    }
3489    #[cfg(not(unix))]
3490    {
3491        0
3492    }
3493}
3494
3495struct StoreLock {
3496    // Held only so `Drop` can release the `flock` on Linux.
3497    #[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/// Whether an owning subsystem currently retains a logical reference to a
5157/// Managed Secret. No Secret material or storage path is exposed.
5158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5159pub enum ManagedSecretReferenceStatus {
5160    Referenced,
5161    Unreferenced,
5162}
5163
5164/// Safe failure returned when a reference-owning subsystem cannot be inspected.
5165#[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
5175/// Narrow capability implemented by each authoritative reference owner.
5176pub trait ManagedSecretReferenceAuthority: Send + Sync {
5177    fn reference_status(
5178        &self,
5179        reference: &ManagedSecretReference,
5180    ) -> Result<ManagedSecretReferenceStatus, ManagedSecretReferenceInspectionError>;
5181}
5182
5183/// The authoritative subsystem that blocked a Managed-secret removal proof.
5184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5185pub enum ManagedSecretReferenceAuthorityKind {
5186    ApplicationConfiguration,
5187    DatastoreBindings,
5188    SessionRecords,
5189    AuthenticationArtifacts,
5190}
5191
5192/// Disclosure-safe reason that removal proof could not be issued.
5193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5194pub enum ManagedSecretRemovalProofErrorKind {
5195    Referenced,
5196    InspectionUnavailable,
5197}
5198
5199/// A fail-closed result from the fixed Managed-secret reference checks.
5200#[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
5217/// Fixed set of reference owners that must all prove absence before removal.
5218pub 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/// Unforgeable evidence that every required reference authority reported
5283/// absence for one canonical Managed-secret reference.
5284#[derive(Debug)]
5285pub struct ManagedSecretRemovalProof {
5286    reference: ManagedSecretReference,
5287}