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 }