1mod 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#[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
47pub const MAX_SECRET_REFERENCE_BYTES: usize = 512;
49pub const MAX_SECRET_MATERIAL_BYTES: usize = 64 * 1024;
51pub const INJECTED_SECRET_ROOT: &str = "/run/secrets";
53
54const MANAGED_SCHEME: &str = "managed://";
55const INJECTED_SCHEME: &str = "injected://";
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub enum SecretTier {
60 Managed,
61 Injected,
62}
63
64#[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#[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#[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#[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
353pub 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
370pub 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#[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
421pub 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
430pub 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
443pub 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
520#[error("Local provider credential could not be initialized")]
521pub struct LocalProviderCredentialError;
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
525#[error("Local root identity could not be initialized")]
526pub struct LocalRootIdentityProvisionError;
527
528pub 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
554pub 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
595pub 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
616pub 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
629pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
702#[error("Injected Secret projection could not be initialized")]
703pub struct InjectedSecretProvisionError;
704
705#[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
719pub 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#[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
825pub 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
857pub struct InjectedSecretSnapshot {
859 entries: BTreeMap<InjectedSecretReference, ResolvedInjectedSecret>,
860}
861
862impl InjectedSecretSnapshot {
863 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 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#[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#[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 ¤t,
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 ¤t_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 let effective_uid = unsafe { libc::geteuid() };
1557 let effective_gid = unsafe { libc::getegid() };
1559 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 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}