use chrono::Utc;
use ring::{digest, signature::UnparsedPublicKey};
use crate::utils::{base64url_decode, gen_random_string};
use crate::passkey::config::{
PASSKEY_CHALLENGE_TIMEOUT, PASSKEY_RP_ID, PASSKEY_TIMEOUT, PASSKEY_USER_VERIFICATION,
};
use crate::passkey::errors::PasskeyError;
use crate::passkey::storage::PasskeyStore;
use crate::passkey::types::{
CredentialId, PasskeyCredential, PublicKeyCredentialUserEntity, StoredOptions,
};
use super::challenge::{get_and_validate_options, remove_options};
#[derive(Debug)]
pub(crate) struct AuthenticationResult {
pub user_id: String,
pub user_name: String,
pub user_handle: String,
pub aaguid: String,
}
use super::types::{
AllowCredential, AuthenticationOptions, AuthenticatorData, AuthenticatorResponse,
ParsedClientData,
};
use super::utils::name2cid_str_vec;
use crate::storage::{CacheErrorConversion, CacheKey, CachePrefix, store_cache_keyed};
pub(crate) async fn start_authentication(
username: Option<String>,
) -> Result<AuthenticationOptions, PasskeyError> {
let mut allow_credentials = Vec::new();
match username.clone() {
Some(username) => {
let credential_id_strs = name2cid_str_vec(&username).await?;
for credential in credential_id_strs {
allow_credentials.push(AllowCredential {
type_: "public-key".to_string(),
id: credential.credential_id,
});
}
}
None => {
}
}
let challenge_str = gen_random_string(32)?;
let auth_id = gen_random_string(16)?;
let stored_options = StoredOptions {
challenge: challenge_str.clone(),
user: PublicKeyCredentialUserEntity {
user_handle: "temp".to_string(),
name: "temp".to_string(),
display_name: "temp".to_string(),
},
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
ttl: *PASSKEY_CHALLENGE_TIMEOUT as u64,
};
let cache_prefix = CachePrefix::auth_challenge();
let cache_key = CacheKey::new(auth_id.clone()).map_err(PasskeyError::convert_storage_error)?;
store_cache_keyed::<_, PasskeyError>(
cache_prefix,
cache_key,
stored_options,
(*PASSKEY_CHALLENGE_TIMEOUT).into(),
)
.await?;
let auth_option = AuthenticationOptions {
challenge: challenge_str,
timeout: (*PASSKEY_TIMEOUT) * 1000, rp_id: PASSKEY_RP_ID.to_string(),
allow_credentials,
user_verification: PASSKEY_USER_VERIFICATION.to_string(),
auth_id,
};
tracing::debug!("Auth options: {:?}", auth_option);
Ok(auth_option)
}
pub(crate) async fn finish_authentication(
auth_response: AuthenticatorResponse,
) -> Result<AuthenticationResult, PasskeyError> {
tracing::debug!(
"Starting authentication verification for response: {:?}",
auth_response
);
let challenge_type = crate::passkey::types::ChallengeType::authentication();
let challenge_id = crate::passkey::types::ChallengeId::new(auth_response.auth_id.clone())
.map_err(|e| PasskeyError::Challenge(format!("Invalid auth ID: {e}")))?;
let stored_options = get_and_validate_options(&challenge_type, &challenge_id).await?;
tracing::debug!(
"Parsing client data: {}",
&auth_response.response.client_data_json
);
let client_data = ParsedClientData::from_base64(&auth_response.response.client_data_json)?;
tracing::debug!("Parsed client data: {:?}", client_data);
client_data.verify(&stored_options.challenge)?;
tracing::debug!(
"Parsing authenticator data: {}",
&auth_response.response.authenticator_data
);
let auth_data = AuthenticatorData::from_base64(&auth_response.response.authenticator_data)?;
tracing::debug!("Parsed authenticator data: {:?}", auth_data);
auth_data.verify()?;
let credential_id = CredentialId::new(auth_response.id.clone())
.map_err(|e| PasskeyError::Validation(format!("Invalid credential ID: {e}")))?;
let stored_credential = PasskeyStore::get_credential(credential_id.clone())
.await?
.ok_or_else(|| {
tracing::error!("Credential not found");
PasskeyError::NotFound("Credential not found".into())
})?;
tracing::debug!(
"finish_authentication: Credential &id: {:?}, id: {}",
&auth_response.id,
auth_response.id
);
tracing::debug!("Found credential: {:?}", stored_credential);
tracing::debug!(
"Credential properties:\n\
- Type: {}\n\
- User present: {}\n\
- User verified: {}\n\
- Backed up: {}",
if auth_data.is_discoverable() {
"discoverable"
} else {
"server-side"
},
auth_data.is_user_present(),
auth_data.is_user_verified(),
auth_data.is_backed_up(),
);
verify_user_handle(
&auth_response,
&stored_credential,
auth_data.is_discoverable(),
)?;
verify_counter(credential_id.clone(), &auth_data, &stored_credential).await?;
verify_signature(&auth_response, &client_data, &auth_data, &stored_credential).await?;
PasskeyStore::update_credential_last_used_at(credential_id, Utc::now()).await?;
let cache_prefix = CachePrefix::auth_challenge();
let cache_key = CacheKey::new(auth_response.auth_id.clone())
.map_err(PasskeyError::convert_storage_error)?;
remove_options(cache_prefix, cache_key).await?;
let user_name = stored_credential.user.name.clone();
let user_id = stored_credential.user_id.clone();
let user_handle = stored_credential.user.user_handle.clone();
let aaguid = stored_credential.aaguid.clone();
Ok(AuthenticationResult {
user_id,
user_name,
user_handle,
aaguid,
})
}
fn verify_user_handle(
auth_response: &AuthenticatorResponse,
stored_credential: &PasskeyCredential,
is_discoverable: bool,
) -> Result<(), PasskeyError> {
let user_handle = auth_response.response.user_handle.clone();
tracing::debug!(
"User handle: {:?}, Stored handle: {:?}, User handle raw: {:?}, Is discoverable: {}",
user_handle,
&stored_credential.user.user_handle,
auth_response.response.user_handle,
is_discoverable,
);
match (
user_handle,
&stored_credential.user.user_handle,
is_discoverable,
) {
(Some(handle), stored_handle, _) if handle != *stored_handle => {
tracing::error!("User handle mismatch: {} != {}", handle, stored_handle);
return Err(PasskeyError::Authentication(
"User handle mismatch. For more details, run with RUST_LOG=debug".into(),
));
}
(None, _, true) => {
return Err(PasskeyError::Authentication(
"Missing required user handle for discoverable credential. For more details, run with RUST_LOG=debug".into(),
));
}
(None, _, false) => {
tracing::debug!("No user handle provided for non-discoverable credential");
}
_ => {
tracing::debug!("User handle verified successfully");
}
}
Ok(())
}
async fn verify_counter(
credential_id: CredentialId,
auth_data: &AuthenticatorData,
stored_credential: &PasskeyCredential,
) -> Result<(), PasskeyError> {
let auth_counter = auth_data.counter;
tracing::debug!(
"Counter verification - stored: {}, received: {}",
stored_credential.counter,
auth_counter
);
if auth_counter == 0 {
tracing::info!("Authenticator does not support counters (received counter=0)");
} else {
let updated =
PasskeyStore::atomic_update_credential_counter(credential_id.clone(), auth_counter)
.await?;
if updated {
tracing::debug!(
"Counter verification successful - previously fetched: {}, received: {}",
stored_credential.counter,
auth_counter
);
} else {
tracing::warn!(
"Counter verification failed - previously fetched: {}, received: {} (actual DB value may differ due to concurrent updates)",
stored_credential.counter,
auth_counter
);
return Err(PasskeyError::Authentication(
"Counter value decreased - possible credential cloning detected. For more details, run with RUST_LOG=debug".into(),
));
}
}
Ok(())
}
async fn verify_signature(
auth_response: &AuthenticatorResponse,
client_data: &ParsedClientData,
auth_data: &AuthenticatorData,
stored_credential: &PasskeyCredential,
) -> Result<(), PasskeyError> {
let verification_algorithm = &ring::signature::ECDSA_P256_SHA256_ASN1;
let public_key = base64url_decode(&stored_credential.public_key)
.map_err(|e| PasskeyError::Format(format!("Invalid public key: {e}")))?;
let unparsed_public_key = UnparsedPublicKey::new(verification_algorithm, &public_key);
let signature = base64url_decode(&auth_response.response.signature)
.map_err(|e| PasskeyError::Format(format!("Invalid signature: {e}")))?;
tracing::debug!("Decoded signature length: {}", signature.len());
let client_data_hash = digest::digest(&digest::SHA256, &client_data.raw_data);
let mut signed_data = Vec::new();
signed_data.extend_from_slice(&auth_data.raw_data);
signed_data.extend_from_slice(client_data_hash.as_ref());
tracing::debug!("Signed data length: {}", signed_data.len());
match unparsed_public_key.verify(&signed_data, &signature) {
Ok(_) => {
tracing::info!("Signature verification successful");
Ok(())
}
Err(e) => {
tracing::error!("Signature verification failed: {:?}", e);
Err(PasskeyError::Verification(
"Signature verification failed. For more details, run with RUST_LOG=debug".into(),
))
}
}
}
#[cfg(test)]
mod tests;