1mod client_data;
18pub use client_data::*;
19
20use std::{borrow::Cow, fmt::Display, marker::PhantomData};
21
22use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod};
23use passkey_crypto::{
24 CryptoBackend, PublicKeyT, SecretKeyT, coset::Algorithm, hash::Sha256Backend, iana::EnumI64,
25};
26use passkey_types::{
27 Bytes, Passkey, 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
40#[cfg(all(feature = "linux", target_os = "linux"))]
41pub mod linux;
42#[cfg(all(feature = "windows", target_os = "windows"))]
43pub mod windows;
44
45pub use self::rp_id_verifier::{Fetcher, RelatedOriginResponse, RpIdVerifier};
46
47#[cfg(feature = "android-asset-validation")]
48pub use self::rp_id_verifier::android::{UnverifiedAssetLink, ValidationError, valid_fingerprint};
49
50#[cfg(test)]
51mod tests;
52
53#[cfg_attr(feature = "typeshare", typeshare)]
54#[derive(Debug, serde::Serialize, PartialEq, Eq)]
55#[serde(tag = "type", content = "content")]
56#[non_exhaustive]
57pub enum WebauthnError {
59 CredentialIdTooLong,
61 OriginMissingDomain,
63 OriginRpMissmatch,
65 UnprotectedOrigin,
67 InsecureLocalhostNotAllowed,
69 CredentialNotFound,
71 InvalidRpId,
73 AuthenticatorError(u8),
75 NotSupportedError,
77 SyntaxError,
79 ValidationError {
81 context: &'static str,
83 },
84 RequiresRelatedOriginsSupport,
86 FetcherError,
88 RedirectError,
90 ExceedsMaxLabelLimit,
92 SerializationError,
94 TimeoutError,
96}
97
98const MAX_CREDENTIAL_ID_LENGTH: usize = 1023;
99
100#[cfg(feature = "tokio")]
104const MIN_TIMEOUT: u32 = 300_000;
105#[cfg(feature = "tokio")]
106const MAX_TIMEOUT: u32 = 600_000;
107
108impl WebauthnError {
109 pub fn is_vendor_error(&self) -> bool {
111 matches!(self, WebauthnError::AuthenticatorError(ctap_error) if ctap2::VendorError::try_from(*ctap_error).is_ok())
112 }
113}
114
115impl From<ctap2::StatusCode> for WebauthnError {
116 fn from(value: ctap2::StatusCode) -> Self {
117 match value {
118 ctap2::StatusCode::Known(ctap2::Ctap2Error::NoCredentials) => {
119 WebauthnError::CredentialNotFound
120 }
121 ctap2code => WebauthnError::AuthenticatorError(ctap2code.into()),
122 }
123 }
124}
125
126pub(crate) fn build_client_data<D, E, C>(
131 client_data: &D,
132 ty: webauthn::ClientDataType,
133 challenge: &[u8],
134 origin: &Origin<'_>,
135 _crypto: PhantomData<C>,
136) -> Result<(String, Vec<u8>), WebauthnError>
137where
138 D: ClientData<E>,
139 E: Serialize + Clone,
140 C: CryptoBackend,
141{
142 let collected_client_data = webauthn::CollectedClientData::<E> {
143 ty,
144 challenge: encoding::base64url(challenge),
145 origin: origin.to_string(),
146 cross_origin: None,
147 extra_data: client_data.extra_client_data(),
148 unknown_keys: Default::default(),
149 };
150 let client_data_json = serde_json::to_string(&collected_client_data)
151 .map_err(|_| WebauthnError::SerializationError)?;
152 let client_data_hash = client_data
153 .client_data_hash()
154 .filter(|v| !v.is_empty())
155 .unwrap_or_else(|| C::Sha256::sha256(client_data_json.as_bytes()).to_vec());
156 Ok((client_data_json, client_data_hash))
157}
158
159pub(crate) fn map_rk(
162 criteria: Option<&AuthenticatorSelectionCriteria>,
163 auth_info: &ctap2::get_info::Response,
164) -> bool {
165 let supports_rk = auth_info.options.as_ref().is_some_and(|o| o.rk);
166
167 match criteria.unwrap_or(&Default::default()) {
168 AuthenticatorSelectionCriteria {
170 resident_key: Some(ResidentKeyRequirement::Required),
171 ..
172 } => true,
173
174 AuthenticatorSelectionCriteria {
177 resident_key: Some(ResidentKeyRequirement::Preferred),
178 ..
179 } => supports_rk,
180
181 AuthenticatorSelectionCriteria {
183 resident_key: Some(ResidentKeyRequirement::Discouraged),
184 ..
185 } => false,
186
187 AuthenticatorSelectionCriteria {
189 resident_key: None,
190 require_resident_key,
191 ..
192 } => *require_resident_key,
193 }
194}
195
196pub(crate) fn ctap_uv_option(
199 requirement: UserVerificationRequirement,
200 uv_when_preferred: bool,
201) -> bool {
202 match requirement {
203 UserVerificationRequirement::Discouraged => false,
204 UserVerificationRequirement::Required => true,
205 UserVerificationRequirement::Preferred => uv_when_preferred,
206 }
207}
208
209pub enum Origin<'a> {
211 Web(Cow<'a, Url>),
213 #[cfg(feature = "android-asset-validation")]
216 Android(UnverifiedAssetLink<'a>),
217}
218
219impl From<Url> for Origin<'_> {
220 fn from(value: Url) -> Self {
221 Origin::Web(Cow::Owned(value))
222 }
223}
224
225impl<'a> From<&'a Url> for Origin<'a> {
226 fn from(value: &'a Url) -> Self {
227 Origin::Web(Cow::Borrowed(value))
228 }
229}
230
231impl Display for Origin<'_> {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 Origin::Web(url) => write!(f, "{}", url.as_str().trim_end_matches('/')),
235 #[cfg(feature = "android-asset-validation")]
236 Origin::Android(target_link) => {
237 write!(
238 f,
239 "android:apk-key-hash:{}",
240 encoding::base64url(target_link.sha256_cert_fingerprint())
241 )
242 }
243 }
244 }
245}
246
247pub struct Client<S, U, C, P, F>
257where
258 S: CredentialStore + Sync,
259 U: UserValidationMethod + Sync,
260 C: CryptoBackend,
261 P: public_suffix::EffectiveTLDProvider + Sync + 'static,
262{
263 authenticator: Authenticator<S, U, C>,
264 rp_id_verifier: RpIdVerifier<P, F>,
265 uv_when_preferred: bool,
267 crypto: PhantomData<C>,
268}
269
270impl<S, U, C> Client<S, U, C, public_suffix::PublicSuffixList, ()>
271where
272 S: CredentialStore + Sync,
273 U: UserValidationMethod + Sync,
274 C: CryptoBackend + Sync,
275 Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
276{
277 pub fn new(authenticator: Authenticator<S, U, C>) -> Self {
280 Self {
281 authenticator,
282 rp_id_verifier: RpIdVerifier::new(public_suffix::DEFAULT_PROVIDER, None),
283 uv_when_preferred: true,
284 crypto: PhantomData,
285 }
286 }
287}
288
289impl<S, U, C, P, F> Client<S, U, C, P, F>
290where
291 S: CredentialStore + Sync,
292 U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
293 C: CryptoBackend,
294 P: public_suffix::EffectiveTLDProvider + Sync + 'static,
295 F: Fetcher + Sync,
296{
297 pub fn new_with_custom_tld_provider(
300 authenticator: Authenticator<S, U, C>,
301 custom_provider: P,
302 fetcher: Option<F>,
303 ) -> Self {
304 Self {
305 authenticator,
306 rp_id_verifier: RpIdVerifier::new(custom_provider, fetcher),
307 uv_when_preferred: true,
308 crypto: PhantomData,
309 }
310 }
311
312 pub fn allows_insecure_localhost(mut self, is_allowed: bool) -> Self {
314 self.rp_id_verifier = self.rp_id_verifier.allows_insecure_localhost(is_allowed);
315 self
316 }
317
318 pub fn authenticator(&self) -> &Authenticator<S, U, C> {
320 &self.authenticator
321 }
322
323 pub fn authenticator_mut(&mut self) -> &mut Authenticator<S, U, C> {
325 &mut self.authenticator
326 }
327
328 pub fn user_verification_when_preferred(mut self, enabled: bool) -> Self {
331 self.uv_when_preferred = enabled;
332 self
333 }
334
335 pub async fn register<D: ClientData<E>, E: Serialize + Clone>(
339 &mut self,
340 origin: impl Into<Origin<'_>>,
341 request: webauthn::CredentialCreationOptions,
342 client_data: D,
343 ) -> Result<webauthn::CreatedPublicKeyCredential, WebauthnError> {
344 let origin = origin.into();
345
346 let request = request.public_key;
348 let auth_info = self.authenticator.get_info().await;
349
350 let pub_key_cred_params = if request.pub_key_cred_params.is_empty() {
351 webauthn::PublicKeyCredentialParameters::default_algorithms()
352 } else {
353 request.pub_key_cred_params
354 };
355
356 #[cfg(feature = "tokio")]
359 let timeout = std::time::Duration::from_millis(
360 request
361 .timeout
362 .map(|t| t.clamp(MIN_TIMEOUT, MAX_TIMEOUT))
363 .unwrap_or(MIN_TIMEOUT)
364 .into(),
365 ); let rp_id = self
368 .rp_id_verifier
369 .assert_domain(&origin, request.rp.id.as_deref())
370 .await?;
371
372 let (client_data_json, client_data_json_hash) = build_client_data(
373 &client_data,
374 webauthn::ClientDataType::Create,
375 &request.challenge,
376 &origin,
377 PhantomData::<C>,
378 )?;
379
380 let extension_request = request.extensions.and_then(|e| e.zip_contents());
381
382 let ctap_extensions = self.registration_extension_ctap2_input(
383 extension_request.as_ref(),
384 auth_info.extensions.as_deref().unwrap_or_default(),
385 )?;
386
387 let rk = map_rk(request.authenticator_selection.as_ref(), &auth_info);
388 let uv_requirement = request
389 .authenticator_selection
390 .as_ref()
391 .map(|s| s.user_verification)
392 .unwrap_or_default();
393 let uv = ctap_uv_option(uv_requirement, self.uv_when_preferred);
394
395 let make_credential_request = ctap2::make_credential::Request {
396 client_data_hash: client_data_json_hash.into(),
397 rp: ctap2::make_credential::PublicKeyCredentialRpEntity {
398 id: rp_id.to_owned(),
399 name: Some(request.rp.name),
400 },
401 user: request.user,
402 pub_key_cred_params,
403 exclude_list: request.exclude_credentials,
404 extensions: ctap_extensions,
405 options: ctap2::make_credential::Options { rk, up: true, uv },
406 pin_auth: None,
407 pin_protocol: None,
408 };
409
410 #[cfg(feature = "tokio")]
411 let ctap2_response = tokio::time::timeout(
412 timeout,
413 self.authenticator.make_credential(make_credential_request),
414 )
415 .await
416 .map_err(|_| WebauthnError::TimeoutError)?
417 .map_err(|sc| WebauthnError::AuthenticatorError(sc.into()))?;
418
419 #[cfg(not(feature = "tokio"))]
420 let ctap2_response = self
421 .authenticator
422 .make_credential(make_credential_request)
423 .await
424 .map_err(|sc| WebauthnError::AuthenticatorError(sc.into()))?;
425
426 let credential_id = ctap2_response
431 .auth_data
432 .attested_credential_data
433 .as_ref()
434 .unwrap();
435
436 if credential_id.credential_id().len() > MAX_CREDENTIAL_ID_LENGTH {
439 return Err(WebauthnError::CredentialIdTooLong);
440 }
441
442 let alg = match credential_id.key.alg.as_ref().unwrap() {
443 Algorithm::PrivateUse(val) => *val,
444 Algorithm::Assigned(alg) => alg.to_i64(),
445 Algorithm::Text(_) => {
446 unreachable!()
447 }
448 };
449 let public_key = Some(
450 <<C as CryptoBackend>::SecretKey as SecretKeyT>::PublicKey::der_from_cose_key(
451 &credential_id.key,
452 )
453 .map(Bytes::from)
454 .map_err(|e| WebauthnError::AuthenticatorError(ctap2::Ctap2Error::from(e).into()))?,
455 );
456
457 let attestation_object = ctap2_response.as_webauthn_bytes();
458 let store_info = self.authenticator.store().get_info().await;
459 let client_extension_results = self.registration_extension_outputs(
460 extension_request.as_ref(),
461 store_info,
462 rk,
463 ctap2_response.unsigned_extension_outputs,
464 );
465
466 let response = webauthn::CreatedPublicKeyCredential {
467 id: encoding::base64url(credential_id.credential_id()),
468 raw_id: credential_id.credential_id().to_vec().into(),
469 ty: webauthn::PublicKeyCredentialType::PublicKey,
470 response: webauthn::AuthenticatorAttestationResponse {
471 client_data_json: Vec::from(client_data_json).into(),
472 authenticator_data: ctap2_response.auth_data.to_vec().into(),
473 public_key,
474 public_key_algorithm: alg,
475 attestation_object,
476 transports: auth_info.transports,
477 },
478 authenticator_attachment: Some(self.authenticator().attachment_type()),
479 client_extension_results,
480 };
481
482 Ok(response)
483 }
484
485 pub async fn authenticate<D: ClientData<E>, E: Serialize + Clone>(
489 &mut self,
490 origin: impl Into<Origin<'_>>,
491 request: webauthn::CredentialRequestOptions,
492 client_data: D,
493 ) -> Result<webauthn::AuthenticatedPublicKeyCredential, WebauthnError> {
494 let origin = origin.into();
495
496 let request = request.public_key;
498 let auth_info = self.authenticator().get_info().await;
499
500 #[cfg(feature = "tokio")]
501 let timeout = std::time::Duration::from_millis(
502 request
503 .timeout
504 .map(|t| t.clamp(MIN_TIMEOUT, MAX_TIMEOUT))
505 .unwrap_or(MIN_TIMEOUT)
506 .into(),
507 ); let rp_id = self
510 .rp_id_verifier
511 .assert_domain(&origin, request.rp_id.as_deref())
512 .await?;
513
514 let (client_data_json, client_data_json_hash) = build_client_data(
515 &client_data,
516 webauthn::ClientDataType::Get,
517 &request.challenge,
518 &origin,
519 PhantomData::<C>,
520 )?;
521
522 let ctap_extensions = self.auth_extension_ctap2_input(
523 &request,
524 auth_info.extensions.unwrap_or_default().as_slice(),
525 )?;
526 let uv = ctap_uv_option(request.user_verification, self.uv_when_preferred);
527
528 let get_assertion_request = ctap2::get_assertion::Request {
529 rp_id: rp_id.to_owned(),
530 client_data_hash: client_data_json_hash.into(),
531 allow_list: request.allow_credentials,
532 extensions: ctap_extensions,
533 options: ctap2::get_assertion::Options { up: true, uv },
534 pin_auth: None,
535 pin_protocol: None,
536 };
537
538 #[cfg(feature = "tokio")]
539 let ctap2_response = tokio::time::timeout(
540 timeout,
541 self.authenticator.get_assertion(get_assertion_request),
542 )
543 .await
544 .map_err(|_| WebauthnError::TimeoutError)?
545 .map_err(Into::<WebauthnError>::into)?;
546
547 #[cfg(not(feature = "tokio"))]
548 let ctap2_response = self
549 .authenticator
550 .get_assertion(get_assertion_request)
551 .await
552 .map_err(Into::<WebauthnError>::into)?;
553
554 let client_extension_results =
555 self.auth_extension_outputs(ctap2_response.unsigned_extension_outputs);
556
557 let credential_id_bytes = ctap2_response.credential.unwrap().id;
562 Ok(webauthn::AuthenticatedPublicKeyCredential {
563 id: encoding::base64url(&credential_id_bytes),
564 raw_id: credential_id_bytes.to_vec().into(),
565 ty: webauthn::PublicKeyCredentialType::PublicKey,
566 response: webauthn::AuthenticatorAssertionResponse {
567 client_data_json: Vec::from(client_data_json).into(),
568 authenticator_data: ctap2_response.auth_data.to_vec().into(),
569 signature: ctap2_response.signature,
570 user_handle: ctap2_response.user.map(|user| user.id),
571 attestation_object: None,
572 },
573 authenticator_attachment: Some(self.authenticator().attachment_type()),
574 client_extension_results,
575 })
576 }
577}