lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

provider.rs (10489B)


      1 //! Secret-provider contracts and capability selection.
      2 
      3 use crate::error::{Error, PolicyRequirement};
      4 use crate::id::BackendKind;
      5 use crate::wrapping::KeyWrapping;
      6 
      7 /// Secret residency required by a host.
      8 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
      9 #[non_exhaustive]
     10 pub enum ResidencyPolicy {
     11     /// The provider may use its normal host-selected residency.
     12     Any,
     13     /// The provider must keep material local to the current device.
     14     DeviceLocal,
     15 }
     16 
     17 /// User-presence behavior required by a host.
     18 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     19 #[non_exhaustive]
     20 pub enum UserPresencePolicy {
     21     /// User presence is not required for the operation.
     22     NotRequired,
     23     /// The provider must require user presence.
     24     Required,
     25 }
     26 
     27 /// Hardware-backed behavior requested by a host.
     28 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     29 #[non_exhaustive]
     30 pub enum HardwarePolicy {
     31     /// Hardware-backed protection is not required.
     32     Any,
     33     /// Prefer hardware-backed protection when available.
     34     PreferHardwareBacked,
     35     /// Hardware-backed protection is mandatory.
     36     RequireHardwareBacked,
     37 }
     38 
     39 /// Provider residency support.
     40 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     41 #[non_exhaustive]
     42 pub enum ResidencySupport {
     43     /// Volatile process-local storage.
     44     Volatile,
     45     /// Persistent storage associated with the host user profile.
     46     UserProfile,
     47     /// Persistent storage restricted to the current device.
     48     DeviceLocal,
     49 }
     50 
     51 /// Whether a provider supports an optional security property.
     52 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     53 #[non_exhaustive]
     54 pub enum CapabilitySupport {
     55     /// The property is unavailable.
     56     Unavailable,
     57     /// The property is supported.
     58     Supported,
     59 }
     60 
     61 /// Explicit host security requirements for provider selection.
     62 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     63 pub struct AccessPolicy {
     64     residency: ResidencyPolicy,
     65     user_presence: UserPresencePolicy,
     66     hardware: HardwarePolicy,
     67 }
     68 
     69 impl AccessPolicy {
     70     /// Creates an explicit access policy.
     71     #[must_use]
     72     pub const fn new(
     73         residency: ResidencyPolicy,
     74         user_presence: UserPresencePolicy,
     75         hardware: HardwarePolicy,
     76     ) -> Self {
     77         Self {
     78             residency,
     79             user_presence,
     80             hardware,
     81         }
     82     }
     83 
     84     /// Returns a policy suitable for an explicitly selected local adapter.
     85     #[must_use]
     86     pub const fn standard() -> Self {
     87         Self::new(
     88             ResidencyPolicy::Any,
     89             UserPresencePolicy::NotRequired,
     90             HardwarePolicy::Any,
     91         )
     92     }
     93 }
     94 
     95 /// Security properties reported by a provider without performing access.
     96 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     97 pub struct SecretCapabilities {
     98     available: bool,
     99     residency: ResidencySupport,
    100     user_presence: CapabilitySupport,
    101     hardware_backed: CapabilitySupport,
    102 }
    103 
    104 impl SecretCapabilities {
    105     /// Reports an unavailable provider without probing or mutating it.
    106     #[must_use]
    107     pub const fn unavailable() -> Self {
    108         Self {
    109             available: false,
    110             residency: ResidencySupport::Volatile,
    111             user_presence: CapabilitySupport::Unavailable,
    112             hardware_backed: CapabilitySupport::Unavailable,
    113         }
    114     }
    115 
    116     /// Reports the static security properties of an available provider.
    117     #[must_use]
    118     pub const fn available(
    119         residency: ResidencySupport,
    120         user_presence: CapabilitySupport,
    121         hardware_backed: CapabilitySupport,
    122     ) -> Self {
    123         Self {
    124             available: true,
    125             residency,
    126             user_presence,
    127             hardware_backed,
    128         }
    129     }
    130 
    131     /// Returns whether the provider is available for explicit selection.
    132     #[must_use]
    133     pub const fn is_available(self) -> bool {
    134         self.available
    135     }
    136 
    137     /// Returns the strongest residency guarantee reported by the provider.
    138     #[must_use]
    139     pub const fn residency(self) -> ResidencySupport {
    140         self.residency
    141     }
    142 
    143     /// Returns user-presence support.
    144     #[must_use]
    145     pub const fn user_presence(self) -> CapabilitySupport {
    146         self.user_presence
    147     }
    148 
    149     /// Returns hardware-backed protection support.
    150     #[must_use]
    151     pub const fn hardware_backed(self) -> CapabilitySupport {
    152         self.hardware_backed
    153     }
    154 
    155     fn validate(self, backend: BackendKind, policy: AccessPolicy) -> Result<(), Error> {
    156         if !self.available {
    157             return Err(Error::BackendUnavailable { backend });
    158         }
    159         if matches!(policy.residency, ResidencyPolicy::DeviceLocal)
    160             && !matches!(self.residency, ResidencySupport::DeviceLocal)
    161         {
    162             return Err(Error::PolicyUnsupported {
    163                 backend,
    164                 requirement: PolicyRequirement::DeviceLocal,
    165             });
    166         }
    167         if matches!(policy.user_presence, UserPresencePolicy::Required)
    168             && !matches!(self.user_presence, CapabilitySupport::Supported)
    169         {
    170             return Err(Error::PolicyUnsupported {
    171                 backend,
    172                 requirement: PolicyRequirement::UserPresence,
    173             });
    174         }
    175         if matches!(policy.hardware, HardwarePolicy::RequireHardwareBacked)
    176             && !matches!(self.hardware_backed, CapabilitySupport::Supported)
    177         {
    178             return Err(Error::PolicyUnsupported {
    179                 backend,
    180                 requirement: PolicyRequirement::HardwareBacked,
    181             });
    182         }
    183         Ok(())
    184     }
    185 }
    186 
    187 /// A wrapping provider selected and owned by the host.
    188 pub trait SecretProvider: KeyWrapping + Send + Sync {
    189     /// Returns the adapter family implemented by this provider.
    190     fn backend_kind(&self) -> BackendKind;
    191 
    192     /// Reports capabilities without accessing secret storage.
    193     fn capabilities(&self) -> SecretCapabilities;
    194 }
    195 
    196 /// Exact provider selection with no implicit fallback.
    197 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    198 pub struct SelectionPolicy {
    199     backend: BackendKind,
    200     access: AccessPolicy,
    201 }
    202 
    203 impl SelectionPolicy {
    204     /// Selects one backend family and its mandatory security properties.
    205     #[must_use]
    206     pub const fn new(backend: BackendKind, access: AccessPolicy) -> Self {
    207         Self { backend, access }
    208     }
    209 
    210     /// Resolves the exact provider without probing a fallback backend.
    211     pub fn select<'a>(
    212         self,
    213         candidates: &'a [&'a dyn SecretProvider],
    214     ) -> Result<&'a dyn SecretProvider, Error> {
    215         let provider = candidates
    216             .iter()
    217             .copied()
    218             .find(|candidate| candidate.backend_kind() == self.backend)
    219             .ok_or(Error::BackendUnavailable {
    220                 backend: self.backend,
    221             })?;
    222         provider
    223             .capabilities()
    224             .validate(self.backend, self.access)?;
    225         Ok(provider)
    226     }
    227 }
    228 
    229 #[cfg(test)]
    230 mod tests {
    231     use super::*;
    232 
    233     #[test]
    234     fn capability_validation_covers_every_policy_requirement() {
    235         let unavailable = SecretCapabilities::unavailable();
    236         assert!(!unavailable.is_available());
    237         assert_eq!(unavailable.residency(), ResidencySupport::Volatile);
    238         assert_eq!(unavailable.user_presence(), CapabilitySupport::Unavailable);
    239         assert_eq!(
    240             unavailable.hardware_backed(),
    241             CapabilitySupport::Unavailable
    242         );
    243         assert_eq!(
    244             unavailable.validate(BackendKind::Memory, AccessPolicy::standard()),
    245             Err(Error::BackendUnavailable {
    246                 backend: BackendKind::Memory
    247             })
    248         );
    249 
    250         let basic = SecretCapabilities::available(
    251             ResidencySupport::UserProfile,
    252             CapabilitySupport::Unavailable,
    253             CapabilitySupport::Unavailable,
    254         );
    255         assert!(basic.is_available());
    256         assert!(
    257             basic
    258                 .validate(BackendKind::File, AccessPolicy::standard())
    259                 .is_ok()
    260         );
    261         assert_eq!(
    262             basic.validate(
    263                 BackendKind::File,
    264                 AccessPolicy::new(
    265                     ResidencyPolicy::DeviceLocal,
    266                     UserPresencePolicy::NotRequired,
    267                     HardwarePolicy::Any
    268                 )
    269             ),
    270             Err(Error::PolicyUnsupported {
    271                 backend: BackendKind::File,
    272                 requirement: PolicyRequirement::DeviceLocal
    273             })
    274         );
    275         assert_eq!(
    276             basic.validate(
    277                 BackendKind::File,
    278                 AccessPolicy::new(
    279                     ResidencyPolicy::Any,
    280                     UserPresencePolicy::Required,
    281                     HardwarePolicy::Any
    282                 )
    283             ),
    284             Err(Error::PolicyUnsupported {
    285                 backend: BackendKind::File,
    286                 requirement: PolicyRequirement::UserPresence
    287             })
    288         );
    289         assert_eq!(
    290             basic.validate(
    291                 BackendKind::File,
    292                 AccessPolicy::new(
    293                     ResidencyPolicy::Any,
    294                     UserPresencePolicy::NotRequired,
    295                     HardwarePolicy::RequireHardwareBacked
    296                 )
    297             ),
    298             Err(Error::PolicyUnsupported {
    299                 backend: BackendKind::File,
    300                 requirement: PolicyRequirement::HardwareBacked
    301             })
    302         );
    303         assert!(
    304             basic
    305                 .validate(
    306                     BackendKind::File,
    307                     AccessPolicy::new(
    308                         ResidencyPolicy::Any,
    309                         UserPresencePolicy::NotRequired,
    310                         HardwarePolicy::PreferHardwareBacked
    311                     )
    312                 )
    313                 .is_ok()
    314         );
    315 
    316         let complete = SecretCapabilities::available(
    317             ResidencySupport::DeviceLocal,
    318             CapabilitySupport::Supported,
    319             CapabilitySupport::Supported,
    320         );
    321         assert_eq!(complete.residency(), ResidencySupport::DeviceLocal);
    322         assert_eq!(complete.user_presence(), CapabilitySupport::Supported);
    323         assert_eq!(complete.hardware_backed(), CapabilitySupport::Supported);
    324         assert!(
    325             complete
    326                 .validate(
    327                     BackendKind::Keyring,
    328                     AccessPolicy::new(
    329                         ResidencyPolicy::DeviceLocal,
    330                         UserPresencePolicy::Required,
    331                         HardwarePolicy::RequireHardwareBacked
    332                     )
    333                 )
    334                 .is_ok()
    335         );
    336     }
    337 }