use super::*;
use crate::passkey::main::types;
use crate::storage::{CacheKey, CachePrefix};
use crate::test_utils::init_test_environment;
use crate::passkey::main::test_utils as passkey_test_utils;
fn create_test_authenticator_response(
user_handle: Option<String>,
auth_id: String,
) -> AuthenticatorResponse {
AuthenticatorResponse::new_for_test(
"test_credential_id".to_string(),
types::AuthenticatorAssertionResponse {
client_data_json: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0".to_string(), authenticator_data: "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABA".to_string(),
signature: "MEUCIQDsignature".to_string(),
user_handle,
},
auth_id,
)
}
fn create_test_passkey_credential(user_handle: String) -> PasskeyCredential {
PasskeyCredential {
sequence_number: None,
credential_id: "test_credential_id".to_string(),
user_id: "test_user_id".to_string(),
public_key: "test_public_key".to_string(),
aaguid: "test_aaguid".to_string(),
rp_id: "localhost".to_string(),
counter: 1,
user: PublicKeyCredentialUserEntity {
user_handle,
name: "test_user".to_string(),
display_name: "Test User".to_string(),
},
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
last_used_at: chrono::Utc::now(),
}
}
fn create_test_authenticator_data(counter: u32) -> AuthenticatorData {
AuthenticatorData {
rp_id_hash: vec![0; 32],
flags: 0x01 | 0x04, counter,
raw_data: vec![],
}
}
#[cfg(test)]
use serial_test::serial;
#[tokio::test]
async fn test_start_authentication_no_username() {
init_test_environment().await;
let result = start_authentication(None).await;
assert!(result.is_ok());
let auth_options = result.unwrap();
assert!(auth_options.allow_credentials.is_empty());
assert!(!auth_options.challenge.is_empty());
assert!(!auth_options.auth_id.is_empty());
assert_eq!(auth_options.rp_id, *crate::passkey::config::PASSKEY_RP_ID);
assert_eq!(
auth_options.user_verification,
*crate::passkey::config::PASSKEY_USER_VERIFICATION
);
}
#[tokio::test]
async fn test_start_authentication_generates_unique_ids() {
init_test_environment().await;
let result1 = start_authentication(None).await;
let result2 = start_authentication(None).await;
assert!(result1.is_ok());
assert!(result2.is_ok());
let auth1 = result1.unwrap();
let auth2 = result2.unwrap();
assert_ne!(auth1.challenge, auth2.challenge);
assert_ne!(auth1.auth_id, auth2.auth_id);
}
#[test]
fn test_verify_user_handle_real_function_matching_handles() {
let auth_response = create_test_authenticator_response(
Some("test_user_handle".to_string()),
"test_auth_id".to_string(),
);
let credential = create_test_passkey_credential("test_user_handle".to_string());
let result1 = verify_user_handle(&auth_response, &credential, true);
let result2 = verify_user_handle(&auth_response, &credential, false);
assert!(
result1.is_ok(),
"Should succeed with matching handles (discoverable)"
);
assert!(
result2.is_ok(),
"Should succeed with matching handles (non-discoverable)"
);
}
#[test]
fn test_verify_user_handle_real_function_mismatched_handles() {
let auth_response = create_test_authenticator_response(
Some("wrong_handle".to_string()),
"test_auth_id".to_string(),
);
let credential = create_test_passkey_credential("correct_handle".to_string());
let result1 = verify_user_handle(&auth_response, &credential, true);
let result2 = verify_user_handle(&auth_response, &credential, false);
assert!(
result1.is_err(),
"Should fail with mismatched handles (discoverable)"
);
if let Err(PasskeyError::Authentication(msg)) = &result1 {
assert!(
msg.contains("User handle mismatch"),
"Expected 'User handle mismatch' error but got: {msg}"
);
} else {
panic!("Expected PasskeyError::Authentication but got: {result1:?}");
}
assert!(
result2.is_err(),
"Should fail with mismatched handles (non-discoverable)"
);
if let Err(PasskeyError::Authentication(msg)) = &result2 {
assert!(
msg.contains("User handle mismatch"),
"Expected 'User handle mismatch' error but got: {msg}"
);
} else {
panic!("Expected PasskeyError::Authentication but got: {result2:?}");
}
}
#[test]
fn test_verify_user_handle_real_function_missing_handle() {
let credential = create_test_passkey_credential("test_handle".to_string());
let auth_response_discoverable =
create_test_authenticator_response(None, "test_auth_id".to_string());
let result_discoverable = verify_user_handle(&auth_response_discoverable, &credential, true);
assert!(
result_discoverable.is_err(),
"Should fail with missing user handle for discoverable credential"
);
if let Err(PasskeyError::Authentication(msg)) = &result_discoverable {
assert!(
msg.contains("Missing required user handle"),
"Expected 'Missing required user handle' error but got: {msg}"
);
} else {
panic!("Expected PasskeyError::Authentication but got: {result_discoverable:?}");
}
let auth_response_non_discoverable =
create_test_authenticator_response(None, "test_auth_id".to_string());
let result_non_discoverable =
verify_user_handle(&auth_response_non_discoverable, &credential, false);
assert!(
result_non_discoverable.is_ok(),
"Non-discoverable credential should allow missing user handle"
);
}
#[test]
fn test_verify_user_handle_edge_cases() {
let auth_response_empty =
create_test_authenticator_response(Some("".to_string()), "test_auth_id".to_string());
let credential_empty = create_test_passkey_credential("".to_string());
let result_empty = verify_user_handle(&auth_response_empty, &credential_empty, true);
assert!(
result_empty.is_ok(),
"Should succeed with matching empty handles"
);
let credential_non_empty = create_test_passkey_credential("non_empty".to_string());
let result_mismatch = verify_user_handle(&auth_response_empty, &credential_non_empty, false);
assert!(
result_mismatch.is_err(),
"Should fail with empty vs non-empty handle mismatch"
);
}
#[tokio::test]
async fn test_verify_counter_authenticator_no_counter_support() {
let passkey = create_test_passkey_credential("test_user".to_string());
let auth_data = create_test_authenticator_data(0);
let result = verify_counter(
CredentialId::new(passkey.credential_id.clone()).expect("Valid credential ID"),
&auth_data,
&passkey,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_verify_counter_replay_attack_detection() {
let mut passkey = create_test_passkey_credential("test_user".to_string());
passkey.counter = 10;
let auth_data = create_test_authenticator_data(5);
let result = verify_counter(
CredentialId::new(passkey.credential_id.clone()).expect("Valid credential ID"),
&auth_data,
&passkey,
)
.await;
assert!(result.is_err());
if let Err(PasskeyError::Authentication(msg)) = result {
assert!(msg.contains("credential cloning detected"));
} else {
panic!("Expected Authentication error");
}
}
#[tokio::test]
async fn test_verify_counter_equal_counter_replay_attack() {
let mut passkey = create_test_passkey_credential("test_user".to_string());
passkey.counter = 10;
let auth_data = create_test_authenticator_data(10);
let result = verify_counter(
CredentialId::new(passkey.credential_id.clone()).expect("Valid credential ID"),
&auth_data,
&passkey,
)
.await;
assert!(result.is_err());
if let Err(PasskeyError::Authentication(msg)) = result {
assert!(msg.contains("credential cloning detected"));
} else {
panic!("Expected Authentication error");
}
}
#[tokio::test]
async fn test_verify_counter_valid_increment() {
let mut passkey = create_test_passkey_credential("test_user".to_string());
passkey.counter = 10;
let auth_data = create_test_authenticator_data(15);
let result = verify_counter_without_db(&auth_data, &passkey).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_verify_counter_zero_to_positive() {
let mut passkey = create_test_passkey_credential("test_user".to_string());
passkey.counter = 0; let auth_data = create_test_authenticator_data(1);
let result = verify_counter_without_db(&auth_data, &passkey).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_verify_counter_large_increment() {
let mut passkey = create_test_passkey_credential("test_user".to_string());
passkey.counter = 100;
let auth_data = create_test_authenticator_data(1000);
let result = verify_counter_without_db(&auth_data, &passkey).await;
assert!(result.is_ok());
}
#[cfg(test)]
async fn verify_counter_without_db(
auth_data: &AuthenticatorData,
stored_credential: &PasskeyCredential,
) -> Result<(), PasskeyError> {
let auth_counter = auth_data.counter;
if auth_counter == 0 {
tracing::info!("Authenticator does not support counters (received counter=0)");
} else if auth_counter <= stored_credential.counter {
return Err(PasskeyError::Authentication(
"Counter value decreased - possible credential cloning detected. For more details, run with RUST_LOG=debug".into(),
));
}
Ok(())
}
fn create_test_parsed_client_data(challenge: &str) -> ParsedClientData {
ParsedClientData {
challenge: challenge.to_string(),
origin: "https://example.com".to_string(),
type_: "webauthn.get".to_string(),
raw_data: b"test_client_data".to_vec(),
}
}
fn create_test_authenticator_data_with_raw(counter: u32, raw_data: Vec<u8>) -> AuthenticatorData {
AuthenticatorData {
rp_id_hash: vec![0; 32],
flags: 0x01 | 0x04, counter,
raw_data,
}
}
#[tokio::test]
async fn test_verify_signature_invalid_public_key_format() {
let auth_response = create_test_authenticator_response(
Some("test_user".to_string()),
"test_auth_id".to_string(),
);
let client_data = create_test_parsed_client_data("test_challenge");
let auth_data = create_test_authenticator_data_with_raw(1, vec![0; 37]);
let mut credential = create_test_passkey_credential("test_user".to_string());
credential.public_key = "invalid_base64!".to_string();
let result = verify_signature(&auth_response, &client_data, &auth_data, &credential).await;
assert!(result.is_err());
if let Err(PasskeyError::Format(msg)) = result {
assert!(msg.contains("Invalid public key"));
} else {
panic!("Expected Format error for invalid public key");
}
}
#[tokio::test]
async fn test_verify_signature_invalid_signature_format() {
let mut auth_response = create_test_authenticator_response(
Some("test_user".to_string()),
"test_auth_id".to_string(),
);
auth_response.response.signature = "invalid_base64!".to_string();
let client_data = create_test_parsed_client_data("test_challenge");
let auth_data = create_test_authenticator_data_with_raw(1, vec![0; 37]);
let mut credential = create_test_passkey_credential("test_user".to_string());
credential.public_key = crate::utils::base64url_encode(vec![0; 64]).unwrap();
let result = verify_signature(&auth_response, &client_data, &auth_data, &credential).await;
assert!(result.is_err());
match result {
Err(error) => {
if let PasskeyError::Format(ref msg) = error {
assert!(msg.contains("Invalid signature"));
} else {
panic!("Expected Format error for invalid signature format, got: {error:?}");
}
}
Ok(_) => panic!("Expected error but got success"),
}
}
#[tokio::test]
async fn test_verify_signature_verification_failure() {
let auth_response = create_test_authenticator_response(
Some("test_user".to_string()),
"test_auth_id".to_string(),
);
let client_data = create_test_parsed_client_data("test_challenge");
let auth_data = create_test_authenticator_data_with_raw(1, vec![0; 37]);
let mut credential = create_test_passkey_credential("test_user".to_string());
credential.public_key = crate::utils::base64url_encode(vec![0; 64]).unwrap();
let result = verify_signature(&auth_response, &client_data, &auth_data, &credential).await;
assert!(result.is_err());
if let Err(PasskeyError::Verification(msg)) = result {
assert!(msg.contains("Signature verification failed"));
} else {
panic!("Expected Verification error for signature mismatch");
}
}
#[tokio::test]
async fn test_verify_signature_empty_signature() {
let mut auth_response = create_test_authenticator_response(
Some("test_user".to_string()),
"test_auth_id".to_string(),
);
auth_response.response.signature = "".to_string();
let client_data = create_test_parsed_client_data("test_challenge");
let auth_data = create_test_authenticator_data_with_raw(1, vec![0; 37]);
let mut credential = create_test_passkey_credential("test_user".to_string());
credential.public_key = crate::utils::base64url_encode(vec![0; 64]).unwrap();
let result = verify_signature(&auth_response, &client_data, &auth_data, &credential).await;
assert!(result.is_err());
match result {
Err(error) => {
if let PasskeyError::Verification(ref msg) = error {
assert!(msg.contains("Signature verification failed"));
} else {
panic!("Expected Verification error for empty signature, got: {error:?}");
}
}
Ok(_) => panic!("Expected error but got success"),
}
}
#[tokio::test]
async fn test_verify_signature_empty_public_key() {
let auth_response = create_test_authenticator_response(
Some("test_user".to_string()),
"test_auth_id".to_string(),
);
let client_data = create_test_parsed_client_data("test_challenge");
let auth_data = create_test_authenticator_data_with_raw(1, vec![0; 37]);
let mut credential = create_test_passkey_credential("test_user".to_string());
credential.public_key = "".to_string();
let result = verify_signature(&auth_response, &client_data, &auth_data, &credential).await;
assert!(result.is_err());
match result {
Err(error) => {
if let PasskeyError::Verification(ref msg) = error {
assert!(msg.contains("Signature verification failed"));
} else {
panic!("Expected Verification error for empty public key, got: {error:?}");
}
}
Ok(_) => panic!("Expected error but got success"),
}
}
#[tokio::test]
async fn test_verify_signature_malformed_data_structures() {
let auth_response = create_test_authenticator_response(
Some("test_user".to_string()),
"test_auth_id".to_string(),
);
let client_data = create_test_parsed_client_data("test_challenge");
let auth_data_empty = create_test_authenticator_data_with_raw(1, vec![]);
let credential = create_test_passkey_credential("test_user".to_string());
let result =
verify_signature(&auth_response, &client_data, &auth_data_empty, &credential).await;
assert!(result.is_err());
match result {
Err(PasskeyError::Format(_)) | Err(PasskeyError::Verification(_)) => {
}
_ => panic!("Expected Format or Verification error for malformed data"),
}
}
#[tokio::test]
#[serial]
async fn test_finish_authentication_integration_test() {
init_test_environment().await;
let credential_id = "test_credential_id_123";
let user_id = "test_user_id_123";
let user_handle = "test_user_handle_123";
let public_key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEckXwaEBJmwp0EVElviOu9HLgrk3TA/RG4hxcXGYkcCKZ0FIwSkFS6YmGAhRC1nckV0/KQ0/Qpw8WTgK2KQEteA==";
let aaguid = "f8e2d612-b2cc-4536-a028-ec advocating1951db";
let credential_data = passkey_test_utils::TestCredentialData::new(
credential_id,
user_id,
user_handle,
"Test User",
"Test Display Name",
public_key,
aaguid,
42,
);
let result = passkey_test_utils::insert_test_user_and_credential(credential_data).await;
if let Err(e) = &result {
println!("Error inserting test credential: {e:?}");
}
assert!(
result.is_ok(),
"Failed to insert test credential: {result:?}"
);
let get_result = PasskeyStore::get_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(get_result.is_ok(), "Failed to retrieve test credential");
let credential_option = get_result.unwrap();
assert!(credential_option.is_some(), "Credential should exist");
let credential = credential_option.unwrap();
assert_eq!(credential.user_id, user_id);
assert_eq!(credential.user.user_handle, user_handle);
let cleanup_result = passkey_test_utils::cleanup_test_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(cleanup_result.is_ok(), "Failed to clean up test credential");
let verify_deleted = PasskeyStore::get_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(
verify_deleted.unwrap().is_none(),
"Credential should be deleted after cleanup"
);
}
#[tokio::test]
#[serial]
async fn test_start_authentication_integration() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::storage::GENERIC_CACHE_STORE;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let credential_id = "auth_test_credential_id";
let user_id = "auth_test_user_id";
let user_handle = "auth_test_user_handle";
let public_key = "test_public_key_auth";
let username = "auth_test_user";
let credential_data = passkey_test_utils::TestCredentialData::new(
credential_id,
user_id,
user_handle,
username,
"Auth Test User",
public_key,
"test_aaguid",
10, );
let insert_result = passkey_test_utils::insert_test_user_and_credential(credential_data).await;
assert!(insert_result.is_ok(), "Failed to insert test credential");
let auth_options = super::start_authentication(Some(username.to_string())).await;
assert!(auth_options.is_ok(), "Failed to start authentication");
let options = auth_options.unwrap();
assert!(
!options.allow_credentials.is_empty(),
"No credentials found in options"
);
assert_eq!(options.allow_credentials[0].id, credential_id);
let auth_id = options.auth_id;
let cache_prefix = CachePrefix::new("authentication".to_string()).unwrap();
let cache_key = CacheKey::new(auth_id.clone()).unwrap();
let cache_get = GENERIC_CACHE_STORE
.lock()
.await
.get(cache_prefix, cache_key)
.await;
assert!(cache_get.is_ok());
assert!(cache_get.unwrap().is_some(), "Challenge should be in cache");
let cache_prefix = CachePrefix::new("authentication".to_string()).unwrap();
let cache_key = CacheKey::new(auth_id.clone()).unwrap();
let remove_cache = passkey_test_utils::remove_from_cache(cache_prefix, cache_key).await;
assert!(remove_cache.is_ok(), "Failed to clean up cache");
let remove_credential = passkey_test_utils::cleanup_test_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(remove_credential.is_ok(), "Failed to clean up credential");
}
#[tokio::test]
#[serial]
async fn test_verify_counter_and_update() {
use crate::passkey::main::test_utils as passkey_test_utils;
use crate::test_utils::init_test_environment;
init_test_environment().await;
let credential_id = "counter_test_credential_id";
let initial_counter = 10;
let credential_data = passkey_test_utils::TestCredentialData::new(
credential_id,
"counter_test_user_id",
"counter_test_user_handle",
"counter_test_user",
"Counter Test User",
"test_public_key",
"test_aaguid",
initial_counter,
);
let insert_result = passkey_test_utils::insert_test_user_and_credential(credential_data).await;
assert!(insert_result.is_ok(), "Failed to insert test credential");
let credential = crate::passkey::PasskeyStore::get_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await
.expect("Failed to get credential")
.expect("Credential not found");
let auth_data = create_test_authenticator_data(initial_counter + 5);
let verify_result = super::verify_counter(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
&auth_data,
&credential,
)
.await;
assert!(verify_result.is_ok(), "Counter verification failed");
let updated_credential = crate::passkey::PasskeyStore::get_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await
.expect("Failed to get credential")
.expect("Credential not found");
assert_eq!(
updated_credential.counter,
initial_counter + 5,
"Counter was not updated correctly"
);
let auth_data_same = create_test_authenticator_data(initial_counter);
let verify_result_2 = super::verify_counter(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
&auth_data_same,
&updated_credential,
)
.await;
assert!(
verify_result_2.is_err(),
"Should fail with non-increasing counter"
);
let cleanup = passkey_test_utils::cleanup_test_credential(
CredentialId::new(credential_id.to_string()).expect("Valid credential ID"),
)
.await;
assert!(cleanup.is_ok(), "Failed to clean up test credential");
}
#[tokio::test]
async fn test_verify_user_handle() {
let stored_credential = create_test_passkey_credential("test_user".to_string());
let auth_response_no_handle =
create_test_authenticator_response(None, "test_auth_id".to_string());
let result1 = super::verify_user_handle(&auth_response_no_handle, &stored_credential, false);
assert!(
result1.is_ok(),
"Non-discoverable credential without handle should pass"
);
let auth_response_matching = create_test_authenticator_response(
Some(stored_credential.user.user_handle.clone()),
"test_auth_id".to_string(),
);
let result2 = super::verify_user_handle(&auth_response_matching, &stored_credential, false);
assert!(
result2.is_ok(),
"Non-discoverable credential with matching handle should pass"
);
let result3 = super::verify_user_handle(&auth_response_no_handle, &stored_credential, true);
assert!(
result3.is_err(),
"Discoverable credential without handle should fail"
);
let auth_response_mismatched = create_test_authenticator_response(
Some("different_user_handle".to_string()),
"test_auth_id".to_string(),
);
let result4 = super::verify_user_handle(&auth_response_mismatched, &stored_credential, false);
assert!(
result4.is_err(),
"Credential with mismatched handle should fail"
);
}