mod client_data;
pub use client_data::*;
use std::{borrow::Cow, fmt::Display, marker::PhantomData};
use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod};
use passkey_crypto::{
CryptoBackend, PublicKeyT, SecretKeyT, coset::Algorithm, hash::Sha256Backend, iana::EnumI64,
};
use passkey_types::{
Bytes, Passkey, ctap2, encoding,
webauthn::{
self, AuthenticatorSelectionCriteria, ResidentKeyRequirement, UserVerificationRequirement,
},
};
use serde::Serialize;
#[cfg(feature = "typeshare")]
use typeshare::typeshare;
use url::Url;
mod extensions;
mod rp_id_verifier;
#[cfg(all(feature = "linux", target_os = "linux"))]
pub mod linux;
#[cfg(all(feature = "windows", target_os = "windows"))]
pub mod windows;
pub use self::rp_id_verifier::{Fetcher, RelatedOriginResponse, RpIdVerifier};
#[cfg(feature = "android-asset-validation")]
pub use self::rp_id_verifier::android::{UnverifiedAssetLink, ValidationError, valid_fingerprint};
#[cfg(test)]
mod tests;
#[cfg_attr(feature = "typeshare", typeshare)]
#[derive(Debug, serde::Serialize, PartialEq, Eq)]
#[serde(tag = "type", content = "content")]
#[non_exhaustive]
pub enum WebauthnError {
CredentialIdTooLong,
OriginMissingDomain,
OriginRpMissmatch,
UnprotectedOrigin,
InsecureLocalhostNotAllowed,
CredentialNotFound,
InvalidRpId,
AuthenticatorError(u8),
NotSupportedError,
SyntaxError,
ValidationError {
context: &'static str,
},
RequiresRelatedOriginsSupport,
FetcherError,
RedirectError,
ExceedsMaxLabelLimit,
SerializationError,
TimeoutError,
}
const MAX_CREDENTIAL_ID_LENGTH: usize = 1023;
#[cfg(feature = "tokio")]
const MIN_TIMEOUT: u32 = 300_000;
#[cfg(feature = "tokio")]
const MAX_TIMEOUT: u32 = 600_000;
impl WebauthnError {
pub fn is_vendor_error(&self) -> bool {
matches!(self, WebauthnError::AuthenticatorError(ctap_error) if ctap2::VendorError::try_from(*ctap_error).is_ok())
}
}
impl From<ctap2::StatusCode> for WebauthnError {
fn from(value: ctap2::StatusCode) -> Self {
match value {
ctap2::StatusCode::Known(ctap2::Ctap2Error::NoCredentials) => {
WebauthnError::CredentialNotFound
}
ctap2code => WebauthnError::AuthenticatorError(ctap2code.into()),
}
}
}
pub(crate) fn build_client_data<D, E, C>(
client_data: &D,
ty: webauthn::ClientDataType,
challenge: &[u8],
origin: &Origin<'_>,
_crypto: PhantomData<C>,
) -> Result<(String, Vec<u8>), WebauthnError>
where
D: ClientData<E>,
E: Serialize + Clone,
C: CryptoBackend,
{
let collected_client_data = webauthn::CollectedClientData::<E> {
ty,
challenge: encoding::base64url(challenge),
origin: origin.to_string(),
cross_origin: None,
extra_data: client_data.extra_client_data(),
unknown_keys: Default::default(),
};
let client_data_json = serde_json::to_string(&collected_client_data)
.map_err(|_| WebauthnError::SerializationError)?;
let client_data_hash = client_data
.client_data_hash()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| C::Sha256::sha256(client_data_json.as_bytes()).to_vec());
Ok((client_data_json, client_data_hash))
}
pub(crate) fn map_rk(
criteria: Option<&AuthenticatorSelectionCriteria>,
auth_info: &ctap2::get_info::Response,
) -> bool {
let supports_rk = auth_info.options.as_ref().is_some_and(|o| o.rk);
match criteria.unwrap_or(&Default::default()) {
AuthenticatorSelectionCriteria {
resident_key: Some(ResidentKeyRequirement::Required),
..
} => true,
AuthenticatorSelectionCriteria {
resident_key: Some(ResidentKeyRequirement::Preferred),
..
} => supports_rk,
AuthenticatorSelectionCriteria {
resident_key: Some(ResidentKeyRequirement::Discouraged),
..
} => false,
AuthenticatorSelectionCriteria {
resident_key: None,
require_resident_key,
..
} => *require_resident_key,
}
}
pub(crate) fn ctap_uv_option(
requirement: UserVerificationRequirement,
uv_when_preferred: bool,
) -> bool {
match requirement {
UserVerificationRequirement::Discouraged => false,
UserVerificationRequirement::Required => true,
UserVerificationRequirement::Preferred => uv_when_preferred,
}
}
pub enum Origin<'a> {
Web(Cow<'a, Url>),
#[cfg(feature = "android-asset-validation")]
Android(UnverifiedAssetLink<'a>),
}
impl From<Url> for Origin<'_> {
fn from(value: Url) -> Self {
Origin::Web(Cow::Owned(value))
}
}
impl<'a> From<&'a Url> for Origin<'a> {
fn from(value: &'a Url) -> Self {
Origin::Web(Cow::Borrowed(value))
}
}
impl Display for Origin<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Origin::Web(url) => write!(f, "{}", url.as_str().trim_end_matches('/')),
#[cfg(feature = "android-asset-validation")]
Origin::Android(target_link) => {
write!(
f,
"android:apk-key-hash:{}",
encoding::base64url(target_link.sha256_cert_fingerprint())
)
}
}
}
}
pub struct Client<S, U, C, P, F>
where
S: CredentialStore + Sync,
U: UserValidationMethod + Sync,
C: CryptoBackend,
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
{
authenticator: Authenticator<S, U, C>,
rp_id_verifier: RpIdVerifier<P, F>,
uv_when_preferred: bool,
crypto: PhantomData<C>,
}
impl<S, U, C> Client<S, U, C, public_suffix::PublicSuffixList, ()>
where
S: CredentialStore + Sync,
U: UserValidationMethod + Sync,
C: CryptoBackend + Sync,
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
{
pub fn new(authenticator: Authenticator<S, U, C>) -> Self {
Self {
authenticator,
rp_id_verifier: RpIdVerifier::new(public_suffix::DEFAULT_PROVIDER, None),
uv_when_preferred: true,
crypto: PhantomData,
}
}
}
impl<S, U, C, P, F> Client<S, U, C, P, F>
where
S: CredentialStore + Sync,
U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
C: CryptoBackend,
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
F: Fetcher + Sync,
{
pub fn new_with_custom_tld_provider(
authenticator: Authenticator<S, U, C>,
custom_provider: P,
fetcher: Option<F>,
) -> Self {
Self {
authenticator,
rp_id_verifier: RpIdVerifier::new(custom_provider, fetcher),
uv_when_preferred: true,
crypto: PhantomData,
}
}
pub fn allows_insecure_localhost(mut self, is_allowed: bool) -> Self {
self.rp_id_verifier = self.rp_id_verifier.allows_insecure_localhost(is_allowed);
self
}
pub fn authenticator(&self) -> &Authenticator<S, U, C> {
&self.authenticator
}
pub fn authenticator_mut(&mut self) -> &mut Authenticator<S, U, C> {
&mut self.authenticator
}
pub fn user_verification_when_preferred(mut self, enabled: bool) -> Self {
self.uv_when_preferred = enabled;
self
}
pub async fn register<D: ClientData<E>, E: Serialize + Clone>(
&mut self,
origin: impl Into<Origin<'_>>,
request: webauthn::CredentialCreationOptions,
client_data: D,
) -> Result<webauthn::CreatedPublicKeyCredential, WebauthnError> {
let origin = origin.into();
let request = request.public_key;
let auth_info = self.authenticator.get_info().await;
let pub_key_cred_params = if request.pub_key_cred_params.is_empty() {
webauthn::PublicKeyCredentialParameters::default_algorithms()
} else {
request.pub_key_cred_params
};
#[cfg(feature = "tokio")]
let timeout = std::time::Duration::from_millis(
request
.timeout
.map(|t| t.clamp(MIN_TIMEOUT, MAX_TIMEOUT))
.unwrap_or(MIN_TIMEOUT)
.into(),
);
let rp_id = self
.rp_id_verifier
.assert_domain(&origin, request.rp.id.as_deref())
.await?;
let (client_data_json, client_data_json_hash) = build_client_data(
&client_data,
webauthn::ClientDataType::Create,
&request.challenge,
&origin,
PhantomData::<C>,
)?;
let extension_request = request.extensions.and_then(|e| e.zip_contents());
let ctap_extensions = self.registration_extension_ctap2_input(
extension_request.as_ref(),
auth_info.extensions.as_deref().unwrap_or_default(),
)?;
let rk = map_rk(request.authenticator_selection.as_ref(), &auth_info);
let uv_requirement = request
.authenticator_selection
.as_ref()
.map(|s| s.user_verification)
.unwrap_or_default();
let uv = ctap_uv_option(uv_requirement, self.uv_when_preferred);
let make_credential_request = ctap2::make_credential::Request {
client_data_hash: client_data_json_hash.into(),
rp: ctap2::make_credential::PublicKeyCredentialRpEntity {
id: rp_id.to_owned(),
name: Some(request.rp.name),
},
user: request.user,
pub_key_cred_params,
exclude_list: request.exclude_credentials,
extensions: ctap_extensions,
options: ctap2::make_credential::Options { rk, up: true, uv },
pin_auth: None,
pin_protocol: None,
};
#[cfg(feature = "tokio")]
let ctap2_response = tokio::time::timeout(
timeout,
self.authenticator.make_credential(make_credential_request),
)
.await
.map_err(|_| WebauthnError::TimeoutError)?
.map_err(|sc| WebauthnError::AuthenticatorError(sc.into()))?;
#[cfg(not(feature = "tokio"))]
let ctap2_response = self
.authenticator
.make_credential(make_credential_request)
.await
.map_err(|sc| WebauthnError::AuthenticatorError(sc.into()))?;
let credential_id = ctap2_response
.auth_data
.attested_credential_data
.as_ref()
.unwrap();
if credential_id.credential_id().len() > MAX_CREDENTIAL_ID_LENGTH {
return Err(WebauthnError::CredentialIdTooLong);
}
let alg = match credential_id.key.alg.as_ref().unwrap() {
Algorithm::PrivateUse(val) => *val,
Algorithm::Assigned(alg) => alg.to_i64(),
Algorithm::Text(_) => {
unreachable!()
}
};
let public_key = Some(
<<C as CryptoBackend>::SecretKey as SecretKeyT>::PublicKey::der_from_cose_key(
&credential_id.key,
)
.map(Bytes::from)
.map_err(|e| WebauthnError::AuthenticatorError(ctap2::Ctap2Error::from(e).into()))?,
);
let attestation_object = ctap2_response.as_webauthn_bytes();
let store_info = self.authenticator.store().get_info().await;
let client_extension_results = self.registration_extension_outputs(
extension_request.as_ref(),
store_info,
rk,
ctap2_response.unsigned_extension_outputs,
);
let response = webauthn::CreatedPublicKeyCredential {
id: encoding::base64url(credential_id.credential_id()),
raw_id: credential_id.credential_id().to_vec().into(),
ty: webauthn::PublicKeyCredentialType::PublicKey,
response: webauthn::AuthenticatorAttestationResponse {
client_data_json: Vec::from(client_data_json).into(),
authenticator_data: ctap2_response.auth_data.to_vec().into(),
public_key,
public_key_algorithm: alg,
attestation_object,
transports: auth_info.transports,
},
authenticator_attachment: Some(self.authenticator().attachment_type()),
client_extension_results,
};
Ok(response)
}
pub async fn authenticate<D: ClientData<E>, E: Serialize + Clone>(
&mut self,
origin: impl Into<Origin<'_>>,
request: webauthn::CredentialRequestOptions,
client_data: D,
) -> Result<webauthn::AuthenticatedPublicKeyCredential, WebauthnError> {
let origin = origin.into();
let request = request.public_key;
let auth_info = self.authenticator().get_info().await;
#[cfg(feature = "tokio")]
let timeout = std::time::Duration::from_millis(
request
.timeout
.map(|t| t.clamp(MIN_TIMEOUT, MAX_TIMEOUT))
.unwrap_or(MIN_TIMEOUT)
.into(),
);
let rp_id = self
.rp_id_verifier
.assert_domain(&origin, request.rp_id.as_deref())
.await?;
let (client_data_json, client_data_json_hash) = build_client_data(
&client_data,
webauthn::ClientDataType::Get,
&request.challenge,
&origin,
PhantomData::<C>,
)?;
let ctap_extensions = self.auth_extension_ctap2_input(
&request,
auth_info.extensions.unwrap_or_default().as_slice(),
)?;
let uv = ctap_uv_option(request.user_verification, self.uv_when_preferred);
let get_assertion_request = ctap2::get_assertion::Request {
rp_id: rp_id.to_owned(),
client_data_hash: client_data_json_hash.into(),
allow_list: request.allow_credentials,
extensions: ctap_extensions,
options: ctap2::get_assertion::Options { up: true, uv },
pin_auth: None,
pin_protocol: None,
};
#[cfg(feature = "tokio")]
let ctap2_response = tokio::time::timeout(
timeout,
self.authenticator.get_assertion(get_assertion_request),
)
.await
.map_err(|_| WebauthnError::TimeoutError)?
.map_err(Into::<WebauthnError>::into)?;
#[cfg(not(feature = "tokio"))]
let ctap2_response = self
.authenticator
.get_assertion(get_assertion_request)
.await
.map_err(Into::<WebauthnError>::into)?;
let client_extension_results =
self.auth_extension_outputs(ctap2_response.unsigned_extension_outputs);
let credential_id_bytes = ctap2_response.credential.unwrap().id;
Ok(webauthn::AuthenticatedPublicKeyCredential {
id: encoding::base64url(&credential_id_bytes),
raw_id: credential_id_bytes.to_vec().into(),
ty: webauthn::PublicKeyCredentialType::PublicKey,
response: webauthn::AuthenticatorAssertionResponse {
client_data_json: Vec::from(client_data_json).into(),
authenticator_data: ctap2_response.auth_data.to_vec().into(),
signature: ctap2_response.signature,
user_handle: ctap2_response.user.map(|user| user.id),
attestation_object: None,
},
authenticator_attachment: Some(self.authenticator().attachment_type()),
client_extension_results,
})
}
}