mod client_data;
pub use client_data::*;
use std::{borrow::Cow, fmt::Display};
use coset::{Algorithm, iana::EnumI64};
use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod};
use passkey_types::{
Passkey,
crypto::sha256,
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;
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")]
pub enum WebauthnError {
CredentialIdTooLong,
OriginMissingDomain,
OriginRpMissmatch,
UnprotectedOrigin,
InsecureLocalhostNotAllowed,
CredentialNotFound,
InvalidRpId,
AuthenticatorError(u8),
NotSupportedError,
SyntaxError,
ValidationError,
RequiresRelatedOriginsSupport,
FetcherError,
RedirectError,
ExceedsMaxLabelLimit,
SerializationError,
}
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::Ctap1(u2f) => WebauthnError::AuthenticatorError(u2f.into()),
ctap2::StatusCode::Ctap2(ctap2::Ctap2Code::Known(ctap2::Ctap2Error::NoCredentials)) => {
WebauthnError::CredentialNotFound
}
ctap2::StatusCode::Ctap2(ctap2code) => {
WebauthnError::AuthenticatorError(ctap2code.into())
}
}
}
}
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, P, F>
where
S: CredentialStore + Sync,
U: UserValidationMethod + Sync,
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
{
authenticator: Authenticator<S, U>,
rp_id_verifier: RpIdVerifier<P, F>,
}
impl<S, U> Client<S, U, public_suffix::PublicSuffixList, ()>
where
S: CredentialStore + Sync,
U: UserValidationMethod + Sync,
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
{
pub fn new(authenticator: Authenticator<S, U>) -> Self {
Self {
authenticator,
rp_id_verifier: RpIdVerifier::new(public_suffix::DEFAULT_PROVIDER, None),
}
}
}
impl<S, U, P, F> Client<S, U, P, F>
where
S: CredentialStore + Sync,
U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
F: Fetcher + Sync,
{
pub fn new_with_custom_tld_provider(
authenticator: Authenticator<S, U>,
custom_provider: P,
fetcher: Option<F>,
) -> Self {
Self {
authenticator,
rp_id_verifier: RpIdVerifier::new(custom_provider, fetcher),
}
}
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> {
&self.authenticator
}
pub fn authenticator_mut(&mut self) -> &mut Authenticator<S, U> {
&mut self.authenticator
}
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
};
let rp_id = self
.rp_id_verifier
.assert_domain(&origin, request.rp.id.as_deref())
.await?;
let collected_client_data = webauthn::CollectedClientData::<E> {
ty: webauthn::ClientDataType::Create,
challenge: encoding::base64url(&request.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_json_hash = client_data
.client_data_hash()
.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());
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 = self.map_rk(&request.authenticator_selection, &auth_info);
let uv = request.authenticator_selection.map(|s| s.user_verification)
!= Some(UserVerificationRequirement::Discouraged);
let ctap2_response = self
.authenticator
.make_credential(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,
})
.await
.map_err(|sc| WebauthnError::AuthenticatorError(sc.into()))?;
let credential_id = ctap2_response
.auth_data
.attested_credential_data
.as_ref()
.unwrap();
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(
passkey_authenticator::public_key_der_from_cose_key(&credential_id.key)
.map_err(|e| WebauthnError::AuthenticatorError(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;
let rp_id = self
.rp_id_verifier
.assert_domain(&origin, request.rp_id.as_deref())
.await?;
let collected_client_data = webauthn::CollectedClientData::<E> {
ty: webauthn::ClientDataType::Get,
challenge: encoding::base64url(&request.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_json_hash = client_data
.client_data_hash()
.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());
let ctap_extensions = self.auth_extension_ctap2_input(
&request,
auth_info.extensions.unwrap_or_default().as_slice(),
)?;
let rk = false;
let uv = request.user_verification != UserVerificationRequirement::Discouraged;
let ctap2_response = self
.authenticator
.get_assertion(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 { rk, up: true, uv },
pin_auth: None,
pin_protocol: None,
})
.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,
})
}
fn map_rk(
&self,
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.as_ref().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,
}
}
}