passkey_authenticator/authenticator.rs
1use coset::iana;
2use passkey_types::{
3 ctap2::{Aaguid, Ctap2Error, Flags},
4 webauthn,
5};
6
7use crate::{CredentialStore, UserValidationMethod, user_validation::UiHint};
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 [`webauthn::AuthenticatorTransport::Internal`] and
80 /// [`webauthn::AuthenticatorTransport::Hybrid`].
81 transports: Vec<webauthn::AuthenticatorTransport>,
82 /// Provider of user verification factor.
83 user_validation: U,
84
85 /// Value to control whether the authenticator will save new credentials with a signature counter.
86 /// The default value is `false`.
87 ///
88 /// NOTE: Using a counter with a credential that will sync is not recommended and can cause friction
89 /// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
90 make_credentials_with_signature_counter: bool,
91
92 /// The length of the credentialId made during a creation operation.
93 credential_id_length: CredentialIdLength,
94
95 /// Supported authenticator extensions
96 extensions: Extensions,
97}
98
99impl<S, U> Authenticator<S, U>
100where
101 S: CredentialStore,
102 U: UserValidationMethod,
103{
104 /// Create an authenticator with a known aaguid, a backing storage and a User verification system.
105 pub fn new(aaguid: Aaguid, store: S, user: U) -> Self {
106 Self {
107 aaguid,
108 store,
109 // TODO: Change this to a method on the cryptographic backend
110 algs: vec![iana::Algorithm::ES256],
111 transports: vec![
112 webauthn::AuthenticatorTransport::Internal,
113 webauthn::AuthenticatorTransport::Hybrid,
114 ],
115 user_validation: user,
116 make_credentials_with_signature_counter: false,
117 credential_id_length: CredentialIdLength::default(),
118 extensions: Extensions::default(),
119 }
120 }
121
122 /// Set whether the authenticator should save new credentials with a signature counter.
123 ///
124 /// NOTE: Using a counter with a credential that will sync is not recommended and can cause friction
125 /// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
126 pub fn set_make_credentials_with_signature_counter(&mut self, value: bool) {
127 self.make_credentials_with_signature_counter = value;
128 }
129
130 /// Get whether the authenticator will save new credentials with a signature counter.
131 pub fn make_credentials_with_signature_counter(&self) -> bool {
132 self.make_credentials_with_signature_counter
133 }
134
135 /// Set the length of credentialId to generate when creating a new credential.
136 pub fn set_make_credential_id_length(&mut self, length: CredentialIdLength) {
137 self.credential_id_length = length;
138 }
139
140 /// Get the current length of credential that will be generated when making a new credential.
141 pub fn make_credential_id_length(&self) -> CredentialIdLength {
142 self.credential_id_length
143 }
144
145 /// Access the [`CredentialStore`] to look into what is stored.
146 pub fn store(&self) -> &S {
147 &self.store
148 }
149
150 /// Exclusively access the [`CredentialStore`] to look into what is stored and modify it if needed.
151 pub fn store_mut(&mut self) -> &mut S {
152 &mut self.store
153 }
154
155 /// Access the authenticator's [`Aaguid`]
156 pub fn aaguid(&self) -> &Aaguid {
157 &self.aaguid
158 }
159
160 /// Return the current attachment type for this authenticator.
161 pub fn attachment_type(&self) -> webauthn::AuthenticatorAttachment {
162 // TODO: Make this variable depending on the transport.
163 webauthn::AuthenticatorAttachment::Platform
164 }
165
166 /// Validate `params` with the following steps
167 /// 1. For each element of `params`:
168 /// 1-2: Handled during deserialization
169 /// 3. If the element specifies an algorithm that is supported by the authenticator, and
170 /// no algorithm has yet been chosen by this loop, then let the algorithm specified by
171 /// the current element be the chosen algorithm.
172 /// 2. If the loop completes and no algorithm was chosen then return [`Ctap2Error::UnsupportedAlgorithm`].
173 /// Note: This loop chooses the first occurrence of an algorithm identifier supported by this
174 /// authenticator but always iterates over every element of `params` to validate them.
175 pub fn choose_algorithm(
176 &self,
177 params: &[webauthn::PublicKeyCredentialParameters],
178 ) -> Result<iana::Algorithm, Ctap2Error> {
179 params
180 .iter()
181 .find(|param| self.algs.contains(¶m.alg))
182 .map(|param| param.alg)
183 .ok_or(Ctap2Error::UnsupportedAlgorithm)
184 }
185
186 /// Builder method for overwriting the authenticator's supported transports.
187 pub fn transports(self, transports: Vec<webauthn::AuthenticatorTransport>) -> Self {
188 Self { transports, ..self }
189 }
190
191 /// Collect user consent if required. This step MUST happen before the following steps due
192 /// to privacy reasons (i.e., authenticator cannot disclose existence of a credential
193 /// until the user interacted with the device):
194 /// 1. If the "uv" option was specified and set to true:
195 /// 1. If device doesn’t support user-identifiable gestures, return the
196 /// CTAP2_ERR_UNSUPPORTED_OPTION error.
197 /// 2. Collect a user-identifiable gesture. If gesture validation fails, return the
198 /// CTAP2_ERR_OPERATION_DENIED error.
199 /// 2. If the "up" option was specified and set to true, collect the user’s consent.
200 /// 1. If no consent is obtained and a timeout occurs, return the
201 /// CTAP2_ERR_OPERATION_DENIED error.
202 async fn check_user(
203 &self,
204 hint: UiHint<'_, <U as UserValidationMethod>::PasskeyItem>,
205 options: &passkey_types::ctap2::make_credential::Options,
206 ) -> Result<Flags, Ctap2Error> {
207 if options.uv && self.user_validation.is_verification_enabled() != Some(true) {
208 return Err(Ctap2Error::UnsupportedOption);
209 };
210
211 let check_result = self
212 .user_validation
213 .check_user(hint, options.up, options.uv)
214 .await?;
215
216 if options.up && !check_result.presence {
217 return Err(Ctap2Error::OperationDenied);
218 }
219
220 if options.uv && !check_result.verification {
221 return Err(Ctap2Error::OperationDenied);
222 }
223
224 let mut flags = Flags::empty();
225 if check_result.presence {
226 flags |= Flags::UP;
227 }
228
229 if check_result.verification {
230 flags |= Flags::UV;
231 }
232
233 Ok(flags)
234 }
235
236 /// Set the hmac-secret extension as a supported extension
237 pub fn hmac_secret(mut self, ext: extensions::HmacSecretConfig) -> Self {
238 self.extensions.hmac_secret = Some(ext);
239 self
240 }
241}
242
243#[cfg(test)]
244mod tests;