#[allow(dead_code)]
mod harness;
use harness::AuthenticatorHarness;
use soft_fido2::request::{ClientDataHash, MakeCredentialRequest};
use soft_fido2::{Client, TransportList};
use soft_fido2_ctap::types::{RelyingParty, User};
use base64::Engine;
use sha2::{Digest, Sha256};
const RP_ID: &str = "example.com";
const ORIGIN: &str = "https://example.com";
const CMD_PASSLESS_RESET_UV_RETRIES: u8 = 0x42;
const RESET_UV_RETRIES_SUBCOMMAND: u8 = 0x01;
fn connect_to_authenticator() -> Result<soft_fido2::Transport, Box<dyn std::error::Error>> {
let list = TransportList::enumerate()?;
if list.is_empty() {
return Err("No authenticators found".into());
}
Ok(list.get(0).ok_or("Failed to get authenticator")?)
}
fn generate_client_data_hash_for_registration(challenge: &[u8]) -> ClientDataHash {
let client_data = format!(
r#"{{"type":"webauthn.create","challenge":"{}","origin":"{}","crossOrigin":false}}"#,
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(challenge),
ORIGIN
);
let hash = Sha256::digest(client_data.as_bytes());
ClientDataHash::from_slice(&hash).expect("Failed to create client data hash")
}
fn register_credential(
transport: &mut soft_fido2::Transport,
user_id: &[u8],
user_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
register_credential_with_rp(transport, user_id, user_name, RP_ID)
}
fn register_credential_with_rp(
transport: &mut soft_fido2::Transport,
user_id: &[u8],
user_name: &str,
rp_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let challenge: [u8; 32] = rand::random();
let client_data_hash = generate_client_data_hash_for_registration(&challenge);
let rp = RelyingParty {
id: rp_id.to_string(),
name: Some("Example Corp".to_string()),
};
let user = User {
id: user_id.to_vec(),
name: Some(user_name.to_string()),
display_name: Some(user_name.split('@').next().unwrap().to_string()),
};
let request = MakeCredentialRequest::new(client_data_hash, rp, user)
.with_resident_key(true)
.with_user_verification(true);
Client::make_credential(transport, request)?;
Ok(())
}
#[allow(dead_code)]
fn get_credential_count(
transport: &mut soft_fido2::Transport,
) -> Result<u32, Box<dyn std::error::Error>> {
use soft_fido2_ctap::cbor::MapBuilder;
let mut builder = MapBuilder::new();
builder = builder.insert(0x01, 0x01u8)?; let params = builder.build()?;
let response = transport.send_ctap_command(0x0A, ¶ms, 30000)?;
if response.is_empty() {
return Err("Empty response".into());
}
if response[0] != 0x00 {
eprintln!(
"DEBUG: Bad status: {:02x}, response len: {}",
response[0],
response.len()
);
eprintln!(
"DEBUG: First 20 bytes: {:02x?}",
&response[..20.min(response.len())]
);
return Err(format!("Bad status: {:02x}", response[0]).into());
}
use ciborium::Value;
let cbor_value: Value = ciborium::from_reader(&response[1..])?;
if let Value::Map(map) = cbor_value {
for (k, v) in map {
if let Value::Integer(key) = k
&& key == 1.into()
&& let Value::Integer(count) = v
{
return Ok(count.try_into().unwrap_or(0));
}
}
}
Err("Failed to get count".into())
}
#[test]
#[ignore]
fn test_client_info() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Client Info Command");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📋 Getting authenticator info...");
let response = Client::authenticator_get_info(&mut transport).expect("Failed to get info");
println!(" ✓ Received {} bytes", response.len());
assert!(!response.is_empty());
use ciborium::Value;
let cbor_value: Value = ciborium::from_reader(&response[..]).expect("Failed to decode CBOR");
if let Value::Map(map) = cbor_value {
println!(" ✓ Decoded authenticator info with {} fields", map.len());
for (k, v) in &map {
if let Value::Integer(key) = k
&& *key == 1.into()
&& let Value::Array(versions) = v
{
println!(" ✓ Found {} version(s)", versions.len());
assert!(!versions.is_empty(), "No versions");
}
}
let mut saw_options = false;
for (k, v) in &map {
if let Value::Integer(key) = k
&& *key == 4.into()
&& let Value::Map(options) = v
{
saw_options = true;
let option_bool = |option_name: &str| {
options.iter().find_map(|(option_key, option_value)| {
if let Value::Text(name) = option_key
&& name == option_name
&& let Value::Bool(value) = option_value
{
return Some(*value);
}
None
})
};
assert_eq!(option_bool("rk"), Some(true));
assert_eq!(option_bool("up"), Some(true));
assert_eq!(option_bool("plat"), Some(true));
assert_eq!(option_bool("uv"), Some(true));
assert_eq!(option_bool("clientPin"), Some(false));
assert_eq!(option_bool("alwaysUv"), Some(false));
}
}
assert!(saw_options, "getInfo response should include options");
} else {
panic!("Expected CBOR map");
}
println!("\n✅ Client info command works!\n");
}
#[test]
#[ignore]
fn test_client_list_credentials() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Client List Command");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📝 Registering test credentials...");
register_credential(&mut transport, &[1, 2, 3, 4], "alice@example.com")
.expect("Failed to register credential 1");
println!(" ✓ Registered credential 1");
register_credential(&mut transport, &[5, 6, 7, 8], "bob@example.com")
.expect("Failed to register credential 2");
println!(" ✓ Registered credential 2");
println!("\n📋 Testing credential management API...");
use soft_fido2::request::CredentialManagementRequest;
let request = CredentialManagementRequest::new(None);
match Client::get_credentials_metadata(&mut transport, request) {
Ok(metadata) => {
println!(
" ✓ Existing credentials: {}",
metadata.existing_resident_credentials_count
);
println!(
" ✓ Max remaining: {}",
metadata.max_possible_remaining_resident_credentials_count
);
assert_eq!(
metadata.existing_resident_credentials_count, 2,
"Expected 2 credentials"
);
}
Err(e) => {
println!(" ⚠ Could not get metadata: {:?}", e);
}
}
let request = CredentialManagementRequest::new(None);
match Client::enumerate_rps(&mut transport, request) {
Ok(rps) => {
println!(" ✓ Found {} RP(s)", rps.len());
assert!(!rps.is_empty(), "Expected at least one RP");
for rp in &rps {
println!(" ✓ RP: {}", rp.id);
let creds_request =
soft_fido2::request::EnumerateCredentialsRequest::new(None, rp.rp_id_hash);
match Client::enumerate_credentials(&mut transport, creds_request) {
Ok(creds) => {
println!(" ✓ Found {} credential(s)", creds.len());
for (i, cred) in creds.iter().enumerate() {
if let Some(name) = &cred.user.name {
println!(" {}. User: {}", i + 1, name);
}
}
}
Err(e) => {
println!(" ⚠ Could not enumerate credentials: {:?}", e);
}
}
}
}
Err(e) => {
println!(" ⚠ Could not enumerate RPs: {:?}", e);
}
}
println!("\n✅ Client list command works!\n");
}
#[test]
#[ignore]
fn test_client_delete_credential() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Client Delete Command");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📝 Registering test credential...");
register_credential(&mut transport, &[1, 2, 3, 4], "alice@example.com")
.expect("Failed to register credential");
println!(" ✓ Registered credential");
use soft_fido2::PinProtocol;
println!("\n🔐 Acquiring UV token for credential management...");
let pin_uv_auth =
match Client::get_uv_token_for_credential_management(&mut transport, PinProtocol::V2) {
Ok(token) => {
println!(" ✓ UV token acquired");
Some(token)
}
Err(e) => {
println!(" ⚠ UV token failed: {:?}", e);
println!(" Note: This test requires UV support");
println!("\n✅ Client delete command structure is implemented!");
return;
}
};
use soft_fido2::request::CredentialManagementRequest;
let request = CredentialManagementRequest::new(pin_uv_auth.clone());
let rps = Client::enumerate_rps(&mut transport, request).expect("Failed to enumerate RPs");
assert!(!rps.is_empty(), "No RPs found");
let rp = &rps[0];
let creds_request =
soft_fido2::request::EnumerateCredentialsRequest::new(pin_uv_auth.clone(), rp.rp_id_hash);
let creds = Client::enumerate_credentials(&mut transport, creds_request)
.expect("Failed to enumerate credentials");
assert!(!creds.is_empty(), "No credentials found");
let credential_id = creds[0].credential_id.id.clone();
println!(
" ✓ Found credential ID: {}",
hex::encode(&credential_id[..8.min(credential_id.len())])
);
println!("\n🗑️ Deleting credential...");
let delete_request =
soft_fido2::request::DeleteCredentialRequest::new(pin_uv_auth.clone(), credential_id);
Client::delete_credential(&mut transport, delete_request).expect("Failed to delete credential");
println!(" ✓ Credential deleted successfully");
let request = CredentialManagementRequest::new(pin_uv_auth);
match Client::get_credentials_metadata(&mut transport, request) {
Ok(metadata) => {
println!(
" ✓ Credentials after deletion: {}",
metadata.existing_resident_credentials_count
);
assert_eq!(
metadata.existing_resident_credentials_count, 0,
"Credential was not deleted"
);
}
Err(e) => {
println!(" ⚠ Could not verify deletion: {:?}", e);
}
}
println!("\n✅ Client delete command works!\n");
}
#[test]
#[ignore]
fn test_client_reset() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Client Reset Command");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📝 Registering test credentials...");
register_credential(&mut transport, &[1, 2, 3, 4], "alice@example.com")
.expect("Failed to register credential 1");
register_credential(&mut transport, &[5, 6, 7, 8], "bob@example.com")
.expect("Failed to register credential 2");
println!(" ✓ Registered 2 credentials");
println!("\n🔄 Resetting authenticator...");
transport
.send_ctap_command(0x07, &[], 30000)
.expect("Failed to reset");
println!(" ✓ Reset command executed successfully");
println!(" Note: Reset clears all credentials from storage");
println!(" ✓ Reset command structure is implemented");
println!("\n✅ Client reset command works!\n");
}
#[test]
fn test_client_cli_help() {
use clap::CommandFactory;
use passless_core::Args;
let app = Args::command();
let client_cmd = app
.find_subcommand("client")
.expect("client subcommand not found");
let subcommands: Vec<_> = client_cmd.get_subcommands().map(|c| c.get_name()).collect();
assert!(subcommands.contains(&"info"));
assert!(subcommands.contains(&"reset"));
assert!(subcommands.contains(&"list"));
assert!(subcommands.contains(&"delete"));
assert!(subcommands.contains(&"pin"));
println!("✓ All client subcommands are registered:");
for cmd in &subcommands {
if *cmd != "help" {
println!(" - {}", cmd);
}
}
}
#[test]
#[ignore]
fn test_client_list_with_rp_filter() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Client List with RP Filter");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📝 Registering test credentials for multiple RPs...");
register_credential_with_rp(
&mut transport,
&[1, 2, 3],
"alice@example.com",
"example.com",
)
.expect("Failed to register credential 1");
println!(" ✓ Registered credential for example.com");
register_credential_with_rp(&mut transport, &[4, 5, 6], "bob@github.com", "github.com")
.expect("Failed to register credential 2");
println!(" ✓ Registered credential for github.com");
register_credential_with_rp(
&mut transport,
&[7, 8, 9],
"charlie@google.com",
"google.com",
)
.expect("Failed to register credential 3");
println!(" ✓ Registered credential for google.com");
println!("\n📋 Verifying all RPs are present using API...");
use soft_fido2::PinProtocol;
use soft_fido2::request::CredentialManagementRequest;
let pin_uv_auth =
Client::get_uv_token_for_credential_management(&mut transport, PinProtocol::V2)
.expect("Failed to get UV token");
let request = CredentialManagementRequest::new(Some(pin_uv_auth.clone()));
let all_rps = Client::enumerate_rps(&mut transport, request).expect("Failed to enumerate RPs");
println!(" ✓ Found {} RPs total", all_rps.len());
assert_eq!(all_rps.len(), 3, "Expected 3 RPs");
let rp_ids: Vec<String> = all_rps.iter().map(|rp| rp.id.clone()).collect();
assert!(
rp_ids.contains(&"example.com".to_string()),
"Should have example.com"
);
assert!(
rp_ids.contains(&"github.com".to_string()),
"Should have github.com"
);
assert!(
rp_ids.contains(&"google.com".to_string()),
"Should have google.com"
);
println!("\n🔍 Testing RP filter logic (simulated)...");
println!("\n Simulating filter: 'github'");
let filtered: Vec<_> = all_rps
.iter()
.filter(|rp| rp.id.contains("github"))
.collect();
println!(" ✓ Would show {} RPs", filtered.len());
assert_eq!(filtered.len(), 1, "Should match 1 RP");
assert_eq!(filtered[0].id, "github.com", "Should match github.com");
println!("\n Simulating filter: 'com'");
let filtered: Vec<_> = all_rps.iter().filter(|rp| rp.id.contains("com")).collect();
println!(" ✓ Would show {} RPs", filtered.len());
assert_eq!(filtered.len(), 3, "Should match all 3 RPs ending in .com");
println!("\n Simulating filter: 'nonexistent'");
let filtered: Vec<_> = all_rps
.iter()
.filter(|rp| rp.id.contains("nonexistent"))
.collect();
println!(" ✓ Would show {} RPs", filtered.len());
assert_eq!(filtered.len(), 0, "Should match 0 RPs");
println!("\n✅ RP filtering logic works correctly!");
}
#[test]
#[ignore]
fn test_client_device_selection() {
println!(
"
═══════════════════════════════════════"
);
println!(" TEST: Device Selection");
println!(
"═══════════════════════════════════════
"
);
let mut harness1 = AuthenticatorHarness::with_local().expect("Failed to create harness 1");
harness1.set_device_ids(0x1111, 0x0001);
let mut harness2 = AuthenticatorHarness::with_local().expect("Failed to create harness 2");
harness2.set_device_ids(0x2222, 0x0002);
println!("Starting first authenticator...");
harness1.start().expect("Failed to start authenticator 1");
println!(
" ✓ First authenticator started
"
);
println!("Starting second authenticator...");
harness2
.start_and_wait_for_device_count(2)
.expect("Failed to start authenticator 2");
println!(
" ✓ Second authenticator started
"
);
println!("📋 Enumerating devices...");
let list = TransportList::enumerate().expect("Failed to enumerate");
println!(" ✓ Found {} device(s)", list.len());
assert_eq!(list.len(), 2, "Expected 2 authenticators");
println!(
"
📝 Registering credentials on each device..."
);
let mut transport1 = list.get(0).expect("Failed to get device 0");
transport1.open().expect("Failed to open device 0");
register_credential_with_rp(
&mut transport1,
&[1, 2, 3],
"alice@device0.com",
"device0.example.com",
)
.expect("Failed to register on device 0");
println!(" ✓ Registered credential on device 0");
transport1.close();
let mut transport2 = list.get(1).expect("Failed to get device 1");
transport2.open().expect("Failed to open device 1");
register_credential_with_rp(
&mut transport2,
&[4, 5, 6],
"bob@device1.com",
"device1.example.com",
)
.expect("Failed to register on device 1");
println!(" ✓ Registered credential on device 1");
transport2.close();
println!(
"
🔍 Testing device selection..."
);
println!(
"
Checking device 0:"
);
let mut transport = list.get(0).expect("Failed to get device 0");
transport.open().expect("Failed to open device 0");
use soft_fido2::PinProtocol;
let pin_uv_auth =
Client::get_uv_token_for_credential_management(&mut transport, PinProtocol::V2)
.expect("Failed to get UV token");
let request = soft_fido2::request::CredentialManagementRequest::new(Some(pin_uv_auth));
let rps = Client::enumerate_rps(&mut transport, request)
.expect("Failed to enumerate RPs on device 0");
assert_eq!(rps.len(), 1, "Device 0 should have 1 RP");
assert_eq!(
rps[0].id, "device0.example.com",
"Device 0 should have device0.example.com"
);
println!(" ✓ Device 0 has credential for device0.example.com");
transport.close();
println!(
"
Checking device 1:"
);
let mut transport = list.get(1).expect("Failed to get device 1");
transport.open().expect("Failed to open device 1");
let pin_uv_auth =
Client::get_uv_token_for_credential_management(&mut transport, PinProtocol::V2)
.expect("Failed to get UV token");
let request = soft_fido2::request::CredentialManagementRequest::new(Some(pin_uv_auth));
let rps = Client::enumerate_rps(&mut transport, request)
.expect("Failed to enumerate RPs on device 1");
assert_eq!(rps.len(), 1, "Device 1 should have 1 RP");
assert_eq!(
rps[0].id, "device1.example.com",
"Device 1 should have device1.example.com"
);
println!(" ✓ Device 1 has credential for device1.example.com");
transport.close();
println!(
"
✅ Device selection works correctly!"
);
println!(" - Each device maintains separate credential storage");
println!(" - Devices can be accessed independently by index");
}
#[test]
#[ignore]
fn test_client_pin_set_and_change() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Client PIN Set and Change");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📋 Getting initial authenticator info...");
let response = Client::authenticator_get_info(&mut transport).expect("Failed to get info");
use ciborium::Value;
let cbor_value: Value = ciborium::from_reader(&response[..]).expect("Failed to decode CBOR");
let mut client_pin_present = false;
if let Value::Map(map) = &cbor_value {
for (k, v) in map {
if let Value::Integer(key) = k
&& *key == 4.into()
&& let Value::Map(options) = v
{
for (opt_k, opt_v) in options {
if let (Value::Text(k), Value::Bool(_v)) = (opt_k, opt_v)
&& k == "clientPin"
{
client_pin_present = true;
}
}
}
}
}
assert!(client_pin_present, "Authenticator should support clientPin");
println!(" ✓ Authenticator supports clientPin");
println!("\n🔐 Testing PIN set...");
let test_pin = "1234";
let mut encapsulation =
soft_fido2::PinUvAuthEncapsulation::new(&mut transport, soft_fido2::PinProtocol::V2)
.expect("Failed to create PIN encapsulation");
encapsulation
.set_pin(&mut transport, test_pin)
.expect("Failed to set PIN");
println!(" ✓ PIN set successfully");
println!("\n📋 Verifying PIN is set...");
let response = Client::authenticator_get_info(&mut transport).expect("Failed to get info");
let cbor_value: Value = ciborium::from_reader(&response[..]).expect("Failed to decode CBOR");
let mut pin_set = false;
if let Value::Map(map) = &cbor_value {
for (k, v) in map {
if let Value::Integer(key) = k
&& *key == 4.into()
&& let Value::Map(options) = v
{
for (opt_k, opt_v) in options {
if let (Value::Text(k), Value::Bool(v)) = (opt_k, opt_v)
&& k == "clientPin"
{
pin_set = *v;
}
}
}
}
}
assert!(pin_set, "PIN should be set");
println!(" ✓ PIN is set on authenticator");
println!("\n🔐 Testing PIN change...");
let new_pin = "5678";
let mut encapsulation =
soft_fido2::PinUvAuthEncapsulation::new(&mut transport, soft_fido2::PinProtocol::V2)
.expect("Failed to create PIN encapsulation");
encapsulation
.change_pin(&mut transport, test_pin, new_pin)
.expect("Failed to change PIN");
println!(" ✓ PIN changed successfully");
println!("\n✅ PIN set and change commands work!\n");
}
fn is_pin_set(transport: &mut soft_fido2::Transport) -> bool {
let response = Client::authenticator_get_info(transport).expect("Failed to get info");
use ciborium::Value;
let cbor_value: Value = ciborium::from_reader(&response[..]).expect("Failed to decode CBOR");
if let Value::Map(map) = &cbor_value {
for (k, v) in map {
if let Value::Integer(key) = k
&& *key == 4.into()
&& let Value::Map(options) = v
{
for (opt_k, opt_v) in options {
if let (Value::Text(k), Value::Bool(v)) = (opt_k, opt_v)
&& k == "clientPin"
{
return *v;
}
}
}
}
}
false
}
fn set_pin(
transport: &mut soft_fido2::Transport,
pin: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut encapsulation =
soft_fido2::PinUvAuthEncapsulation::new(transport, soft_fido2::PinProtocol::V2)?;
encapsulation.set_pin(transport, pin)?;
Ok(())
}
fn get_uv_token_with_pin(
transport: &mut soft_fido2::Transport,
pin: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut encapsulation =
soft_fido2::PinUvAuthEncapsulation::new(transport, soft_fido2::PinProtocol::V2)?;
let token = encapsulation.get_pin_uv_auth_token_using_pin_with_permissions(
transport, pin, 0x03, None,
)?;
Ok(token)
}
fn get_uv_retries(transport: &mut soft_fido2::Transport) -> Result<u8, Box<dyn std::error::Error>> {
use soft_fido2_ctap::cbor::MapBuilder;
let params = MapBuilder::new().insert(0x02, 0x07u8)?.build()?;
let response = transport.send_ctap_command(0x06, ¶ms, 30000)?;
if response.is_empty() {
return Err("getUvRetries returned empty response".into());
}
use ciborium::Value;
let cbor_value: Value = if response[0] == 0x00 {
ciborium::from_reader(&response[1..])?
} else {
ciborium::from_reader(&response[..])?
};
if let Value::Map(map) = cbor_value {
for (key, value) in map {
if key == Value::Integer(0x05.into())
&& let Value::Integer(retries) = value
{
return u8::try_from(i128::from(retries)).map_err(|_| "invalid uvRetries".into());
}
}
}
Err("uvRetries missing from response".into())
}
fn build_authenticated_uv_reset_payload(
transport: &mut soft_fido2::Transport,
pin: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
use soft_fido2_ctap::cbor::MapBuilder;
let mut encapsulation =
soft_fido2::PinUvAuthEncapsulation::new(transport, soft_fido2::PinProtocol::V2)?;
let token = encapsulation.get_pin_uv_auth_token_using_pin_with_permissions(
transport, pin, 0x04, None,
)?;
let auth_data = [CMD_PASSLESS_RESET_UV_RETRIES, RESET_UV_RETRIES_SUBCOMMAND];
let pin_uv_auth_param = encapsulation.authenticate(&auth_data, &token)?;
Ok(MapBuilder::new()
.insert(0x01, RESET_UV_RETRIES_SUBCOMMAND)?
.insert(0x03, 0x02u8)?
.insert_bytes(0x04, &pin_uv_auth_param)?
.build()?)
}
#[test]
#[ignore]
fn test_authenticated_uv_retry_reset_command() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Authenticated UV Retry Reset");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
set_pin(&mut transport, "1234").expect("Failed to set PIN");
let unauthenticated = transport.send_ctap_command(CMD_PASSLESS_RESET_UV_RETRIES, &[], 30000);
if let Ok(response) = unauthenticated {
assert_ne!(response, vec![0x00, 0xa0]);
}
let payload = build_authenticated_uv_reset_payload(&mut transport, "1234")
.expect("Failed to build authenticated reset payload");
let authenticated = transport
.send_ctap_command(CMD_PASSLESS_RESET_UV_RETRIES, &payload, 30000)
.expect("Failed to send authenticated reset command");
assert!(authenticated == vec![0x00, 0xa0] || authenticated == vec![0xa0]);
assert_eq!(
get_uv_retries(&mut transport).expect("Failed to get UV retries"),
3
);
harness.stop();
}
#[test]
#[ignore]
fn test_pin_persistence_after_restart() {
println!("\n═══════════════════════════════════════");
println!(" TEST: PIN Persistence After Restart");
println!("═══════════════════════════════════════\n");
let storage_path = std::env::temp_dir().join("passless_pin_persistence_test");
let _ = std::fs::remove_dir_all(&storage_path); std::fs::create_dir_all(&storage_path).expect("Failed to create storage dir");
println!("📦 Starting first authenticator instance...");
println!(" Storage path: {}", storage_path.display());
let mut harness = AuthenticatorHarness::with_local_path(storage_path.clone());
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📋 Checking initial PIN state...");
assert!(
!is_pin_set(&mut transport),
"PIN should not be set initially"
);
println!(" ✓ No PIN set initially");
println!("\n🔐 Setting PIN...");
set_pin(&mut transport, "1234").expect("Failed to set PIN");
println!(" ✓ PIN set");
assert!(is_pin_set(&mut transport), "PIN should be set");
println!(" ✓ PIN is set in authenticator info");
let pin_file = storage_path.join("pin_state.json");
assert!(pin_file.exists(), "PIN state file should exist");
println!(" ✓ PIN state file exists at {:?}", pin_file);
println!("\n🔄 Stopping authenticator...");
harness.stop();
std::thread::sleep(std::time::Duration::from_millis(1000));
println!("📦 Starting second authenticator instance...");
println!(" Storage path: {}", storage_path.display());
let mut harness2 = AuthenticatorHarness::with_local_path(storage_path.clone());
harness2
.start()
.expect("Failed to start second authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport2 = connect_to_authenticator().expect("Failed to connect");
println!("📋 Verifying PIN persistence...");
assert!(
is_pin_set(&mut transport2),
"PIN should still be set after restart"
);
println!(" ✓ PIN persisted across restart");
println!("\n🔐 Verifying PIN authentication works...");
let token = get_uv_token_with_pin(&mut transport2, "1234");
assert!(
token.is_ok(),
"Should be able to get UV token with correct PIN"
);
println!(" ✓ PIN authentication successful");
println!("\n✅ PIN persistence test passed!\n");
harness2.stop();
let _ = std::fs::remove_dir_all(&storage_path);
}
#[test]
#[ignore]
fn test_pin_enforcement_required_blocks_without_pin() {
println!("\n═══════════════════════════════════════");
println!(" TEST: PIN Enforcement Required");
println!("═══════════════════════════════════════\n");
println!("📋 This test verifies that when pin.enforcement=required:");
println!(" - Operations require PIN UV auth param");
println!(" - Notification fallback is NOT available");
println!();
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("🔐 Setting PIN...");
set_pin(&mut transport, "1234").expect("Failed to set PIN");
println!(" ✓ PIN set");
println!("\n📝 Testing registration with PIN set (enforcement=optional)...");
register_credential(&mut transport, &[1, 2, 3, 4], "test@example.com")
.expect("Registration should succeed with notification fallback");
println!(" ✓ Registration succeeded (notification fallback worked)");
println!("\n✅ PIN enforcement behavior verified!\n");
println!(" Note: To fully test enforcement=required, start authenticator with:");
println!(" PASSLESS_PIN_ENFORCEMENT=required");
}
#[test]
#[ignore]
fn test_pin_required_for_make_credential_with_always_uv() {
println!("\n═══════════════════════════════════════");
println!(" TEST: PIN Required for MakeCredential with always_uv");
println!("═══════════════════════════════════════\n");
println!("📋 Test scenario:");
println!(" - enforcement=optional (default)");
println!(" - always_uv=true");
println!(" - PIN is set");
println!(" -> PIN should be required for UV");
println!();
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("🔐 Setting PIN...");
set_pin(&mut transport, "1234").expect("Failed to set PIN");
println!(" ✓ PIN set");
println!("\n🔐 Getting UV token with correct PIN...");
let token_result = get_uv_token_with_pin(&mut transport, "1234");
assert!(token_result.is_ok(), "Should get UV token with correct PIN");
println!(" ✓ UV token obtained with correct PIN");
println!("\n🔐 Testing UV token with wrong PIN...");
let wrong_token = get_uv_token_with_pin(&mut transport, "0000");
assert!(
wrong_token.is_err(),
"Should NOT get UV token with wrong PIN"
);
println!(" ✓ UV token correctly rejected with wrong PIN");
println!("\n✅ PIN verification for UV token works correctly!\n");
}
#[test]
#[ignore]
fn test_pin_registration_without_pin_set() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Registration Without PIN Set");
println!("═══════════════════════════════════════\n");
println!("📋 This test verifies that when no PIN is set:");
println!(" - Registration uses notification-based UV");
println!(" - No PIN is required");
println!();
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📋 Checking PIN state...");
assert!(!is_pin_set(&mut transport), "PIN should not be set");
println!(" ✓ No PIN set");
println!("\n📝 Registering credential without PIN...");
register_credential(&mut transport, &[1, 2, 3, 4], "nopin@example.com")
.expect("Registration should succeed without PIN");
println!(" ✓ Registration succeeded via notification UV");
println!("\n🔓 Authenticating without PIN...");
let challenge: [u8; 32] = rand::random();
let client_data_hash = generate_client_data_hash_for_registration(&challenge);
use soft_fido2::request::GetAssertionRequest;
let request =
GetAssertionRequest::new(client_data_hash, RP_ID.to_string()).with_user_verification(true);
let result = Client::get_assertion(&mut transport, request);
assert!(result.is_ok(), "Authentication should succeed without PIN");
println!(" ✓ Authentication succeeded via notification UV");
println!("\n✅ Registration without PIN works correctly!\n");
}
#[test]
#[ignore]
fn test_pin_info_shows_correct_state() {
println!("\n═══════════════════════════════════════");
println!(" TEST: Authenticator Info Shows Correct PIN State");
println!("═══════════════════════════════════════\n");
let mut harness = AuthenticatorHarness::with_local().expect("Failed to create harness");
harness.start().expect("Failed to start authenticator");
std::thread::sleep(std::time::Duration::from_millis(500));
let mut transport = connect_to_authenticator().expect("Failed to connect");
println!("📋 Initial state (no PIN):");
let response = Client::authenticator_get_info(&mut transport).expect("Failed to get info");
use ciborium::Value;
let cbor_value: Value = ciborium::from_reader(&response[..]).expect("Failed to decode CBOR");
let mut client_pin_value = None;
let mut uv_value = None;
if let Value::Map(map) = &cbor_value {
for (k, v) in map {
if let Value::Integer(key) = k
&& *key == 4.into()
&& let Value::Map(options) = v
{
for (opt_k, opt_v) in options {
if let (Value::Text(k), Value::Bool(v)) = (opt_k, opt_v) {
match k.as_str() {
"clientPin" => client_pin_value = Some(*v),
"uv" => uv_value = Some(*v),
_ => {}
}
}
}
}
}
}
println!(" clientPin: {:?}", client_pin_value);
println!(" uv: {:?}", uv_value);
assert_eq!(
client_pin_value,
Some(false),
"clientPin should be false (not set)"
);
assert_eq!(
uv_value,
Some(true),
"uv should be true (capability exists)"
);
println!("\n🔐 Setting PIN...");
set_pin(&mut transport, "1234").expect("Failed to set PIN");
println!(" ✓ PIN set");
println!("\n📋 State after PIN is set:");
let response = Client::authenticator_get_info(&mut transport).expect("Failed to get info");
let cbor_value: Value = ciborium::from_reader(&response[..]).expect("Failed to decode CBOR");
let mut client_pin_value = None;
let mut uv_value = None;
if let Value::Map(map) = &cbor_value {
for (k, v) in map {
if let Value::Integer(key) = k
&& *key == 4.into()
&& let Value::Map(options) = v
{
for (opt_k, opt_v) in options {
if let (Value::Text(k), Value::Bool(v)) = (opt_k, opt_v) {
match k.as_str() {
"clientPin" => client_pin_value = Some(*v),
"uv" => uv_value = Some(*v),
_ => {}
}
}
}
}
}
}
println!(" clientPin: {:?}", client_pin_value);
println!(" uv: {:?}", uv_value);
assert_eq!(
client_pin_value,
Some(true),
"clientPin should be true (PIN is set)"
);
println!("\n✅ Authenticator info shows correct PIN state!\n");
}