@@ -13,6 +13,59 @@ mod make_credential;
1313
1414use extensions:: Extensions ;
1515
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) ]
25+ pub struct CredentialIdLength ( u8 ) ;
26+
27+ impl 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+
47+ impl Default for CredentialIdLength {
48+ fn default ( ) -> Self {
49+ Self :: DEFAULT
50+ }
51+ }
52+
53+ impl 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+
63+ impl From < CredentialIdLength > for usize {
64+ fn from ( value : CredentialIdLength ) -> Self {
65+ usize:: from ( value. 0 )
66+ }
67+ }
68+
1669/// A virtual authenticator with all the necessary state and information.
1770pub struct Authenticator < S , U > {
1871 /// The authenticator's AAGUID
@@ -35,6 +88,9 @@ pub struct Authenticator<S, U> {
3588 /// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
3689 make_credentials_with_signature_counter : bool ,
3790
91+ /// The length of the credentialId made during a creation operation.
92+ credential_id_length : CredentialIdLength ,
93+
3894 /// Supported authenticator extensions
3995 extensions : Extensions ,
4096}
57113 ] ,
58114 user_validation : user,
59115 make_credentials_with_signature_counter : false ,
116+ credential_id_length : CredentialIdLength :: default ( ) ,
60117 extensions : Extensions :: default ( ) ,
61118 }
62119 }
@@ -84,6 +141,16 @@ where
84141 self . make_credentials_with_signature_counter
85142 }
86143
144+ /// Set the length of credentialId to generate when creating a new credential.
145+ pub fn set_make_credential_id_length ( & mut self , length : CredentialIdLength ) {
146+ self . credential_id_length = length;
147+ }
148+
149+ /// Get the current length of credential that will be generated when making a new credential.
150+ pub fn make_credential_id_length ( & self ) -> CredentialIdLength {
151+ self . credential_id_length
152+ }
153+
87154 /// Access the [`CredentialStore`] to look into what is stored.
88155 pub fn store ( & self ) -> & S {
89156 & self . store
@@ -186,7 +253,7 @@ where
186253mod tests {
187254 use passkey_types:: ctap2:: { Aaguid , Flags } ;
188255
189- use crate :: { Authenticator , MockUserValidationMethod , UserCheck } ;
256+ use crate :: { Authenticator , CredentialIdLength , MockUserValidationMethod , UserCheck } ;
190257
191258 #[ tokio:: test]
192259 async fn check_user_does_not_check_up_or_uv_when_not_requested ( ) {
@@ -440,4 +507,35 @@ mod tests {
440507 // Assert
441508 assert_eq ! ( result, Flags :: UP | Flags :: UV ) ;
442509 }
510+
511+ #[ test]
512+ fn credential_id_lengths_validate ( ) {
513+ for num in 0 ..u8:: MAX {
514+ let length = CredentialIdLength :: from ( num) ;
515+ if !( 16 ..=64 ) . contains ( & num) {
516+ if num < 16 {
517+ // Lower values should be rounded up.
518+ assert_eq ! ( length. 0 , CredentialIdLength :: DEFAULT . 0 ) ;
519+ } else {
520+ // Higher values should be clamped
521+ assert_eq ! ( length. 0 , 64 ) ;
522+ }
523+ }
524+ }
525+
526+ assert_eq ! (
527+ CredentialIdLength :: DEFAULT . 0 ,
528+ CredentialIdLength :: default ( ) . 0
529+ ) ;
530+ }
531+
532+ #[ test]
533+ fn credential_id_generation ( ) {
534+ let mut rng = rand:: thread_rng ( ) ;
535+ let valid_range = 0 ..=64 ;
536+ for _ in 0 ..=100 {
537+ let length = CredentialIdLength :: randomized ( & mut rng) . 0 ;
538+ assert ! ( valid_range. contains( & length) ) ;
539+ }
540+ }
443541}
0 commit comments