provider_contract.rs (10205B)
1 use futures_executor::block_on; 2 use radroots_secrets::context::{ 3 EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, 4 }; 5 use radroots_secrets::error::{Operation, PolicyRequirement}; 6 use radroots_secrets::id::{BackendKind, KeyVersion}; 7 use radroots_secrets::provider::{ 8 AccessPolicy, CapabilitySupport, HardwarePolicy, ResidencyPolicy, ResidencySupport, 9 SecretCapabilities, SelectionPolicy, UserPresencePolicy, 10 }; 11 use radroots_secrets::wrapping::{ 12 BoxFuture, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret, 13 }; 14 use radroots_secrets::{Error, KeyWrapping, SecretId, SecretProvider, SecretRef}; 15 16 struct MockProvider { 17 backend: BackendKind, 18 capabilities: SecretCapabilities, 19 fail: bool, 20 } 21 22 impl KeyWrapping for MockProvider { 23 fn wrap<'a>(&'a self, request: WrapRequest<'a>) -> BoxFuture<'a, Result<WrappedSecret, Error>> { 24 Box::pin(async move { 25 self.validate_reference(request.reference())?; 26 if self.fail { 27 return Err(Error::BackendFailure { 28 backend: self.backend, 29 operation: Operation::Wrap, 30 }); 31 } 32 let mut encoded = Vec::from(request.context().authentication_digest()); 33 encoded.extend(request.plaintext().expose_secret(|plaintext| { 34 plaintext.iter().map(|byte| byte ^ 0xA5).collect::<Vec<_>>() 35 })); 36 WrappedSecret::from_bytes(encoded) 37 }) 38 } 39 40 fn unwrap<'a>( 41 &'a self, 42 request: UnwrapRequest<'a>, 43 ) -> BoxFuture<'a, Result<SecretMaterial, Error>> { 44 Box::pin(async move { 45 self.validate_reference(request.reference())?; 46 if self.fail { 47 return Err(Error::BackendFailure { 48 backend: self.backend, 49 operation: Operation::Unwrap, 50 }); 51 } 52 let (context_digest, wrapped) = request 53 .wrapped() 54 .as_bytes() 55 .split_at_checked(32) 56 .ok_or(Error::BackendFailure { 57 backend: self.backend, 58 operation: Operation::Unwrap, 59 })?; 60 let expected_digest = request.context().authentication_digest(); 61 if context_digest != expected_digest { 62 return Err(Error::BackendFailure { 63 backend: self.backend, 64 operation: Operation::Unwrap, 65 }); 66 } 67 let decoded = wrapped.iter().map(|byte| byte ^ 0xA5).collect::<Vec<_>>(); 68 SecretMaterial::from_slice(decoded.as_slice()) 69 }) 70 } 71 } 72 73 impl SecretProvider for MockProvider { 74 fn backend_kind(&self) -> BackendKind { 75 self.backend 76 } 77 78 fn capabilities(&self) -> SecretCapabilities { 79 self.capabilities 80 } 81 } 82 83 impl MockProvider { 84 fn validate_reference(&self, reference: &SecretRef) -> Result<(), Error> { 85 if reference.backend() != self.backend { 86 return Err(Error::BackendMismatch { 87 provider: self.backend, 88 reference: reference.backend(), 89 }); 90 } 91 Ok(()) 92 } 93 } 94 95 fn provider(backend: BackendKind, capabilities: SecretCapabilities) -> MockProvider { 96 MockProvider { 97 backend, 98 capabilities, 99 fail: false, 100 } 101 } 102 103 fn reference(backend: BackendKind) -> SecretRef { 104 SecretRef::new( 105 SecretId::parse("test-wrapping-key").expect("valid id"), 106 backend, 107 KeyVersion::new(1).expect("valid version"), 108 ) 109 } 110 111 fn context() -> EnvelopeContext { 112 EnvelopeContext::new( 113 EnvelopePurpose::parse("radroots.provider_test").expect("purpose"), 114 EnvelopeSubject::parse("provider_test", "fixture").expect("subject"), 115 PayloadSchemaId::parse("radroots.provider_test.v1").expect("schema"), 116 ) 117 } 118 119 #[test] 120 fn provider_traits_are_dyn_compatible_and_round_trip_opaque_material() { 121 fn accept_dyn(_: &dyn SecretProvider) {} 122 123 let provider = provider( 124 BackendKind::Memory, 125 SecretCapabilities::available( 126 ResidencySupport::Volatile, 127 CapabilitySupport::Unavailable, 128 CapabilitySupport::Unavailable, 129 ), 130 ); 131 accept_dyn(&provider); 132 133 let reference = reference(BackendKind::Memory); 134 let plaintext = SecretMaterial::from_slice(b"caller-owned-data-key").expect("material"); 135 let context = context(); 136 let wrapped = 137 block_on(provider.wrap(WrapRequest::new(&reference, &context, &plaintext))).expect("wrap"); 138 let opened = block_on(provider.unwrap(UnwrapRequest::new(&reference, &context, &wrapped))) 139 .expect("unwrap"); 140 opened.expose_secret(|bytes| assert_eq!(bytes, b"caller-owned-data-key")); 141 142 assert_eq!(format!("{plaintext:?}"), "SecretMaterial(<redacted>)"); 143 assert_eq!(format!("{wrapped:?}"), "WrappedSecret(<redacted>)"); 144 } 145 146 #[test] 147 fn exact_selection_never_falls_back_to_another_backend() { 148 let memory = provider( 149 BackendKind::Memory, 150 SecretCapabilities::available( 151 ResidencySupport::Volatile, 152 CapabilitySupport::Unavailable, 153 CapabilitySupport::Unavailable, 154 ), 155 ); 156 let file = provider( 157 BackendKind::File, 158 SecretCapabilities::available( 159 ResidencySupport::DeviceLocal, 160 CapabilitySupport::Unavailable, 161 CapabilitySupport::Unavailable, 162 ), 163 ); 164 let candidates: [&dyn SecretProvider; 2] = [&memory, &file]; 165 166 let selected = SelectionPolicy::new(BackendKind::File, AccessPolicy::standard()) 167 .select(&candidates) 168 .expect("file selected"); 169 assert_eq!(selected.backend_kind(), BackendKind::File); 170 171 assert!(matches!( 172 SelectionPolicy::new(BackendKind::Keyring, AccessPolicy::standard()).select(&candidates), 173 Err(Error::BackendUnavailable { 174 backend: BackendKind::Keyring 175 }) 176 )); 177 } 178 179 #[test] 180 fn selection_enforces_device_user_presence_and_hardware_policy() { 181 let keyring = provider( 182 BackendKind::Keyring, 183 SecretCapabilities::available( 184 ResidencySupport::UserProfile, 185 CapabilitySupport::Unavailable, 186 CapabilitySupport::Unavailable, 187 ), 188 ); 189 let candidates: [&dyn SecretProvider; 1] = [&keyring]; 190 191 let cases = [ 192 ( 193 AccessPolicy::new( 194 ResidencyPolicy::DeviceLocal, 195 UserPresencePolicy::NotRequired, 196 HardwarePolicy::Any, 197 ), 198 PolicyRequirement::DeviceLocal, 199 ), 200 ( 201 AccessPolicy::new( 202 ResidencyPolicy::Any, 203 UserPresencePolicy::Required, 204 HardwarePolicy::Any, 205 ), 206 PolicyRequirement::UserPresence, 207 ), 208 ( 209 AccessPolicy::new( 210 ResidencyPolicy::Any, 211 UserPresencePolicy::NotRequired, 212 HardwarePolicy::RequireHardwareBacked, 213 ), 214 PolicyRequirement::HardwareBacked, 215 ), 216 ]; 217 218 for (access, expected) in cases { 219 assert_eq!( 220 SelectionPolicy::new(BackendKind::Keyring, access) 221 .select(&candidates) 222 .map(SecretProvider::backend_kind), 223 Err(Error::PolicyUnsupported { 224 backend: BackendKind::Keyring, 225 requirement: expected, 226 }) 227 ); 228 } 229 } 230 231 #[test] 232 fn provider_errors_are_normalized_and_secret_safe() { 233 let provider = MockProvider { 234 backend: BackendKind::External, 235 capabilities: SecretCapabilities::available( 236 ResidencySupport::DeviceLocal, 237 CapabilitySupport::Supported, 238 CapabilitySupport::Supported, 239 ), 240 fail: true, 241 }; 242 let reference = reference(BackendKind::External); 243 let plaintext = SecretMaterial::from_slice(b"must-not-appear").expect("material"); 244 let error = block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))) 245 .expect_err("backend failure"); 246 assert_eq!( 247 error, 248 Error::BackendFailure { 249 backend: BackendKind::External, 250 operation: Operation::Wrap, 251 } 252 ); 253 assert!(!error.to_string().contains("must-not-appear")); 254 assert!(!format!("{error:?}").contains("must-not-appear")); 255 } 256 257 #[test] 258 fn reference_backend_mismatch_fails_before_wrapping() { 259 let provider = provider( 260 BackendKind::Memory, 261 SecretCapabilities::available( 262 ResidencySupport::Volatile, 263 CapabilitySupport::Unavailable, 264 CapabilitySupport::Unavailable, 265 ), 266 ); 267 let reference = reference(BackendKind::File); 268 let plaintext = SecretMaterial::from_slice(b"data-key").expect("material"); 269 assert_eq!( 270 block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))), 271 Err(Error::BackendMismatch { 272 provider: BackendKind::Memory, 273 reference: BackendKind::File, 274 }) 275 ); 276 } 277 278 #[test] 279 fn external_wrapping_contract_rejects_cross_context_copy() { 280 let provider = provider( 281 BackendKind::Memory, 282 SecretCapabilities::available( 283 ResidencySupport::Volatile, 284 CapabilitySupport::Unavailable, 285 CapabilitySupport::Unavailable, 286 ), 287 ); 288 let reference = reference(BackendKind::Memory); 289 let plaintext = SecretMaterial::from_slice(b"data-key").expect("material"); 290 let wrapped = block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))) 291 .expect("wrap"); 292 let different = EnvelopeContext::new( 293 EnvelopePurpose::parse("radroots.provider_test").expect("purpose"), 294 EnvelopeSubject::parse("provider_test", "different").expect("subject"), 295 PayloadSchemaId::parse("radroots.provider_test.v1").expect("schema"), 296 ); 297 assert!(matches!( 298 block_on(provider.unwrap(UnwrapRequest::new(&reference, &different, &wrapped,))), 299 Err(Error::BackendFailure { 300 backend: BackendKind::Memory, 301 operation: Operation::Unwrap, 302 }) 303 )); 304 }