Skip to main content

rocketmq_security_api/
secure_deployment.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Pure profile resolution and readiness contracts for secure deployments.
16
17use std::env;
18use std::fmt;
19use std::fs::File;
20use std::net::SocketAddr;
21use std::path::Path;
22use std::path::PathBuf;
23use std::str::FromStr;
24use std::time::SystemTime;
25
26use crate::DeploymentProfile;
27use crate::SecurityContractViolation;
28use crate::SecurityOperation;
29use crate::SecurityProviderError;
30use crate::SecurityProviderFailure;
31
32/// The security profile env constant.
33pub const SECURITY_PROFILE_ENV: &str = "ROCKETMQ_SECURITY_PROFILE";
34/// The security trust anchor env constant.
35pub const SECURITY_TRUST_ANCHOR_ENV: &str = "ROCKETMQ_SECURITY_TRUST_ANCHOR";
36/// The security tls cert env constant.
37pub const SECURITY_TLS_CERT_ENV: &str = "ROCKETMQ_SECURITY_TLS_CERT";
38/// The security tls key env constant.
39pub const SECURITY_TLS_KEY_ENV: &str = "ROCKETMQ_SECURITY_TLS_KEY";
40/// The security secret provider env constant.
41pub const SECURITY_SECRET_PROVIDER_ENV: &str = "ROCKETMQ_SECURITY_SECRET_PROVIDER";
42/// The security admin identity env constant.
43pub const SECURITY_ADMIN_IDENTITY_ENV: &str = "ROCKETMQ_SECURITY_ADMIN_IDENTITY";
44/// The security request policy env constant.
45pub const SECURITY_REQUEST_POLICY_ENV: &str = "ROCKETMQ_SECURITY_REQUEST_POLICY";
46/// The mounted files secret provider constant.
47pub const MOUNTED_FILES_SECRET_PROVIDER: &str = "mounted-files";
48
49/// Explicit startup posture shared by every production service composition root.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum SecurityBootstrapProfile {
52    /// Represents the development insecure loopback case.
53    DevelopmentInsecureLoopback,
54    /// Represents the secure enforced case.
55    SecureEnforced,
56}
57
58impl SecurityBootstrapProfile {
59    /// Borrows this value as str.
60    pub const fn as_str(self) -> &'static str {
61        match self {
62            Self::DevelopmentInsecureLoopback => "development-insecure-loopback",
63            Self::SecureEnforced => "secure-enforced",
64        }
65    }
66}
67
68impl FromStr for SecurityBootstrapProfile {
69    type Err = SecurityContractViolation;
70
71    fn from_str(value: &str) -> Result<Self, Self::Err> {
72        match value.trim().to_ascii_lowercase().as_str() {
73            "development-insecure-loopback" => Ok(Self::DevelopmentInsecureLoopback),
74            "secure-enforced" => Ok(Self::SecureEnforced),
75            _ => Err(SecurityContractViolation::UnknownSecurityProfile),
76        }
77    }
78}
79
80/// Security material required before a service is allowed to bind listeners.
81#[derive(Clone)]
82pub struct SecurityBootstrapConfig {
83    profile: SecurityBootstrapProfile,
84    trust_anchor: Option<PathBuf>,
85    tls_certificate: Option<PathBuf>,
86    tls_private_key: Option<PathBuf>,
87    secret_provider: Option<String>,
88    admin_identity: Option<PathBuf>,
89    request_policy: Option<PathBuf>,
90}
91
92/// Resolved process-wide security bootstrap state.
93#[derive(Debug, Clone)]
94pub enum SecurityBootstrap {
95    /// Represents the disabled case.
96    Disabled,
97    /// Represents the enabled case.
98    Enabled(SecurityBootstrapConfig),
99}
100
101impl SecurityBootstrap {
102    /// Returns whether enabled.
103    pub const fn is_enabled(&self) -> bool {
104        matches!(self, Self::Enabled(_))
105    }
106
107    /// Reports whether the selected profile requires request authentication and authorization.
108    pub const fn requires_authentication(&self) -> bool {
109        matches!(self, Self::Enabled(config) if matches!(config.profile, SecurityBootstrapProfile::SecureEnforced))
110    }
111
112    /// Validates enabled security bootstrap or records that it was intentionally disabled.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`SecurityProviderError`] when an enabled profile is incomplete, unsafe, or unavailable.
117    pub fn validate(
118        &self,
119        listener_addresses: &[SocketAddr],
120    ) -> Result<SecurityBootstrapOutcome, SecurityProviderError> {
121        match self {
122            Self::Disabled => Ok(SecurityBootstrapOutcome::Disabled),
123            Self::Enabled(config) => config
124                .validate(listener_addresses)
125                .map(SecurityBootstrapOutcome::Validated),
126        }
127    }
128}
129
130#[derive(Default)]
131struct SecurityBootstrapEnvironment {
132    profile: Option<String>,
133    trust_anchor: Option<PathBuf>,
134    tls_certificate: Option<PathBuf>,
135    tls_private_key: Option<PathBuf>,
136    secret_provider: Option<String>,
137    admin_identity: Option<PathBuf>,
138    request_policy: Option<PathBuf>,
139}
140
141impl SecurityBootstrapEnvironment {
142    fn from_env() -> Result<Self, SecurityContractViolation> {
143        Ok(Self {
144            profile: optional_env(SECURITY_PROFILE_ENV)?,
145            trust_anchor: optional_path_env(SECURITY_TRUST_ANCHOR_ENV)?,
146            tls_certificate: optional_path_env(SECURITY_TLS_CERT_ENV)?,
147            tls_private_key: optional_path_env(SECURITY_TLS_KEY_ENV)?,
148            secret_provider: optional_env(SECURITY_SECRET_PROVIDER_ENV)?,
149            admin_identity: optional_path_env(SECURITY_ADMIN_IDENTITY_ENV)?,
150            request_policy: optional_path_env(SECURITY_REQUEST_POLICY_ENV)?,
151        })
152    }
153
154    fn resolve(self) -> Result<SecurityBootstrap, SecurityContractViolation> {
155        let Self {
156            profile,
157            trust_anchor,
158            tls_certificate,
159            tls_private_key,
160            secret_provider,
161            admin_identity,
162            request_policy,
163        } = self;
164        let Some(profile) = profile else {
165            if trust_anchor.is_some()
166                || tls_certificate.is_some()
167                || tls_private_key.is_some()
168                || secret_provider.is_some()
169                || admin_identity.is_some()
170                || request_policy.is_some()
171            {
172                return Err(SecurityContractViolation::BootstrapProfileRequired);
173            }
174            return Ok(SecurityBootstrap::Disabled);
175        };
176        let profile = profile.parse::<SecurityBootstrapProfile>()?;
177        Ok(SecurityBootstrap::Enabled(SecurityBootstrapConfig {
178            profile,
179            trust_anchor,
180            tls_certificate,
181            tls_private_key,
182            secret_provider,
183            admin_identity,
184            request_policy,
185        }))
186    }
187}
188
189impl fmt::Debug for SecurityBootstrapConfig {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        f.debug_struct("SecurityBootstrapConfig")
192            .field("profile", &self.profile)
193            .field("trust_anchor_configured", &self.trust_anchor.is_some())
194            .field("tls_certificate_configured", &self.tls_certificate.is_some())
195            .field("tls_private_key_configured", &self.tls_private_key.is_some())
196            .field("secret_provider_configured", &self.secret_provider.is_some())
197            .field("admin_identity_configured", &self.admin_identity.is_some())
198            .field("request_policy_configured", &self.request_policy.is_some())
199            .finish()
200    }
201}
202
203impl SecurityBootstrapConfig {
204    /// Creates a new `SecurityBootstrapConfig`.
205    pub const fn new(profile: SecurityBootstrapProfile) -> Self {
206        Self {
207            profile,
208            trust_anchor: None,
209            tls_certificate: None,
210            tls_private_key: None,
211            secret_provider: None,
212            admin_identity: None,
213            request_policy: None,
214        }
215    }
216
217    /// Loads the canonical bootstrap environment without exposing configured values in errors.
218    ///
219    /// Security bootstrap is disabled only when neither the profile nor any bootstrap field is
220    /// configured.
221    ///
222    /// # Errors
223    ///
224    /// Returns a typed error when fields are configured without a profile, the profile is
225    /// unknown, or any configured environment field is non-UTF-8.
226    pub fn from_env() -> Result<SecurityBootstrap, SecurityContractViolation> {
227        SecurityBootstrapEnvironment::from_env()?.resolve()
228    }
229
230    /// Sets trust anchor and returns the updated value.
231    pub fn with_trust_anchor(mut self, path: impl Into<PathBuf>) -> Self {
232        self.trust_anchor = Some(path.into());
233        self
234    }
235
236    /// Sets tls identity and returns the updated value.
237    pub fn with_tls_identity(mut self, certificate: impl Into<PathBuf>, private_key: impl Into<PathBuf>) -> Self {
238        self.tls_certificate = Some(certificate.into());
239        self.tls_private_key = Some(private_key.into());
240        self
241    }
242
243    /// Sets secret provider and returns the updated value.
244    pub fn with_secret_provider(mut self, provider: impl Into<String>) -> Self {
245        self.secret_provider = Some(provider.into());
246        self
247    }
248
249    /// Sets admin identity and returns the updated value.
250    pub fn with_admin_identity(mut self, path: impl Into<PathBuf>) -> Self {
251        self.admin_identity = Some(path.into());
252        self
253    }
254
255    /// Sets request policy and returns the updated value.
256    pub fn with_request_policy(mut self, path: impl Into<PathBuf>) -> Self {
257        self.request_policy = Some(path.into());
258        self
259    }
260
261    /// Validates all security prerequisites before the caller binds any listener.
262    ///
263    /// # Errors
264    ///
265    /// Secure mode fails closed for missing or unreadable material and unsupported providers.
266    /// Development mode fails closed when any supplied listener is not loopback.
267    pub fn validate(
268        &self,
269        listener_addresses: &[SocketAddr],
270    ) -> Result<ValidatedSecurityBootstrap, SecurityProviderError> {
271        match self.profile {
272            SecurityBootstrapProfile::DevelopmentInsecureLoopback => {
273                if listener_addresses.iter().any(|address| !address.ip().is_loopback()) {
274                    return Err(SecurityProviderError::contract(
275                        SecurityOperation::Validate,
276                        SecurityContractViolation::DevelopmentListenerNotLoopback,
277                    ));
278                }
279            }
280            SecurityBootstrapProfile::SecureEnforced => {
281                inspect_bootstrap_file(self.trust_anchor.as_deref(), SecurityBootstrapMaterial::TrustAnchor)?;
282                inspect_bootstrap_file(
283                    self.tls_certificate.as_deref(),
284                    SecurityBootstrapMaterial::TlsCertificate,
285                )?;
286                inspect_bootstrap_file(
287                    self.tls_private_key.as_deref(),
288                    SecurityBootstrapMaterial::TlsPrivateKey,
289                )?;
290                let provider = self
291                    .secret_provider
292                    .as_deref()
293                    .map(str::trim)
294                    .filter(|provider| !provider.is_empty())
295                    .ok_or(SecurityContractViolation::SecretProviderRequired)
296                    .map_err(|violation| SecurityProviderError::contract(SecurityOperation::Validate, violation))?;
297                if provider != MOUNTED_FILES_SECRET_PROVIDER {
298                    return Err(SecurityProviderError::contract(
299                        SecurityOperation::Validate,
300                        SecurityContractViolation::SecretProviderUnsupported,
301                    ));
302                }
303                inspect_bootstrap_file(self.admin_identity.as_deref(), SecurityBootstrapMaterial::AdminIdentity)?;
304                inspect_bootstrap_file(self.request_policy.as_deref(), SecurityBootstrapMaterial::RequestPolicy)?;
305            }
306        }
307
308        Ok(ValidatedSecurityBootstrap {
309            profile: self.profile,
310            listener_count: listener_addresses.len(),
311        })
312    }
313}
314
315/// Loads and validates the canonical process environment before listener bind.
316///
317/// # Errors
318///
319/// Returns [`SecurityProviderError`] for every incomplete, unsupported, unsafe, or unavailable profile.
320pub fn validate_security_bootstrap_from_env(
321    listener_addresses: &[SocketAddr],
322) -> Result<SecurityBootstrapOutcome, SecurityProviderError> {
323    SecurityBootstrapConfig::from_env()
324        .map_err(SecurityProviderError::from)?
325        .validate(listener_addresses)
326}
327
328/// Result of resolving and, when enabled, validating process security bootstrap.
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum SecurityBootstrapOutcome {
331    /// Represents the disabled case.
332    Disabled,
333    /// Represents the validated case.
334    Validated(ValidatedSecurityBootstrap),
335}
336
337/// Non-sensitive proof that canonical bootstrap completed before listener ownership begins.
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub struct ValidatedSecurityBootstrap {
340    profile: SecurityBootstrapProfile,
341    listener_count: usize,
342}
343
344impl ValidatedSecurityBootstrap {
345    /// Returns the profile.
346    pub const fn profile(self) -> SecurityBootstrapProfile {
347        self.profile
348    }
349
350    /// Returns the listener count.
351    pub const fn listener_count(self) -> usize {
352        self.listener_count
353    }
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357/// Identifies the security bootstrap material state.
358pub enum SecurityBootstrapMaterial {
359    /// Represents the trust anchor case.
360    TrustAnchor,
361    /// Represents the tls certificate case.
362    TlsCertificate,
363    /// Represents the tls private key case.
364    TlsPrivateKey,
365    /// Represents the admin identity case.
366    AdminIdentity,
367    /// Represents the request policy case.
368    RequestPolicy,
369}
370
371impl fmt::Display for SecurityBootstrapMaterial {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        f.write_str(match self {
374            Self::TrustAnchor => "trust anchor",
375            Self::TlsCertificate => "TLS certificate",
376            Self::TlsPrivateKey => "TLS private key",
377            Self::AdminIdentity => "administrator identity",
378            Self::RequestPolicy => "request policy",
379        })
380    }
381}
382
383fn optional_path_env(name: &'static str) -> Result<Option<PathBuf>, SecurityContractViolation> {
384    optional_env(name).map(|value| value.map(PathBuf::from))
385}
386
387fn optional_env(name: &'static str) -> Result<Option<String>, SecurityContractViolation> {
388    let Some(value) = env::var_os(name) else {
389        return Ok(None);
390    };
391    let value = value
392        .into_string()
393        .map_err(|_| SecurityContractViolation::BootstrapEnvironmentEncoding)?;
394    let value = value.trim();
395    Ok((!value.is_empty()).then(|| value.to_string()))
396}
397
398fn inspect_bootstrap_file(
399    path: Option<&Path>,
400    material: SecurityBootstrapMaterial,
401) -> Result<(), SecurityProviderError> {
402    let path = path
403        .ok_or(SecurityContractViolation::BootstrapMaterialRequired { material })
404        .map_err(|violation| SecurityProviderError::contract(SecurityOperation::Validate, violation))?;
405    let metadata = path.metadata().map_err(|source| {
406        SecurityProviderError::caused_by(
407            SecurityProviderFailure::Unavailable,
408            SecurityOperation::InspectBootstrapMaterial,
409            source,
410        )
411    })?;
412    if !metadata.is_file() {
413        return Err(SecurityProviderError::contract(
414            SecurityOperation::InspectBootstrapMaterial,
415            SecurityContractViolation::BootstrapMaterialNotRegularFile { material },
416        ));
417    }
418    let file = File::open(path).map_err(|source| {
419        SecurityProviderError::caused_by(
420            SecurityProviderFailure::Unavailable,
421            SecurityOperation::InspectBootstrapMaterial,
422            source,
423        )
424    })?;
425    let metadata = file.metadata().map_err(|source| {
426        SecurityProviderError::caused_by(
427            SecurityProviderFailure::OperationFailed,
428            SecurityOperation::InspectBootstrapMaterial,
429            source,
430        )
431    })?;
432    if !metadata.is_file() {
433        return Err(SecurityProviderError::contract(
434            SecurityOperation::InspectBootstrapMaterial,
435            SecurityContractViolation::BootstrapMaterialNotRegularFile { material },
436        ));
437    }
438    if metadata.len() == 0 {
439        return Err(SecurityProviderError::contract(
440            SecurityOperation::InspectBootstrapMaterial,
441            SecurityContractViolation::BootstrapMaterialEmpty { material },
442        ));
443    }
444    Ok(())
445}
446
447/// Whether configuration belongs to a newly created or already deployed installation.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum DeploymentOrigin {
450    /// Represents the new case.
451    New,
452    /// Represents the existing case.
453    Existing,
454}
455
456/// Profile input before the compatibility-preserving default is resolved.
457#[derive(Debug, Clone, Copy)]
458pub struct SecurityProfileSelection<'a> {
459    origin: DeploymentOrigin,
460    configured_profile: Option<&'a str>,
461}
462
463impl<'a> SecurityProfileSelection<'a> {
464    /// Creates a new `SecurityProfileSelection`.
465    pub const fn new(origin: DeploymentOrigin) -> Self {
466        Self {
467            origin,
468            configured_profile: None,
469        }
470    }
471
472    /// Sets configured profile and returns the updated value.
473    pub const fn with_configured_profile(mut self, profile: &'a str) -> Self {
474        self.configured_profile = Some(profile);
475        self
476    }
477}
478
479/// Migration action produced alongside the effective profile.
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum SecurityMigrationStatus {
482    /// Represents the not required case.
483    NotRequired,
484    /// Represents the compatibility profile must be persisted case.
485    CompatibilityProfileMustBePersisted,
486    /// Represents the migration to secure pending case.
487    MigrationToSecurePending,
488}
489
490/// Effective profile and an explicit compatibility migration report.
491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub struct SecurityProfileResolution {
493    profile: DeploymentProfile,
494    migration_status: SecurityMigrationStatus,
495}
496
497impl SecurityProfileResolution {
498    /// Returns the profile.
499    pub const fn profile(self) -> DeploymentProfile {
500        self.profile
501    }
502
503    /// Returns the migration status.
504    pub const fn migration_status(self) -> SecurityMigrationStatus {
505        self.migration_status
506    }
507}
508
509/// Resolves a profile without silently changing an identified existing deployment.
510///
511/// # Errors
512///
513/// Returns [`SecurityContractViolation::UnknownSecurityProfile`] for every non-empty unknown value.
514pub fn resolve_security_profile(
515    selection: SecurityProfileSelection<'_>,
516) -> Result<SecurityProfileResolution, SecurityContractViolation> {
517    let configured = selection
518        .configured_profile
519        .map(str::trim)
520        .filter(|profile| !profile.is_empty());
521    let profile = match configured {
522        Some(profile) => DeploymentProfile::from_str(profile)?,
523        None if selection.origin == DeploymentOrigin::New => DeploymentProfile::Secure,
524        None => DeploymentProfile::Compatibility,
525    };
526    let migration_status = match (selection.origin, configured, profile) {
527        (DeploymentOrigin::Existing, None, DeploymentProfile::Compatibility) => {
528            SecurityMigrationStatus::CompatibilityProfileMustBePersisted
529        }
530        (_, _, DeploymentProfile::Compatibility | DeploymentProfile::Development) => {
531            SecurityMigrationStatus::MigrationToSecurePending
532        }
533        _ => SecurityMigrationStatus::NotRequired,
534    };
535    Ok(SecurityProfileResolution {
536        profile,
537        migration_status,
538    })
539}
540
541/// Readiness inputs for the one-time-token bootstrap path.
542#[derive(Debug, Clone, Copy)]
543pub struct BootstrapReadinessView {
544    expires_at: SystemTime,
545    material_available: bool,
546    verified_tls_listener: bool,
547}
548
549impl BootstrapReadinessView {
550    /// Creates a new `BootstrapReadinessView`.
551    pub const fn new(expires_at: SystemTime) -> Self {
552        Self {
553            expires_at,
554            material_available: false,
555            verified_tls_listener: false,
556        }
557    }
558
559    /// Sets available material and returns the updated value.
560    pub const fn with_available_material(mut self) -> Self {
561        self.material_available = true;
562        self
563    }
564
565    /// Sets verified tls listener and returns the updated value.
566    pub const fn with_verified_tls_listener(mut self) -> Self {
567        self.verified_tls_listener = true;
568        self
569    }
570}
571
572/// Complete pure input used before a service binds its data or management listeners.
573#[derive(Debug, Clone, Copy)]
574pub struct DeploymentSecurityConfigView<'a> {
575    profile: SecurityProfileSelection<'a>,
576    trust_anchor: Option<&'a Path>,
577    secret_provider_registered: bool,
578    provisioned_admin_identity: bool,
579    one_time_bootstrap: Option<BootstrapReadinessView>,
580    insecure_downgrade: bool,
581}
582
583impl<'a> DeploymentSecurityConfigView<'a> {
584    /// Creates a new `DeploymentSecurityConfigView`.
585    pub const fn new(profile: SecurityProfileSelection<'a>) -> Self {
586        Self {
587            profile,
588            trust_anchor: None,
589            secret_provider_registered: false,
590            provisioned_admin_identity: false,
591            one_time_bootstrap: None,
592            insecure_downgrade: false,
593        }
594    }
595
596    /// Sets trust anchor and returns the updated value.
597    pub const fn with_trust_anchor(mut self, path: &'a Path) -> Self {
598        self.trust_anchor = Some(path);
599        self
600    }
601
602    /// Sets registered secret provider and returns the updated value.
603    pub const fn with_registered_secret_provider(mut self) -> Self {
604        self.secret_provider_registered = true;
605        self
606    }
607
608    /// Sets provisioned admin identity and returns the updated value.
609    pub const fn with_provisioned_admin_identity(mut self) -> Self {
610        self.provisioned_admin_identity = true;
611        self
612    }
613
614    /// Sets one time bootstrap and returns the updated value.
615    pub const fn with_one_time_bootstrap(mut self, bootstrap: BootstrapReadinessView) -> Self {
616        self.one_time_bootstrap = Some(bootstrap);
617        self
618    }
619
620    /// Sets insecure downgrade and returns the updated value.
621    pub const fn with_insecure_downgrade(mut self) -> Self {
622        self.insecure_downgrade = true;
623        self
624    }
625}
626
627/// Stable reasons a resolved deployment is not safe to serve traffic.
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
629pub enum DeploymentSecurityFailure {
630    /// Represents the missing trust anchor case.
631    MissingTrustAnchor,
632    /// Represents the trust anchor unavailable case.
633    TrustAnchorUnavailable,
634    /// Represents the trust anchor not regular file case.
635    TrustAnchorNotRegularFile,
636    /// Represents the missing secret provider case.
637    MissingSecretProvider,
638    /// Represents the missing identity bootstrap case.
639    MissingIdentityBootstrap,
640    /// Represents the multiple identity bootstrap sources case.
641    MultipleIdentityBootstrapSources,
642    /// Represents the missing bootstrap material case.
643    MissingBootstrapMaterial,
644    /// Represents the expired bootstrap case.
645    ExpiredBootstrap,
646    /// Represents the bootstrap listener not tls case.
647    BootstrapListenerNotTls,
648    /// Represents the insecure downgrade case.
649    InsecureDowngrade,
650}
651
652/// Resolved profile, migration report, and fail-closed readiness failures.
653#[derive(Debug, Clone, PartialEq, Eq)]
654pub struct DeploymentSecurityReport {
655    resolution: SecurityProfileResolution,
656    failures: Vec<DeploymentSecurityFailure>,
657}
658
659impl DeploymentSecurityReport {
660    /// Returns whether ready.
661    pub fn is_ready(&self) -> bool {
662        self.failures.is_empty()
663    }
664
665    /// Returns the resolution.
666    pub const fn resolution(&self) -> SecurityProfileResolution {
667        self.resolution
668    }
669
670    /// Returns the failures.
671    pub fn failures(&self) -> &[DeploymentSecurityFailure] {
672        &self.failures
673    }
674}
675
676/// Resolves the effective profile and validates the secure identity bootstrap prerequisites.
677///
678/// # Errors
679///
680/// Returns [`SecurityContractViolation::UnknownSecurityProfile`] instead of choosing a fallback.
681pub fn validate_deployment_security(
682    view: DeploymentSecurityConfigView<'_>,
683    now: SystemTime,
684) -> Result<DeploymentSecurityReport, SecurityContractViolation> {
685    let resolution = resolve_security_profile(view.profile)?;
686    let mut failures = Vec::new();
687    if resolution.profile == DeploymentProfile::Secure {
688        inspect_trust_anchor(view.trust_anchor, &mut failures);
689        if !view.secret_provider_registered {
690            failures.push(DeploymentSecurityFailure::MissingSecretProvider);
691        }
692        match (view.provisioned_admin_identity, view.one_time_bootstrap) {
693            (false, None) => failures.push(DeploymentSecurityFailure::MissingIdentityBootstrap),
694            (true, Some(_)) => failures.push(DeploymentSecurityFailure::MultipleIdentityBootstrapSources),
695            _ => {}
696        }
697        if let Some(bootstrap) = view.one_time_bootstrap {
698            if !bootstrap.material_available {
699                failures.push(DeploymentSecurityFailure::MissingBootstrapMaterial);
700            }
701            if bootstrap.expires_at <= now {
702                failures.push(DeploymentSecurityFailure::ExpiredBootstrap);
703            }
704            if !bootstrap.verified_tls_listener {
705                failures.push(DeploymentSecurityFailure::BootstrapListenerNotTls);
706            }
707        }
708        if view.insecure_downgrade {
709            failures.push(DeploymentSecurityFailure::InsecureDowngrade);
710        }
711    }
712    Ok(DeploymentSecurityReport { resolution, failures })
713}
714
715fn inspect_trust_anchor(path: Option<&Path>, failures: &mut Vec<DeploymentSecurityFailure>) {
716    let Some(path) = path else {
717        failures.push(DeploymentSecurityFailure::MissingTrustAnchor);
718        return;
719    };
720    let Ok(path_metadata) = path.metadata() else {
721        failures.push(DeploymentSecurityFailure::TrustAnchorUnavailable);
722        return;
723    };
724    if !path_metadata.is_file() {
725        failures.push(DeploymentSecurityFailure::TrustAnchorNotRegularFile);
726        return;
727    }
728    let Ok(file) = File::open(path) else {
729        failures.push(DeploymentSecurityFailure::TrustAnchorUnavailable);
730        return;
731    };
732    match file.metadata() {
733        Ok(metadata) if metadata.is_file() => {}
734        Ok(_) => failures.push(DeploymentSecurityFailure::TrustAnchorNotRegularFile),
735        Err(_) => failures.push(DeploymentSecurityFailure::TrustAnchorUnavailable),
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use std::time::Duration;
742
743    use tempfile::NamedTempFile;
744
745    use super::*;
746
747    fn contract_source(error: &SecurityProviderError) -> SecurityContractViolation {
748        *error
749            .source()
750            .and_then(|source| source.downcast_ref::<SecurityContractViolation>())
751            .expect("provider error should retain its contract source")
752    }
753
754    #[test]
755    fn security_bootstrap_environment_without_profile_or_fields_is_disabled() {
756        let security_bootstrap = SecurityBootstrapEnvironment::default()
757            .resolve()
758            .expect("empty security bootstrap environment should resolve");
759
760        assert!(matches!(security_bootstrap, SecurityBootstrap::Disabled));
761    }
762
763    #[test]
764    fn security_bootstrap_environment_rejects_every_field_without_profile() {
765        let configured_environments = [
766            SecurityBootstrapEnvironment {
767                trust_anchor: Some(PathBuf::from("ca.crt")),
768                ..SecurityBootstrapEnvironment::default()
769            },
770            SecurityBootstrapEnvironment {
771                tls_certificate: Some(PathBuf::from("tls.crt")),
772                ..SecurityBootstrapEnvironment::default()
773            },
774            SecurityBootstrapEnvironment {
775                tls_private_key: Some(PathBuf::from("tls.key")),
776                ..SecurityBootstrapEnvironment::default()
777            },
778            SecurityBootstrapEnvironment {
779                secret_provider: Some(MOUNTED_FILES_SECRET_PROVIDER.to_string()),
780                ..SecurityBootstrapEnvironment::default()
781            },
782            SecurityBootstrapEnvironment {
783                admin_identity: Some(PathBuf::from("admin.identity")),
784                ..SecurityBootstrapEnvironment::default()
785            },
786            SecurityBootstrapEnvironment {
787                request_policy: Some(PathBuf::from("request-policy.json")),
788                ..SecurityBootstrapEnvironment::default()
789            },
790        ];
791
792        for environment in configured_environments {
793            assert_eq!(
794                environment
795                    .resolve()
796                    .expect_err("security fields without a profile must fail"),
797                SecurityContractViolation::BootstrapProfileRequired
798            );
799        }
800    }
801
802    #[test]
803    fn security_bootstrap_environment_enables_known_profiles_and_rejects_unknown_profile() {
804        let development = SecurityBootstrapEnvironment {
805            profile: Some("development-insecure-loopback".to_string()),
806            ..SecurityBootstrapEnvironment::default()
807        }
808        .resolve()
809        .expect("development profile should resolve");
810        let SecurityBootstrap::Enabled(development) = development else {
811            panic!("development profile must enable security bootstrap");
812        };
813        assert_eq!(
814            development
815                .validate(&[])
816                .expect("development profile without listeners should validate")
817                .profile(),
818            SecurityBootstrapProfile::DevelopmentInsecureLoopback
819        );
820
821        let secure = SecurityBootstrapEnvironment {
822            profile: Some("secure-enforced".to_string()),
823            ..SecurityBootstrapEnvironment::default()
824        }
825        .resolve()
826        .expect("secure profile should resolve");
827        let SecurityBootstrap::Enabled(secure) = secure else {
828            panic!("secure profile must enable security bootstrap");
829        };
830        let error = secure
831            .validate(&[])
832            .expect_err("secure profile without material must fail closed");
833        assert_eq!(
834            contract_source(&error),
835            SecurityContractViolation::BootstrapMaterialRequired {
836                material: SecurityBootstrapMaterial::TrustAnchor,
837            }
838        );
839
840        assert_eq!(
841            SecurityBootstrapEnvironment {
842                profile: Some("unknown".to_string()),
843                ..SecurityBootstrapEnvironment::default()
844            }
845            .resolve()
846            .expect_err("unknown profile must fail closed"),
847            SecurityContractViolation::UnknownSecurityProfile
848        );
849    }
850
851    #[test]
852    fn new_defaults_secure_while_existing_defaults_compatibility_with_report() {
853        let new = resolve_security_profile(SecurityProfileSelection::new(DeploymentOrigin::New)).unwrap();
854        assert_eq!(new.profile(), DeploymentProfile::Secure);
855        assert_eq!(new.migration_status(), SecurityMigrationStatus::NotRequired);
856
857        let existing = resolve_security_profile(SecurityProfileSelection::new(DeploymentOrigin::Existing)).unwrap();
858        assert_eq!(existing.profile(), DeploymentProfile::Compatibility);
859        assert_eq!(
860            existing.migration_status(),
861            SecurityMigrationStatus::CompatibilityProfileMustBePersisted
862        );
863    }
864
865    #[test]
866    fn unknown_profile_never_downgrades() {
867        let selection = SecurityProfileSelection::new(DeploymentOrigin::Existing).with_configured_profile("unknown");
868        assert_eq!(
869            resolve_security_profile(selection).unwrap_err(),
870            SecurityContractViolation::UnknownSecurityProfile
871        );
872    }
873
874    #[test]
875    fn secure_provisioned_identity_requires_trust_and_provider() {
876        let now = SystemTime::now();
877        let missing = validate_deployment_security(
878            DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
879                .with_provisioned_admin_identity(),
880            now,
881        )
882        .unwrap();
883        assert_eq!(
884            missing.failures(),
885            &[
886                DeploymentSecurityFailure::MissingTrustAnchor,
887                DeploymentSecurityFailure::MissingSecretProvider,
888            ]
889        );
890
891        let trust_anchor = NamedTempFile::new().unwrap();
892        let ready = validate_deployment_security(
893            DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
894                .with_trust_anchor(trust_anchor.path())
895                .with_registered_secret_provider()
896                .with_provisioned_admin_identity(),
897            now,
898        )
899        .unwrap();
900        assert!(ready.is_ready());
901    }
902
903    #[test]
904    fn token_bootstrap_requires_one_source_material_expiry_and_tls() {
905        let now = SystemTime::now();
906        let trust_anchor = NamedTempFile::new().unwrap();
907        let invalid = validate_deployment_security(
908            DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
909                .with_trust_anchor(trust_anchor.path())
910                .with_registered_secret_provider()
911                .with_provisioned_admin_identity()
912                .with_one_time_bootstrap(BootstrapReadinessView::new(now - Duration::from_secs(1))),
913            now,
914        )
915        .unwrap();
916        assert_eq!(
917            invalid.failures(),
918            &[
919                DeploymentSecurityFailure::MultipleIdentityBootstrapSources,
920                DeploymentSecurityFailure::MissingBootstrapMaterial,
921                DeploymentSecurityFailure::ExpiredBootstrap,
922                DeploymentSecurityFailure::BootstrapListenerNotTls,
923            ]
924        );
925
926        let ready = validate_deployment_security(
927            DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::New))
928                .with_trust_anchor(trust_anchor.path())
929                .with_registered_secret_provider()
930                .with_one_time_bootstrap(
931                    BootstrapReadinessView::new(now + Duration::from_secs(60))
932                        .with_available_material()
933                        .with_verified_tls_listener(),
934                ),
935            now,
936        )
937        .unwrap();
938        assert!(ready.is_ready());
939    }
940
941    #[test]
942    fn compatibility_is_ready_without_secure_material_but_reports_migration() {
943        let report = validate_deployment_security(
944            DeploymentSecurityConfigView::new(SecurityProfileSelection::new(DeploymentOrigin::Existing)),
945            SystemTime::now(),
946        )
947        .unwrap();
948        assert!(report.is_ready());
949        assert_eq!(
950            report.resolution().migration_status(),
951            SecurityMigrationStatus::CompatibilityProfileMustBePersisted
952        );
953    }
954}