#[cfg(not(target_os = "linux"))]
fn main() {
eprintln!("This example requires Linux with UHID support.");
}
#[cfg(target_os = "linux")]
use soft_fido2::{
Authenticator, AuthenticatorCallbacks, AuthenticatorConfig, AuthenticatorOptions, Credential,
CredentialRef, Error, Result, UpResult, UvResult,
};
#[cfg(target_os = "linux")]
use soft_fido2_transport::{CommandHandler, UhidDevice};
#[cfg(target_os = "linux")]
use std::collections::HashMap;
#[cfg(target_os = "linux")]
use std::sync::{Arc, Mutex};
#[cfg(target_os = "linux")]
use std::time::Duration;
#[cfg(target_os = "linux")]
struct AuthenticatorHandler<C: AuthenticatorCallbacks> {
authenticator: Mutex<Authenticator<C>>,
}
#[cfg(target_os = "linux")]
impl<C: AuthenticatorCallbacks> AuthenticatorHandler<C> {
fn new(authenticator: Authenticator<C>) -> Self {
Self {
authenticator: Mutex::new(authenticator),
}
}
}
#[cfg(target_os = "linux")]
impl<C: AuthenticatorCallbacks> CommandHandler for AuthenticatorHandler<C> {
fn handle_command(
&mut self,
cmd: soft_fido2_transport::Cmd,
data: &[u8],
) -> soft_fido2_transport::Result<Vec<u8>> {
if cmd != soft_fido2_transport::Cmd::Cbor {
return Err(soft_fido2_transport::Error::InvalidCommand);
}
let mut auth = self.authenticator.lock().map_err(|_| {
soft_fido2_transport::Error::Other("Failed to lock authenticator".to_string())
})?;
let mut response = Vec::new();
auth.handle(data, &mut response)
.map_err(|_| soft_fido2_transport::Error::Other("Command failed".to_string()))?;
Ok(response)
}
}
#[cfg(target_os = "linux")]
struct UhidAuthenticator<C: AuthenticatorCallbacks> {
device: UhidDevice,
handler: soft_fido2_transport::CtapHidHandler<AuthenticatorHandler<C>>,
}
#[cfg(target_os = "linux")]
impl<C: AuthenticatorCallbacks> UhidAuthenticator<C> {
fn new(authenticator: Authenticator<C>, config: &AuthenticatorConfig) -> Result<Self> {
let device = UhidDevice::create_fido_device_with_ids(
config.device_name.as_deref(),
config.vendor_id,
config.product_id,
config.device_version,
)
.map_err(|_| Error::Other)?;
let auth_handler = AuthenticatorHandler::new(authenticator);
let handler = soft_fido2_transport::CtapHidHandler::new(auth_handler);
Ok(Self { device, handler })
}
fn process_one(&mut self) -> Result<bool> {
let mut packet_data = [0u8; 64];
match self.device.read_packet(&mut packet_data) {
Ok(Some(_len)) => {
let packet = soft_fido2_transport::Packet::from_bytes(packet_data);
let response_packets = self
.handler
.process_packet(packet)
.map_err(|_| Error::Other)?;
for response_packet in response_packets {
self.device
.write_packet(response_packet.as_bytes())
.map_err(|_| Error::Other)?;
}
Ok(true)
}
Ok(None) => Ok(false), Err(_) => Err(Error::Timeout),
}
}
fn run(&mut self) -> Result<()> {
let mut request_count = 0u64;
loop {
match self.process_one() {
Ok(true) => {
request_count += 1;
}
Ok(false) => {
std::thread::sleep(Duration::from_millis(10));
}
Err(Error::Timeout) => {
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => {
eprintln!("✗ Error processing packet: {:?}", e);
std::thread::sleep(Duration::from_millis(100));
}
}
if request_count > 0 && request_count.is_multiple_of(100) {
eprintln!(" [Stats] Processed {} requests", request_count);
}
}
}
}
#[cfg(target_os = "linux")]
struct VirtualAuthCallbacks {
credentials: Arc<Mutex<HashMap<Vec<u8>, Credential>>>,
}
#[cfg(target_os = "linux")]
impl VirtualAuthCallbacks {
fn new() -> Self {
Self {
credentials: Arc::new(Mutex::new(HashMap::new())),
}
}
}
#[cfg(target_os = "linux")]
impl AuthenticatorCallbacks for VirtualAuthCallbacks {
fn request_up(&self, info: &str, user: Option<&str>, rp: &str) -> Result<UpResult> {
println!("\n [UP] 👆 User Presence Requested");
println!(" Info: {}", info);
if let Some(u) = user {
println!(" User: {}", u);
}
println!(" RP: {}", rp);
println!(" ✓ AUTO-APPROVED");
Ok(UpResult::Accepted)
}
fn request_uv(&self, info: &str, user: Option<&str>, rp: &str) -> Result<UvResult> {
println!("\n [UV] 🔐 User Verification Requested");
println!(" Info: {}", info);
if let Some(u) = user {
println!(" User: {}", u);
}
println!(" RP: {}", rp);
println!(" ✓ AUTO-APPROVED (biometric/PIN simulated)");
Ok(UvResult::Accepted)
}
fn write_credential(&self, cred: &CredentialRef) -> Result<()> {
let mut store = self.credentials.lock().unwrap();
store.insert(cred.id.to_vec(), cred.to_owned());
println!("\n✓ CREDENTIAL REGISTERED");
println!(" RP ID: {}", cred.rp_id);
if let Some(user_name) = cred.user_name {
println!(" User: {}", user_name);
}
if let Some(rp_name) = cred.rp_name {
println!(" RP Name: {}", rp_name);
}
println!(" User ID: {} bytes", cred.user_id.len());
println!(" Credential ID: {} bytes", cred.id.len());
println!(" Discoverable: {}", cred.discoverable);
if let Some(cp) = cred.cred_protect {
println!(" CredProtect: 0x{:02x}", cp);
}
if let Some(cr) = cred.cred_random {
println!(
" CredRandom: {} bytes (hmac-secret enabled)",
cr.as_slice().len()
);
} else {
println!(" CredRandom: None (hmac-secret NOT enabled)");
}
println!(" Total credentials stored: {}\n", store.len());
Ok(())
}
fn read_credential(&self, cred_id: &[u8]) -> Result<Option<Credential>> {
let store = self.credentials.lock().unwrap();
match store.get(cred_id) {
Some(cred) => {
println!("\n [AUTH] 🔑 Credential Retrieved");
println!(" RP: {}", cred.rp.id);
if let Some(ref name) = cred.user.name {
println!(" User: {}", name);
}
println!(" Sign count: {}", cred.sign_count);
if cred.extensions.cred_random.is_some() {
println!(" CredRandom: present (hmac-secret available)");
} else {
println!(" CredRandom: None (hmac-secret NOT available)");
}
Ok(Some(cred.clone()))
}
None => {
println!("\n [AUTH] ✗ Credential not found");
Ok(None)
}
}
}
fn delete_credential(&self, cred_id: &[u8]) -> Result<()> {
let mut store = self.credentials.lock().unwrap();
store.remove(cred_id);
println!(" [DELETE] Credential removed\n");
Ok(())
}
fn list_credentials(&self, rp_id: &str, user_id: Option<&[u8]>) -> Result<Vec<Credential>> {
let store = self.credentials.lock().unwrap();
let filtered: Vec<Credential> = store
.values()
.filter(|c| {
if c.rp.id != rp_id {
return false;
}
if let Some(uid) = user_id {
c.user.id == uid
} else {
true
}
})
.cloned()
.collect();
println!(
" [READ] Found {} credential(s) for RP: {}",
filtered.len(),
rp_id
);
Ok(filtered)
}
fn enumerate_rps(&self) -> Result<Vec<(String, Option<String>, usize)>> {
let store = self.credentials.lock().unwrap();
let mut rp_map: HashMap<String, (Option<String>, usize)> = HashMap::new();
for cred in store.values() {
let entry = rp_map
.entry(cred.rp.id.clone())
.or_insert((cred.rp.name.clone(), 0));
entry.1 += 1;
}
let result: Vec<(String, Option<String>, usize)> = rp_map
.into_iter()
.map(|(rp_id, (rp_name, count))| (rp_id, rp_name, count))
.collect();
println!(" [RPS] Enumerated {} RPs", result.len());
Ok(result)
}
fn credential_count(&self) -> Result<usize> {
let store = self.credentials.lock().unwrap();
let count = store.len();
println!(" [COUNT] Total credentials stored: {}", count);
Ok(count)
}
fn get_timestamp_ms(&self) -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}
#[cfg(target_os = "linux")]
fn main() -> Result<()> {
println!("╔═══════════════════════════════════════════════════════════╗");
println!("║ Virtual FIDO2 Authenticator (UHID) ║");
println!("╚═══════════════════════════════════════════════════════════╝\n");
let callbacks = VirtualAuthCallbacks::new();
let config = AuthenticatorConfig::builder()
.aaguid([
0x73, 0x6f, 0x66, 0x74, 0x2d, 0x66, 0x69, 0x64, 0x6f, 0x32, 0x2d, 0x76, 0x69, 0x72,
0x74, 0x75,
])
.max_credentials(100)
.extensions(vec![
"credProtect".to_string(),
"hmac-secret".to_string(),
"largeBlobKey".to_string(),
])
.options(
AuthenticatorOptions::new()
.with_resident_keys(true) .with_user_presence(true) .with_user_verification(Some(true)) .with_client_pin(None) .with_pin_uv_auth_token(Some(true)) .with_make_cred_uv_not_required(Some(true)), )
.device_name("Custom FIDO2 Authenticator".to_string())
.vendor_id(0x1234)
.product_id(0x5678)
.device_version(0x0100)
.build();
println!("╔═══════════════════════════════════════════════════════════╗");
println!("║ Authenticator Configuration ║");
println!("╚═══════════════════════════════════════════════════════════╝");
println!(" AAGUID: soft-fido2-virtu");
println!(" Algorithms: ES256 (-7)");
println!(" Resident Keys (rk): ✓ Supported");
println!(" Force Resident Keys: ✓ Enabled by default");
println!(" User Presence (up): ✓ Supported (auto-approved)");
println!(" User Verification (uv): ✓ Supported (auto-approved)");
println!(" UV Token: PIN/UV auth token (pinUvAuthToken=true)");
println!(" UV Flexibility: makeCredUvNotRqd=true (flexible UV behavior)");
println!(" Extensions: credProtect, hmac-secret, largeBlobKey");
println!(" Max Credentials: 100");
println!();
println!(" NOTE: Configuration optimized for WebAuthn test compatibility:");
println!(" - force_resident_keys=true (all credentials stored)");
println!(" - makeCredUvNotRqd=true (consistent UV behavior)");
println!();
let auth = Authenticator::with_config(callbacks, config.clone())?;
println!("Creating UHID virtual device...");
let mut uhid_auth = UhidAuthenticator::new(auth, &config).map_err(|e| {
eprintln!("\n✗ Failed to create UHID device: {:?}", e);
eprintln!("\nTroubleshooting:");
eprintln!(" 1. Check UHID module: sudo modprobe uhid");
eprintln!(" 2. Check permissions: groups | grep fido");
eprintln!(" 3. Check udev rules: cat /etc/udev/rules.d/90-uhid.rules");
eprintln!(" 4. Log out and back in if you just added the group");
eprintln!();
e
})?;
println!("✓ UHID device created successfully!\n");
println!("╔═══════════════════════════════════════════════════════════╗");
println!("║ Authenticator Ready - Waiting for WebAuthn requests... ║");
println!("╚═══════════════════════════════════════════════════════════╝\n");
println!("Test with:");
println!(" • https://webauthn.firstyear.id.au/ (webauthn-rs demo)");
println!(" • https://webauthn.io/");
println!(" • https://www.passwordless.dev/test");
println!();
println!("Press Ctrl+C to stop\n");
uhid_auth.run()
}