passkey_client/
lib.rs

1//! # Passkey Client
2//!
3//! [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-client)
4//! [![version]](https://crates.io/crates/passkey-client)
5//! [![documentation]](https://docs.rs/passkey-client/)
6//!
7//! This crate defines a [`Client`] type along with a basic implementation of the [Webauthn]
8//! specification. The [`Client`] uses an [`Authenticator`] to perform the actual cryptographic
9//! operations, while the Client itself marshals data to and from the structs received from the Relying Party.
10//!
11//! This crate does not provide any code to perform networking requests to and from Relying Parties.
12//!
13//! [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--client-informational?logo=github&style=flat
14//! [version]: https://img.shields.io/crates/v/passkey-client?logo=rust&style=flat
15//! [documentation]: https://img.shields.io/docsrs/passkey-client/latest?logo=docs.rs&style=flat
16//! [Webauthn]: https://w3c.github.io/webauthn/
17mod client_data;
18pub use client_data::*;
19
20use std::{borrow::Cow, fmt::Display};
21
22use coset::{Algorithm, iana::EnumI64};
23use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod};
24use passkey_types::{
25    Passkey,
26    crypto::sha256,
27    ctap2, encoding,
28    webauthn::{
29        self, AuthenticatorSelectionCriteria, ResidentKeyRequirement, UserVerificationRequirement,
30    },
31};
32use serde::Serialize;
33#[cfg(feature = "typeshare")]
34use typeshare::typeshare;
35use url::Url;
36
37mod extensions;
38mod rp_id_verifier;
39
40pub use self::rp_id_verifier::{Fetcher, RelatedOriginResponse, RpIdVerifier};
41
42#[cfg(feature = "android-asset-validation")]
43pub use self::rp_id_verifier::android::{UnverifiedAssetLink, ValidationError, valid_fingerprint};
44
45#[cfg(test)]
46mod tests;
47
48#[cfg_attr(feature = "typeshare", typeshare)]
49#[derive(Debug, serde::Serialize, PartialEq, Eq)]
50#[serde(tag = "type", content = "content")]
51/// Errors produced by Webauthn Operations.
52pub enum WebauthnError {
53    /// A credential ID can be a maximum of 1023 bytes.
54    CredentialIdTooLong,
55    /// The request origin was missing a proper domain part.
56    OriginMissingDomain,
57    /// The request origin is not a sub-domain of the RP ID.
58    OriginRpMissmatch,
59    /// The origin of the request does not use HTTPS.
60    UnprotectedOrigin,
61    /// Origin was set to localhost but allows_insecure_localhost was not set.
62    InsecureLocalhostNotAllowed,
63    /// No credential was found
64    CredentialNotFound,
65    /// The RP ID is invalid.
66    InvalidRpId,
67    /// Internal authenticator error whose value represents a `ctap2::StatusCode`
68    AuthenticatorError(u8),
69    /// The operation is not supported.
70    NotSupportedError,
71    /// The string did not match the expected pattern.
72    SyntaxError,
73    /// The input failed validation
74    ValidationError,
75    /// The given RpId has possibly rolled out related origins
76    RequiresRelatedOriginsSupport,
77    /// An error when fetching remote resources
78    FetcherError,
79    /// A redirect that was not allowed occured
80    RedirectError,
81    /// Related Origins endpoint contains a number of labels exceeding the max limit
82    ExceedsMaxLabelLimit,
83    /// JSON serialization error
84    SerializationError,
85}
86
87impl WebauthnError {
88    /// Was the error a vendor error?
89    pub fn is_vendor_error(&self) -> bool {
90        matches!(self, WebauthnError::AuthenticatorError(ctap_error) if ctap2::VendorError::try_from(*ctap_error).is_ok())
91    }
92}
93
94impl From<ctap2::StatusCode> for WebauthnError {
95    fn from(value: ctap2::StatusCode) -> Self {
96        match value {
97            ctap2::StatusCode::Ctap1(u2f) => WebauthnError::AuthenticatorError(u2f.into()),
98            ctap2::StatusCode::Ctap2(ctap2::Ctap2Code::Known(ctap2::Ctap2Error::NoCredentials)) => {
99                WebauthnError::CredentialNotFound
100            }
101            ctap2::StatusCode::Ctap2(ctap2code) => {
102                WebauthnError::AuthenticatorError(ctap2code.into())
103            }
104        }
105    }
106}
107
108/// The origin of a WebAuthn request.
109pub enum Origin<'a> {
110    /// A Url, meant for a request in the web browser.
111    Web(Cow<'a, Url>),
112    /// An android digital asset fingerprint.
113    /// Meant for a request coming from an android application.
114    #[cfg(feature = "android-asset-validation")]
115    Android(UnverifiedAssetLink<'a>),
116}
117
118impl From<Url> for Origin<'_> {
119    fn from(value: Url) -> Self {
120        Origin::Web(Cow::Owned(value))
121    }
122}
123
124impl<'a> From<&'a Url> for Origin<'a> {
125    fn from(value: &'a Url) -> Self {
126        Origin::Web(Cow::Borrowed(value))
127    }
128}
129
130impl Display for Origin<'_> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Origin::Web(url) => write!(f, "{}", url.as_str().trim_end_matches('/')),
134            #[cfg(feature = "android-asset-validation")]
135            Origin::Android(target_link) => {
136                write!(
137                    f,
138                    "android:apk-key-hash:{}",
139                    encoding::base64url(target_link.sha256_cert_fingerprint())
140                )
141            }
142        }
143    }
144}
145
146/// A `Client` represents a Webauthn client. Users of this struct should supply a
147/// [`CredentialStore`], a [`UserValidationMethod`] and, optionally, an implementation of
148/// [`public_suffix::EffectiveTLDProvider`].
149///
150/// The `tld_provider` is used to verify effective Top-Level Domains for request origins presented
151/// to the client. Most applications can use the `new()` function, which creates a `Client` with a
152/// default provider implementation. Use `new_with_custom_tld_provider()` to provide a custom
153/// `EffectiveTLDProvider` if your application needs to interpret eTLDs differently from the Mozilla
154/// Public Suffix List.
155pub struct Client<S, U, P, F>
156where
157    S: CredentialStore + Sync,
158    U: UserValidationMethod + Sync,
159    P: public_suffix::EffectiveTLDProvider + Sync + 'static,
160{
161    authenticator: Authenticator<S, U>,
162    rp_id_verifier: RpIdVerifier<P, F>,
163}
164
165impl<S, U> Client<S, U, public_suffix::PublicSuffixList, ()>
166where
167    S: CredentialStore + Sync,
168    U: UserValidationMethod + Sync,
169    Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
170{
171    /// Create a `Client` with a given `Authenticator` that uses the default
172    /// TLD verifier provided by `[public_suffix]`.
173    pub fn new(authenticator: Authenticator<S, U>) -> Self {
174        Self {
175            authenticator,
176            rp_id_verifier: RpIdVerifier::new(public_suffix::DEFAULT_PROVIDER, None),
177        }
178    }
179}
180
181impl<S, U, P, F> Client<S, U, P, F>
182where
183    S: CredentialStore + Sync,
184    U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
185    P: public_suffix::EffectiveTLDProvider + Sync + 'static,
186    F: Fetcher + Sync,
187{
188    /// Create a `Client` with a given `Authenticator` and a custom TLD provider
189    /// that implements `[public_suffix::EffectiveTLDProvider]`.
190    pub fn new_with_custom_tld_provider(
191        authenticator: Authenticator<S, U>,
192        custom_provider: P,
193        fetcher: Option<F>,
194    ) -> Self {
195        Self {
196            authenticator,
197            rp_id_verifier: RpIdVerifier::new(custom_provider, fetcher),
198        }
199    }
200
201    /// Allows the internal [RpIdVerifier] to pass through localhost requests.
202    pub fn allows_insecure_localhost(mut self, is_allowed: bool) -> Self {
203        self.rp_id_verifier = self.rp_id_verifier.allows_insecure_localhost(is_allowed);
204        self
205    }
206
207    /// Read access to the Client's `Authenticator`.
208    pub fn authenticator(&self) -> &Authenticator<S, U> {
209        &self.authenticator
210    }
211
212    /// Write access to the Client's `Authenticator`.
213    pub fn authenticator_mut(&mut self) -> &mut Authenticator<S, U> {
214        &mut self.authenticator
215    }
216
217    /// Register a webauthn `request` from the given `origin`.
218    ///
219    /// Returns either a [`webauthn::CreatedPublicKeyCredential`] on success or some [`WebauthnError`]
220    pub async fn register<D: ClientData<E>, E: Serialize + Clone>(
221        &mut self,
222        origin: impl Into<Origin<'_>>,
223        request: webauthn::CredentialCreationOptions,
224        client_data: D,
225    ) -> Result<webauthn::CreatedPublicKeyCredential, WebauthnError> {
226        let origin = origin.into();
227
228        // extract inner value of request as there is nothing else of value directly in CredentialCreationOptions
229        let request = request.public_key;
230        let auth_info = self.authenticator.get_info().await;
231
232        let pub_key_cred_params = if request.pub_key_cred_params.is_empty() {
233            webauthn::PublicKeyCredentialParameters::default_algorithms()
234        } else {
235            request.pub_key_cred_params
236        };
237        // TODO: Handle given timeout here, If the value is not within what we consider a reasonable range
238        // override to our default
239        // let timeout = request
240        //     .timeout
241        //     .map(|t| t.clamp(MIN_TIMEOUT, MAX_TIMEOUT))
242        //     .unwrap_or(MAX_TIMEOUT);
243
244        let rp_id = self
245            .rp_id_verifier
246            .assert_domain(&origin, request.rp.id.as_deref())
247            .await?;
248
249        let collected_client_data = webauthn::CollectedClientData::<E> {
250            ty: webauthn::ClientDataType::Create,
251            challenge: encoding::base64url(&request.challenge),
252            origin: origin.to_string(),
253            cross_origin: None,
254            extra_data: client_data.extra_client_data(),
255            unknown_keys: Default::default(),
256        };
257
258        let client_data_json = serde_json::to_string(&collected_client_data)
259            .map_err(|_| WebauthnError::SerializationError)?;
260        let client_data_json_hash = client_data
261            .client_data_hash()
262            .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());
263
264        let extension_request = request.extensions.and_then(|e| e.zip_contents());
265
266        let ctap_extensions = self.registration_extension_ctap2_input(
267            extension_request.as_ref(),
268            auth_info.extensions.as_deref().unwrap_or_default(),
269        )?;
270
271        let rk = self.map_rk(&request.authenticator_selection, &auth_info);
272        let uv = request.authenticator_selection.map(|s| s.user_verification)
273            != Some(UserVerificationRequirement::Discouraged);
274
275        let ctap2_response = self
276            .authenticator
277            .make_credential(ctap2::make_credential::Request {
278                client_data_hash: client_data_json_hash.into(),
279                rp: ctap2::make_credential::PublicKeyCredentialRpEntity {
280                    id: rp_id.to_owned(),
281                    name: Some(request.rp.name),
282                },
283                user: request.user,
284                pub_key_cred_params,
285                exclude_list: request.exclude_credentials,
286                extensions: ctap_extensions,
287                options: ctap2::make_credential::Options { rk, up: true, uv },
288                pin_auth: None,
289                pin_protocol: None,
290            })
291            .await
292            .map_err(|sc| WebauthnError::AuthenticatorError(sc.into()))?;
293
294        // SAFETY: this unwrap is safe because the ctap2_response was just created in make_credential()
295        // above, which currently sets auth_data.attested_credential_data unconditionally.
296        // If this fails, it's a programmer error in that the postconditions of make_credential will
297        // have changed.
298        let credential_id = ctap2_response
299            .auth_data
300            .attested_credential_data
301            .as_ref()
302            .unwrap();
303        let alg = match credential_id.key.alg.as_ref().unwrap() {
304            Algorithm::PrivateUse(val) => *val,
305            Algorithm::Assigned(alg) => alg.to_i64(),
306            Algorithm::Text(_) => {
307                unreachable!()
308            }
309        };
310        let public_key = Some(
311            passkey_authenticator::public_key_der_from_cose_key(&credential_id.key)
312                .map_err(|e| WebauthnError::AuthenticatorError(e.into()))?,
313        );
314
315        let attestation_object = ctap2_response.as_webauthn_bytes();
316        let store_info = self.authenticator.store().get_info().await;
317        let client_extension_results = self.registration_extension_outputs(
318            extension_request.as_ref(),
319            store_info,
320            rk,
321            ctap2_response.unsigned_extension_outputs,
322        );
323
324        let response = webauthn::CreatedPublicKeyCredential {
325            id: encoding::base64url(credential_id.credential_id()),
326            raw_id: credential_id.credential_id().to_vec().into(),
327            ty: webauthn::PublicKeyCredentialType::PublicKey,
328            response: webauthn::AuthenticatorAttestationResponse {
329                client_data_json: Vec::from(client_data_json).into(),
330                authenticator_data: ctap2_response.auth_data.to_vec().into(),
331                public_key,
332                public_key_algorithm: alg,
333                attestation_object,
334                transports: auth_info.transports,
335            },
336            authenticator_attachment: Some(self.authenticator().attachment_type()),
337            client_extension_results,
338        };
339
340        Ok(response)
341    }
342
343    /// Authenticate a Webauthn request.
344    ///
345    /// Returns either an [`webauthn::AuthenticatedPublicKeyCredential`] on success or some [`WebauthnError`].
346    pub async fn authenticate<D: ClientData<E>, E: Serialize + Clone>(
347        &mut self,
348        origin: impl Into<Origin<'_>>,
349        request: webauthn::CredentialRequestOptions,
350        client_data: D,
351    ) -> Result<webauthn::AuthenticatedPublicKeyCredential, WebauthnError> {
352        let origin = origin.into();
353
354        // extract inner value of request as there is nothing else of value directly in CredentialRequestOptions
355        let request = request.public_key;
356        let auth_info = self.authenticator().get_info().await;
357
358        // TODO: Handle given timeout here, If the value is not within what we consider a reasonable range
359        // override to our default
360        // let timeout = request
361        //     .timeout
362        //     .map(|t| t.clamp(MIN_TIMEOUT, MAX_TIMEOUT))
363        //     .unwrap_or(MAX_TIMEOUT);
364
365        let rp_id = self
366            .rp_id_verifier
367            .assert_domain(&origin, request.rp_id.as_deref())
368            .await?;
369
370        let collected_client_data = webauthn::CollectedClientData::<E> {
371            ty: webauthn::ClientDataType::Get,
372            challenge: encoding::base64url(&request.challenge),
373            origin: origin.to_string(),
374            cross_origin: None, //Some(false),
375            extra_data: client_data.extra_client_data(),
376            unknown_keys: Default::default(),
377        };
378
379        let client_data_json = serde_json::to_string(&collected_client_data)
380            .map_err(|_| WebauthnError::SerializationError)?;
381        let client_data_json_hash = client_data
382            .client_data_hash()
383            .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());
384
385        let ctap_extensions = self.auth_extension_ctap2_input(
386            &request,
387            auth_info.extensions.unwrap_or_default().as_slice(),
388        )?;
389        let rk = false;
390        let uv = request.user_verification != UserVerificationRequirement::Discouraged;
391
392        let ctap2_response = self
393            .authenticator
394            .get_assertion(ctap2::get_assertion::Request {
395                rp_id: rp_id.to_owned(),
396                client_data_hash: client_data_json_hash.into(),
397                allow_list: request.allow_credentials,
398                extensions: ctap_extensions,
399                options: ctap2::get_assertion::Options { rk, up: true, uv },
400                pin_auth: None,
401                pin_protocol: None,
402            })
403            .await
404            .map_err(Into::<WebauthnError>::into)?;
405
406        let client_extension_results =
407            self.auth_extension_outputs(ctap2_response.unsigned_extension_outputs);
408
409        // SAFETY: This unwrap is safe because ctap2_response was created immedately
410        // above and the postcondition of that function is that response.credential
411        // will yield a credential. If none was found, we will have already returned
412        // a WebauthnError::CredentialNotFound error from map_err in that line.
413        let credential_id_bytes = ctap2_response.credential.unwrap().id;
414        Ok(webauthn::AuthenticatedPublicKeyCredential {
415            id: encoding::base64url(&credential_id_bytes),
416            raw_id: credential_id_bytes.to_vec().into(),
417            ty: webauthn::PublicKeyCredentialType::PublicKey,
418            response: webauthn::AuthenticatorAssertionResponse {
419                client_data_json: Vec::from(client_data_json).into(),
420                authenticator_data: ctap2_response.auth_data.to_vec().into(),
421                signature: ctap2_response.signature,
422                user_handle: ctap2_response.user.map(|user| user.id),
423                attestation_object: None,
424            },
425            authenticator_attachment: Some(self.authenticator().attachment_type()),
426            client_extension_results,
427        })
428    }
429
430    fn map_rk(
431        &self,
432        criteria: &Option<AuthenticatorSelectionCriteria>,
433        auth_info: &ctap2::get_info::Response,
434    ) -> bool {
435        let supports_rk = auth_info.options.as_ref().is_some_and(|o| o.rk);
436
437        match criteria.as_ref().unwrap_or(&Default::default()) {
438            // > If pkOptions.authenticatorSelection.residentKey:
439            // > is present and set to required
440            AuthenticatorSelectionCriteria {
441                resident_key: Some(ResidentKeyRequirement::Required),
442                ..
443            // > Let requireResidentKey be true.
444            } => true,
445
446            // > is present and set to preferred
447            AuthenticatorSelectionCriteria {
448                resident_key: Some(ResidentKeyRequirement::Preferred),
449                ..
450            // >  And the authenticator is capable of client-side credential storage modality
451            //    > Let requireResidentKey be true.
452            // >  And the authenticator is not capable of client-side credential storage modality, or if the client cannot determine authenticator capability,
453            //    > Let requireResidentKey be false.
454            } => supports_rk,
455
456            // > is present and set to discouraged
457            AuthenticatorSelectionCriteria {
458                resident_key: Some(ResidentKeyRequirement::Discouraged),
459                ..
460            // > Let requireResidentKey be false.
461            } => false,
462
463            // > If pkOptions.authenticatorSelection.residentKey is not present
464            AuthenticatorSelectionCriteria {
465                resident_key: None,
466                require_resident_key,
467                ..
468            // > Let requireResidentKey be the value of pkOptions.authenticatorSelection.requireResidentKey.
469            } => *require_resident_key,
470        }
471    }
472}