lib

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

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 }