Skip to main content

soft_fido2_ctap/
key_provider.rs

1//! Credential key provider abstraction
2//!
3//! Allows credential signing keys to be generated and used by external providers
4//! (TPM, PKCS#11, secure elements, etc.) without exposing private key material
5//! to the CTAP core.
6
7use crate::SecBytes;
8use crate::status::StatusCode;
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use serde::{Deserialize, Serialize};
14
15/// Software provider identifier
16pub const SOFTWARE_PROVIDER_ID: &[u8; 11] = b"software-v1";
17
18/// Maximum provider ID length
19pub const MAX_PROVIDER_ID_LENGTH: usize = 64;
20
21/// Stable provider identifier used in serialized credential keys
22#[derive(Clone, Debug, PartialEq, Eq, Hash)]
23pub struct CredentialKeyProviderId(Vec<u8>);
24
25impl CredentialKeyProviderId {
26    pub fn new(id: &[u8]) -> Self {
27        debug_assert!(
28            id.len() <= MAX_PROVIDER_ID_LENGTH,
29            "provider ID exceeds MAX_PROVIDER_ID_LENGTH ({MAX_PROVIDER_ID_LENGTH} bytes)"
30        );
31        Self(id.to_vec())
32    }
33
34    pub fn software() -> Self {
35        Self(SOFTWARE_PROVIDER_ID.to_vec())
36    }
37
38    pub fn as_bytes(&self) -> &[u8] {
39        &self.0
40    }
41
42    pub fn is_software(&self) -> bool {
43        self.0 == SOFTWARE_PROVIDER_ID.as_slice()
44    }
45}
46
47impl Serialize for CredentialKeyProviderId {
48    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
49    where
50        S: serde::Serializer,
51    {
52        serde_bytes::serialize(&self.0, serializer)
53    }
54}
55
56impl<'de> Deserialize<'de> for CredentialKeyProviderId {
57    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
58    where
59        D: serde::Deserializer<'de>,
60    {
61        let bytes: Vec<u8> = serde_bytes::deserialize(deserializer)?;
62        if bytes.len() > MAX_PROVIDER_ID_LENGTH {
63            return Err(serde::de::Error::custom(alloc::format!(
64                "provider ID exceeds MAX_PROVIDER_ID_LENGTH ({} bytes)",
65                MAX_PROVIDER_ID_LENGTH
66            )));
67        }
68        Ok(Self(bytes))
69    }
70}
71
72/// Opaque credential key record owned by a provider
73///
74/// The CTAP core treats this as opaque data. It never inspects or interprets
75/// the `material` field.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct CredentialKey {
78    /// Stable provider identifier
79    pub provider: CredentialKeyProviderId,
80
81    /// Version of the provider-owned opaque encoding
82    pub format_version: u16,
83
84    /// Opaque provider material
85    pub material: SecBytes,
86}
87
88impl CredentialKey {
89    pub fn new(provider: CredentialKeyProviderId, format_version: u16, material: SecBytes) -> Self {
90        Self {
91            provider,
92            format_version,
93            material,
94        }
95    }
96
97    pub fn software(material: SecBytes) -> Self {
98        Self {
99            provider: CredentialKeyProviderId::software(),
100            format_version: 1,
101            material,
102        }
103    }
104}
105
106impl Serialize for CredentialKey {
107    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
108    where
109        S: serde::Serializer,
110    {
111        use serde::ser::SerializeMap;
112        let mut map = serializer.serialize_map(None)?;
113        map.serialize_entry("provider", &self.provider)?;
114        map.serialize_entry("format_version", &self.format_version)?;
115        map.serialize_entry(
116            "material",
117            &serde_bytes::Bytes::new(self.material.as_slice()),
118        )?;
119        map.end()
120    }
121}
122
123impl<'de> Deserialize<'de> for CredentialKey {
124    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
125    where
126        D: serde::Deserializer<'de>,
127    {
128        let value = crate::cbor::Value::deserialize(deserializer)?;
129        let map = match value {
130            crate::cbor::Value::Map(m) => m,
131            _ => return Err(serde::de::Error::custom("expected map")),
132        };
133
134        let mut provider: Option<CredentialKeyProviderId> = None;
135        let mut format_version: Option<u16> = None;
136        let mut material: Option<SecBytes> = None;
137
138        for (k, v) in map {
139            let key_str = match k {
140                crate::cbor::Value::Text(s) => s,
141                _ => continue,
142            };
143            match key_str.as_str() {
144                "provider" => {
145                    let p: CredentialKeyProviderId =
146                        crate::cbor::from_value(&v).map_err(serde::de::Error::custom)?;
147                    provider = Some(p);
148                }
149                "format_version" => {
150                    if let crate::cbor::Value::Integer(i) = v {
151                        if i < 0 || i > u16::MAX as i128 {
152                            return Err(serde::de::Error::custom(
153                                "format_version out of range for u16",
154                            ));
155                        }
156                        format_version = Some(i as u16);
157                    }
158                }
159                "material" => {
160                    if let crate::cbor::Value::Bytes(b) = v {
161                        material = Some(SecBytes::from_slice(&b));
162                    }
163                }
164                _ => {}
165            }
166        }
167
168        Ok(CredentialKey {
169            provider: provider.ok_or_else(|| serde::de::Error::missing_field("provider"))?,
170            format_version: format_version
171                .ok_or_else(|| serde::de::Error::missing_field("format_version"))?,
172            material: material.ok_or_else(|| serde::de::Error::missing_field("material"))?,
173        })
174    }
175}
176
177/// Result of key generation by a provider
178#[derive(Debug, PartialEq, Eq)]
179pub struct GeneratedCredentialKey {
180    /// The opaque credential key
181    pub key: CredentialKey,
182
183    /// COSE-encoded public key bytes
184    pub cose_public_key: Vec<u8>,
185}
186
187/// Errors from credential key provider operations
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum CredentialKeyError {
190    UnsupportedAlgorithm,
191    UnsupportedProvider,
192    UnsupportedFormatVersion,
193    InvalidKeyMaterial,
194    KeyNotFound,
195    AuthorizationDenied,
196    Timeout,
197    TransientFailure(String),
198    PermanentFailure(String),
199}
200
201impl From<CredentialKeyError> for StatusCode {
202    fn from(err: CredentialKeyError) -> Self {
203        match err {
204            CredentialKeyError::UnsupportedAlgorithm => StatusCode::UnsupportedAlgorithm,
205            CredentialKeyError::UnsupportedProvider => StatusCode::InvalidCredential,
206            CredentialKeyError::UnsupportedFormatVersion => StatusCode::InvalidCredential,
207            CredentialKeyError::InvalidKeyMaterial => StatusCode::InvalidCredential,
208            CredentialKeyError::KeyNotFound => StatusCode::NoCredentials,
209            CredentialKeyError::AuthorizationDenied => StatusCode::OperationDenied,
210            CredentialKeyError::Timeout => StatusCode::UserActionTimeout,
211            CredentialKeyError::TransientFailure(_) => StatusCode::Other,
212            CredentialKeyError::PermanentFailure(_) => StatusCode::Other,
213        }
214    }
215}
216
217/// Trait for credential key providers
218///
219/// Providers own key generation and signing operations. The CTAP core
220/// delegates these operations to the configured provider and treats
221/// key material as opaque.
222pub trait CredentialKeyProvider {
223    /// Stable ID used in serialized CredentialKey values
224    fn provider_id(&self) -> CredentialKeyProviderId;
225
226    /// Whether this provider can generate and sign with the COSE algorithm
227    fn supports_algorithm(&self, algorithm: i32) -> bool;
228
229    /// Generate a new credential key and its COSE public key
230    fn generate(
231        &self,
232        algorithm: i32,
233    ) -> core::result::Result<GeneratedCredentialKey, CredentialKeyError>;
234
235    /// Sign the exact WebAuthn message supplied by the CTAP implementation
236    fn sign(
237        &self,
238        key: &CredentialKey,
239        algorithm: i32,
240        message: &[u8],
241    ) -> core::result::Result<Vec<u8>, CredentialKeyError>;
242
243    /// Optional lifecycle hook for providers that allocate persistent external objects
244    fn delete(&self, _key: &CredentialKey) -> core::result::Result<(), CredentialKeyError> {
245        Ok(())
246    }
247}
248
249/// Default software credential key provider
250///
251/// Uses the built-in ES256 (P-256) and EdDSA (Ed25519) implementations.
252/// Opaque material is the raw 32-byte private key.
253pub struct SoftwareCredentialKeyProvider;
254
255impl CredentialKeyProvider for SoftwareCredentialKeyProvider {
256    fn provider_id(&self) -> CredentialKeyProviderId {
257        CredentialKeyProviderId::software()
258    }
259
260    fn supports_algorithm(&self, algorithm: i32) -> bool {
261        matches!(algorithm, -7 | -8 | -19)
262    }
263
264    fn generate(
265        &self,
266        algorithm: i32,
267    ) -> core::result::Result<GeneratedCredentialKey, CredentialKeyError> {
268        if !self.supports_algorithm(algorithm) {
269            return Err(CredentialKeyError::UnsupportedAlgorithm);
270        }
271
272        match algorithm {
273            -8 | -19 => {
274                let (sk, pk) = soft_fido2_crypto::eddsa::generate_keypair();
275                let key = CredentialKey::software(SecBytes::from_slice(&sk[..]));
276                Ok(GeneratedCredentialKey {
277                    key,
278                    cose_public_key: pk,
279                })
280            }
281            _ => {
282                let (sk, pk) = soft_fido2_crypto::ecdsa::generate_keypair();
283                let key = CredentialKey::software(SecBytes::from_slice(&sk[..]));
284                Ok(GeneratedCredentialKey {
285                    key,
286                    cose_public_key: pk,
287                })
288            }
289        }
290    }
291
292    fn sign(
293        &self,
294        key: &CredentialKey,
295        algorithm: i32,
296        message: &[u8],
297    ) -> core::result::Result<Vec<u8>, CredentialKeyError> {
298        if !key.provider.is_software() {
299            return Err(CredentialKeyError::UnsupportedProvider);
300        }
301
302        let key_bytes = key.material.as_slice();
303        if key_bytes.len() != 32 {
304            return Err(CredentialKeyError::InvalidKeyMaterial);
305        }
306
307        let mut arr = [0u8; 32];
308        arr.copy_from_slice(key_bytes);
309        let priv_key = zeroize::Zeroizing::new(arr);
310
311        match algorithm {
312            -8 | -19 => soft_fido2_crypto::eddsa::sign(&priv_key, message)
313                .map_err(|e| CredentialKeyError::TransientFailure(alloc::format!("{:?}", e))),
314            _ => soft_fido2_crypto::ecdsa::sign(&priv_key, message)
315                .map_err(|e| CredentialKeyError::TransientFailure(alloc::format!("{:?}", e))),
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_software_provider_id() {
326        let id = CredentialKeyProviderId::software();
327        assert!(id.is_software());
328        assert_eq!(id.as_bytes(), SOFTWARE_PROVIDER_ID.as_slice());
329    }
330
331    #[test]
332    fn test_custom_provider_id() {
333        let id = CredentialKeyProviderId::new(b"tpm-v1");
334        assert!(!id.is_software());
335    }
336
337    #[test]
338    fn test_software_generate_es256() {
339        let provider = SoftwareCredentialKeyProvider;
340        let result = provider.generate(-7).unwrap();
341        assert!(result.key.provider.is_software());
342        assert_eq!(result.key.material.as_slice().len(), 32);
343        assert_eq!(result.cose_public_key.len(), 65);
344        assert_eq!(result.cose_public_key[0], 0x04);
345    }
346
347    #[test]
348    fn test_software_generate_eddsa() {
349        let provider = SoftwareCredentialKeyProvider;
350        let result = provider.generate(-8).unwrap();
351        assert!(result.key.provider.is_software());
352        assert_eq!(result.key.material.as_slice().len(), 32);
353        assert_eq!(result.cose_public_key.len(), 32);
354    }
355
356    #[test]
357    fn test_software_sign_and_verify_es256() {
358        let provider = SoftwareCredentialKeyProvider;
359        let generated = provider.generate(-7).unwrap();
360        let message = b"test message";
361        let signature = provider.sign(&generated.key, -7, message).unwrap();
362
363        assert!(
364            soft_fido2_crypto::ecdsa::verify(&generated.cose_public_key, message, &signature)
365                .is_ok()
366        );
367    }
368
369    #[test]
370    fn test_software_sign_and_verify_eddsa() {
371        let provider = SoftwareCredentialKeyProvider;
372        let generated = provider.generate(-8).unwrap();
373        let message = b"test message";
374        let signature = provider.sign(&generated.key, -8, message).unwrap();
375
376        assert!(
377            soft_fido2_crypto::eddsa::verify(&generated.cose_public_key, message, &signature)
378                .is_ok()
379        );
380    }
381
382    #[test]
383    fn test_software_sign_rejects_non_software_key() {
384        let provider = SoftwareCredentialKeyProvider;
385        let key = CredentialKey::new(
386            CredentialKeyProviderId::new(b"other"),
387            1,
388            SecBytes::new(vec![0u8; 32]),
389        );
390        let result = provider.sign(&key, -7, b"test");
391        assert_eq!(result, Err(CredentialKeyError::UnsupportedProvider));
392    }
393
394    #[test]
395    fn test_software_sign_rejects_wrong_length() {
396        let provider = SoftwareCredentialKeyProvider;
397        let key = CredentialKey::software(SecBytes::new(vec![0u8; 16]));
398        let result = provider.sign(&key, -7, b"test");
399        assert_eq!(result, Err(CredentialKeyError::InvalidKeyMaterial));
400    }
401
402    #[test]
403    fn test_software_supports_algorithm() {
404        let provider = SoftwareCredentialKeyProvider;
405        assert!(provider.supports_algorithm(-7));
406        assert!(provider.supports_algorithm(-8));
407        assert!(provider.supports_algorithm(-19));
408        assert!(!provider.supports_algorithm(-257));
409    }
410
411    #[test]
412    fn test_credential_key_error_to_status_code() {
413        assert_eq!(
414            StatusCode::from(CredentialKeyError::UnsupportedAlgorithm),
415            StatusCode::UnsupportedAlgorithm
416        );
417        assert_eq!(
418            StatusCode::from(CredentialKeyError::InvalidKeyMaterial),
419            StatusCode::InvalidCredential
420        );
421        assert_eq!(
422            StatusCode::from(CredentialKeyError::KeyNotFound),
423            StatusCode::NoCredentials
424        );
425        assert_eq!(
426            StatusCode::from(CredentialKeyError::AuthorizationDenied),
427            StatusCode::OperationDenied
428        );
429    }
430
431    #[test]
432    fn test_credential_key_software_constructor() {
433        let key = CredentialKey::software(SecBytes::new(vec![0u8; 32]));
434        assert!(key.provider.is_software());
435        assert_eq!(key.format_version, 1);
436    }
437
438    #[test]
439    fn test_credential_key_serde_roundtrip() {
440        let key = CredentialKey::software(SecBytes::new(vec![42u8; 32]));
441        let mut buf = Vec::new();
442        crate::cbor::into_writer(&key, &mut buf).unwrap();
443        let restored: CredentialKey = crate::cbor::decode(&buf).unwrap();
444        assert_eq!(restored.provider, key.provider);
445        assert_eq!(restored.format_version, key.format_version);
446        assert_eq!(restored.material.as_slice(), key.material.as_slice());
447    }
448
449    #[test]
450    fn test_software_generate_rejects_unsupported_algorithm() {
451        let provider = SoftwareCredentialKeyProvider;
452        let result = provider.generate(-257);
453        assert_eq!(result, Err(CredentialKeyError::UnsupportedAlgorithm));
454    }
455
456    #[test]
457    fn test_provider_id_deserialization_rejects_oversized() {
458        use crate::cbor::Value;
459
460        let oversized = Value::Bytes(vec![0u8; MAX_PROVIDER_ID_LENGTH + 1]);
461        let mut buf = Vec::new();
462        crate::cbor::into_writer(&oversized, &mut buf).unwrap();
463        let result: core::result::Result<CredentialKeyProviderId, _> = crate::cbor::decode(&buf);
464        assert!(result.is_err());
465    }
466
467    #[test]
468    fn test_credential_key_deserialization_rejects_out_of_range_format_version() {
469        use crate::cbor::Value;
470
471        let map = Value::Map(vec![
472            (
473                Value::Text("provider".to_string()),
474                Value::Bytes(b"software-v1".to_vec()),
475            ),
476            (
477                Value::Text("format_version".to_string()),
478                Value::Integer(u16::MAX as i128 + 1),
479            ),
480            (
481                Value::Text("material".to_string()),
482                Value::Bytes(vec![0u8; 32]),
483            ),
484        ]);
485        let mut buf = Vec::new();
486        crate::cbor::into_writer(&map, &mut buf).unwrap();
487        let result: core::result::Result<CredentialKey, _> = crate::cbor::decode(&buf);
488        assert!(result.is_err());
489    }
490
491    #[cfg(test)]
492    mod mock_external_provider {
493        use super::*;
494        use alloc::collections::BTreeMap;
495
496        #[cfg(feature = "std")]
497        use std::sync::Mutex;
498
499        #[cfg(not(feature = "std"))]
500        use spin::Mutex;
501
502        struct MockExternalProvider {
503            #[allow(clippy::type_complexity)]
504            keys: Mutex<BTreeMap<Vec<u8>, (Vec<u8>, Vec<u8>)>>,
505            next_handle: Mutex<u64>,
506        }
507
508        impl MockExternalProvider {
509            fn new() -> Self {
510                Self {
511                    keys: Mutex::new(BTreeMap::new()),
512                    next_handle: Mutex::new(1),
513                }
514            }
515        }
516
517        impl CredentialKeyProvider for MockExternalProvider {
518            fn provider_id(&self) -> CredentialKeyProviderId {
519                CredentialKeyProviderId::new(b"mock-external-v1")
520            }
521
522            fn supports_algorithm(&self, algorithm: i32) -> bool {
523                algorithm == -7
524            }
525
526            fn generate(
527                &self,
528                algorithm: i32,
529            ) -> core::result::Result<GeneratedCredentialKey, CredentialKeyError> {
530                if algorithm != -7 {
531                    return Err(CredentialKeyError::UnsupportedAlgorithm);
532                }
533
534                let (sk, pk) = soft_fido2_crypto::ecdsa::generate_keypair();
535
536                let handle = {
537                    let mut h = self.next_handle.lock().unwrap();
538                    let val = *h;
539                    *h += 1;
540                    val
541                };
542
543                let opaque_handle = handle.to_be_bytes().to_vec();
544
545                #[cfg(feature = "std")]
546                self.keys
547                    .lock()
548                    .unwrap()
549                    .insert(opaque_handle.clone(), (sk.to_vec(), pk.clone()));
550
551                #[cfg(not(feature = "std"))]
552                self.keys
553                    .lock()
554                    .insert(opaque_handle.clone(), (sk.to_vec(), pk.clone()));
555
556                let key =
557                    CredentialKey::new(self.provider_id(), 1, SecBytes::from_slice(&opaque_handle));
558
559                Ok(GeneratedCredentialKey {
560                    key,
561                    cose_public_key: pk,
562                })
563            }
564
565            fn sign(
566                &self,
567                key: &CredentialKey,
568                algorithm: i32,
569                message: &[u8],
570            ) -> core::result::Result<Vec<u8>, CredentialKeyError> {
571                if key.provider != self.provider_id() {
572                    return Err(CredentialKeyError::UnsupportedProvider);
573                }
574
575                if key.material.as_slice().len() != 8 {
576                    return Err(CredentialKeyError::InvalidKeyMaterial);
577                }
578
579                let handle = key.material.as_slice().to_vec();
580
581                #[cfg(feature = "std")]
582                let keys = self.keys.lock().unwrap();
583                #[cfg(not(feature = "std"))]
584                let keys = self.keys.lock();
585
586                let (sk_bytes, _) = keys.get(&handle).ok_or(CredentialKeyError::KeyNotFound)?;
587
588                if sk_bytes.len() != 32 {
589                    return Err(CredentialKeyError::InvalidKeyMaterial);
590                }
591
592                let mut arr = [0u8; 32];
593                arr.copy_from_slice(sk_bytes);
594                let priv_key = zeroize::Zeroizing::new(arr);
595
596                match algorithm {
597                    -7 => soft_fido2_crypto::ecdsa::sign(&priv_key, message).map_err(|e| {
598                        CredentialKeyError::TransientFailure(alloc::format!("{:?}", e))
599                    }),
600                    _ => Err(CredentialKeyError::UnsupportedAlgorithm),
601                }
602            }
603        }
604
605        #[test]
606        fn test_mock_provider_generate_returns_opaque_handle() {
607            let provider = MockExternalProvider::new();
608            let generated = provider.generate(-7).unwrap();
609
610            assert!(!generated.key.provider.is_software());
611            assert_eq!(generated.key.provider.as_bytes(), b"mock-external-v1");
612            assert_ne!(generated.key.material.as_slice().len(), 32);
613            assert_eq!(generated.key.material.as_slice().len(), 8);
614            assert_eq!(generated.cose_public_key.len(), 65);
615        }
616
617        #[test]
618        fn test_mock_provider_sign_and_verify() {
619            let provider = MockExternalProvider::new();
620            let generated = provider.generate(-7).unwrap();
621            let message = b"test message for mock provider";
622            let signature = provider.sign(&generated.key, -7, message).unwrap();
623
624            assert!(
625                soft_fido2_crypto::ecdsa::verify(&generated.cose_public_key, message, &signature)
626                    .is_ok()
627            );
628        }
629
630        #[test]
631        fn test_mock_provider_rejects_unsupported_algorithm() {
632            let provider = MockExternalProvider::new();
633            let result = provider.generate(-8);
634            assert_eq!(result, Err(CredentialKeyError::UnsupportedAlgorithm));
635        }
636
637        #[test]
638        fn test_mock_provider_rejects_wrong_provider_id() {
639            let provider = MockExternalProvider::new();
640            let key = CredentialKey::new(
641                CredentialKeyProviderId::new(b"wrong-provider"),
642                1,
643                SecBytes::new(vec![0u8; 8]),
644            );
645            let result = provider.sign(&key, -7, b"test");
646            assert_eq!(result, Err(CredentialKeyError::UnsupportedProvider));
647        }
648
649        #[test]
650        fn test_mock_provider_rejects_invalid_handle_length() {
651            let provider = MockExternalProvider::new();
652            let key = CredentialKey::new(provider.provider_id(), 1, SecBytes::new(vec![0u8; 16]));
653            let result = provider.sign(&key, -7, b"test");
654            assert_eq!(result, Err(CredentialKeyError::InvalidKeyMaterial));
655        }
656
657        #[test]
658        fn test_mock_provider_rejects_unknown_handle() {
659            let provider = MockExternalProvider::new();
660            let key = CredentialKey::new(provider.provider_id(), 1, SecBytes::new(vec![0xFFu8; 8]));
661            let result = provider.sign(&key, -7, b"test");
662            assert_eq!(result, Err(CredentialKeyError::KeyNotFound));
663        }
664
665        #[test]
666        fn test_mock_provider_multiple_keys() {
667            let provider = MockExternalProvider::new();
668
669            let gen1 = provider.generate(-7).unwrap();
670            let gen2 = provider.generate(-7).unwrap();
671
672            assert_ne!(gen1.key.material.as_slice(), gen2.key.material.as_slice());
673
674            let msg1 = b"message 1";
675            let msg2 = b"message 2";
676
677            let sig1 = provider.sign(&gen1.key, -7, msg1).unwrap();
678            let sig2 = provider.sign(&gen2.key, -7, msg2).unwrap();
679
680            assert!(soft_fido2_crypto::ecdsa::verify(&gen1.cose_public_key, msg1, &sig1).is_ok());
681            assert!(soft_fido2_crypto::ecdsa::verify(&gen2.cose_public_key, msg2, &sig2).is_ok());
682
683            assert!(soft_fido2_crypto::ecdsa::verify(&gen1.cose_public_key, msg2, &sig1).is_err());
684        }
685
686        #[test]
687        fn test_mock_provider_with_authenticator() {
688            use crate::authenticator::{Authenticator, AuthenticatorConfig};
689            use crate::test_utils::MockCallbacks;
690
691            let provider = MockExternalProvider::new();
692            let config = AuthenticatorConfig::new();
693            let auth = Authenticator::new_with_key_provider(config, MockCallbacks, provider);
694
695            assert_eq!(
696                auth.key_provider().provider_id().as_bytes(),
697                b"mock-external-v1"
698            );
699            assert!(auth.key_provider().supports_algorithm(-7));
700            assert!(!auth.key_provider().supports_algorithm(-8));
701        }
702    }
703}