1mod 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")]
51pub enum WebauthnError {
53 CredentialIdTooLong,
55 OriginMissingDomain,
57 OriginRpMissmatch,
59 UnprotectedOrigin,
61 InsecureLocalhostNotAllowed,
63 CredentialNotFound,
65 InvalidRpId,
67 AuthenticatorError(u8),
69 NotSupportedError,
71 SyntaxError,
73 ValidationError,
75 RequiresRelatedOriginsSupport,
77 FetcherError,
79 RedirectError,
81 ExceedsMaxLabelLimit,
83 SerializationError,
85}
86
87impl WebauthnError {
88 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
108pub enum Origin<'a> {
110 Web(Cow<'a, Url>),
112 #[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
146pub 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 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 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 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 pub fn authenticator(&self) -> &Authenticator<S, U> {
209 &self.authenticator
210 }
211
212 pub fn authenticator_mut(&mut self) -> &mut Authenticator<S, U> {
214 &mut self.authenticator
215 }
216
217 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 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 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 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 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 let request = request.public_key;
356 let auth_info = self.authenticator().get_info().await;
357
358 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, 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 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 AuthenticatorSelectionCriteria {
441 resident_key: Some(ResidentKeyRequirement::Required),
442 ..
443 } => true,
445
446 AuthenticatorSelectionCriteria {
448 resident_key: Some(ResidentKeyRequirement::Preferred),
449 ..
450 } => supports_rk,
455
456 AuthenticatorSelectionCriteria {
458 resident_key: Some(ResidentKeyRequirement::Discouraged),
459 ..
460 } => false,
462
463 AuthenticatorSelectionCriteria {
465 resident_key: None,
466 require_resident_key,
467 ..
468 } => *require_resident_key,
470 }
471 }
472}