#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(warnings)]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
#[macro_use]
extern crate tracing;
mod interface;
use std::time::Duration;
use url::Url;
use uuid::Uuid;
use webauthn_rs_core::error::{WebauthnError, WebauthnResult};
use webauthn_rs_core::proto::*;
use webauthn_rs_core::WebauthnCore;
use crate::interface::*;
pub mod fake {
pub use webauthn_rs_core::fake::*;
}
pub mod prelude {
pub use crate::interface::*;
pub use crate::{Webauthn, WebauthnBuilder};
pub use base64urlsafedata::Base64UrlSafeData;
pub use url::Url;
pub use uuid::Uuid;
pub use webauthn_rs_core::error::{WebauthnError, WebauthnResult};
#[cfg(feature = "danger-credential-internals")]
pub use webauthn_rs_core::proto::Credential;
pub use webauthn_rs_core::proto::{
AttestationCa, AttestationCaList, AttestationCaListBuilder, AttestationFormat,
AuthenticatorAttachment,
};
pub use webauthn_rs_core::proto::{
AttestationMetadata, AuthenticationResult, AuthenticationState, CreationChallengeResponse,
CredentialID, ParsedAttestation, ParsedAttestationData, PublicKeyCredential,
RegisterPublicKeyCredential, RequestChallengeResponse,
};
pub use webauthn_rs_core::proto::{
COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, COSEKeyTypeId, COSEOKPKey, COSERSAKey,
ECDSACurve, EDDSACurve,
};
}
pub const DEFAULT_AUTHENTICATOR_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug)]
pub struct WebauthnBuilder<'a> {
rp_name: Option<&'a str>,
rp_id: &'a str,
allowed_origins: Vec<Url>,
allow_subdomains: bool,
allow_any_port: bool,
timeout: Duration,
algorithms: Vec<COSEAlgorithm>,
user_presence_only_security_keys: bool,
}
impl<'a> WebauthnBuilder<'a> {
pub fn new(rp_id: &'a str, rp_origin: &'a Url) -> WebauthnResult<Self> {
let valid = rp_origin
.domain()
.map(|effective_domain| {
effective_domain.ends_with(&format!(".{rp_id}")) || effective_domain == rp_id
})
.unwrap_or(false);
if valid {
Ok(WebauthnBuilder {
rp_name: None,
rp_id,
allowed_origins: vec![rp_origin.to_owned()],
allow_subdomains: false,
allow_any_port: false,
timeout: DEFAULT_AUTHENTICATOR_TIMEOUT,
algorithms: COSEAlgorithm::secure_algs(),
user_presence_only_security_keys: false,
})
} else {
error!("rp_id is not an effective_domain of rp_origin");
Err(WebauthnError::Configuration)
}
}
pub fn allow_subdomains(mut self, allow: bool) -> Self {
self.allow_subdomains = allow;
self
}
pub fn allow_any_port(mut self, allow: bool) -> Self {
self.allow_any_port = allow;
self
}
pub fn append_allowed_origin(mut self, origin: &Url) -> Self {
self.allowed_origins.push(origin.to_owned());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn rp_name(mut self, rp_name: &'a str) -> Self {
self.rp_name = Some(rp_name);
self
}
#[cfg(feature = "danger-user-presence-only-security-keys")]
pub fn danger_set_user_presence_only_security_keys(mut self, enable: bool) -> Self {
self.user_presence_only_security_keys = enable;
self
}
pub fn build(self) -> WebauthnResult<Webauthn> {
Ok(Webauthn {
core: WebauthnCore::new_unsafe_experts_only(
self.rp_name.unwrap_or(self.rp_id),
self.rp_id,
self.allowed_origins,
self.timeout,
Some(self.allow_subdomains),
Some(self.allow_any_port),
),
algorithms: self.algorithms,
user_presence_only_security_keys: self.user_presence_only_security_keys,
})
}
}
#[derive(Debug, Clone)]
pub struct Webauthn {
core: WebauthnCore,
algorithms: Vec<COSEAlgorithm>,
user_presence_only_security_keys: bool,
}
impl Webauthn {
pub fn get_allowed_origins(&self) -> &[Url] {
self.core.get_allowed_origins()
}
pub fn start_passkey_registration(
&self,
user_unique_id: Uuid,
user_name: &str,
user_display_name: &str,
exclude_credentials: Option<Vec<CredentialID>>,
) -> WebauthnResult<(CreationChallengeResponse, PasskeyRegistration)> {
let extensions = Some(RequestRegistrationExtensions {
cred_protect: Some(CredProtect {
credential_protection_policy: CredentialProtectionPolicy::UserVerificationRequired,
enforce_credential_protection_policy: Some(false),
}),
uvm: Some(true),
cred_props: Some(true),
min_pin_length: None,
hmac_create_secret: None,
});
let builder = self
.core
.new_challenge_register_builder(
user_unique_id.as_bytes(),
user_name,
user_display_name,
)?
.attestation(AttestationConveyancePreference::None)
.credential_algorithms(self.algorithms.clone())
.require_resident_key(false)
.authenticator_attachment(None)
.user_verification_policy(UserVerificationPolicy::Required)
.reject_synchronised_authenticators(false)
.exclude_credentials(exclude_credentials)
.hints(None)
.extensions(extensions);
self.core
.generate_challenge_register(builder)
.map(|(ccr, rs)| (ccr, PasskeyRegistration { rs }))
}
#[cfg(any(
all(doc, not(doctest)),
feature = "workaround-google-passkey-specific-issues"
))]
pub fn start_google_passkey_in_google_password_manager_only_registration(
&self,
user_unique_id: Uuid,
user_name: &str,
user_display_name: &str,
exclude_credentials: Option<Vec<CredentialID>>,
) -> WebauthnResult<(CreationChallengeResponse, PasskeyRegistration)> {
let extensions = Some(RequestRegistrationExtensions {
cred_protect: None,
uvm: Some(true),
cred_props: Some(true),
min_pin_length: None,
hmac_create_secret: None,
});
let builder = self
.core
.new_challenge_register_builder(
user_unique_id.as_bytes(),
user_name,
user_display_name,
)?
.attestation(AttestationConveyancePreference::None)
.credential_algorithms(self.algorithms.clone())
.require_resident_key(true)
.authenticator_attachment(Some(AuthenticatorAttachment::Platform))
.user_verification_policy(UserVerificationPolicy::Required)
.reject_synchronised_authenticators(false)
.exclude_credentials(exclude_credentials)
.hints(Some(vec![PublicKeyCredentialHints::ClientDevice]))
.extensions(extensions);
self.core
.generate_challenge_register(builder)
.map(|(ccr, rs)| (ccr, PasskeyRegistration { rs }))
}
pub fn finish_passkey_registration(
&self,
reg: &RegisterPublicKeyCredential,
state: &PasskeyRegistration,
) -> WebauthnResult<Passkey> {
self.core
.register_credential(reg, &state.rs, None)
.map(|cred| Passkey { cred })
}
pub fn start_passkey_authentication(
&self,
creds: &[Passkey],
) -> WebauthnResult<(RequestChallengeResponse, PasskeyAuthentication)> {
let extensions = None;
let creds = creds.iter().map(|sk| sk.cred.clone()).collect();
let policy = Some(UserVerificationPolicy::Required);
let allow_backup_eligible_upgrade = true;
let hints = None;
self.core
.new_challenge_authenticate_builder(creds, policy)
.map(|builder| {
builder
.extensions(extensions)
.allow_backup_eligible_upgrade(allow_backup_eligible_upgrade)
.hints(hints)
})
.and_then(|b| self.core.generate_challenge_authenticate(b))
.map(|(rcr, ast)| (rcr, PasskeyAuthentication { ast }))
}
pub fn finish_passkey_authentication(
&self,
reg: &PublicKeyCredential,
state: &PasskeyAuthentication,
) -> WebauthnResult<AuthenticationResult> {
self.core.authenticate_credential(reg, &state.ast)
}
pub fn start_securitykey_registration(
&self,
user_unique_id: Uuid,
user_name: &str,
user_display_name: &str,
exclude_credentials: Option<Vec<CredentialID>>,
attestation_ca_list: Option<AttestationCaList>,
ui_hint_authenticator_attachment: Option<AuthenticatorAttachment>,
) -> WebauthnResult<(CreationChallengeResponse, SecurityKeyRegistration)> {
let attestation = if let Some(ca_list) = attestation_ca_list.as_ref() {
if ca_list.is_empty() {
return Err(WebauthnError::MissingAttestationCaList);
} else {
AttestationConveyancePreference::Direct
}
} else {
AttestationConveyancePreference::None
};
let cred_protect = if self.user_presence_only_security_keys {
None
} else {
Some(CredProtect {
credential_protection_policy: CredentialProtectionPolicy::UserVerificationRequired,
enforce_credential_protection_policy: Some(false),
})
};
let extensions = Some(RequestRegistrationExtensions {
cred_protect,
uvm: Some(true),
cred_props: Some(true),
min_pin_length: None,
hmac_create_secret: None,
});
let policy = if self.user_presence_only_security_keys {
UserVerificationPolicy::Discouraged_DO_NOT_USE
} else {
UserVerificationPolicy::Preferred
};
let builder = self
.core
.new_challenge_register_builder(
user_unique_id.as_bytes(),
user_name,
user_display_name,
)?
.attestation(attestation)
.credential_algorithms(self.algorithms.clone())
.require_resident_key(false)
.authenticator_attachment(ui_hint_authenticator_attachment)
.user_verification_policy(policy)
.reject_synchronised_authenticators(false)
.exclude_credentials(exclude_credentials)
.hints(Some(vec![PublicKeyCredentialHints::SecurityKey]))
.extensions(extensions);
self.core
.generate_challenge_register(builder)
.map(|(ccr, rs)| {
(
ccr,
SecurityKeyRegistration {
rs,
ca_list: attestation_ca_list,
},
)
})
}
pub fn finish_securitykey_registration(
&self,
reg: &RegisterPublicKeyCredential,
state: &SecurityKeyRegistration,
) -> WebauthnResult<SecurityKey> {
self.core
.register_credential(reg, &state.rs, state.ca_list.as_ref())
.map(|cred| SecurityKey { cred })
}
pub fn start_securitykey_authentication(
&self,
creds: &[SecurityKey],
) -> WebauthnResult<(RequestChallengeResponse, SecurityKeyAuthentication)> {
let extensions = None;
let creds = creds.iter().map(|sk| sk.cred.clone()).collect();
let allow_backup_eligible_upgrade = false;
let policy = if self.user_presence_only_security_keys {
Some(UserVerificationPolicy::Discouraged_DO_NOT_USE)
} else {
Some(UserVerificationPolicy::Preferred)
};
let hints = Some(vec![PublicKeyCredentialHints::SecurityKey]);
self.core
.new_challenge_authenticate_builder(creds, policy)
.map(|builder| {
builder
.extensions(extensions)
.allow_backup_eligible_upgrade(allow_backup_eligible_upgrade)
.hints(hints)
})
.and_then(|b| self.core.generate_challenge_authenticate(b))
.map(|(rcr, ast)| (rcr, SecurityKeyAuthentication { ast }))
}
pub fn finish_securitykey_authentication(
&self,
reg: &PublicKeyCredential,
state: &SecurityKeyAuthentication,
) -> WebauthnResult<AuthenticationResult> {
self.core.authenticate_credential(reg, &state.ast)
}
}
#[cfg(any(all(doc, not(doctest)), feature = "attestation"))]
impl Webauthn {
pub fn start_attested_passkey_registration(
&self,
user_unique_id: Uuid,
user_name: &str,
user_display_name: &str,
exclude_credentials: Option<Vec<CredentialID>>,
attestation_ca_list: AttestationCaList,
ui_hint_authenticator_attachment: Option<AuthenticatorAttachment>,
) -> WebauthnResult<(CreationChallengeResponse, AttestedPasskeyRegistration)> {
if attestation_ca_list.is_empty() {
return Err(WebauthnError::MissingAttestationCaList);
}
let extensions = Some(RequestRegistrationExtensions {
cred_protect: Some(CredProtect {
credential_protection_policy: CredentialProtectionPolicy::UserVerificationRequired,
enforce_credential_protection_policy: Some(true),
}),
uvm: Some(true),
cred_props: Some(true),
min_pin_length: Some(true),
hmac_create_secret: Some(true),
});
let builder = self
.core
.new_challenge_register_builder(
user_unique_id.as_bytes(),
user_name,
user_display_name,
)?
.attestation(AttestationConveyancePreference::Direct)
.credential_algorithms(self.algorithms.clone())
.require_resident_key(false)
.authenticator_attachment(ui_hint_authenticator_attachment)
.user_verification_policy(UserVerificationPolicy::Required)
.reject_synchronised_authenticators(true)
.exclude_credentials(exclude_credentials)
.hints(Some(
vec![
PublicKeyCredentialHints::ClientDevice,
PublicKeyCredentialHints::SecurityKey,
],
))
.attestation_formats(Some(vec![
AttestationFormat::Packed,
AttestationFormat::Tpm,
]))
.extensions(extensions);
self.core
.generate_challenge_register(builder)
.map(|(ccr, rs)| {
(
ccr,
AttestedPasskeyRegistration {
rs,
ca_list: attestation_ca_list,
},
)
})
}
pub fn finish_attested_passkey_registration(
&self,
reg: &RegisterPublicKeyCredential,
state: &AttestedPasskeyRegistration,
) -> WebauthnResult<AttestedPasskey> {
self.core
.register_credential(reg, &state.rs, Some(&state.ca_list))
.map(|cred| AttestedPasskey { cred })
}
pub fn start_attested_passkey_authentication(
&self,
creds: &[AttestedPasskey],
) -> WebauthnResult<(RequestChallengeResponse, AttestedPasskeyAuthentication)> {
let creds = creds.iter().map(|sk| sk.cred.clone()).collect();
let extensions = Some(RequestAuthenticationExtensions {
appid: None,
uvm: Some(true),
hmac_get_secret: None,
});
let policy = Some(UserVerificationPolicy::Required);
let allow_backup_eligible_upgrade = false;
let hints = Some(vec![
PublicKeyCredentialHints::SecurityKey,
PublicKeyCredentialHints::ClientDevice,
]);
self.core
.new_challenge_authenticate_builder(creds, policy)
.map(|builder| {
builder
.extensions(extensions)
.allow_backup_eligible_upgrade(allow_backup_eligible_upgrade)
.hints(hints)
})
.and_then(|b| self.core.generate_challenge_authenticate(b))
.map(|(rcr, ast)| (rcr, AttestedPasskeyAuthentication { ast }))
}
pub fn finish_attested_passkey_authentication(
&self,
reg: &PublicKeyCredential,
state: &AttestedPasskeyAuthentication,
) -> WebauthnResult<AuthenticationResult> {
self.core.authenticate_credential(reg, &state.ast)
}
}
#[cfg(any(all(doc, not(doctest)), feature = "conditional-ui"))]
impl Webauthn {
pub fn start_discoverable_authentication(
&self,
) -> WebauthnResult<(RequestChallengeResponse, DiscoverableAuthentication)> {
let policy = Some(UserVerificationPolicy::Required);
let extensions = Some(RequestAuthenticationExtensions {
appid: None,
uvm: Some(true),
hmac_get_secret: None,
});
let allow_backup_eligible_upgrade = false;
let hints = None;
self.core
.new_challenge_authenticate_builder(Vec::with_capacity(0), policy)
.map(|builder| {
builder
.extensions(extensions)
.allow_backup_eligible_upgrade(allow_backup_eligible_upgrade)
.hints(hints)
})
.and_then(|b| self.core.generate_challenge_authenticate(b))
.map(|(mut rcr, ast)| {
rcr.mediation = Some(Mediation::Conditional);
(rcr, DiscoverableAuthentication { ast })
})
}
pub fn identify_discoverable_authentication<'a>(
&'_ self,
reg: &'a PublicKeyCredential,
) -> WebauthnResult<(Uuid, &'a [u8])> {
let cred_id = reg.get_credential_id();
reg.get_user_unique_id()
.and_then(|b| Uuid::from_slice(b).ok())
.map(|u| (u, cred_id))
.ok_or(WebauthnError::InvalidUserUniqueId)
}
pub fn finish_discoverable_authentication(
&self,
reg: &PublicKeyCredential,
mut state: DiscoverableAuthentication,
creds: &[DiscoverableKey],
) -> WebauthnResult<AuthenticationResult> {
let creds = creds.iter().map(|dk| dk.cred.clone()).collect();
state.ast.set_allowed_credentials(creds);
self.core.authenticate_credential(reg, &state.ast)
}
}
#[cfg(any(all(doc, not(doctest)), feature = "resident-key-support"))]
impl Webauthn {
pub fn start_attested_resident_key_registration(
&self,
user_unique_id: Uuid,
user_name: &str,
user_display_name: &str,
exclude_credentials: Option<Vec<CredentialID>>,
attestation_ca_list: AttestationCaList,
ui_hint_authenticator_attachment: Option<AuthenticatorAttachment>,
) -> WebauthnResult<(CreationChallengeResponse, AttestedResidentKeyRegistration)> {
if attestation_ca_list.is_empty() {
return Err(WebauthnError::MissingAttestationCaList);
}
let extensions = Some(RequestRegistrationExtensions {
cred_protect: Some(CredProtect {
credential_protection_policy: CredentialProtectionPolicy::UserVerificationRequired,
enforce_credential_protection_policy: Some(true),
}),
uvm: Some(true),
cred_props: Some(true),
min_pin_length: Some(true),
hmac_create_secret: Some(true),
});
let builder = self
.core
.new_challenge_register_builder(
user_unique_id.as_bytes(),
user_name,
user_display_name,
)?
.attestation(AttestationConveyancePreference::Direct)
.credential_algorithms(self.algorithms.clone())
.require_resident_key(true)
.authenticator_attachment(ui_hint_authenticator_attachment)
.user_verification_policy(UserVerificationPolicy::Required)
.reject_synchronised_authenticators(true)
.exclude_credentials(exclude_credentials)
.hints(Some(
vec![
PublicKeyCredentialHints::ClientDevice,
PublicKeyCredentialHints::SecurityKey,
],
))
.attestation_formats(Some(vec![
AttestationFormat::Packed,
AttestationFormat::Tpm,
]))
.extensions(extensions);
self.core
.generate_challenge_register(builder)
.map(|(ccr, rs)| {
(
ccr,
AttestedResidentKeyRegistration {
rs,
ca_list: attestation_ca_list,
},
)
})
}
pub fn finish_attested_resident_key_registration(
&self,
reg: &RegisterPublicKeyCredential,
state: &AttestedResidentKeyRegistration,
) -> WebauthnResult<AttestedResidentKey> {
let cred = self
.core
.register_credential(reg, &state.rs, Some(&state.ca_list))?;
trace!("finish attested_resident_key -> {:?}", cred);
Ok(AttestedResidentKey { cred })
}
pub fn start_attested_resident_key_authentication(
&self,
creds: &[AttestedResidentKey],
) -> WebauthnResult<(RequestChallengeResponse, AttestedResidentKeyAuthentication)> {
let creds = creds.iter().map(|sk| sk.cred.clone()).collect();
let extensions = Some(RequestAuthenticationExtensions {
appid: None,
uvm: Some(true),
hmac_get_secret: None,
});
let policy = Some(UserVerificationPolicy::Required);
let allow_backup_eligible_upgrade = false;
let hints = Some(vec![
PublicKeyCredentialHints::SecurityKey,
PublicKeyCredentialHints::ClientDevice,
]);
self.core
.new_challenge_authenticate_builder(creds, policy)
.map(|builder| {
builder
.extensions(extensions)
.allow_backup_eligible_upgrade(allow_backup_eligible_upgrade)
.hints(hints)
})
.and_then(|b| self.core.generate_challenge_authenticate(b))
.map(|(rcr, ast)| (rcr, AttestedResidentKeyAuthentication { ast }))
}
pub fn finish_attested_resident_key_authentication(
&self,
reg: &PublicKeyCredential,
state: &AttestedResidentKeyAuthentication,
) -> WebauthnResult<AuthenticationResult> {
self.core.authenticate_credential(reg, &state.ast)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_webauthnbuilder_chrome_url() -> Result<(), Box<dyn std::error::Error>> {
use crate::prelude::*;
let rp_id = "2114c9f524d0cbd74dbe846a51c3e5b34b83ac02c5220ec5cdff751096fa25a5";
let rp_origin = Url::parse(&format!("chrome-extension://{rp_id}"))?;
eprintln!("{rp_origin:?}");
let builder = WebauthnBuilder::new(rp_id, &rp_origin)?;
eprintln!("rp_id: {:?}", builder.rp_id);
let built = builder.build()?;
eprintln!("rp_name: {}", built.core.rp_name());
Ok(())
}
}