Skip to main content

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(&param.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;