use ciborium::value::Value as CborValue;
use ring::digest;
use serde::{Deserialize, Serialize};
use crate::passkey::{
config::{ORIGIN, PASSKEY_RP_ID, PASSKEY_USER_VERIFICATION},
errors::PasskeyError,
types::PublicKeyCredentialUserEntity,
};
use crate::utils::base64url_decode;
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticationOptions {
pub(super) challenge: String,
pub(super) timeout: u32,
pub(super) rp_id: String,
pub(super) allow_credentials: Vec<AllowCredential>,
pub(super) user_verification: String,
pub(super) auth_id: String,
}
#[derive(Serialize, Debug)]
pub(super) struct AllowCredential {
pub(super) type_: String,
pub(super) id: String,
}
#[derive(Serialize, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct AuthenticatorSelection {
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) authenticator_attachment: Option<String>,
pub(super) resident_key: String,
pub(super) user_verification: String,
pub(super) require_resident_key: bool,
}
#[allow(unused)]
#[derive(Deserialize, Debug)]
pub struct AuthenticatorResponse {
pub(super) id: String,
raw_id: String,
pub(super) response: AuthenticatorAssertionResponse,
authenticator_attachment: Option<String>,
pub(super) auth_id: String,
}
impl AuthenticatorResponse {
pub(crate) fn credential_id(&self) -> &str {
&self.id
}
#[cfg(test)]
pub(super) fn new_for_test(
id: String,
response: AuthenticatorAssertionResponse,
auth_id: String,
) -> Self {
Self {
id,
raw_id: "test_raw_id".to_string(),
response,
authenticator_attachment: None,
auth_id,
}
}
}
#[derive(Deserialize, Debug)]
pub(super) struct AuthenticatorAssertionResponse {
pub(super) client_data_json: String,
pub(super) authenticator_data: String,
pub(super) signature: String,
pub(super) user_handle: Option<String>,
}
#[derive(Serialize, Debug)]
pub(super) struct PubKeyCredParam {
#[serde(rename = "type")]
pub(super) type_: String,
pub(super) alg: i32,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RegistrationOptions {
pub(super) challenge: String,
pub(super) rp_id: String,
pub(super) rp: RelyingParty,
pub(super) user: PublicKeyCredentialUserEntity,
pub(super) pub_key_cred_params: Vec<PubKeyCredParam>,
pub(super) authenticator_selection: AuthenticatorSelection,
pub(super) timeout: u32,
pub(super) attestation: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(super) exclude_credentials: Vec<ExcludeCredentialDescriptor>,
}
#[derive(Serialize, Debug)]
pub(super) struct ExcludeCredentialDescriptor {
#[serde(rename = "type")]
pub(super) type_: String,
pub(super) id: String,
}
#[derive(Serialize, Debug)]
pub(super) struct RelyingParty {
pub(super) name: String,
pub(super) id: String,
}
#[allow(unused)]
#[derive(Deserialize, Debug)]
pub struct RegisterCredential {
pub(super) id: String,
pub(super) raw_id: String,
pub(super) response: AuthenticatorAttestationResponse,
#[serde(rename = "type")]
pub(super) type_: String,
pub(super) user_handle: Option<String>,
}
impl RegisterCredential {
pub(crate) async fn get_registration_user_fields(&self) -> (String, String) {
if let Some(handle) = &self.user_handle {
let challenge_type = crate::passkey::types::ChallengeType::registration();
let challenge_id = match crate::passkey::types::ChallengeId::new(handle.clone()) {
Ok(id) => id,
Err(_) => {
tracing::warn!("Invalid challenge ID format, using defaults");
return ("Passkey User".to_string(), "Passkey User".to_string());
}
};
match super::challenge::get_and_validate_options(&challenge_type, &challenge_id).await {
Ok(stored_options) => (stored_options.user.name, stored_options.user.display_name),
Err(e) => {
tracing::warn!("Failed to get stored user: {}", e);
("Passkey User".to_string(), "Passkey User".to_string())
}
}
} else {
("Passkey User".to_string(), "Passkey User".to_string())
}
}
}
#[derive(Deserialize, Debug)]
pub(super) struct AuthenticatorAttestationResponse {
pub(super) client_data_json: String,
pub(super) attestation_object: String,
}
#[derive(Debug)]
pub(super) struct AttestationObject {
pub(super) fmt: String,
pub(super) auth_data: Vec<u8>,
pub(super) att_stmt: Vec<(CborValue, CborValue)>,
}
#[derive(Debug)]
pub(super) struct ParsedClientData {
pub(super) challenge: String,
pub(super) origin: String,
pub(super) type_: String,
pub(super) raw_data: Vec<u8>,
}
impl ParsedClientData {
pub(super) fn from_base64(client_data_json: &str) -> Result<Self, PasskeyError> {
let raw_data = base64url_decode(client_data_json)
.map_err(|e| PasskeyError::Format(format!("Failed to decode: {e}")))?;
let data_str = String::from_utf8(raw_data.clone())
.map_err(|e| PasskeyError::Format(format!("Invalid UTF-8: {e}")))?;
let data: serde_json::Value = serde_json::from_str(&data_str)
.map_err(|e| PasskeyError::Format(format!("Invalid JSON: {e}")))?;
let challenge_str = data["challenge"]
.as_str()
.ok_or_else(|| PasskeyError::ClientData("Missing challenge".into()))?;
Ok(Self {
challenge: challenge_str.to_string(),
origin: data["origin"]
.as_str()
.ok_or_else(|| PasskeyError::ClientData("Missing origin".into()))?
.to_string(),
type_: data["type"]
.as_str()
.ok_or_else(|| PasskeyError::ClientData("Missing type".into()))?
.to_string(),
raw_data,
})
}
pub(super) fn verify(&self, stored_challenge: &str) -> Result<(), PasskeyError> {
if self.challenge != stored_challenge {
return Err(PasskeyError::Challenge(
"Challenge mismatch. For more details, run with RUST_LOG=debug".into(),
));
}
if self.origin != *ORIGIN {
return Err(PasskeyError::ClientData(format!(
"Invalid origin. Expected: {}, Got: {}",
*ORIGIN, self.origin
)));
}
if self.type_ != "webauthn.get" {
return Err(PasskeyError::ClientData(format!(
"Invalid type. Expected 'webauthn.get', Got: {}",
self.type_
)));
}
Ok(())
}
}
#[derive(Debug)]
pub(super) struct AuthenticatorData {
pub(super) rp_id_hash: Vec<u8>,
pub(super) flags: u8,
pub(super) counter: u32,
pub(super) raw_data: Vec<u8>,
}
mod auth_data_flags {
pub(super) const UP: u8 = 1 << 0;
pub(super) const UV: u8 = 1 << 2;
pub(super) const BE: u8 = 1 << 3;
pub(super) const BS: u8 = 1 << 4;
pub(super) const AT: u8 = 1 << 6;
pub(super) const ED: u8 = 1 << 7;
}
impl AuthenticatorData {
pub(super) fn from_base64(auth_data: &str) -> Result<Self, PasskeyError> {
let data = base64url_decode(auth_data)
.map_err(|e| PasskeyError::Format(format!("Failed to decode: {e}")))?;
if data.len() < 37 {
return Err(PasskeyError::AuthenticatorData(
"Authenticator data too short. For more details, run with RUST_LOG=debug".into(),
));
}
Ok(Self {
rp_id_hash: data[..32].to_vec(),
flags: data[32],
counter: u32::from_be_bytes([data[33], data[34], data[35], data[36]]),
raw_data: data,
})
}
pub(super) fn is_user_present(&self) -> bool {
(self.flags & auth_data_flags::UP) != 0
}
pub(super) fn is_user_verified(&self) -> bool {
(self.flags & auth_data_flags::UV) != 0
}
pub(super) fn is_discoverable(&self) -> bool {
(self.flags & auth_data_flags::BE) != 0
}
pub(super) fn is_backed_up(&self) -> bool {
(self.flags & auth_data_flags::BS) != 0
}
pub(super) fn has_attested_credential_data(&self) -> bool {
(self.flags & auth_data_flags::AT) != 0
}
pub(super) fn has_extension_data(&self) -> bool {
(self.flags & auth_data_flags::ED) != 0
}
pub(super) fn verify(&self) -> Result<(), PasskeyError> {
let expected_hash = digest::digest(&digest::SHA256, PASSKEY_RP_ID.as_bytes());
if self.rp_id_hash != expected_hash.as_ref() {
return Err(PasskeyError::AuthenticatorData(format!(
"Invalid RP ID hash. Expected: {:?}, Got: {:?}",
expected_hash.as_ref(),
self.rp_id_hash
)));
}
if !self.is_user_present() {
return Err(PasskeyError::Authentication(
"User not present. For more details, run with RUST_LOG=debug".into(),
));
}
if *PASSKEY_USER_VERIFICATION == "required" && !self.is_user_verified() {
return Err(PasskeyError::AuthenticatorData(format!(
"User verification required but flag not set. Flags: {:02x}",
self.flags
)));
}
tracing::debug!("Authenticator data verification passed");
tracing::debug!("User present: {}", self.is_user_present());
tracing::debug!("User verified: {}", self.is_user_verified());
tracing::debug!("Discoverable credential: {}", self.is_discoverable());
tracing::debug!("Backed up: {}", self.is_backed_up());
tracing::debug!(
"Attested credential data: {}",
self.has_attested_credential_data()
);
tracing::debug!("Extension data: {}", self.has_extension_data());
Ok(())
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(super) struct WebAuthnClientData {
#[serde(rename = "type")]
pub(super) type_: String,
pub(super) challenge: String, pub(super) origin: String,
}
#[cfg(test)]
mod tests;