passkey_authenticator/
authenticator.rs

1use coset::iana;
2use passkey_types::{
3    ctap2::{Aaguid, Ctap2Error, Flags},
4    webauthn,
5};
6
7use crate::{CredentialStore, UserValidationMethod};
8
9pub mod extensions;
10mod get_assertion;
11mod get_info;
12mod make_credential;
13
14use extensions::Extensions;
15
16/// The length of credentialId that should be randomly generated during a credential creation operation.
17///
18/// The value has a maximum of `64` per the [webauthn specification]. The minimum is a library enforced as `16`.
19///
20/// It is recommended to randomize this if possible to avoid authenticator fingerprinting.
21///
22/// [webauthn specification]: https://www.w3.org/TR/webauthn-3/#user-handle
23#[derive(Debug, Clone, Copy)]
24#[repr(transparent)]
25pub struct CredentialIdLength(u8);
26
27impl CredentialIdLength {
28    /// The default length of a credentialId to generate.
29    ///
30    /// This value is the same as [`Self::default`], but available in
31    /// `const` contexts.
32    pub const DEFAULT: Self = Self(Self::MIN);
33
34    const MIN: u8 = 16;
35
36    // "A user handle is an opaque byte sequence with a maximum size of 64 bytes..."
37    // Ref: https://www.w3.org/TR/webauthn-3/#user-handle
38    const MAX: u8 = 64;
39
40    /// Generates and returns a uniformly random [CredentialIdLength].
41    pub fn randomized(rng: &mut impl rand::Rng) -> Self {
42        let length = rng.gen_range(Self::MIN..=Self::MAX);
43        Self(length)
44    }
45}
46
47impl Default for CredentialIdLength {
48    fn default() -> Self {
49        Self::DEFAULT
50    }
51}
52
53impl From<u8> for CredentialIdLength {
54    fn from(value: u8) -> Self {
55        // Clamp to the specification's maximum.
56        let value = core::cmp::min(Self::MAX, value);
57        // Round values less then what we support up to the default.
58        let value = core::cmp::max(Self::MIN, value);
59        Self(value)
60    }
61}
62
63impl From<CredentialIdLength> for usize {
64    fn from(value: CredentialIdLength) -> Self {
65        usize::from(value.0)
66    }
67}
68
69/// A virtual authenticator with all the necessary state and information.
70pub struct Authenticator<S, U> {
71    /// The authenticator's AAGUID
72    aaguid: Aaguid,
73    /// Provides credential storage capabilities
74    store: S,
75    /// Current supported algorithms by the authenticator
76    algs: Vec<iana::Algorithm>,
77    /// Current supported transports that this authenticator can use to communicate.
78    ///
79    /// Default values are [`AuthenticatorTransport::Internal`] and [`AuthenticatorTransport::Hybrid`].
80    transports: Vec<webauthn::AuthenticatorTransport>,
81    /// Provider of user verification factor.
82    user_validation: U,
83
84    /// Value to control whether the authenticator will save new credentials with a signature counter.
85    /// The default value is `false`.
86    ///
87    /// NOTE: Using a counter with a credential that will sync is not recommended and can cause friction
88    /// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
89    make_credentials_with_signature_counter: bool,
90
91    /// The length of the credentialId made during a creation operation.
92    credential_id_length: CredentialIdLength,
93
94    /// Supported authenticator extensions
95    extensions: Extensions,
96}
97
98impl<S, U> Authenticator<S, U>
99where
100    S: CredentialStore,
101    U: UserValidationMethod,
102{
103    /// Create an authenticator with a known aaguid, a backing storage and a User verification system.
104    pub fn new(aaguid: Aaguid, store: S, user: U) -> Self {
105        Self {
106            aaguid,
107            store,
108            // TODO: Change this to a method on the cryptographic backend
109            algs: vec![iana::Algorithm::ES256],
110            transports: vec![
111                webauthn::AuthenticatorTransport::Internal,
112                webauthn::AuthenticatorTransport::Hybrid,
113            ],
114            user_validation: user,
115            make_credentials_with_signature_counter: false,
116            credential_id_length: CredentialIdLength::default(),
117            extensions: Extensions::default(),
118        }
119    }
120
121    /// Set whether the authenticator should save new credentials with a signature counter.
122    ///
123    /// NOTE: Using a counter with a credential that will sync is not recommended and can cause friction
124    /// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
125    pub fn set_make_credentials_with_signature_counter(&mut self, value: bool) {
126        self.make_credentials_with_signature_counter = value;
127    }
128
129    /// Get whether the authenticator will save new credentials with a signature counter.
130    pub fn make_credentials_with_signature_counter(&self) -> bool {
131        self.make_credentials_with_signature_counter
132    }
133
134    /// Set the length of credentialId to generate when creating a new credential.
135    pub fn set_make_credential_id_length(&mut self, length: CredentialIdLength) {
136        self.credential_id_length = length;
137    }
138
139    /// Get the current length of credential that will be generated when making a new credential.
140    pub fn make_credential_id_length(&self) -> CredentialIdLength {
141        self.credential_id_length
142    }
143
144    /// Access the [`CredentialStore`] to look into what is stored.
145    pub fn store(&self) -> &S {
146        &self.store
147    }
148
149    /// Exclusively access the [`CredentialStore`] to look into what is stored and modify it if needed.
150    pub fn store_mut(&mut self) -> &mut S {
151        &mut self.store
152    }
153
154    /// Access the authenticator's [`Aaguid`]
155    pub fn aaguid(&self) -> &Aaguid {
156        &self.aaguid
157    }
158
159    /// Return the current attachment type for this authenticator.
160    pub fn attachment_type(&self) -> webauthn::AuthenticatorAttachment {
161        // TODO: Make this variable depending on the transport.
162        webauthn::AuthenticatorAttachment::Platform
163    }
164
165    /// Validate `params` with the following steps
166    ///     1. For each element of `params`:
167    ///         1-2: Handled during deserialization
168    ///         3. If the element specifies an algorithm that is supported by the authenticator, and
169    ///            no algorithm has yet been chosen by this loop, then let the algorithm specified by
170    ///            the current element be the chosen algorithm.
171    ///     2. If the loop completes and no algorithm was chosen then return [`Ctap2Error::UnsupportedAlgorithm`].
172    /// Note: This loop chooses the first occurrence of an algorithm identifier supported by this
173    ///       authenticator but always iterates over every element of `params` to validate them.
174    pub fn choose_algorithm(
175        &self,
176        params: &[webauthn::PublicKeyCredentialParameters],
177    ) -> Result<iana::Algorithm, Ctap2Error> {
178        params
179            .iter()
180            .find(|param| self.algs.contains(&param.alg))
181            .map(|param| param.alg)
182            .ok_or(Ctap2Error::UnsupportedAlgorithm)
183    }
184
185    /// Builder method for overwriting the authenticator's supported transports.
186    pub fn transports(self, transports: Vec<webauthn::AuthenticatorTransport>) -> Self {
187        Self { transports, ..self }
188    }
189
190    /// Collect user consent if required. This step MUST happen before the following steps due
191    ///    to privacy reasons (i.e., authenticator cannot disclose existence of a credential
192    ///    until the user interacted with the device):
193    ///     1. If the "uv" option was specified and set to true:
194    ///         1. If device doesn’t support user-identifiable gestures, return the
195    ///            CTAP2_ERR_UNSUPPORTED_OPTION error.
196    ///         2. Collect a user-identifiable gesture. If gesture validation fails, return the
197    ///            CTAP2_ERR_OPERATION_DENIED error.
198    ///     2. If the "up" option was specified and set to true, collect the user’s consent.
199    ///         1. If no consent is obtained and a timeout occurs, return the
200    ///            CTAP2_ERR_OPERATION_DENIED error.
201    async fn check_user(
202        &self,
203        options: &passkey_types::ctap2::make_credential::Options,
204        credential: Option<&<U as UserValidationMethod>::PasskeyItem>,
205    ) -> Result<Flags, Ctap2Error> {
206        if options.uv && self.user_validation.is_verification_enabled() != Some(true) {
207            return Err(Ctap2Error::UnsupportedOption);
208        };
209
210        let check_result = self
211            .user_validation
212            .check_user(credential, options.up, options.uv)
213            .await?;
214
215        if options.up && !check_result.presence {
216            return Err(Ctap2Error::OperationDenied);
217        }
218
219        if options.uv && !check_result.verification {
220            return Err(Ctap2Error::OperationDenied);
221        }
222
223        let mut flags = Flags::empty();
224        if check_result.presence {
225            flags |= Flags::UP;
226        }
227
228        if check_result.verification {
229            flags |= Flags::UV;
230        }
231
232        Ok(flags)
233    }
234
235    /// Set the hmac-secret extension as a supported extension
236    pub fn hmac_secret(mut self, ext: extensions::HmacSecretConfig) -> Self {
237        self.extensions.hmac_secret = Some(ext);
238        self
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use passkey_types::ctap2::{Aaguid, Flags};
245
246    use crate::{Authenticator, CredentialIdLength, MockUserValidationMethod, UserCheck};
247
248    #[tokio::test]
249    async fn check_user_does_not_check_up_or_uv_when_not_requested() {
250        // Arrange & Assert
251        let mut user_mock = MockUserValidationMethod::new();
252        user_mock
253            .expect_check_user()
254            .with(
255                mockall::predicate::always(),
256                mockall::predicate::eq(false),
257                mockall::predicate::eq(false),
258            )
259            .returning(|_, _, _| {
260                Ok(UserCheck {
261                    presence: false,
262                    verification: false,
263                })
264            })
265            .once();
266
267        // Arrange
268        let store = None;
269        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
270        let options = passkey_types::ctap2::make_credential::Options {
271            up: false,
272            uv: false,
273            ..Default::default()
274        };
275
276        // Act
277        let result = authenticator.check_user(&options, None).await.unwrap();
278
279        // Assert
280        assert_eq!(result, Flags::empty());
281    }
282
283    #[tokio::test]
284    async fn check_user_checks_up_when_requested() {
285        // Arrange & Assert
286        let mut user_mock = MockUserValidationMethod::new();
287        user_mock
288            .expect_check_user()
289            .with(
290                mockall::predicate::always(),
291                mockall::predicate::eq(true),
292                mockall::predicate::eq(false),
293            )
294            .returning(|_, _, _| {
295                Ok(UserCheck {
296                    presence: true,
297                    verification: false,
298                })
299            })
300            .once();
301
302        // Arrange
303        let store = None;
304        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
305        let options = passkey_types::ctap2::make_credential::Options {
306            up: true,
307            uv: false,
308            ..Default::default()
309        };
310
311        // Act
312        let result = authenticator.check_user(&options, None).await.unwrap();
313
314        // Assert
315        assert_eq!(result, Flags::UP);
316    }
317
318    #[tokio::test]
319    async fn check_user_checks_uv_when_requested() {
320        // Arrange & Assert
321        let mut user_mock = MockUserValidationMethod::new();
322        user_mock
323            .expect_is_verification_enabled()
324            .returning(|| Some(true));
325        user_mock
326            .expect_check_user()
327            .with(
328                mockall::predicate::always(),
329                mockall::predicate::eq(true),
330                mockall::predicate::eq(true),
331            )
332            .returning(|_, _, _| {
333                Ok(UserCheck {
334                    presence: true,
335                    verification: true,
336                })
337            })
338            .once();
339
340        // Arrange
341        let store = None;
342        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
343        let options = passkey_types::ctap2::make_credential::Options {
344            up: true,
345            uv: true,
346            ..Default::default()
347        };
348
349        // Act
350        let result = authenticator.check_user(&options, None).await.unwrap();
351
352        // Assert
353        assert_eq!(result, Flags::UP | Flags::UV);
354    }
355
356    #[tokio::test]
357    async fn check_user_returns_operation_denied_when_up_was_requested_but_not_returned() {
358        // Arrange & Assert
359        let mut user_mock = MockUserValidationMethod::new();
360        user_mock
361            .expect_check_user()
362            .with(
363                mockall::predicate::always(),
364                mockall::predicate::eq(true),
365                mockall::predicate::eq(false),
366            )
367            .returning(|_, _, _| {
368                Ok(UserCheck {
369                    presence: false,
370                    verification: false,
371                })
372            })
373            .once();
374
375        // Arrange
376        let store = None;
377        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
378        let options = passkey_types::ctap2::make_credential::Options {
379            up: true,
380            uv: false,
381            ..Default::default()
382        };
383
384        // Act
385        let result = authenticator.check_user(&options, None).await;
386
387        // Assert
388        assert_eq!(
389            result,
390            Err(passkey_types::ctap2::Ctap2Error::OperationDenied)
391        );
392    }
393
394    #[tokio::test]
395    async fn check_user_returns_operation_denied_when_uv_was_requested_but_not_returned() {
396        // Arrange & Assert
397        let mut user_mock = MockUserValidationMethod::new();
398        user_mock
399            .expect_is_verification_enabled()
400            .returning(|| Some(true));
401        user_mock
402            .expect_check_user()
403            .with(
404                mockall::predicate::always(),
405                mockall::predicate::eq(true),
406                mockall::predicate::eq(true),
407            )
408            .returning(|_, _, _| {
409                Ok(UserCheck {
410                    presence: true,
411                    verification: false,
412                })
413            })
414            .once();
415
416        // Arrange
417        let store = None;
418        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
419        let options = passkey_types::ctap2::make_credential::Options {
420            up: true,
421            uv: true,
422            ..Default::default()
423        };
424
425        // Act
426        let result = authenticator.check_user(&options, None).await;
427
428        // Assert
429        assert_eq!(
430            result,
431            Err(passkey_types::ctap2::Ctap2Error::OperationDenied)
432        );
433    }
434
435    #[tokio::test]
436    async fn check_user_returns_unsupported_option_when_uv_was_requested_but_is_not_supported() {
437        // Arrange & Assert
438        let mut user_mock = MockUserValidationMethod::new();
439        user_mock
440            .expect_is_verification_enabled()
441            .returning(|| None);
442
443        // Arrange
444        let store = None;
445        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
446        let options = passkey_types::ctap2::make_credential::Options {
447            up: true,
448            uv: true,
449            ..Default::default()
450        };
451
452        // Act
453        let result = authenticator.check_user(&options, None).await;
454
455        // Assert
456        assert_eq!(
457            result,
458            Err(passkey_types::ctap2::Ctap2Error::UnsupportedOption)
459        );
460    }
461
462    #[tokio::test]
463    async fn check_user_returns_up_and_uv_flags_when_neither_up_or_uv_was_requested_but_performed_anyways(
464    ) {
465        // Arrange & Assert
466        let mut user_mock = MockUserValidationMethod::new();
467        user_mock
468            .expect_is_verification_enabled()
469            .returning(|| Some(true));
470        user_mock
471            .expect_check_user()
472            .with(
473                mockall::predicate::always(),
474                mockall::predicate::eq(false),
475                mockall::predicate::eq(false),
476            )
477            .returning(|_, _, _| {
478                Ok(UserCheck {
479                    presence: true,
480                    verification: true,
481                })
482            })
483            .once();
484
485        // Arrange
486        let store = None;
487        let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
488        let options = passkey_types::ctap2::make_credential::Options {
489            up: false,
490            uv: false,
491            ..Default::default()
492        };
493
494        // Act
495        let result = authenticator.check_user(&options, None).await.unwrap();
496
497        // Assert
498        assert_eq!(result, Flags::UP | Flags::UV);
499    }
500
501    #[test]
502    fn credential_id_lengths_validate() {
503        for num in 0..u8::MAX {
504            let length = CredentialIdLength::from(num);
505            if !(16..=64).contains(&num) {
506                if num < 16 {
507                    // Lower values should be rounded up.
508                    assert_eq!(length.0, CredentialIdLength::DEFAULT.0);
509                } else {
510                    // Higher values should be clamped
511                    assert_eq!(length.0, 64);
512                }
513            }
514        }
515
516        assert_eq!(
517            CredentialIdLength::DEFAULT.0,
518            CredentialIdLength::default().0
519        );
520    }
521
522    #[test]
523    fn credential_id_generation() {
524        let mut rng = rand::thread_rng();
525        let valid_range = 0..=64;
526        for _ in 0..=100 {
527            let length = CredentialIdLength::randomized(&mut rng).0;
528            assert!(valid_range.contains(&length));
529        }
530    }
531}