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