Skip to main content

ahri_tre_secrets/
lib.rs

1//! Canonical Secret references and Secret resolution infrastructure.
2
3mod acquisition;
4mod managed;
5pub use acquisition::{SourceCredentialError, SourceCredentials};
6
7use age::x25519;
8
9pub use managed::{
10    LocalManagedSecretStore, MANAGED_SECRET_ROOT, MANAGED_SECRET_ROTATION_ROOT,
11    ManagedSecretBackend, ManagedSecretError, ManagedSecretErrorKind, ManagedSecretIdentity,
12    ManagedSecretMetadata, ManagedSecretVerification, ManagedSecretVersion,
13    NEXT_ROOT_IDENTITY_PATH, ROOT_IDENTITY_PATH, ResolvedManagedSecret, RootRotationError,
14    RootRotationPhase, RootRotationVerification, SelectedManagedSecretReader,
15};
16
17/// Repository-internal Trusted-runtime capability surface.
18#[doc(hidden)]
19pub mod internal {
20    pub use super::managed::{
21        ManagedSecretOwnerBackend, ManagedSecretReferenceAuthorities,
22        ManagedSecretReferenceAuthority, ManagedSecretReferenceAuthorityKind,
23        ManagedSecretReferenceInspectionError, ManagedSecretReferenceStatus,
24        ManagedSecretRemovalProof, ManagedSecretRemovalProofError,
25        ManagedSecretRemovalProofErrorKind, ManagedSecretTrustedBackend, StagedManagedSecret,
26    };
27}
28
29#[cfg(target_os = "linux")]
30use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
31use secrecy::{ExposeSecret, SecretSlice, zeroize::Zeroize};
32use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
33use std::collections::{BTreeMap, BTreeSet};
34use std::fmt;
35use std::fs::{self, File, OpenOptions};
36#[cfg(target_os = "linux")]
37use std::io::Read;
38use std::io::Write;
39#[cfg(target_os = "linux")]
40use std::os::fd::AsRawFd;
41#[cfg(unix)]
42use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
43use std::path::{Path, PathBuf};
44use std::str::FromStr;
45use thiserror::Error;
46
47/// Maximum encoded length of a canonical Secret reference.
48pub const MAX_SECRET_REFERENCE_BYTES: usize = 512;
49/// Maximum logical value size for either Secret tier.
50pub const MAX_SECRET_MATERIAL_BYTES: usize = 64 * 1024;
51/// Fixed production root for externally projected Injected secrets.
52pub const INJECTED_SECRET_ROOT: &str = "/run/secrets";
53
54const MANAGED_SCHEME: &str = "managed://";
55const INJECTED_SCHEME: &str = "injected://";
56
57/// The storage tier named by a canonical Secret reference.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub enum SecretTier {
60    Managed,
61    Injected,
62}
63
64/// A bounded canonical logical Secret reference.
65#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct SecretReference {
67    canonical: Box<str>,
68    tier: SecretTier,
69}
70
71impl SecretReference {
72    pub fn as_str(&self) -> &str {
73        &self.canonical
74    }
75
76    pub fn tier(&self) -> SecretTier {
77        self.tier
78    }
79
80    pub fn segments(&self) -> impl Iterator<Item = &str> {
81        self.path().split('/')
82    }
83
84    fn path(&self) -> &str {
85        match self.tier {
86            SecretTier::Managed => &self.canonical[MANAGED_SCHEME.len()..],
87            SecretTier::Injected => &self.canonical[INJECTED_SCHEME.len()..],
88        }
89    }
90}
91
92impl fmt::Debug for SecretReference {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        formatter
95            .debug_tuple("SecretReference")
96            .field(&self.canonical)
97            .finish()
98    }
99}
100
101impl fmt::Display for SecretReference {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        formatter.write_str(&self.canonical)
104    }
105}
106
107impl FromStr for SecretReference {
108    type Err = SecretReferenceParseError;
109
110    fn from_str(input: &str) -> Result<Self, Self::Err> {
111        parse_reference(input)
112    }
113}
114
115impl Serialize for SecretReference {
116    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117    where
118        S: Serializer,
119    {
120        serializer.serialize_str(self.as_str())
121    }
122}
123
124impl<'de> Deserialize<'de> for SecretReference {
125    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126    where
127        D: Deserializer<'de>,
128    {
129        let input = <&str>::deserialize(deserializer)?;
130        Self::from_str(input).map_err(de::Error::custom)
131    }
132}
133
134/// A canonical reference to the encrypted Managed-secret tier.
135#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
136#[serde(transparent)]
137pub struct ManagedSecretReference(SecretReference);
138
139impl ManagedSecretReference {
140    pub fn as_str(&self) -> &str {
141        self.0.as_str()
142    }
143
144    pub fn as_secret_reference(&self) -> &SecretReference {
145        &self.0
146    }
147}
148
149impl fmt::Display for ManagedSecretReference {
150    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151        self.0.fmt(formatter)
152    }
153}
154
155impl FromStr for ManagedSecretReference {
156    type Err = SecretReferenceParseError;
157
158    fn from_str(input: &str) -> Result<Self, Self::Err> {
159        let reference = SecretReference::from_str(input)?;
160        if reference.tier() != SecretTier::Managed {
161            return Err(SecretReferenceParseError::WrongTier);
162        }
163        Ok(Self(reference))
164    }
165}
166
167impl<'de> Deserialize<'de> for ManagedSecretReference {
168    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
169    where
170        D: Deserializer<'de>,
171    {
172        let input = <&str>::deserialize(deserializer)?;
173        Self::from_str(input).map_err(de::Error::custom)
174    }
175}
176
177/// A canonical reference to the externally projected Injected-secret tier.
178#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
179#[serde(transparent)]
180pub struct InjectedSecretReference(SecretReference);
181
182impl InjectedSecretReference {
183    pub fn as_str(&self) -> &str {
184        self.0.as_str()
185    }
186
187    pub fn as_secret_reference(&self) -> &SecretReference {
188        &self.0
189    }
190
191    pub fn segments(&self) -> impl Iterator<Item = &str> {
192        self.0.segments()
193    }
194}
195
196impl fmt::Display for InjectedSecretReference {
197    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198        self.0.fmt(formatter)
199    }
200}
201
202impl FromStr for InjectedSecretReference {
203    type Err = SecretReferenceParseError;
204
205    fn from_str(input: &str) -> Result<Self, Self::Err> {
206        let reference = SecretReference::from_str(input)?;
207        if reference.tier() != SecretTier::Injected {
208            return Err(SecretReferenceParseError::WrongTier);
209        }
210        Ok(Self(reference))
211    }
212}
213
214impl<'de> Deserialize<'de> for InjectedSecretReference {
215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216    where
217        D: Deserializer<'de>,
218    {
219        let input = <&str>::deserialize(deserializer)?;
220        Self::from_str(input).map_err(de::Error::custom)
221    }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
225pub enum SecretReferenceParseError {
226    #[error("Secret reference uses an unsupported or non-canonical scheme")]
227    InvalidScheme,
228    #[error("Secret reference exceeds the maximum encoded length")]
229    TooLong,
230    #[error("Secret reference must contain between one and eight segments")]
231    InvalidSegmentCount,
232    #[error("Secret reference contains a non-canonical segment")]
233    InvalidSegment,
234    #[error("Secret reference names the wrong Secret tier")]
235    WrongTier,
236}
237
238fn parse_reference(input: &str) -> Result<SecretReference, SecretReferenceParseError> {
239    if input.len() > MAX_SECRET_REFERENCE_BYTES {
240        return Err(SecretReferenceParseError::TooLong);
241    }
242
243    let (tier, path) = if let Some(path) = input.strip_prefix(MANAGED_SCHEME) {
244        (SecretTier::Managed, path)
245    } else if let Some(path) = input.strip_prefix(INJECTED_SCHEME) {
246        (SecretTier::Injected, path)
247    } else {
248        return Err(SecretReferenceParseError::InvalidScheme);
249    };
250
251    let segments = path.split('/').collect::<Vec<_>>();
252    if !(1..=8).contains(&segments.len()) {
253        return Err(SecretReferenceParseError::InvalidSegmentCount);
254    }
255    if segments.iter().any(|segment| !valid_segment(segment)) {
256        return Err(SecretReferenceParseError::InvalidSegment);
257    }
258
259    Ok(SecretReference {
260        canonical: input.into(),
261        tier,
262    })
263}
264
265fn valid_segment(segment: &str) -> bool {
266    if segment.is_empty() || segment.len() > 63 {
267        return false;
268    }
269    let bytes = segment.as_bytes();
270    if !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
271        || !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
272    {
273        return false;
274    }
275
276    let mut previous_hyphen = false;
277    for byte in bytes {
278        match byte {
279            b'a'..=b'z' | b'0'..=b'9' => previous_hyphen = false,
280            b'-' if !previous_hyphen => previous_hyphen = true,
281            _ => return false,
282        }
283    }
284    true
285}
286
287/// An exact externally supplied version label for an Injected secret.
288#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
289#[serde(transparent)]
290pub struct InjectedSecretVersion(Box<str>);
291
292impl InjectedSecretVersion {
293    pub fn as_str(&self) -> &str {
294        &self.0
295    }
296}
297
298impl fmt::Debug for InjectedSecretVersion {
299    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
300        formatter
301            .debug_tuple("InjectedSecretVersion")
302            .field(&self.0)
303            .finish()
304    }
305}
306
307impl fmt::Display for InjectedSecretVersion {
308    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309        formatter.write_str(&self.0)
310    }
311}
312
313impl FromStr for InjectedSecretVersion {
314    type Err = InjectedSecretVersionParseError;
315
316    fn from_str(input: &str) -> Result<Self, Self::Err> {
317        if input.is_empty() || input.len() > 64 {
318            return Err(InjectedSecretVersionParseError);
319        }
320        let bytes = input.as_bytes();
321        if !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
322            || !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
323        {
324            return Err(InjectedSecretVersionParseError);
325        }
326
327        let mut previous_separator = false;
328        for byte in bytes {
329            match byte {
330                b'a'..=b'z' | b'0'..=b'9' => previous_separator = false,
331                b'.' | b'_' | b'-' if !previous_separator => previous_separator = true,
332                _ => return Err(InjectedSecretVersionParseError),
333            }
334        }
335        Ok(Self(input.into()))
336    }
337}
338
339impl<'de> Deserialize<'de> for InjectedSecretVersion {
340    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
341    where
342        D: Deserializer<'de>,
343    {
344        let input = <&str>::deserialize(deserializer)?;
345        Self::from_str(input).map_err(de::Error::custom)
346    }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
350#[error("Injected-secret version label is not canonical")]
351pub struct InjectedSecretVersionParseError;
352
353/// Bounded Secret bytes protected from accidental formatting or serialization.
354///
355/// Access is deliberately scoped to a closure, and ordinary Serde output is not
356/// implemented:
357///
358/// ```compile_fail
359/// let material = ahri_tre_secrets::SecretMaterial::try_from(b"secret".to_vec()).unwrap();
360/// let _ = serde_json::to_string(&material);
361/// ```
362pub struct SecretMaterial(SecretSlice<u8>);
363
364impl SecretMaterial {
365    pub fn expose<R>(&self, use_material: impl FnOnce(&[u8]) -> R) -> R {
366        use_material(self.0.expose_secret())
367    }
368}
369
370/// Generates deployment-local bootstrap material without exposing raw Secret
371/// ownership to configuration or runtime crates.
372pub fn generate_local_foundation_secret() -> SecretMaterial {
373    let material = format!(
374        "{}{}",
375        uuid::Uuid::new_v4().simple(),
376        uuid::Uuid::new_v4().simple()
377    )
378    .into_bytes();
379    SecretMaterial::try_from(material).expect("UUID bootstrap material is bounded")
380}
381
382/// A fixed private provider artifact owned by the Local deployment foundation.
383///
384/// Keeping the path selection in this crate prevents the provisioner from
385/// becoming a second, general-purpose Secret-file writer.
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum LocalProviderCredential {
388    CertificateAuthorityPrivateKey,
389    RuntimeTlsPrivateKey,
390    WebTlsPrivateKey,
391    PostgresTlsPrivateKey,
392    PostgresInternalTlsPrivateKey,
393    BrowserTlsPrivateKey,
394    OidcIdentityPassword,
395    OidcIdentityArchive,
396    OidcSigningPrivateKey,
397    OidcClientSecret,
398    DiagnosticPassword,
399}
400
401impl LocalProviderCredential {
402    fn path(self) -> &'static Path {
403        Path::new(match self {
404            Self::CertificateAuthorityPrivateKey => "/var/lib/postgresql/trust/ca.key",
405            Self::RuntimeTlsPrivateKey => "/var/lib/postgresql/trust/runtime.key",
406            Self::WebTlsPrivateKey => "/var/lib/postgresql/trust/web.key",
407            Self::PostgresTlsPrivateKey => "/run/postgres-tls/postgres.key",
408            Self::PostgresInternalTlsPrivateKey => {
409                "/var/lib/postgresql/trust/postgres-internal.key"
410            }
411            Self::BrowserTlsPrivateKey => "/run/browser-tls/browser.key",
412            Self::OidcIdentityPassword => "/run/oidc/identity-password",
413            Self::OidcIdentityArchive => "/run/oidc/identity.p12",
414            Self::OidcSigningPrivateKey => "/run/oidc/signing-key.pem",
415            Self::OidcClientSecret => "/run/oidc/client-secret",
416            Self::DiagnosticPassword => "/run/diagnostic-input/password",
417        })
418    }
419}
420
421/// Creates one allow-listed Local provider credential with protected initial
422/// permissions. Existing state is never replaced.
423pub fn initialize_local_provider_credential(
424    target: LocalProviderCredential,
425    material: &SecretMaterial,
426) -> Result<(), LocalProviderCredentialError> {
427    write_new_local_provider_credential(target.path(), material)
428}
429
430/// Generates and creates the fixed, short-lived Local database-diagnostic
431/// password export without returning its material to deployment shell code.
432pub fn initialize_local_diagnostic_password() -> Result<(), LocalProviderCredentialError> {
433    let value = format!(
434        "diagnostic-{}{}",
435        uuid::Uuid::new_v4().simple(),
436        uuid::Uuid::new_v4().simple()
437    );
438    let material =
439        SecretMaterial::try_from(value.into_bytes()).map_err(|_| LocalProviderCredentialError)?;
440    initialize_local_provider_credential(LocalProviderCredential::DiagnosticPassword, &material)
441}
442
443/// Replaces only PostgreSQL's temporary Local diagnostic TLS credential.
444/// The broad foundation writer remains create-only; this narrower lifecycle
445/// capability exists solely for explicit diagnostic expose/revoke.
446pub fn replace_local_postgres_tls_credential_for_diagnostic(
447    material: &SecretMaterial,
448) -> Result<(), LocalProviderCredentialError> {
449    replace_local_provider_credential(
450        LocalProviderCredential::PostgresTlsPrivateKey.path(),
451        material,
452    )
453}
454
455fn replace_local_provider_credential(
456    path: &Path,
457    material: &SecretMaterial,
458) -> Result<(), LocalProviderCredentialError> {
459    let metadata = fs::symlink_metadata(path).map_err(|_| LocalProviderCredentialError)?;
460    #[cfg(unix)]
461    {
462        // SAFETY: geteuid has no preconditions and retains no pointers.
463        let effective_uid = unsafe { libc::geteuid() };
464        if !metadata.is_file()
465            || metadata.file_type().is_symlink()
466            || metadata.uid() != effective_uid
467            || metadata.permissions().mode() & 0o777 != 0o400
468            || metadata.nlink() != 1
469        {
470            return Err(LocalProviderCredentialError);
471        }
472    }
473    let parent = path.parent().ok_or(LocalProviderCredentialError)?;
474    let staged = parent.join(format!(".provider-{}.tmp", uuid::Uuid::new_v4()));
475    if let Err(error) = write_new_local_provider_credential(&staged, material) {
476        let _ = fs::remove_file(&staged);
477        return Err(error);
478    }
479    if fs::rename(&staged, path).is_err() {
480        let _ = fs::remove_file(&staged);
481        return Err(LocalProviderCredentialError);
482    }
483    File::open(parent)
484        .and_then(|directory| directory.sync_all())
485        .map_err(|_| LocalProviderCredentialError)
486}
487
488fn write_new_local_provider_credential(
489    path: &Path,
490    material: &SecretMaterial,
491) -> Result<(), LocalProviderCredentialError> {
492    let parent = path.parent().ok_or(LocalProviderCredentialError)?;
493    let metadata = fs::symlink_metadata(parent).map_err(|_| LocalProviderCredentialError)?;
494    if !metadata.is_dir() || metadata.file_type().is_symlink() {
495        return Err(LocalProviderCredentialError);
496    }
497    #[cfg(unix)]
498    {
499        // SAFETY: geteuid has no preconditions and retains no pointers.
500        let effective_uid = unsafe { libc::geteuid() };
501        if metadata.uid() != effective_uid || metadata.permissions().mode() & 0o077 != 0 {
502            return Err(LocalProviderCredentialError);
503        }
504    }
505    let mut options = OpenOptions::new();
506    options.write(true).create_new(true);
507    #[cfg(unix)]
508    options.mode(0o400);
509    let mut file = options
510        .open(path)
511        .map_err(|_| LocalProviderCredentialError)?;
512    material
513        .expose(|bytes| file.write_all(bytes))
514        .and_then(|_| file.sync_all())
515        .map_err(|_| LocalProviderCredentialError)
516}
517
518/// Path-free failure while creating an allow-listed Local provider credential.
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
520#[error("Local provider credential could not be initialized")]
521pub struct LocalProviderCredentialError;
522
523/// Path-free failure while creating Local root-identity authority.
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
525#[error("Local root identity could not be initialized")]
526pub struct LocalRootIdentityProvisionError;
527
528/// Generates and projects a new Local root identity, then initializes the
529/// Deployment-bound Managed store without returning identity material.
530pub fn initialize_local_foundation_managed_store(
531    deployment_id: uuid::Uuid,
532) -> Result<(), LocalRootIdentityProvisionError> {
533    let identity = x25519::Identity::generate().to_string();
534    let material = SecretMaterial::try_from(identity.expose_secret().as_bytes().to_vec())
535        .map_err(|_| LocalRootIdentityProvisionError)?;
536    let reference = InjectedSecretReference::from_str("injected://ahri-tre/root-identity")
537        .map_err(|_| LocalRootIdentityProvisionError)?;
538    let version =
539        InjectedSecretVersion::from_str("local-v1").map_err(|_| LocalRootIdentityProvisionError)?;
540    initialize_injected_projection_for_local_foundation(
541        Path::new(INJECTED_SECRET_ROOT),
542        &reference,
543        &version,
544        &material,
545    )
546    .map_err(|_| LocalRootIdentityProvisionError)?;
547    let identity = ManagedSecretIdentity::from_material(&material)
548        .map_err(|_| LocalRootIdentityProvisionError)?;
549    LocalManagedSecretStore::initialize(deployment_id, identity)
550        .map_err(|_| LocalRootIdentityProvisionError)?;
551    Ok(())
552}
553
554/// Creates the distinct next-root projection consumed only by the stopped
555/// offline rotation workload. Existing next-root state is never replaced.
556pub fn initialize_local_foundation_next_root_identity()
557-> Result<(), LocalRootIdentityProvisionError> {
558    let identity = x25519::Identity::generate().to_string();
559    let material = SecretMaterial::try_from(identity.expose_secret().as_bytes().to_vec())
560        .map_err(|_| LocalRootIdentityProvisionError)?;
561    let reference = InjectedSecretReference::from_str("injected://ahri-tre/root-identity-next")
562        .map_err(|_| LocalRootIdentityProvisionError)?;
563    let version = InjectedSecretVersion::from_str("local-rotation-v1")
564        .map_err(|_| LocalRootIdentityProvisionError)?;
565    initialize_injected_projection_for_local_foundation(
566        Path::new(INJECTED_SECRET_ROOT),
567        &reference,
568        &version,
569        &material,
570    )
571    .map_err(|_| LocalRootIdentityProvisionError)
572}
573
574impl TryFrom<Vec<u8>> for SecretMaterial {
575    type Error = SecretMaterialError;
576
577    fn try_from(mut value: Vec<u8>) -> Result<Self, Self::Error> {
578        if value.len() > MAX_SECRET_MATERIAL_BYTES {
579            value.zeroize();
580            return Err(SecretMaterialError::TooLarge);
581        }
582        Ok(Self(value.into()))
583    }
584}
585
586impl fmt::Debug for SecretMaterial {
587    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
588        formatter
589            .debug_tuple("SecretMaterial")
590            .field(&ahri_tre_security::REDACTED_VALUE)
591            .finish()
592    }
593}
594
595/// Writes one new Local-deployment Injected-secret projection without exposing
596/// the projection layout to the provisioner. The caller owns the selected
597/// deployment volume; a projection is never replaced in place.
598pub fn initialize_injected_projection_for_local_foundation(
599    root: &Path,
600    reference: &InjectedSecretReference,
601    version: &InjectedSecretVersion,
602    material: &SecretMaterial,
603) -> Result<(), InjectedSecretProvisionError> {
604    fs::create_dir_all(root).map_err(|_| InjectedSecretProvisionError)?;
605    set_private_directory_mode(root)?;
606    let directory = reference
607        .segments()
608        .fold(root.to_path_buf(), |path, segment| path.join(segment));
609    fs::create_dir_all(&directory).map_err(|_| InjectedSecretProvisionError)?;
610    set_private_directory_mode(&directory)?;
611    write_new_private_projection_file(&directory.join("value"), material)?;
612    write_new_version_projection_file(&directory.join("version"), version)?;
613    Ok(())
614}
615
616/// Replaces one explicitly temporary Local acceptance projection before its
617/// consuming workload starts. The reference owns a complete leaf directory;
618/// failure leaves that capability absent rather than retaining stale material.
619pub fn replace_injected_projection_for_local_acceptance(
620    root: &Path,
621    reference: &InjectedSecretReference,
622    version: &InjectedSecretVersion,
623    material: &SecretMaterial,
624) -> Result<(), InjectedSecretProvisionError> {
625    remove_injected_projection_for_local_acceptance(root, reference)?;
626    initialize_injected_projection_for_local_foundation(root, reference, version, material)
627}
628
629/// Removes one temporary Local acceptance projection without traversing or
630/// changing any sibling capability.
631pub fn remove_injected_projection_for_local_acceptance(
632    root: &Path,
633    reference: &InjectedSecretReference,
634) -> Result<(), InjectedSecretProvisionError> {
635    let mut directory = root.to_path_buf();
636    for segment in reference.segments() {
637        let metadata = match fs::symlink_metadata(&directory) {
638            Ok(metadata) => metadata,
639            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
640            Err(_) => return Err(InjectedSecretProvisionError),
641        };
642        if metadata.file_type().is_symlink() || !metadata.is_dir() {
643            return Err(InjectedSecretProvisionError);
644        }
645        directory.push(segment);
646    }
647    if let Ok(metadata) = fs::symlink_metadata(&directory)
648        && metadata.file_type().is_symlink()
649    {
650        return Err(InjectedSecretProvisionError);
651    }
652    match fs::remove_dir_all(&directory) {
653        Ok(()) => Ok(()),
654        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
655        Err(_) => Err(InjectedSecretProvisionError),
656    }
657}
658
659fn write_new_private_projection_file(
660    path: &Path,
661    material: &SecretMaterial,
662) -> Result<(), InjectedSecretProvisionError> {
663    let mut options = OpenOptions::new();
664    options.write(true).create_new(true);
665    #[cfg(unix)]
666    options.mode(0o400);
667    let mut file = options
668        .open(path)
669        .map_err(|_| InjectedSecretProvisionError)?;
670    material
671        .expose(|bytes| file.write_all(bytes))
672        .map_err(|_| InjectedSecretProvisionError)?;
673    file.sync_all().map_err(|_| InjectedSecretProvisionError)?;
674    Ok(())
675}
676
677fn write_new_version_projection_file(
678    path: &Path,
679    version: &InjectedSecretVersion,
680) -> Result<(), InjectedSecretProvisionError> {
681    let mut options = OpenOptions::new();
682    options.write(true).create_new(true);
683    #[cfg(unix)]
684    options.mode(0o400);
685    let mut file = options
686        .open(path)
687        .map_err(|_| InjectedSecretProvisionError)?;
688    file.write_all(version.as_str().as_bytes())
689        .and_then(|_| file.sync_all())
690        .map_err(|_| InjectedSecretProvisionError)
691}
692
693fn set_private_directory_mode(path: &Path) -> Result<(), InjectedSecretProvisionError> {
694    #[cfg(unix)]
695    fs::set_permissions(path, fs::Permissions::from_mode(0o700))
696        .map_err(|_| InjectedSecretProvisionError)?;
697    Ok(())
698}
699
700/// A path-free failure from Local foundation Secret projection setup.
701#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
702#[error("Injected Secret projection could not be initialized")]
703pub struct InjectedSecretProvisionError;
704
705/// Safe failure while an existing Local foundation capability is projected to
706/// one additional workload root during an additive deployment migration.
707#[derive(Debug, Error)]
708pub enum LocalInjectedSecretReplicationError {
709    #[error("source Injected Secret could not be captured: {0}")]
710    Source(InjectedSecretError),
711    #[error("target Injected Secret could not be inspected: {0}")]
712    Target(InjectedSecretError),
713    #[error("target Injected Secret does not match its source")]
714    TargetMismatch,
715    #[error(transparent)]
716    Provision(InjectedSecretProvisionError),
717}
718
719/// Replicates one immutable Local foundation projection into another workload
720/// root without returning its material to the caller. An existing valid target
721/// is retained, making the additive migration safe to rerun.
722pub fn replicate_injected_projection_for_local_foundation(
723    source_root: &Path,
724    target_root: &Path,
725    reference: &InjectedSecretReference,
726) -> Result<(), LocalInjectedSecretReplicationError> {
727    let source = InjectedSecretSnapshot::capture_from_root(source_root, reference)
728        .map_err(LocalInjectedSecretReplicationError::Source)?;
729    fs::create_dir_all(target_root).map_err(|_| {
730        LocalInjectedSecretReplicationError::Provision(InjectedSecretProvisionError)
731    })?;
732    set_private_directory_mode(target_root)
733        .map_err(LocalInjectedSecretReplicationError::Provision)?;
734    match InjectedSecretSnapshot::capture_from_root(target_root, reference) {
735        Ok(target) => {
736            let material_matches = source.material().expose(|source_material| {
737                target
738                    .material()
739                    .expose(|target_material| source_material == target_material)
740            });
741            if source.version() == target.version() && material_matches {
742                return Ok(());
743            }
744            return Err(LocalInjectedSecretReplicationError::TargetMismatch);
745        }
746        Err(error) if error.kind() == InjectedSecretErrorKind::Missing => {}
747        Err(error) => return Err(LocalInjectedSecretReplicationError::Target(error)),
748    }
749
750    let directory = reference
751        .segments()
752        .fold(target_root.to_path_buf(), |path, segment| {
753            path.join(segment)
754        });
755    let parent = directory
756        .parent()
757        .ok_or(InjectedSecretProvisionError)
758        .map_err(LocalInjectedSecretReplicationError::Provision)?;
759    fs::create_dir_all(parent).map_err(|_| {
760        LocalInjectedSecretReplicationError::Provision(InjectedSecretProvisionError)
761    })?;
762    set_private_directory_mode(parent).map_err(LocalInjectedSecretReplicationError::Provision)?;
763    if let Ok(metadata) = fs::symlink_metadata(&directory) {
764        if metadata.file_type().is_symlink() || !metadata.is_dir() {
765            return Err(LocalInjectedSecretReplicationError::Provision(
766                InjectedSecretProvisionError,
767            ));
768        }
769        fs::remove_dir_all(&directory).map_err(|_| {
770            LocalInjectedSecretReplicationError::Provision(InjectedSecretProvisionError)
771        })?;
772    }
773
774    let staged = parent.join(format!(".replication-{}", uuid::Uuid::new_v4()));
775    let install = (|| {
776        fs::create_dir(&staged).map_err(|_| InjectedSecretProvisionError)?;
777        set_private_directory_mode(&staged)?;
778        write_new_private_projection_file(&staged.join("value"), source.material())?;
779        write_new_version_projection_file(&staged.join("version"), source.version())?;
780        File::open(&staged)
781            .and_then(|staged_directory| staged_directory.sync_all())
782            .map_err(|_| InjectedSecretProvisionError)?;
783        fs::rename(&staged, &directory).map_err(|_| InjectedSecretProvisionError)?;
784        File::open(parent)
785            .and_then(|parent_directory| parent_directory.sync_all())
786            .map_err(|_| InjectedSecretProvisionError)
787    })();
788    if install.is_err() {
789        let _ = fs::remove_dir_all(&staged);
790    }
791    install.map_err(LocalInjectedSecretReplicationError::Provision)
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
795pub enum SecretMaterialError {
796    #[error("Secret material exceeds the 64 KiB limit")]
797    TooLarge,
798}
799
800/// One Injected reference and the exact projected version required by startup.
801#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
802#[serde(deny_unknown_fields)]
803pub struct InjectedSecretRequirement {
804    uri: InjectedSecretReference,
805    expected_version: InjectedSecretVersion,
806}
807
808impl InjectedSecretRequirement {
809    pub fn new(uri: InjectedSecretReference, expected_version: InjectedSecretVersion) -> Self {
810        Self {
811            uri,
812            expected_version,
813        }
814    }
815
816    pub fn reference(&self) -> &InjectedSecretReference {
817        &self.uri
818    }
819
820    pub fn expected_version(&self) -> &InjectedSecretVersion {
821        &self.expected_version
822    }
823}
824
825/// One protected Injected value retained by a process-lifetime snapshot.
826pub struct ResolvedInjectedSecret {
827    reference: InjectedSecretReference,
828    version: InjectedSecretVersion,
829    material: SecretMaterial,
830}
831
832impl ResolvedInjectedSecret {
833    pub fn reference(&self) -> &InjectedSecretReference {
834        &self.reference
835    }
836
837    pub fn version(&self) -> &InjectedSecretVersion {
838        &self.version
839    }
840
841    pub fn material(&self) -> &SecretMaterial {
842        &self.material
843    }
844}
845
846impl fmt::Debug for ResolvedInjectedSecret {
847    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
848        formatter
849            .debug_struct("ResolvedInjectedSecret")
850            .field("reference", &self.reference)
851            .field("version", &self.version)
852            .field("material", &self.material)
853            .finish()
854    }
855}
856
857/// Immutable set of Injected secrets resolved once during process bootstrap.
858pub struct InjectedSecretSnapshot {
859    entries: BTreeMap<InjectedSecretReference, ResolvedInjectedSecret>,
860}
861
862impl InjectedSecretSnapshot {
863    /// Resolves the complete required set beneath the fixed production root.
864    pub fn load(
865        requirements: impl IntoIterator<Item = InjectedSecretRequirement>,
866    ) -> Result<Self, InjectedSecretError> {
867        Self::load_from_root(Path::new(INJECTED_SECRET_ROOT), requirements)
868    }
869
870    pub fn get(&self, reference: &InjectedSecretReference) -> Option<&ResolvedInjectedSecret> {
871        self.entries.get(reference)
872    }
873
874    pub fn iter(&self) -> impl Iterator<Item = &ResolvedInjectedSecret> {
875        self.entries.values()
876    }
877
878    pub fn len(&self) -> usize {
879        self.entries.len()
880    }
881
882    pub fn is_empty(&self) -> bool {
883        self.entries.is_empty()
884    }
885
886    /// Captures one projection without assigning an expected version. This is
887    /// used at process bootstrap when a later selected path owns the version
888    /// check but must never cause a filesystem reread.
889    pub fn capture(
890        reference: &InjectedSecretReference,
891    ) -> Result<ResolvedInjectedSecret, InjectedSecretError> {
892        Self::capture_from_root(Path::new(INJECTED_SECRET_ROOT), reference)
893    }
894
895    fn load_from_root(
896        root: &Path,
897        requirements: impl IntoIterator<Item = InjectedSecretRequirement>,
898    ) -> Result<Self, InjectedSecretError> {
899        let root = InjectedRoot::open(root)?;
900
901        let mut seen = BTreeSet::new();
902        let mut entries = BTreeMap::new();
903        for requirement in requirements {
904            if !seen.insert(requirement.reference().clone()) {
905                return Err(InjectedSecretError::for_reference(
906                    InjectedSecretErrorKind::DuplicateReference,
907                    requirement.reference(),
908                ));
909            }
910            let resolved = resolve_injected(&root, &requirement)?;
911            entries.insert(requirement.uri, resolved);
912        }
913        Ok(Self { entries })
914    }
915
916    #[cfg(any(test, feature = "test-fixtures"))]
917    #[doc(hidden)]
918    pub fn load_from_root_for_test(
919        root: &Path,
920        requirements: impl IntoIterator<Item = InjectedSecretRequirement>,
921    ) -> Result<Self, InjectedSecretError> {
922        Self::load_from_root(root, requirements)
923    }
924
925    #[cfg(any(test, feature = "test-fixtures"))]
926    #[doc(hidden)]
927    pub fn capture_from_root_for_test(
928        root: &Path,
929        reference: &InjectedSecretReference,
930    ) -> Result<ResolvedInjectedSecret, InjectedSecretError> {
931        Self::capture_from_root(root, reference)
932    }
933
934    fn capture_from_root(
935        root: &Path,
936        reference: &InjectedSecretReference,
937    ) -> Result<ResolvedInjectedSecret, InjectedSecretError> {
938        let root = InjectedRoot::open(root)?;
939        capture_injected(&root, reference)
940    }
941}
942
943impl fmt::Debug for InjectedSecretSnapshot {
944    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
945        formatter
946            .debug_struct("InjectedSecretSnapshot")
947            .field("entries", &self.entries)
948            .finish()
949    }
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq)]
953pub enum InjectedSecretErrorKind {
954    RootUnavailable,
955    Missing,
956    EscapesRoot,
957    NotRegularFile,
958    UnsafePermissions,
959    Unreadable,
960    TooLarge,
961    InvalidVersion,
962    VersionMismatch,
963    ChangedDuringSnapshot,
964    DuplicateReference,
965    UnsupportedPlatform,
966}
967
968impl fmt::Display for InjectedSecretErrorKind {
969    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
970        let category = match self {
971            Self::RootUnavailable => "root unavailable",
972            Self::Missing => "projection missing",
973            Self::EscapesRoot => "projection escapes fixed root",
974            Self::NotRegularFile => "projection is not a regular file",
975            Self::UnsafePermissions => "projection has unsafe permissions",
976            Self::Unreadable => "projection is not readable by the effective identity",
977            Self::TooLarge => "Secret material exceeds the 64 KiB limit",
978            Self::InvalidVersion => "projected version label is invalid",
979            Self::VersionMismatch => "projected version does not match the expected version",
980            Self::ChangedDuringSnapshot => "projection changed while the snapshot was loading",
981            Self::DuplicateReference => "reference occurs more than once in the snapshot request",
982            Self::UnsupportedPlatform => {
983                "filesystem identity checks are unsupported on this platform"
984            }
985        };
986        formatter.write_str(category)
987    }
988}
989
990/// Safe Injected-secret failure containing no material or filesystem path.
991#[derive(Debug, Clone, PartialEq, Eq)]
992pub struct InjectedSecretError {
993    kind: InjectedSecretErrorKind,
994    reference: Option<InjectedSecretReference>,
995}
996
997impl InjectedSecretError {
998    pub fn kind(&self) -> InjectedSecretErrorKind {
999        self.kind
1000    }
1001
1002    pub fn reference(&self) -> Option<&InjectedSecretReference> {
1003        self.reference.as_ref()
1004    }
1005
1006    fn for_reference(kind: InjectedSecretErrorKind, reference: &InjectedSecretReference) -> Self {
1007        Self {
1008            kind,
1009            reference: Some(reference.clone()),
1010        }
1011    }
1012
1013    fn without_reference(kind: InjectedSecretErrorKind) -> Self {
1014        Self {
1015            kind,
1016            reference: None,
1017        }
1018    }
1019}
1020
1021impl fmt::Display for InjectedSecretError {
1022    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1023        match &self.reference {
1024            Some(reference) => write!(formatter, "Injected secret {reference}: {}", self.kind),
1025            None => write!(formatter, "Injected-secret snapshot: {}", self.kind),
1026        }
1027    }
1028}
1029
1030impl std::error::Error for InjectedSecretError {}
1031
1032// The fields are only read by the Linux `openat2` projection path; other
1033// platforms never construct a root because `open` reports `UnsupportedPlatform`.
1034#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
1035struct InjectedRoot {
1036    directory: File,
1037    canonical_path: PathBuf,
1038}
1039
1040impl InjectedRoot {
1041    #[cfg(target_os = "linux")]
1042    fn open(path: &Path) -> Result<Self, InjectedSecretError> {
1043        let directory = File::open(path).map_err(|_| {
1044            InjectedSecretError::without_reference(InjectedSecretErrorKind::RootUnavailable)
1045        })?;
1046        let metadata = directory.metadata().map_err(|_| {
1047            InjectedSecretError::without_reference(InjectedSecretErrorKind::RootUnavailable)
1048        })?;
1049        if !metadata.is_dir() {
1050            return Err(InjectedSecretError::without_reference(
1051                InjectedSecretErrorKind::RootUnavailable,
1052            ));
1053        }
1054        validate_directory_permissions(&metadata)
1055            .map_err(InjectedSecretError::without_reference)?;
1056        let canonical_path =
1057            descriptor_path(&directory).map_err(InjectedSecretError::without_reference)?;
1058        Ok(Self {
1059            directory,
1060            canonical_path,
1061        })
1062    }
1063
1064    #[cfg(not(target_os = "linux"))]
1065    fn open(_path: &Path) -> Result<Self, InjectedSecretError> {
1066        Err(InjectedSecretError::without_reference(
1067            InjectedSecretErrorKind::UnsupportedPlatform,
1068        ))
1069    }
1070}
1071
1072fn resolve_injected(
1073    root: &InjectedRoot,
1074    requirement: &InjectedSecretRequirement,
1075) -> Result<ResolvedInjectedSecret, InjectedSecretError> {
1076    let resolved = capture_injected(root, requirement.reference())?;
1077    if resolved.version() != requirement.expected_version() {
1078        return Err(InjectedSecretError::for_reference(
1079            InjectedSecretErrorKind::VersionMismatch,
1080            requirement.reference(),
1081        ));
1082    }
1083    Ok(resolved)
1084}
1085
1086fn capture_injected(
1087    root: &InjectedRoot,
1088    reference: &InjectedSecretReference,
1089) -> Result<ResolvedInjectedSecret, InjectedSecretError> {
1090    let projection_directory = reference
1091        .segments()
1092        .fold(PathBuf::new(), |path, segment| path.join(segment));
1093    validate_projection_directory_chain(root, reference)?;
1094
1095    let initial_version = read_projection_file(
1096        root,
1097        &projection_directory.join("version"),
1098        ProjectionFile::Version,
1099        reference,
1100    )?;
1101    let version = parse_projected_version(&initial_version.bytes, reference)?;
1102
1103    let mut material_read = read_projection_file(
1104        root,
1105        &projection_directory.join("value"),
1106        ProjectionFile::Value,
1107        reference,
1108    )?;
1109    if !initial_version.same_generation(&material_read) {
1110        material_read.bytes.zeroize();
1111        return Err(InjectedSecretError::for_reference(
1112            InjectedSecretErrorKind::ChangedDuringSnapshot,
1113            reference,
1114        ));
1115    }
1116    let material = SecretMaterial::try_from(material_read.bytes).map_err(|_| {
1117        InjectedSecretError::for_reference(InjectedSecretErrorKind::TooLarge, reference)
1118    })?;
1119    let confirmed_version = read_projection_file(
1120        root,
1121        &projection_directory.join("version"),
1122        ProjectionFile::Version,
1123        reference,
1124    )?;
1125    if !initial_version.same_generation(&confirmed_version)
1126        || !initial_version.same_file_and_content(&confirmed_version)
1127    {
1128        return Err(InjectedSecretError::for_reference(
1129            InjectedSecretErrorKind::ChangedDuringSnapshot,
1130            reference,
1131        ));
1132    }
1133    Ok(ResolvedInjectedSecret {
1134        reference: reference.clone(),
1135        version,
1136        material,
1137    })
1138}
1139
1140pub(crate) fn load_root_identity_material() -> Result<SecretMaterial, InjectedSecretError> {
1141    load_root_identity_material_from_root(Path::new(INJECTED_SECRET_ROOT))
1142}
1143
1144fn load_root_identity_material_from_root(
1145    root_path: &Path,
1146) -> Result<SecretMaterial, InjectedSecretError> {
1147    load_named_identity_material_from_root(root_path, "root-identity")
1148}
1149
1150pub(crate) fn load_next_root_identity_material() -> Result<SecretMaterial, InjectedSecretError> {
1151    load_named_identity_material_from_root(Path::new(INJECTED_SECRET_ROOT), "root-identity-next")
1152}
1153
1154#[cfg(any(test, feature = "test-fixtures"))]
1155pub(crate) fn load_next_root_identity_material_from_root(
1156    root_path: &Path,
1157) -> Result<SecretMaterial, InjectedSecretError> {
1158    load_named_identity_material_from_root(root_path, "root-identity-next")
1159}
1160
1161fn load_named_identity_material_from_root(
1162    root_path: &Path,
1163    name: &str,
1164) -> Result<SecretMaterial, InjectedSecretError> {
1165    let reference = InjectedSecretReference::from_str(&format!("injected://ahri-tre/{name}"))
1166        .map_err(|_| {
1167            InjectedSecretError::without_reference(InjectedSecretErrorKind::UnsupportedPlatform)
1168        })?;
1169    let root = InjectedRoot::open(root_path)?;
1170    validate_projection_directory_chain(&root, &reference)?;
1171    let mut material_read = read_projection_file(
1172        &root,
1173        &Path::new("ahri-tre").join(name).join("value"),
1174        ProjectionFile::Value,
1175        &reference,
1176    )?;
1177    SecretMaterial::try_from(std::mem::take(&mut material_read.bytes)).map_err(|_| {
1178        InjectedSecretError::for_reference(InjectedSecretErrorKind::TooLarge, &reference)
1179    })
1180}
1181
1182fn parse_projected_version(
1183    bytes: &[u8],
1184    reference: &InjectedSecretReference,
1185) -> Result<InjectedSecretVersion, InjectedSecretError> {
1186    let version_text = std::str::from_utf8(bytes).map_err(|_| {
1187        InjectedSecretError::for_reference(InjectedSecretErrorKind::InvalidVersion, reference)
1188    })?;
1189    InjectedSecretVersion::from_str(version_text).map_err(|_| {
1190        InjectedSecretError::for_reference(InjectedSecretErrorKind::InvalidVersion, reference)
1191    })
1192}
1193
1194#[cfg(target_os = "linux")]
1195fn validate_projection_directory_chain(
1196    root: &InjectedRoot,
1197    reference: &InjectedSecretReference,
1198) -> Result<(), InjectedSecretError> {
1199    let projected = reference
1200        .segments()
1201        .fold(PathBuf::new(), |path, segment| path.join(segment));
1202    open_validated_directory_chain(root, &projected, reference)?;
1203    Ok(())
1204}
1205
1206#[cfg(not(target_os = "linux"))]
1207fn validate_projection_directory_chain(
1208    _root: &InjectedRoot,
1209    reference: &InjectedSecretReference,
1210) -> Result<(), InjectedSecretError> {
1211    Err(InjectedSecretError::for_reference(
1212        InjectedSecretErrorKind::UnsupportedPlatform,
1213        reference,
1214    ))
1215}
1216
1217#[derive(Clone, Copy)]
1218enum ProjectionFile {
1219    Value,
1220    Version,
1221}
1222
1223#[cfg(target_os = "linux")]
1224impl ProjectionFile {
1225    fn limit(self) -> usize {
1226        match self {
1227            Self::Value => MAX_SECRET_MATERIAL_BYTES,
1228            Self::Version => 64,
1229        }
1230    }
1231
1232    fn oversized_error(self) -> InjectedSecretErrorKind {
1233        match self {
1234            Self::Value => InjectedSecretErrorKind::TooLarge,
1235            Self::Version => InjectedSecretErrorKind::InvalidVersion,
1236        }
1237    }
1238}
1239
1240struct ProjectionRead {
1241    bytes: Vec<u8>,
1242    identity: FileIdentity,
1243    generation: ProjectionGeneration,
1244}
1245
1246impl ProjectionRead {
1247    fn same_file_and_content(&self, other: &Self) -> bool {
1248        self.identity == other.identity && self.bytes == other.bytes
1249    }
1250
1251    fn same_generation(&self, other: &Self) -> bool {
1252        self.generation == other.generation
1253    }
1254}
1255
1256#[derive(PartialEq, Eq)]
1257struct ProjectionGeneration {
1258    identity: FileIdentity,
1259}
1260
1261#[derive(PartialEq, Eq)]
1262struct FileIdentity {
1263    device: u64,
1264    inode: u64,
1265    length: u64,
1266    modified_seconds: i64,
1267    modified_nanoseconds: i64,
1268    changed_seconds: i64,
1269    changed_nanoseconds: i64,
1270}
1271
1272#[cfg(target_os = "linux")]
1273fn file_identity(metadata: &fs::Metadata) -> FileIdentity {
1274    FileIdentity {
1275        device: metadata.dev(),
1276        inode: metadata.ino(),
1277        length: metadata.len(),
1278        modified_seconds: metadata.mtime(),
1279        modified_nanoseconds: metadata.mtime_nsec(),
1280        changed_seconds: metadata.ctime(),
1281        changed_nanoseconds: metadata.ctime_nsec(),
1282    }
1283}
1284
1285#[cfg(target_os = "linux")]
1286fn read_projection_file(
1287    root: &InjectedRoot,
1288    projected_path: &Path,
1289    file_kind: ProjectionFile,
1290    reference: &InjectedSecretReference,
1291) -> Result<ProjectionRead, InjectedSecretError> {
1292    let file = open_beneath(
1293        root,
1294        projected_path,
1295        OFlags::RDONLY | OFlags::CLOEXEC,
1296        reference,
1297    )?;
1298    let metadata = file.metadata().map_err(|_| {
1299        InjectedSecretError::for_reference(InjectedSecretErrorKind::Unreadable, reference)
1300    })?;
1301    if !metadata.is_file() {
1302        return Err(InjectedSecretError::for_reference(
1303            InjectedSecretErrorKind::NotRegularFile,
1304            reference,
1305        ));
1306    }
1307    validate_file_permissions(&metadata, file_kind)
1308        .map_err(|kind| InjectedSecretError::for_reference(kind, reference))?;
1309    let identity_before_read = file_identity(&metadata);
1310    let generation_before_read =
1311        anchored_generation(root, &file, &identity_before_read, reference)?;
1312
1313    let limit = file_kind.limit();
1314    if metadata.len() > limit as u64 {
1315        return Err(InjectedSecretError::for_reference(
1316            file_kind.oversized_error(),
1317            reference,
1318        ));
1319    }
1320
1321    let mut bytes = Vec::with_capacity(metadata.len() as usize);
1322    if (&file)
1323        .take((limit + 1) as u64)
1324        .read_to_end(&mut bytes)
1325        .is_err()
1326    {
1327        bytes.zeroize();
1328        return Err(InjectedSecretError::for_reference(
1329            InjectedSecretErrorKind::Unreadable,
1330            reference,
1331        ));
1332    }
1333    if bytes.len() > limit {
1334        bytes.zeroize();
1335        return Err(InjectedSecretError::for_reference(
1336            file_kind.oversized_error(),
1337            reference,
1338        ));
1339    }
1340    let identity_after_read = file
1341        .metadata()
1342        .map(|metadata| file_identity(&metadata))
1343        .map_err(|_| {
1344            InjectedSecretError::for_reference(
1345                InjectedSecretErrorKind::ChangedDuringSnapshot,
1346                reference,
1347            )
1348        })?;
1349    if identity_before_read != identity_after_read {
1350        bytes.zeroize();
1351        return Err(InjectedSecretError::for_reference(
1352            InjectedSecretErrorKind::ChangedDuringSnapshot,
1353            reference,
1354        ));
1355    }
1356    let generation_after_read = anchored_generation(root, &file, &identity_after_read, reference)?;
1357    if generation_before_read != generation_after_read {
1358        bytes.zeroize();
1359        return Err(InjectedSecretError::for_reference(
1360            InjectedSecretErrorKind::ChangedDuringSnapshot,
1361            reference,
1362        ));
1363    }
1364    Ok(ProjectionRead {
1365        bytes,
1366        identity: identity_after_read,
1367        generation: generation_after_read,
1368    })
1369}
1370
1371#[cfg(not(target_os = "linux"))]
1372fn read_projection_file(
1373    _root: &InjectedRoot,
1374    _projected_path: &Path,
1375    _file_kind: ProjectionFile,
1376    reference: &InjectedSecretReference,
1377) -> Result<ProjectionRead, InjectedSecretError> {
1378    Err(InjectedSecretError::for_reference(
1379        InjectedSecretErrorKind::UnsupportedPlatform,
1380        reference,
1381    ))
1382}
1383
1384#[cfg(target_os = "linux")]
1385fn open_beneath(
1386    root: &InjectedRoot,
1387    relative_path: &Path,
1388    flags: OFlags,
1389    reference: &InjectedSecretReference,
1390) -> Result<File, InjectedSecretError> {
1391    let descriptor = openat2(
1392        &root.directory,
1393        relative_path,
1394        flags,
1395        Mode::empty(),
1396        ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS,
1397    )
1398    .map_err(|error| {
1399        let kind = if error == rustix::io::Errno::XDEV || error == rustix::io::Errno::LOOP {
1400            InjectedSecretErrorKind::EscapesRoot
1401        } else if error == rustix::io::Errno::NOSYS {
1402            InjectedSecretErrorKind::UnsupportedPlatform
1403        } else if error == rustix::io::Errno::ACCESS {
1404            InjectedSecretErrorKind::Unreadable
1405        } else if error == rustix::io::Errno::AGAIN {
1406            InjectedSecretErrorKind::ChangedDuringSnapshot
1407        } else {
1408            InjectedSecretErrorKind::Missing
1409        };
1410        InjectedSecretError::for_reference(kind, reference)
1411    })?;
1412    Ok(File::from(descriptor))
1413}
1414
1415#[cfg(target_os = "linux")]
1416fn open_validated_directory_chain(
1417    root: &InjectedRoot,
1418    relative_path: &Path,
1419    reference: &InjectedSecretReference,
1420) -> Result<File, InjectedSecretError> {
1421    let mut current = PathBuf::new();
1422    let mut final_directory = None;
1423    for component in relative_path.components() {
1424        let std::path::Component::Normal(component) = component else {
1425            return Err(InjectedSecretError::for_reference(
1426                InjectedSecretErrorKind::EscapesRoot,
1427                reference,
1428            ));
1429        };
1430        current.push(component);
1431        let directory = open_beneath(
1432            root,
1433            &current,
1434            OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
1435            reference,
1436        )?;
1437        let metadata = directory.metadata().map_err(|_| {
1438            InjectedSecretError::for_reference(InjectedSecretErrorKind::Missing, reference)
1439        })?;
1440        validate_directory_permissions(&metadata)
1441            .map_err(|kind| InjectedSecretError::for_reference(kind, reference))?;
1442        final_directory = Some(directory);
1443    }
1444    final_directory.ok_or_else(|| {
1445        InjectedSecretError::for_reference(InjectedSecretErrorKind::EscapesRoot, reference)
1446    })
1447}
1448
1449#[cfg(target_os = "linux")]
1450fn descriptor_path(file: &File) -> Result<PathBuf, InjectedSecretErrorKind> {
1451    fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))
1452        .map_err(|_| InjectedSecretErrorKind::UnsupportedPlatform)
1453}
1454
1455#[cfg(target_os = "linux")]
1456fn anchored_generation(
1457    root: &InjectedRoot,
1458    file: &File,
1459    expected_file_identity: &FileIdentity,
1460    reference: &InjectedSecretReference,
1461) -> Result<ProjectionGeneration, InjectedSecretError> {
1462    let opened_path = descriptor_path(file)
1463        .map_err(|kind| InjectedSecretError::for_reference(kind, reference))?;
1464    let relative_path = opened_path
1465        .strip_prefix(&root.canonical_path)
1466        .map_err(|_| {
1467            InjectedSecretError::for_reference(InjectedSecretErrorKind::EscapesRoot, reference)
1468        })?;
1469    let parent = relative_path.parent().ok_or_else(|| {
1470        InjectedSecretError::for_reference(InjectedSecretErrorKind::EscapesRoot, reference)
1471    })?;
1472    let file_name = relative_path.file_name().ok_or_else(|| {
1473        InjectedSecretError::for_reference(
1474            InjectedSecretErrorKind::ChangedDuringSnapshot,
1475            reference,
1476        )
1477    })?;
1478    let generation_directory = open_validated_directory_chain(root, parent, reference)?;
1479    let current_file = openat2(
1480        &generation_directory,
1481        Path::new(file_name),
1482        OFlags::PATH | OFlags::CLOEXEC,
1483        Mode::empty(),
1484        ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
1485    )
1486    .map(File::from)
1487    .map_err(|_| {
1488        InjectedSecretError::for_reference(
1489            InjectedSecretErrorKind::ChangedDuringSnapshot,
1490            reference,
1491        )
1492    })?;
1493    let current_identity = current_file
1494        .metadata()
1495        .map(|metadata| file_identity(&metadata))
1496        .map_err(|_| {
1497            InjectedSecretError::for_reference(
1498                InjectedSecretErrorKind::ChangedDuringSnapshot,
1499                reference,
1500            )
1501        })?;
1502    if &current_identity != expected_file_identity {
1503        return Err(InjectedSecretError::for_reference(
1504            InjectedSecretErrorKind::ChangedDuringSnapshot,
1505            reference,
1506        ));
1507    }
1508    let identity = generation_directory
1509        .metadata()
1510        .map(|metadata| file_identity(&metadata))
1511        .map_err(|_| {
1512            InjectedSecretError::for_reference(
1513                InjectedSecretErrorKind::ChangedDuringSnapshot,
1514                reference,
1515            )
1516        })?;
1517    Ok(ProjectionGeneration { identity })
1518}
1519
1520#[cfg(target_os = "linux")]
1521fn validate_directory_permissions(metadata: &fs::Metadata) -> Result<(), InjectedSecretErrorKind> {
1522    if metadata.mode() & 0o022 != 0 {
1523        return Err(InjectedSecretErrorKind::UnsafePermissions);
1524    }
1525    Ok(())
1526}
1527
1528#[cfg(target_os = "linux")]
1529fn validate_file_permissions(
1530    metadata: &fs::Metadata,
1531    file_kind: ProjectionFile,
1532) -> Result<(), InjectedSecretErrorKind> {
1533    let mode = metadata.mode();
1534    match file_kind {
1535        ProjectionFile::Value => {
1536            if mode & 0o333 != 0 || mode & 0o007 != 0 {
1537                return Err(InjectedSecretErrorKind::UnsafePermissions);
1538            }
1539            if !effective_identity_can_read(metadata)? {
1540                return Err(InjectedSecretErrorKind::Unreadable);
1541            }
1542        }
1543        ProjectionFile::Version => {
1544            if mode & 0o133 != 0 {
1545                return Err(InjectedSecretErrorKind::UnsafePermissions);
1546            }
1547        }
1548    }
1549    Ok(())
1550}
1551
1552#[cfg(target_os = "linux")]
1553fn effective_identity_can_read(metadata: &fs::Metadata) -> Result<bool, InjectedSecretErrorKind> {
1554    // SAFETY: These libc identity queries have no preconditions and do not
1555    // dereference caller-provided memory.
1556    let effective_uid = unsafe { libc::geteuid() };
1557    // SAFETY: `getegid` has no preconditions and returns the process identity.
1558    let effective_gid = unsafe { libc::getegid() };
1559    // SAFETY: A zero-sized `getgroups` call accepts a null pointer and returns
1560    // the buffer size required for the subsequent call.
1561    let count = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
1562    if count < 0 {
1563        return Err(InjectedSecretErrorKind::Unreadable);
1564    }
1565    let mut groups = vec![0; count as usize];
1566    if count > 0 {
1567        // SAFETY: `groups` has exactly `count` writable `gid_t` elements.
1568        let result = unsafe { libc::getgroups(count, groups.as_mut_ptr()) };
1569        if result < 0 {
1570            return Err(InjectedSecretErrorKind::Unreadable);
1571        }
1572        groups.truncate(result as usize);
1573    }
1574    groups.push(effective_gid);
1575    Ok(permission_class_can_read(
1576        metadata.mode(),
1577        metadata.uid(),
1578        metadata.gid(),
1579        effective_uid,
1580        &groups,
1581    ))
1582}
1583
1584#[cfg(any(target_os = "linux", test))]
1585fn permission_class_can_read(
1586    mode: u32,
1587    file_uid: libc::uid_t,
1588    file_gid: libc::gid_t,
1589    effective_uid: libc::uid_t,
1590    effective_groups: &[libc::gid_t],
1591) -> bool {
1592    if file_uid == effective_uid {
1593        mode & 0o400 != 0
1594    } else {
1595        mode & 0o040 != 0 && effective_groups.contains(&file_gid)
1596    }
1597}
1598
1599#[cfg(test)]
1600mod tests {
1601    use super::{
1602        InjectedSecretErrorKind, InjectedSecretReference, InjectedSecretRequirement,
1603        InjectedSecretSnapshot, InjectedSecretVersion, LocalInjectedSecretReplicationError,
1604        MAX_SECRET_MATERIAL_BYTES, ManagedSecretReference, SecretMaterial, SecretReference,
1605        initialize_injected_projection_for_local_foundation, permission_class_can_read,
1606        remove_injected_projection_for_local_acceptance,
1607        replace_injected_projection_for_local_acceptance, replace_local_provider_credential,
1608        replicate_injected_projection_for_local_foundation, write_new_local_provider_credential,
1609    };
1610    use std::fs;
1611    use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
1612    use std::path::{Path, PathBuf};
1613    use std::str::FromStr;
1614    use std::sync::atomic::{AtomicU64, Ordering};
1615    use uuid::Uuid;
1616
1617    #[test]
1618    fn canonical_references_accept_every_defined_boundary() {
1619        let one_segment = SecretReference::from_str("managed://a").unwrap();
1620        assert_eq!(one_segment.as_str(), "managed://a");
1621
1622        let eight_segments = SecretReference::from_str(
1623            "injected://a/segment-2/3/4/5/6/7/abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyza",
1624        )
1625        .unwrap();
1626        assert_eq!(eight_segments.segments().count(), 8);
1627
1628        let managed = ManagedSecretReference::from_str("managed://0").unwrap();
1629        assert_eq!(managed.as_str(), "managed://0");
1630        let injected = InjectedSecretReference::from_str("injected://a-1").unwrap();
1631        assert_eq!(injected.as_str(), "injected://a-1");
1632
1633        let maximum_uri = format!(
1634            "managed://{}/{}/{}/{}/{}/{}/{}/{}",
1635            "a".repeat(63),
1636            "b".repeat(63),
1637            "c".repeat(63),
1638            "d".repeat(63),
1639            "e".repeat(63),
1640            "f".repeat(63),
1641            "g".repeat(63),
1642            "h".repeat(54),
1643        );
1644        assert_eq!(maximum_uri.len(), 512);
1645        assert!(SecretReference::from_str(&maximum_uri).is_ok());
1646    }
1647
1648    #[test]
1649    fn local_foundation_writes_a_new_safe_injected_projection() {
1650        let fixture = InjectedFixture::new();
1651        let reference =
1652            InjectedSecretReference::from_str("injected://runtime/private-key").unwrap();
1653        let version = InjectedSecretVersion::from_str("local-v1").unwrap();
1654        let material = SecretMaterial::try_from(b"local-private-material".to_vec()).unwrap();
1655
1656        initialize_injected_projection_for_local_foundation(
1657            fixture.root(),
1658            &reference,
1659            &version,
1660            &material,
1661        )
1662        .unwrap();
1663
1664        let snapshot = InjectedSecretSnapshot::load_from_root_for_test(
1665            fixture.root(),
1666            [InjectedSecretRequirement::new(reference.clone(), version)],
1667        )
1668        .unwrap();
1669        snapshot
1670            .get(&reference)
1671            .unwrap()
1672            .material()
1673            .expose(|value| assert_eq!(value, b"local-private-material"));
1674        assert!(
1675            initialize_injected_projection_for_local_foundation(
1676                fixture.root(),
1677                &reference,
1678                &InjectedSecretVersion::from_str("local-v1").unwrap(),
1679                &material,
1680            )
1681            .is_err()
1682        );
1683    }
1684
1685    #[test]
1686    fn local_foundation_replicates_an_existing_projection_without_exposing_it() {
1687        let source = InjectedFixture::new();
1688        let target = InjectedFixture::new();
1689        let reference =
1690            InjectedSecretReference::from_str("injected://web/workflow-entry-password").unwrap();
1691        let version = InjectedSecretVersion::from_str("local-v1").unwrap();
1692        let material = SecretMaterial::try_from(b"workflow-entry-material".to_vec()).unwrap();
1693        initialize_injected_projection_for_local_foundation(
1694            source.root(),
1695            &reference,
1696            &version,
1697            &material,
1698        )
1699        .unwrap();
1700
1701        replicate_injected_projection_for_local_foundation(
1702            source.root(),
1703            target.root(),
1704            &reference,
1705        )
1706        .unwrap();
1707        replicate_injected_projection_for_local_foundation(
1708            source.root(),
1709            target.root(),
1710            &reference,
1711        )
1712        .unwrap();
1713
1714        let snapshot = InjectedSecretSnapshot::load_from_root_for_test(
1715            target.root(),
1716            [InjectedSecretRequirement::new(reference.clone(), version)],
1717        )
1718        .unwrap();
1719        snapshot
1720            .get(&reference)
1721            .unwrap()
1722            .material()
1723            .expose(|value| assert_eq!(value, b"workflow-entry-material"));
1724    }
1725
1726    #[test]
1727    fn local_foundation_replication_recovers_an_incomplete_target_atomically() {
1728        let source = InjectedFixture::new();
1729        let target = InjectedFixture::new();
1730        let reference =
1731            InjectedSecretReference::from_str("injected://web/workflow-entry-password").unwrap();
1732        let version = InjectedSecretVersion::from_str("local-v1").unwrap();
1733        let material = SecretMaterial::try_from(b"workflow-entry-material".to_vec()).unwrap();
1734        initialize_injected_projection_for_local_foundation(
1735            source.root(),
1736            &reference,
1737            &version,
1738            &material,
1739        )
1740        .unwrap();
1741        let incomplete = target.root().join("web/workflow-entry-password");
1742        fs::create_dir_all(&incomplete).unwrap();
1743        fs::set_permissions(&incomplete, fs::Permissions::from_mode(0o700)).unwrap();
1744        fs::write(incomplete.join("value"), b"partial-material").unwrap();
1745        fs::set_permissions(incomplete.join("value"), fs::Permissions::from_mode(0o400)).unwrap();
1746
1747        replicate_injected_projection_for_local_foundation(
1748            source.root(),
1749            target.root(),
1750            &reference,
1751        )
1752        .unwrap();
1753
1754        let snapshot = InjectedSecretSnapshot::load_from_root_for_test(
1755            target.root(),
1756            [InjectedSecretRequirement::new(reference.clone(), version)],
1757        )
1758        .unwrap();
1759        snapshot
1760            .get(&reference)
1761            .unwrap()
1762            .material()
1763            .expose(|value| assert_eq!(value, b"workflow-entry-material"));
1764    }
1765
1766    #[test]
1767    fn local_foundation_replication_rejects_a_mismatched_existing_target() {
1768        let source = InjectedFixture::new();
1769        let target = InjectedFixture::new();
1770        let reference =
1771            InjectedSecretReference::from_str("injected://web/workflow-entry-password").unwrap();
1772        let version = InjectedSecretVersion::from_str("local-v1").unwrap();
1773        let source_material =
1774            SecretMaterial::try_from(b"workflow-entry-material".to_vec()).unwrap();
1775        let target_material = SecretMaterial::try_from(b"different-material".to_vec()).unwrap();
1776        initialize_injected_projection_for_local_foundation(
1777            source.root(),
1778            &reference,
1779            &version,
1780            &source_material,
1781        )
1782        .unwrap();
1783        initialize_injected_projection_for_local_foundation(
1784            target.root(),
1785            &reference,
1786            &version,
1787            &target_material,
1788        )
1789        .unwrap();
1790
1791        assert!(matches!(
1792            replicate_injected_projection_for_local_foundation(
1793                source.root(),
1794                target.root(),
1795                &reference,
1796            ),
1797            Err(LocalInjectedSecretReplicationError::TargetMismatch)
1798        ));
1799        let snapshot = InjectedSecretSnapshot::load_from_root_for_test(
1800            target.root(),
1801            [InjectedSecretRequirement::new(reference.clone(), version)],
1802        )
1803        .unwrap();
1804        snapshot
1805            .get(&reference)
1806            .unwrap()
1807            .material()
1808            .expose(|value| assert_eq!(value, b"different-material"));
1809    }
1810
1811    #[test]
1812    fn local_provider_credential_is_private_from_creation_and_never_replaced() {
1813        let fixture = InjectedFixture::new();
1814        fs::set_permissions(fixture.root(), fs::Permissions::from_mode(0o700)).unwrap();
1815        let path = fixture.root().join("provider-credential");
1816        let first = SecretMaterial::try_from(b"first-provider-value".to_vec()).unwrap();
1817        let second = SecretMaterial::try_from(b"second-provider-value".to_vec()).unwrap();
1818
1819        write_new_local_provider_credential(&path, &first).unwrap();
1820
1821        assert_eq!(
1822            fs::metadata(&path).unwrap().permissions().mode() & 0o777,
1823            0o400
1824        );
1825        assert_eq!(fs::read(&path).unwrap(), b"first-provider-value");
1826        assert!(write_new_local_provider_credential(&path, &second).is_err());
1827        assert_eq!(fs::read(&path).unwrap(), b"first-provider-value");
1828    }
1829
1830    #[test]
1831    fn diagnostic_provider_replacement_requires_and_preserves_a_private_owned_file() {
1832        let fixture = InjectedFixture::new();
1833        fs::set_permissions(fixture.root(), fs::Permissions::from_mode(0o700)).unwrap();
1834        let path = fixture.root().join("provider-credential");
1835        let first = SecretMaterial::try_from(b"first-provider-value".to_vec()).unwrap();
1836        let second = SecretMaterial::try_from(b"second-provider-value".to_vec()).unwrap();
1837        write_new_local_provider_credential(&path, &first).unwrap();
1838
1839        replace_local_provider_credential(&path, &second).unwrap();
1840
1841        assert_eq!(fs::read(&path).unwrap(), b"second-provider-value");
1842        let metadata = fs::metadata(&path).unwrap();
1843        assert_eq!(metadata.permissions().mode() & 0o777, 0o400);
1844        assert_eq!(metadata.nlink(), 1);
1845    }
1846
1847    #[test]
1848    fn temporary_acceptance_projection_replaces_and_revokes_only_its_capability() {
1849        let fixture = InjectedFixture::new();
1850        let temporary =
1851            InjectedSecretReference::from_str("injected://orcid-sandbox/client-secret").unwrap();
1852        let sibling = InjectedSecretReference::from_str("injected://runtime/private-key").unwrap();
1853        let version = InjectedSecretVersion::from_str("sandbox-v1").unwrap();
1854        let first = SecretMaterial::try_from(b"first-value".to_vec()).unwrap();
1855        let second = SecretMaterial::try_from(b"second-value".to_vec()).unwrap();
1856        let sibling_material = SecretMaterial::try_from(b"runtime-value".to_vec()).unwrap();
1857        initialize_injected_projection_for_local_foundation(
1858            fixture.root(),
1859            &sibling,
1860            &version,
1861            &sibling_material,
1862        )
1863        .unwrap();
1864
1865        replace_injected_projection_for_local_acceptance(
1866            fixture.root(),
1867            &temporary,
1868            &version,
1869            &first,
1870        )
1871        .unwrap();
1872        replace_injected_projection_for_local_acceptance(
1873            fixture.root(),
1874            &temporary,
1875            &version,
1876            &second,
1877        )
1878        .unwrap();
1879        let snapshot = InjectedSecretSnapshot::load_from_root_for_test(
1880            fixture.root(),
1881            [InjectedSecretRequirement::new(
1882                temporary.clone(),
1883                version.clone(),
1884            )],
1885        )
1886        .unwrap();
1887        snapshot
1888            .get(&temporary)
1889            .unwrap()
1890            .material()
1891            .expose(|value| assert_eq!(value, b"second-value"));
1892
1893        remove_injected_projection_for_local_acceptance(fixture.root(), &temporary).unwrap();
1894        assert!(!fixture.root().join("orcid-sandbox/client-secret").exists());
1895        assert!(fixture.root().join("runtime/private-key/value").is_file());
1896    }
1897
1898    #[test]
1899    fn temporary_acceptance_projection_refuses_symlinked_ancestors() {
1900        let fixture = InjectedFixture::new();
1901        let outside = std::env::temp_dir().join(format!("injected-outside-{}", Uuid::new_v4()));
1902        fs::create_dir_all(outside.join("client-secret")).unwrap();
1903        fs::write(outside.join("client-secret/value"), b"must-survive").unwrap();
1904        symlink(&outside, fixture.root().join("orcid-sandbox")).unwrap();
1905        let reference =
1906            InjectedSecretReference::from_str("injected://orcid-sandbox/client-secret").unwrap();
1907
1908        assert!(
1909            remove_injected_projection_for_local_acceptance(fixture.root(), &reference).is_err()
1910        );
1911        assert_eq!(
1912            fs::read(outside.join("client-secret/value")).unwrap(),
1913            b"must-survive"
1914        );
1915        fs::remove_file(fixture.root().join("orcid-sandbox")).unwrap();
1916        fs::remove_dir_all(outside).unwrap();
1917    }
1918
1919    #[test]
1920    fn canonical_references_reject_non_canonical_and_unbounded_forms() {
1921        for rejected in [
1922            "unknown://a",
1923            "MANAGED://a",
1924            "managed://",
1925            "managed:///a",
1926            "managed://a/",
1927            "managed://a//b",
1928            "managed://a/../b",
1929            "managed://a/./b",
1930            "managed://a%2fb",
1931            "managed://A",
1932            "managed://café",
1933            "managed://a_b",
1934            "managed://-a",
1935            "managed://a-",
1936            "managed://a--b",
1937            "managed://a?version=1",
1938            "managed://a#fragment",
1939            "managed://user@a",
1940            "managed://a:443",
1941            "managed://a\\b",
1942            "managed://a/b/c/d/e/f/g/h/i",
1943        ] {
1944            assert!(
1945                SecretReference::from_str(rejected).is_err(),
1946                "accepted {rejected}"
1947            );
1948        }
1949
1950        let long_segment = format!("managed://{}", "a".repeat(64));
1951        assert!(SecretReference::from_str(&long_segment).is_err());
1952
1953        let long_uri = format!(
1954            "managed://{}/{}/{}/{}/{}/{}/{}/{}",
1955            "a".repeat(63),
1956            "b".repeat(63),
1957            "c".repeat(63),
1958            "d".repeat(63),
1959            "e".repeat(63),
1960            "f".repeat(63),
1961            "g".repeat(63),
1962            "h".repeat(63),
1963        );
1964        assert!(long_uri.len() > 512);
1965        assert!(SecretReference::from_str(&long_uri).is_err());
1966    }
1967
1968    #[test]
1969    fn typed_references_reject_the_other_secret_tier() {
1970        assert!(ManagedSecretReference::from_str("injected://a").is_err());
1971        assert!(InjectedSecretReference::from_str("managed://a").is_err());
1972    }
1973
1974    #[test]
1975    fn reference_and_requirement_serde_shapes_are_closed_and_canonical() {
1976        let reference = InjectedSecretReference::from_str("injected://oauth/key").unwrap();
1977        assert_eq!(
1978            serde_json::to_string(&reference).unwrap(),
1979            "\"injected://oauth/key\""
1980        );
1981        let requirement: InjectedSecretRequirement =
1982            serde_json::from_str(r#"{"uri":"injected://oauth/key","expected_version":"v1"}"#)
1983                .unwrap();
1984        assert_eq!(requirement.reference(), &reference);
1985        assert!(
1986            serde_json::from_str::<InjectedSecretRequirement>(
1987                r#"{"uri":"injected://oauth/key","expected_version":"v1","value":"canary"}"#,
1988            )
1989            .is_err()
1990        );
1991    }
1992
1993    #[test]
1994    fn injected_versions_accept_defined_forms_and_boundaries() {
1995        for accepted in [
1996            "a",
1997            "0",
1998            "2026-07-01",
1999            "v2.1.0",
2000            "01234567-89ab-cdef-0123-456789abcdef",
2001            &"a".repeat(64),
2002        ] {
2003            let version = InjectedSecretVersion::from_str(accepted).unwrap();
2004            assert_eq!(version.as_str(), accepted);
2005        }
2006    }
2007
2008    #[test]
2009    fn injected_versions_reject_non_canonical_forms() {
2010        for rejected in [
2011            "", "A", "café", ".v1", "v1.", "v1..0", "v1-_0", "v1/0", "v1 0", "v1\n",
2012        ] {
2013            assert!(
2014                InjectedSecretVersion::from_str(rejected).is_err(),
2015                "accepted {rejected:?}"
2016            );
2017        }
2018        assert!(InjectedSecretVersion::from_str(&"a".repeat(65)).is_err());
2019    }
2020
2021    #[test]
2022    fn secret_material_is_bounded_and_redacted() {
2023        let canary = b"canary-client-secret-value".to_vec();
2024        let material = SecretMaterial::try_from(canary.clone()).unwrap();
2025        assert!(material.expose(|value| value == canary));
2026        let debug = format!("{material:?}");
2027        assert!(debug.contains(ahri_tre_security::REDACTED_VALUE));
2028        assert!(!debug.contains("canary-client-secret-value"));
2029
2030        assert!(SecretMaterial::try_from(vec![0; MAX_SECRET_MATERIAL_BYTES]).is_ok());
2031        assert!(SecretMaterial::try_from(vec![0; MAX_SECRET_MATERIAL_BYTES + 1]).is_err());
2032    }
2033
2034    #[test]
2035    fn value_readability_uses_the_effective_posix_permission_class() {
2036        assert!(permission_class_can_read(0o440, 0, 2000, 1000, &[2000]));
2037        assert!(!permission_class_can_read(0o040, 1000, 2000, 1000, &[2000]));
2038        assert!(!permission_class_can_read(0o400, 0, 2000, 1000, &[2000]));
2039    }
2040
2041    #[test]
2042    fn injected_snapshot_resolves_material_and_exact_version() {
2043        let fixture = InjectedFixture::new();
2044        fixture.project(
2045            "injected://oauth/orcid/client-secret",
2046            b"canary-client-secret-value",
2047            "2026-07-01",
2048        );
2049        let requirement = requirement("injected://oauth/orcid/client-secret", "2026-07-01");
2050
2051        let snapshot =
2052            InjectedSecretSnapshot::load_from_root_for_test(fixture.root(), [requirement.clone()])
2053                .unwrap();
2054        let resolved = snapshot.get(requirement.reference()).unwrap();
2055
2056        assert_eq!(resolved.reference(), requirement.reference());
2057        assert_eq!(resolved.version().as_str(), "2026-07-01");
2058        assert!(
2059            resolved
2060                .material()
2061                .expose(|value| { value == b"canary-client-secret-value" })
2062        );
2063        assert!(!format!("{resolved:?}").contains("canary-client-secret-value"));
2064    }
2065
2066    #[test]
2067    fn injected_snapshot_follows_only_contained_projection_links() {
2068        let fixture = InjectedFixture::new();
2069        let projection =
2070            fixture.project_linked("injected://tls/runtime-key", b"contained-canary", "v1");
2071        let requirement = requirement("injected://tls/runtime-key", "v1");
2072        let snapshot =
2073            InjectedSecretSnapshot::load_from_root_for_test(fixture.root(), [requirement.clone()])
2074                .unwrap();
2075        assert!(
2076            snapshot
2077                .get(requirement.reference())
2078                .unwrap()
2079                .material()
2080                .expose(|value| value == b"contained-canary")
2081        );
2082
2083        let outside_value = fixture.base().join("outside-value");
2084        fs::write(&outside_value, b"outside-canary").unwrap();
2085        fs::set_permissions(&outside_value, fs::Permissions::from_mode(0o400)).unwrap();
2086        fs::remove_file(projection.join("value")).unwrap();
2087        symlink(&outside_value, projection.join("value")).unwrap();
2088
2089        let error = InjectedSecretSnapshot::load_from_root_for_test(fixture.root(), [requirement])
2090            .unwrap_err();
2091        assert_eq!(error.kind(), InjectedSecretErrorKind::EscapesRoot);
2092        assert!(!format!("{error:?}").contains(outside_value.to_string_lossy().as_ref()));
2093    }
2094
2095    #[test]
2096    fn injected_snapshot_does_not_reopen_replaced_projection_files() {
2097        let fixture = InjectedFixture::new();
2098        let projection = fixture.project(
2099            "injected://oauth/orcid/client-secret",
2100            b"generation-one",
2101            "v1",
2102        );
2103        let requirement = requirement("injected://oauth/orcid/client-secret", "v1");
2104        let snapshot =
2105            InjectedSecretSnapshot::load_from_root_for_test(fixture.root(), [requirement.clone()])
2106                .unwrap();
2107
2108        write_protected(&projection.join("value"), b"generation-two", 0o400);
2109        write_protected(&projection.join("version"), b"v2", 0o444);
2110
2111        let resolved = snapshot.get(requirement.reference()).unwrap();
2112        assert_eq!(resolved.version().as_str(), "v1");
2113        assert!(
2114            resolved
2115                .material()
2116                .expose(|value| value == b"generation-one")
2117        );
2118    }
2119
2120    #[test]
2121    fn injected_snapshot_rejects_value_and_version_from_different_generations() {
2122        let fixture = InjectedFixture::new();
2123        let projection = fixture.project_linked_generation(
2124            "injected://tls/runtime-key",
2125            "generation-one",
2126            b"generation-one-value",
2127            "v1",
2128        );
2129        fixture.create_generation("generation-two", b"generation-two-value", "v2");
2130        let requirement = requirement("injected://tls/runtime-key", "v1");
2131        replace_projection_link(&projection, "generation-two", "value");
2132
2133        let error = InjectedSecretSnapshot::load_from_root_for_test(fixture.root(), [requirement])
2134            .unwrap_err();
2135
2136        assert_eq!(error.kind(), InjectedSecretErrorKind::ChangedDuringSnapshot);
2137    }
2138
2139    #[test]
2140    fn injected_snapshot_rejects_version_size_type_and_permission_violations() {
2141        let cases = [
2142            (
2143                Violation::VersionMismatch,
2144                InjectedSecretErrorKind::VersionMismatch,
2145            ),
2146            (
2147                Violation::MalformedVersion,
2148                InjectedSecretErrorKind::InvalidVersion,
2149            ),
2150            (Violation::OversizedValue, InjectedSecretErrorKind::TooLarge),
2151            (
2152                Violation::ValueIsDirectory,
2153                InjectedSecretErrorKind::NotRegularFile,
2154            ),
2155            (
2156                Violation::WritableValue,
2157                InjectedSecretErrorKind::UnsafePermissions,
2158            ),
2159            (
2160                Violation::WorldReadableValue,
2161                InjectedSecretErrorKind::UnsafePermissions,
2162            ),
2163            (
2164                Violation::WritableVersion,
2165                InjectedSecretErrorKind::UnsafePermissions,
2166            ),
2167            (
2168                Violation::WritableDirectory,
2169                InjectedSecretErrorKind::UnsafePermissions,
2170            ),
2171            (
2172                Violation::UnreadableValue,
2173                InjectedSecretErrorKind::Unreadable,
2174            ),
2175        ];
2176
2177        for (violation, expected_kind) in cases {
2178            let fixture = InjectedFixture::new();
2179            let projection = fixture.project("injected://service/key", b"value", "v1");
2180            violation.apply(&projection);
2181            let error = InjectedSecretSnapshot::load_from_root_for_test(
2182                fixture.root(),
2183                [requirement("injected://service/key", "v1")],
2184            )
2185            .unwrap_err();
2186            assert_eq!(error.kind(), expected_kind, "case {violation:?}");
2187            assert!(!format!("{error:?}").contains("value"));
2188        }
2189    }
2190
2191    #[test]
2192    fn injected_snapshot_rejects_missing_duplicate_and_unsafe_root_inputs() {
2193        let fixture = InjectedFixture::new();
2194        let required = requirement("injected://service/key", "v1");
2195        let missing =
2196            InjectedSecretSnapshot::load_from_root_for_test(fixture.root(), [required.clone()])
2197                .unwrap_err();
2198        assert_eq!(missing.kind(), InjectedSecretErrorKind::Missing);
2199
2200        fixture.project("injected://service/key", b"secret", "v1");
2201        let duplicate = InjectedSecretSnapshot::load_from_root_for_test(
2202            fixture.root(),
2203            [required.clone(), required],
2204        )
2205        .unwrap_err();
2206        assert_eq!(
2207            duplicate.kind(),
2208            InjectedSecretErrorKind::DuplicateReference
2209        );
2210
2211        fs::set_permissions(fixture.root(), fs::Permissions::from_mode(0o775)).unwrap();
2212        let unsafe_root = InjectedSecretSnapshot::load_from_root_for_test(
2213            fixture.root(),
2214            [requirement("injected://service/key", "v1")],
2215        )
2216        .unwrap_err();
2217        assert_eq!(
2218            unsafe_root.kind(),
2219            InjectedSecretErrorKind::UnsafePermissions
2220        );
2221        assert!(unsafe_root.reference().is_none());
2222    }
2223
2224    #[test]
2225    fn root_identity_loading_reuses_contained_injected_projection_rules() {
2226        let fixture = InjectedFixture::new();
2227        fixture.project_linked(
2228            "injected://ahri-tre/root-identity",
2229            b"AGE-SECRET-KEY-TEST",
2230            "v1",
2231        );
2232
2233        let material = crate::load_root_identity_material_from_root(fixture.root()).unwrap();
2234        material.expose(|value| assert_eq!(value, b"AGE-SECRET-KEY-TEST"));
2235    }
2236
2237    fn requirement(uri: &str, version: &str) -> InjectedSecretRequirement {
2238        InjectedSecretRequirement::new(
2239            InjectedSecretReference::from_str(uri).unwrap(),
2240            InjectedSecretVersion::from_str(version).unwrap(),
2241        )
2242    }
2243
2244    static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(1);
2245
2246    struct InjectedFixture {
2247        base: PathBuf,
2248        root: PathBuf,
2249    }
2250
2251    impl InjectedFixture {
2252        fn new() -> Self {
2253            let id = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed);
2254            let base =
2255                std::env::temp_dir().join(format!("ahri-tre-secrets-{}-{id}", std::process::id()));
2256            fs::create_dir(&base).unwrap();
2257            let root = base.join("run-secrets");
2258            fs::create_dir(&root).unwrap();
2259            Self { base, root }
2260        }
2261
2262        fn base(&self) -> &Path {
2263            &self.base
2264        }
2265
2266        fn root(&self) -> &Path {
2267            &self.root
2268        }
2269
2270        fn project(&self, uri: &str, value: &[u8], version: &str) -> PathBuf {
2271            let reference = InjectedSecretReference::from_str(uri).unwrap();
2272            let directory = reference
2273                .segments()
2274                .fold(self.root.clone(), |path, segment| path.join(segment));
2275            fs::create_dir_all(&directory).unwrap();
2276            write_protected(&directory.join("value"), value, 0o400);
2277            write_protected(&directory.join("version"), version.as_bytes(), 0o444);
2278            directory
2279        }
2280
2281        fn project_linked(&self, uri: &str, value: &[u8], version: &str) -> PathBuf {
2282            let projection = self.project(uri, b"placeholder", "placeholder");
2283            let data = self.root.join("projection-data");
2284            fs::create_dir(&data).unwrap();
2285            write_protected(&data.join("value"), value, 0o400);
2286            write_protected(&data.join("version"), version.as_bytes(), 0o444);
2287            fs::remove_file(projection.join("value")).unwrap();
2288            fs::remove_file(projection.join("version")).unwrap();
2289            let relative_data = Path::new("../../projection-data");
2290            symlink(relative_data.join("value"), projection.join("value")).unwrap();
2291            symlink(relative_data.join("version"), projection.join("version")).unwrap();
2292            projection
2293        }
2294
2295        fn project_linked_generation(
2296            &self,
2297            uri: &str,
2298            generation: &str,
2299            value: &[u8],
2300            version: &str,
2301        ) -> PathBuf {
2302            let projection = self.project(uri, b"placeholder", "placeholder");
2303            self.create_generation(generation, value, version);
2304            replace_projection_links(&projection, generation);
2305            projection
2306        }
2307
2308        fn create_generation(&self, generation: &str, value: &[u8], version: &str) {
2309            let data = self.root.join(generation);
2310            fs::create_dir(&data).unwrap();
2311            write_protected(&data.join("value"), value, 0o400);
2312            write_protected(&data.join("version"), version.as_bytes(), 0o444);
2313        }
2314    }
2315
2316    impl Drop for InjectedFixture {
2317        fn drop(&mut self) {
2318            fs::remove_dir_all(&self.base).unwrap();
2319        }
2320    }
2321
2322    fn write_protected(path: &Path, value: &[u8], mode: u32) {
2323        if path.exists() {
2324            fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
2325        }
2326        fs::write(path, value).unwrap();
2327        fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap();
2328    }
2329
2330    fn replace_projection_links(projection: &Path, generation: &str) {
2331        for name in ["value", "version"] {
2332            replace_projection_link(projection, generation, name);
2333        }
2334    }
2335
2336    fn replace_projection_link(projection: &Path, generation: &str, name: &str) {
2337        let path = projection.join(name);
2338        if path.exists() || path.symlink_metadata().is_ok() {
2339            fs::remove_file(&path).unwrap();
2340        }
2341        symlink(Path::new("../..").join(generation).join(name), path).unwrap();
2342    }
2343
2344    #[derive(Debug, Clone, Copy)]
2345    enum Violation {
2346        VersionMismatch,
2347        MalformedVersion,
2348        OversizedValue,
2349        ValueIsDirectory,
2350        WritableValue,
2351        WorldReadableValue,
2352        WritableVersion,
2353        WritableDirectory,
2354        UnreadableValue,
2355    }
2356
2357    impl Violation {
2358        fn apply(self, projection: &Path) {
2359            match self {
2360                Self::VersionMismatch => write_protected(&projection.join("version"), b"v2", 0o444),
2361                Self::MalformedVersion => {
2362                    write_protected(&projection.join("version"), b"V1", 0o444)
2363                }
2364                Self::OversizedValue => write_protected(
2365                    &projection.join("value"),
2366                    &vec![b'x'; MAX_SECRET_MATERIAL_BYTES + 1],
2367                    0o400,
2368                ),
2369                Self::ValueIsDirectory => {
2370                    fs::remove_file(projection.join("value")).unwrap();
2371                    fs::create_dir(projection.join("value")).unwrap();
2372                }
2373                Self::WritableValue => {
2374                    fs::set_permissions(
2375                        projection.join("value"),
2376                        fs::Permissions::from_mode(0o600),
2377                    )
2378                    .unwrap();
2379                }
2380                Self::WorldReadableValue => {
2381                    fs::set_permissions(
2382                        projection.join("value"),
2383                        fs::Permissions::from_mode(0o404),
2384                    )
2385                    .unwrap();
2386                }
2387                Self::WritableVersion => {
2388                    fs::set_permissions(
2389                        projection.join("version"),
2390                        fs::Permissions::from_mode(0o464),
2391                    )
2392                    .unwrap();
2393                }
2394                Self::WritableDirectory => {
2395                    fs::set_permissions(projection, fs::Permissions::from_mode(0o775)).unwrap();
2396                }
2397                Self::UnreadableValue => {
2398                    fs::set_permissions(
2399                        projection.join("value"),
2400                        fs::Permissions::from_mode(0o000),
2401                    )
2402                    .unwrap();
2403                }
2404            }
2405        }
2406    }
2407}