From 23d12ba40c3d2644ea744442d12617a29728ab14 Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 14 May 2026 19:48:49 +0300 Subject: [PATCH 1/7] refactor(fido): split raw HID and CBOR response handling --- src/device/fido/hid.rs | 61 ++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 092a503..c9987b6 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -168,6 +168,11 @@ impl HidTransport { self.read_cbor_response(cmd) } + pub fn send_raw(&self, cmd: u8, payload: &[u8]) -> Result, PFError> { + self.write_cbor_request(cmd, payload)?; + self.read_hid_response(cmd) + } + fn write_cbor_request(&self, cmd: u8, payload: &[u8]) -> Result<(), PFError> { log::debug!( "Sending CBOR Command: 0x{:02X}, Payload Size: {} bytes", @@ -235,6 +240,32 @@ impl HidTransport { } fn read_cbor_response(&self, cmd: u8) -> Result, PFError> { + let response_data = self.read_hid_response(cmd)?; + + // Check CTAP Status Byte (First byte of payload) + if response_data.is_empty() { + log::error!("Device sent empty payload response."); + return Err(PFError::Device("Empty response".into())); + } + let status = response_data[0]; + if status != 0x00 { + log::error!("FIDO Operation returned failure status: 0x{:02X}", status); + return Err(PFError::Device(format!( + "FIDO Operation Failed with Status: 0x{:02X}", + status + ))); + } + + log::debug!( + "Command 0x{:02X} successful. Response payload len: {}", + cmd, + response_data.len() - 1 + ); + // Return payload without status byte + Ok(response_data[1..].to_vec()) + } + + fn read_hid_response(&self, cmd: u8) -> Result, PFError> { log::debug!("Waiting for response..."); let mut buf = [0u8; HID_REPORT_SIZE]; @@ -347,27 +378,7 @@ impl HidTransport { read_len += in_pkt; } - // 3. Check CTAP Status Byte (First byte of payload) - if response_data.is_empty() { - log::error!("Device sent empty payload response."); - return Err(PFError::Device("Empty response".into())); - } - let status = response_data[0]; - if status != 0x00 { - log::error!("FIDO Operation returned failure status: 0x{:02X}", status); - return Err(PFError::Device(format!( - "FIDO Operation Failed with Status: 0x{:02X}", - status - ))); - } - - log::debug!( - "Command 0x{:02X} successful. Response payload len: {}", - cmd, - response_data.len() - 1 - ); - // Return payload without status byte - Ok(response_data[1..].to_vec()) + Ok(response_data) } pub fn send_vendor_config( @@ -518,7 +529,7 @@ impl HidTransport { sub_params: Option, ) -> Result, PFError> { let mut sub_params_bytes: Vec = Vec::new(); - + if let Some(ref params) = sub_params { sub_params_bytes = to_vec(¶ms).map_err(|e| PFError::Io(e.to_string()))?; } @@ -599,7 +610,11 @@ impl HidTransport { Value::Integer(new_min_pin_length as i128), ); let sub_params = Value::Map(sub_params_map); - match self.send_config(ConfigSubCommand::SetMinPinLength, pin_token, Some(sub_params)) { + match self.send_config( + ConfigSubCommand::SetMinPinLength, + pin_token, + Some(sub_params), + ) { Ok(_) => { log::info!( "Successfully set minimum PIN length to {}", From 13ba4958f7d4fff6993a265fc62dc9904d651625 Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 14 May 2026 19:52:44 +0300 Subject: [PATCH 2/7] refactor(fido): parse picofido 7.6 getinfo vendor commands --- src/device/fido/mod.rs | 205 ++++++++++++++++++++++------------------- src/device/types.rs | 4 +- 2 files changed, 113 insertions(+), 96 deletions(-) diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index 4e67a02..ef083ae 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -30,7 +30,11 @@ pub(crate) fn get_fido_info() -> Result { let info_val: Value = from_slice(&info_res).map_err(|e| format!("Failed to parse GetInfo CBOR: {}", e))?; - let map = match &info_val { + parse_fido_get_info(&info_val) +} + +fn parse_fido_get_info(info_val: &Value) -> Result { + let map = match info_val { Value::Map(m) => m, _ => return Err("GetInfo response is not a CBOR map".into()), }; @@ -199,26 +203,10 @@ pub(crate) fn get_fido_info() -> Result { ); } } - // 0x13: vendorPrototypeConfigCommands (array of unsigned integers) + // Some firmware versions used 0x13 here. Pico-FIDO 7.6 reports + // vendorPrototypeConfigCommands at 0x15. 0x13 => { - if let Value::Array(arr) = val { - for v in arr { - if let Value::Integer(n) = v { - let cmd_id = *n as u64; - let cmd_name = VendorConfigCommand::from_u64(cmd_id) - .map(|c| format!("{}", c)) - .unwrap_or_else(|| format!("0x{:016X}", cmd_id)); - vendor_config_commands.push(cmd_name); - } - } - log::info!( - "Device supports {} vendor config commands: {:?}", - vendor_config_commands.len(), - vendor_config_commands - ); - } else { - log::info!("Empty vendor config commands list"); - } + parse_get_info_extension_list(val, &mut vendor_config_commands, &mut certifications) } // 0x14: remainingDiscoverableCredentials 0x14 => { @@ -230,36 +218,13 @@ pub(crate) fn get_fido_info() -> Result { ); } } - // 0x15: certifications (map or array of integers) + // Pico-FIDO 7.6 uses 0x15 for vendorPrototypeConfigCommands. 0x15 => { - // log::trace!("Device certifications (0x15): {:?}", val); - match val { - Value::Map(cert_map) => { - for (k, v) in cert_map { - if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { - let display_name = FidoCertification::from_str(name) - .map(|c| format!("{}", c)) - .unwrap_or_else(|| name.clone()); - certifications.insert(display_name, *enabled); - } - } - } - Value::Array(cert_arr) => { - for v in cert_arr { - if let Value::Integer(id) = v { - let cert_id = *id as u64; - let name = FidoCertification::from_u64(cert_id) - .map(|c| format!("{}", c)) - .unwrap_or_else(|| format!("0x{:016X}", cert_id)); - certifications.insert(name, true); - } - } - } - _ => { - log::error!("Unexpected type for device certifications: {:?}", val); - } - } - log::info!("Device certifications (0x15): {:?}", certifications); + parse_get_info_extension_list(val, &mut vendor_config_commands, &mut certifications) + } + // 0x1B/0x1C are Pico-FIDO PIN policy extensions. + 0x1B | 0x1C => { + log::trace!("GetInfo Pico-FIDO extension key 0x{:02X} skipped", key_num); } // All other known keys (0x10-0x12, 0x16) - silently skip 0x10..=0x12 | 0x16 => { @@ -307,6 +272,47 @@ pub(crate) fn get_fido_info() -> Result { }) } +fn parse_get_info_extension_list( + val: &Value, + vendor_config_commands: &mut Vec, + certifications: &mut std::collections::HashMap, +) { + match val { + Value::Array(arr) => { + for v in arr { + if let Value::Integer(n) = v { + let cmd_id = *n as u64; + let cmd_name = VendorConfigCommand::from_u64(cmd_id) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| format!("0x{:016X}", cmd_id)); + if !vendor_config_commands.contains(&cmd_name) { + vendor_config_commands.push(cmd_name); + } + } + } + log::info!( + "Device supports {} vendor config commands: {:?}", + vendor_config_commands.len(), + vendor_config_commands + ); + } + Value::Map(cert_map) => { + for (k, v) in cert_map { + if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { + let display_name = FidoCertification::from_str(name) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| name.clone()); + certifications.insert(display_name, *enabled); + } + } + log::info!("Device certifications: {:?}", certifications); + } + _ => { + log::trace!("Unsupported GetInfo extension list shape: {:?}", val); + } + } +} + pub(crate) fn change_fido_pin( current_pin: Option, new_pin: String, @@ -931,58 +937,69 @@ pub(crate) fn get_enterprise_attestation_csr() -> Result { mod tests { use super::*; use serde_cbor_2::Value; - use std::collections::{BTreeMap, HashMap}; + use std::collections::BTreeMap; - #[test] - fn test_parse_certifications_map() { - let mut certifications = HashMap::new(); - let mut map = BTreeMap::new(); - // Test with both friendly names and hex string names - map.insert(Value::Text("fido-v2".into()), Value::Bool(true)); - map.insert(Value::Text("0x6C07D70FE96C3897".into()), Value::Bool(true)); // PIN Complexity - let val = Value::Map(map); - - if let Value::Map(cert_map) = &val { - for (k, v) in cert_map { - if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { - let display_name = FidoCertification::from_str(name) - .map(|c| format!("{}", c)) - .unwrap_or_else(|| name.clone()); - certifications.insert(display_name, *enabled); - } - } + fn empty_config_input() -> AppConfigInput { + AppConfigInput { + vid: None, + pid: None, + product_name: None, + led_gpio: None, + led_brightness: None, + touch_timeout: None, + led_driver: None, + led_dimmable: None, + power_cycle_on_reset: None, + led_steady: None, + enable_secp256k1: None, } - - assert_eq!(certifications.len(), 2); - assert_eq!(certifications.get("fido-v2"), Some(&true)); - assert_eq!(certifications.get("PIN Complexity"), Some(&true)); } #[test] - fn test_parse_certifications_array() { - let mut certifications = HashMap::new(); - let val = Value::Array(vec![ - Value::Integer(0x6C07D70FE96C3897), // PIN Complexity - Value::Integer(0x03E43F56B34285E2), // Auth Encryption - Value::Integer(0x1234567890ABCDEF), // Unknown - ]); + fn test_parse_get_info_pico_fido_76_vendor_commands_at_0x15() { + let mut map = BTreeMap::new(); + map.insert( + Value::Integer(0x01), + Value::Array(vec![ + Value::Text("U2F_V2".into()), + Value::Text("FIDO_2_1".into()), + Value::Text("FIDO_2_2".into()), + ]), + ); + map.insert(Value::Integer(0x03), Value::Bytes(vec![0x89; 16])); + map.insert(Value::Integer(0x05), Value::Integer(1024)); + map.insert( + Value::Integer(0x06), + Value::Array(vec![Value::Integer(1), Value::Integer(2)]), + ); + map.insert(Value::Integer(0x0D), Value::Integer(4)); + map.insert(Value::Integer(0x0E), Value::Integer(0x0706)); + map.insert( + Value::Integer(0x15), + Value::Array(vec![ + Value::Integer(VendorConfigCommand::AuthEncryptionEnable as u64 as i128), + Value::Integer(VendorConfigCommand::PhysicalVidPid as u64 as i128), + Value::Integer(0x1234567890ABCDEF), + ]), + ); - if let Value::Array(cert_arr) = &val { - for v in cert_arr { - if let Value::Integer(id) = v { - let cert_id = *id as u64; - let name = FidoCertification::from_u64(cert_id) - .map(|c| format!("{}", c)) - .unwrap_or_else(|| format!("0x{:016X}", cert_id)); - certifications.insert(name, true); - } - } - } + let info = parse_fido_get_info(&Value::Map(map)).unwrap(); - assert_eq!(certifications.len(), 3); - assert_eq!(certifications.get("PIN Complexity"), Some(&true)); - assert_eq!(certifications.get("Auth Encryption"), Some(&true)); - assert_eq!(certifications.get("0x1234567890ABCDEF"), Some(&true)); + assert_eq!(info.firmware_version, "7.6"); + assert_eq!(info.pin_protocols, vec![1, 2]); + assert!( + info.vendor_config_commands + .contains(&"AuthEncryptionEnable".to_string()) + ); + assert!( + info.vendor_config_commands + .contains(&"PhysicalVidPid".to_string()) + ); + assert!( + info.vendor_config_commands + .contains(&"0x1234567890ABCDEF".to_string()) + ); + assert!(info.certifications.is_empty()); } #[test] diff --git a/src/device/types.rs b/src/device/types.rs index e14334e..243e74c 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -79,9 +79,9 @@ pub struct FidoDeviceInfo { pub remaining_discoverable_credentials: Option, pub min_pin_length: i128, pub firmware_version: String, - /// Supported vendor config commands (human-readable names), parsed from CTAP GetInfo key 0x13 + /// Supported vendor config commands (human-readable names), parsed from CTAP GetInfo. pub vendor_config_commands: Vec, - /// Device certifications, parsed from CTAP GetInfo key 0x15 + /// Device certifications when firmware exposes them separately from vendor commands. pub certifications: std::collections::HashMap, pub max_credential_count_in_list: Option, pub max_credential_id_length: Option, From 3ddf4026802beaf1bdbacb2cab3211f907bba447 Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 14 May 2026 19:53:24 +0300 Subject: [PATCH 3/7] refactor(fido): gate hardware config by firmware version --- src/device/fido/mod.rs | 606 +++++++++++++++++++++++++++-------------- 1 file changed, 400 insertions(+), 206 deletions(-) diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index ef083ae..2b1417c 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -14,6 +14,10 @@ use hid::*; use serde_cbor_2::{Value, from_slice, to_vec}; use std::collections::BTreeMap; +const LEGACY_PHY_OPT_DIMMABLE: u16 = 0x02; +const LEGACY_PHY_OPT_DISABLE_POWER_RESET: u16 = 0x04; +const LEGACY_PHY_OPT_LED_STEADY: u16 = 0x08; + // Fido functions that require pin: pub(crate) fn get_fido_info() -> Result { @@ -313,6 +317,21 @@ fn parse_get_info_extension_list( } } +pub(crate) fn firmware_supports_legacy_fido_hardware_config(version: &str) -> bool { + let Some((major, minor)) = parse_firmware_version(version) else { + return false; + }; + + major == 7 && minor <= 2 +} + +fn parse_firmware_version(version: &str) -> Option<(u16, u16)> { + let mut parts = version.split('.'); + let major = parts.next()?.parse().ok()?; + let minor = parts.next()?.parse().ok()?; + Some((major, minor)) +} + pub(crate) fn change_fido_pin( current_pin: Option, new_pin: String, @@ -471,6 +490,15 @@ pub(crate) fn delete_credential(pin: String, credential_id_hex: String) -> Resul // Custom Fido functions ( works only with pico-fido firmware ) +#[derive(Debug, Default, Clone, PartialEq, Eq)] +struct ManagementInfo { + serial: Option, + firmware_version: Option, + usb_supported: Option, + usb_enabled: Option, + config_locked: Option, +} + pub fn read_device_details() -> Result { log::info!("Starting FIDO device details read..."); @@ -483,35 +511,56 @@ pub fn read_device_details() -> Result { } })?; - let (aaguid_str, fw_version) = read_device_info(&transport)?; + let fido_info = read_device_info(&transport)?; log::info!( "Device identified: AAGUID={}, FW={}", - aaguid_str, - fw_version + fido_info.aaguid, + fido_info.firmware_version ); - let mem_stats = read_memory_stats(&transport)?; - if let Some((used, total)) = mem_stats { - log::debug!( - "Memory Stats: Used={}KB, Total={}KB", - used / 1024, - total / 1024 - ); + let supports_legacy_hardware_config = + firmware_supports_legacy_fido_hardware_config(&fido_info.firmware_version); + let management = read_management_info(&transport); + let config = AppConfig { + vid: format!("{:04X}", transport.vid), + pid: format!("{:04X}", transport.pid), + product_name: transport.product_name.clone(), + ..Default::default() + }; + let config = if supports_legacy_hardware_config { + read_legacy_physical_config(&transport, config) } else { - log::info!("Memory Stats: Not Available"); - } - - let config = read_physical_config(&transport)?; + config + }; + let mem_stats = if supports_legacy_hardware_config { + read_legacy_memory_stats(&transport).unwrap_or_else(|e| { + log::info!("Legacy FIDO memory stats unavailable: {}", e); + None + }) + } else { + None + }; log::info!("Successfully read all device details."); + let firmware_version = if fido_info.firmware_version != "0.0" { + fido_info.firmware_version + } else { + management + .as_ref() + .and_then(|info| info.firmware_version.clone()) + .unwrap_or_else(|| "Unknown".to_string()) + }; + Ok(FullDeviceStatus { info: DeviceInfo { - serial: "?".to_string(), // Serial number is not available through fido - flash_used: mem_stats.map(|(u, _)| u / 1024), - flash_total: mem_stats.map(|(_, t)| t / 1024), - firmware_version: fw_version, + serial: management + .and_then(|info| info.serial) + .unwrap_or_else(|| "Unknown".to_string()), + flash_used: mem_stats.map(|(used, _)| used / 1024), + flash_total: mem_stats.map(|(_, total)| total / 1024), + firmware_version, }, config, secure_boot: false, @@ -520,7 +569,7 @@ pub fn read_device_details() -> Result { }) } -fn read_device_info(transport: &HidTransport) -> Result<(String, String), PFError> { +fn read_device_info(transport: &HidTransport) -> Result { log::debug!("Sending GetInfo command (0x04)..."); let info_payload = [CtapCommand::GetInfo as u8]; let info_res = transport @@ -537,201 +586,252 @@ fn read_device_info(transport: &HidTransport) -> Result<(String, String), PFErro PFError::Io(e.to_string()) })?; - // NOTE: Key 0x03 is AAGUID, not the unique device Serial. - let aaguid_str = if let Value::Map(m) = &info_val { - m.get(&Value::Integer(0x03)) - .and_then(|v| { - if let Value::Bytes(b) = v { - Some(hex::encode_upper(b)) - } else { - None - } - }) - .unwrap_or_else(|| { - log::warn!("AAGUID not found in GetInfo response"); - "Unknown".into() - }) - } else { - "Unknown".into() - }; - - let fw_version = if let Value::Map(m) = &info_val { - m.get(&Value::Integer(0x0E)) - .and_then(|v| { - if let Value::Integer(i) = v { - Some(format!("{}.{}", (i >> 8) & 0xFF, i & 0xFF)) - } else { - None - } - }) - .unwrap_or_else(|| { - log::warn!("Firmware version not found in GetInfo response"); - "Unknown".into() - }) - } else { - "Unknown".into() - }; - - Ok((aaguid_str, fw_version)) + parse_fido_get_info(&info_val).map_err(PFError::Io) } -fn read_memory_stats(transport: &HidTransport) -> Result, PFError> { - log::debug!("Preparing Memory Stats vendor command..."); +fn read_management_info(transport: &HidTransport) -> Option { + // pico-fido v7.6 src/fido/cbor.c handles HID cmd 0xC2 as raw + // man_get_config() TLV bytes, not as CTAP CBOR with a status byte. + match transport.send_raw(CTAP_VENDOR_CONFIG_CMD, &[]) { + Ok(raw) => match parse_management_info(&raw) { + Ok(info) => Some(info), + Err(e) => { + log::warn!("Failed to parse FIDO management config: {}", e); + None + } + }, + Err(e) => { + log::info!("FIDO management config is not available: {}", e); + None + } + } +} +fn parse_management_info(raw: &[u8]) -> Result { + let data = if raw.first().map(|len| *len as usize) == Some(raw.len().saturating_sub(1)) { + &raw[1..] + } else { + raw + }; + + let mut info = ManagementInfo::default(); + let mut i = 0; + while i < data.len() { + if i + 2 > data.len() { + return Err("truncated management tag header".to_string()); + } + + let tag = data[i]; + let len = data[i + 1] as usize; + i += 2; + + if i + len > data.len() { + return Err(format!("truncated management tag 0x{:02X}", tag)); + } + + let val = &data[i..i + len]; + match tag { + 0x01 => info.usb_supported = parse_management_u16(val), + 0x02 => { + if val.len() == 4 { + info.serial = Some(hex::encode_upper(val)); + } + } + 0x03 => info.usb_enabled = parse_management_u16(val), + 0x05 => { + if val.len() >= 2 { + info.firmware_version = Some(format!("{}.{}", val[0], val[1])); + } + } + 0x0A => { + if let Some(locked) = val.first() { + info.config_locked = Some(*locked != 0); + } + } + _ => {} + } + + i += len; + } + + Ok(info) +} + +fn parse_management_u16(val: &[u8]) -> Option { + match val { + [single] => Some(*single as u16), + [hi, lo] => Some(u16::from_be_bytes([*hi, *lo])), + _ => None, + } +} + +fn read_legacy_memory_stats(transport: &HidTransport) -> Result, PFError> { let mut mem_req = BTreeMap::new(); mem_req.insert( - Value::Integer(1), // Sub-command key (usually 1) + Value::Integer(1), Value::Integer(MemorySubCommand::GetStats as i128), ); - let mem_cbor = to_vec(&Value::Map(mem_req)).map_err(|e| { - log::error!("Failed to encode Memory Stats CBOR: {}", e); - PFError::Io(format!("CBOR encode error: {}", e)) - })?; - + let mem_cbor = to_vec(&Value::Map(mem_req)).map_err(|e| PFError::Io(e.to_string()))?; let mut mem_payload = vec![VendorCommand::Memory as u8]; mem_payload.extend(mem_cbor); - log::debug!("Sending Memory Stats command..."); - let mem_res = transport - .send_cbor(CTAP_VENDOR_CBOR_CMD, &mem_payload) - .map_err(|e| { - // Error code 0x2B means the feature is not supported/removed in this firmware mode - if e.to_string().contains("0x2B") { - log::info!("Memory stats not supported by device firmware (0x2B)."); - return PFError::NoDevice; // We'll handle this specially - } - log::warn!("Failed to fetch memory stats (Vendor Cmd): {}", e); - PFError::Device(format!("Failed to fetch memory stats: {}", e)) - }); - - let mem_res = match mem_res { - Ok(res) => res, - Err(PFError::NoDevice) => return Ok(None), - Err(e) => return Err(e), - }; - - let mem_map: BTreeMap = if !mem_res.is_empty() { - from_slice(&mem_res).map_err(|e| { - log::error!("Failed to parse Memory Stats CBOR response: {}", e); - PFError::Io(format!("Failed to parse Memory Stats CBOR: {}", e)) - })? - } else { - BTreeMap::new() - }; + let mem_res = transport.send_cbor(CTAP_VENDOR_CBOR_CMD, &mem_payload)?; + if mem_res.is_empty() { + return Ok(None); + } + let mem_map: BTreeMap = + from_slice(&mem_res).map_err(|e| PFError::Io(e.to_string()))?; let used = mem_map .get(&(MemoryResponseKey::UsedSpace as i128)) - .cloned() + .copied() .unwrap_or(0) as u32; let total = mem_map .get(&(MemoryResponseKey::TotalSpace as i128)) - .cloned() + .copied() .unwrap_or(0) as u32; Ok(Some((used, total))) } -fn read_physical_config(transport: &HidTransport) -> Result { - log::debug!("Preparing Physical Config vendor command..."); - - // FIX: Only arguments in CBOR map +fn read_legacy_physical_config(transport: &HidTransport, mut config: AppConfig) -> AppConfig { let mut phy_params = BTreeMap::new(); phy_params.insert( - Value::Integer(1), // Sub-command key + Value::Integer(1), Value::Integer(PhysicalOptionsSubCommand::GetOptions as i128), ); - let phy_cbor = to_vec(&Value::Map(phy_params)).map_err(|e| { - log::error!("Failed to encode Physical Config CBOR: {}", e); - PFError::Io(format!("CBOR encode error: {}", e)) - })?; + let Ok(phy_cbor) = to_vec(&Value::Map(phy_params)) else { + return config; + }; let mut phy_payload = vec![VendorCommand::PhysicalOptions as u8]; phy_payload.extend(phy_cbor); - log::debug!("Sending Physical Config command..."); - let phy_res = transport - .send_cbor(CTAP_VENDOR_CBOR_CMD, &phy_payload) - .unwrap_or_else(|e| { - log::warn!("Failed to fetch physical config (Vendor Cmd): {}", e); - Vec::new() - }); - - let mut config = AppConfig { - vid: format!("{:04X}", transport.vid), - pid: format!("{:04X}", transport.pid), - product_name: transport.product_name.clone(), - ..Default::default() + let Ok(phy_res) = transport.send_cbor(CTAP_VENDOR_CBOR_CMD, &phy_payload) else { + return config; }; - if let Ok(Value::Map(m)) = from_slice(&phy_res) { - log::debug!("Parsed Physical Config map successfully"); - if let Some(Value::Integer(v)) = m.get(&Value::Text("gpio".into())) { - config.led_gpio = *v as u8; - } else { - log::warn!("No led_gpio in CBOR map"); - } + let Ok(Value::Map(m)) = from_slice::(&phy_res) else { + return config; + }; - if let Some(Value::Integer(v)) = m.get(&Value::Text("brightness".into())) { - config.led_brightness = *v as u8; - } else { - log::warn!("No led_brightness in CBOR map"); - } - } else if !phy_res.is_empty() { - log::warn!("Physical config response was not a valid CBOR map"); - } else { - log::debug!("Physical config response was empty or already handled."); + if let Some(Value::Integer(opts_raw)) = m.get(&Value::Integer(1)) { + let opts = *opts_raw as u16; + config.led_dimmable = opts & LEGACY_PHY_OPT_DIMMABLE != 0; + config.power_cycle_on_reset = opts & LEGACY_PHY_OPT_DISABLE_POWER_RESET == 0; + config.led_steady = opts & LEGACY_PHY_OPT_LED_STEADY != 0; } - Ok(config) + config } pub fn write_config(config: AppConfigInput, pin: Option) -> Result { log::info!("Starting FIDO write_config..."); - let pin_val = pin.as_deref().ok_or_else(|| { - log::error!("write_config called without any security PIN provided"); - PFError::Device( - "A security PIN is required to be set to change the configuration in fido mode".into(), - ) - })?; + if is_empty_config_input(&config) { + return Ok("No FIDO-only hardware configuration changes were needed.".to_string()); + } - // 1. Open custom HidTransport and obtain PIN token let transport = HidTransport::open().map_err(|e| { log::error!("Failed to open HID transport: {}", e); PFError::Device(format!("Could not open HID transport: {}", e)) })?; + let fido_info = read_device_info(&transport)?; + let supports_legacy_hardware_config = + firmware_supports_legacy_fido_hardware_config(&fido_info.firmware_version); + validate_fido_config_changes(&config, supports_legacy_hardware_config)?; + + let pin_val = pin.as_deref().ok_or_else(|| { + log::error!("write_config called without any security PIN provided"); + PFError::Device( + "A security PIN is required to change legacy FIDO hardware configuration.".into(), + ) + })?; + + write_legacy_hardware_config(&transport, &config, pin_val) +} + +fn is_empty_config_input(config: &AppConfigInput) -> bool { + config.vid.is_none() + && config.pid.is_none() + && config.product_name.is_none() + && config.led_gpio.is_none() + && config.led_brightness.is_none() + && config.touch_timeout.is_none() + && config.led_driver.is_none() + && config.led_dimmable.is_none() + && config.power_cycle_on_reset.is_none() + && config.led_steady.is_none() + && config.enable_secp256k1.is_none() +} + +fn validate_fido_config_changes( + config: &AppConfigInput, + supports_legacy_hardware_config: bool, +) -> Result<(), PFError> { + if !supports_legacy_hardware_config + && (config.vid.is_some() + || config.pid.is_some() + || config.product_name.is_some() + || config.led_gpio.is_some() + || config.led_brightness.is_some() + || config.touch_timeout.is_some() + || config.led_driver.is_some() + || config.led_dimmable.is_some() + || config.power_cycle_on_reset.is_some() + || config.led_steady.is_some() + || config.enable_secp256k1.is_some()) + { + return Err(PFError::Device( + "Pico-FIDO 7.6 does not support hardware configuration over FIDO-only mode. Use rescue mode to change VID/PID, product name, LED, touch timeout, power/reset, or curve settings.".into(), + )); + } + + if supports_legacy_hardware_config { + if config.product_name.is_some() + || config.touch_timeout.is_some() + || config.led_driver.is_some() + || config.enable_secp256k1.is_some() + { + return Err(PFError::Device( + "This firmware only supports VID/PID, LED GPIO, LED brightness, and basic LED/power options over FIDO. Use rescue mode for product name, touch timeout, LED driver, or curve settings.".into(), + )); + } + + if config.vid.is_some() != config.pid.is_some() { + return Err(PFError::Device( + "VID and PID must be changed together in FIDO mode.".into(), + )); + } + } + + Ok(()) +} + +fn write_legacy_hardware_config( + transport: &HidTransport, + config: &AppConfigInput, + pin: &str, +) -> Result { let get_fresh_token = || -> Result, PFError> { - match transport.get_pin_token_with_permission( - pin_val, - PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, - None, - ) { - Ok(token) => { - log::debug!("Successfully obtained PIN token with ACFG permission."); - Ok(token) - } - Err(e) => { + transport + .get_pin_token_with_permission( + pin, + PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, + None, + ) + .or_else(|e| { log::warn!( "Failed to get PIN token with ACFG permission (Error: {:?}). Falling back to standard token.", e ); - // Fallback to standard PIN token (Subcommand 0x05) - let token = transport.get_pin_token(pin_val).map_err(|e2| { - log::error!("Failed to obtain even a standard PIN token: {:?}", e2); - PFError::Device(format!("PIN token acquisition failed: {:?}", e2)) - })?; - log::debug!("Successfully obtained standard PIN token (fallback)."); - Ok(token) - } - } + transport.get_pin_token(pin) + }) }; - // 2. Send vendor commands using the token - - // VID/PID config if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) { let vid = u16::from_str_radix(vid_str, 16).map_err(|e| PFError::Io(e.to_string()))?; let pid = u16::from_str_radix(pid_str, 16).map_err(|e| PFError::Io(e.to_string()))?; @@ -741,68 +841,47 @@ pub fn write_config(config: AppConfigInput, pin: Option) -> Result Date: Thu, 14 May 2026 19:53:42 +0300 Subject: [PATCH 4/7] fix(ui): gate FIDO hardware config by firmware support --- src/ui/views/config.rs | 141 +++++++++++++++++++++++++++++------------ 1 file changed, 100 insertions(+), 41 deletions(-) diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 8cb974a..ea279d6 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,11 +1,7 @@ -use crate::device::io; -use crate::device::types::AppConfigInput; -use crate::ui::components::{ - card::Card, - dialog, - dialog::{PinPromptContent, StatusContent}, - page_view::PageView, -}; +use crate::device::types::{AppConfigInput, DeviceMethod}; +use crate::device::{fido, io}; +use crate::ui::components::dialog::PinPromptContent; +use crate::ui::components::{card::Card, dialog, dialog::StatusContent, page_view::PageView}; use crate::ui::rootview::ApplicationRoot; use crate::ui::types::{DeviceConnectionState, LedDriverType, UsbIdentityPreset}; use gpui::*; @@ -314,7 +310,7 @@ impl ConfigView { // Special case for FIDO 0x3E error (Invalid Subcommand) // This happens when the firmware is too old to support config over FIDO - if method == crate::device::types::DeviceMethod::Fido && err_msg.contains("0x3E") + if method == DeviceMethod::Fido && err_msg.contains("0x3E") { err_msg = "The device firmware does not support being configured in fido only communication mode. \nHave a look at the troubleshooting guide to fix this".to_string(); } @@ -358,7 +354,7 @@ impl ConfigView { let _ = view_handle.update(cx, |this, cx| { this.write_config_to_device( changes.clone(), - crate::device::types::DeviceMethod::Fido, + DeviceMethod::Fido, Some(pin), StatusDialogHandle::Pin(dialog_handle), cx, @@ -467,8 +463,20 @@ impl ConfigView { let method = status.method.clone(); - if method == crate::device::types::DeviceMethod::Fido { - self.open_pin_dialog(changes, window, cx); + if method == DeviceMethod::Fido { + if Self::status_supports_legacy_fido_config(status) { + self.open_pin_dialog(changes, window, cx); + } else { + let handle = + dialog::open_status_dialog("Configuration Requires Rescue Mode", window, cx); + self.write_config_to_device( + changes, + method, + None, + StatusDialogHandle::Status(handle), + cx, + ); + } } else { let handle = dialog::open_status_dialog("Applying Configuration", window, cx); self.write_config_to_device( @@ -481,6 +489,11 @@ impl ConfigView { } } + fn status_supports_legacy_fido_config(status: &crate::device::types::FullDeviceStatus) -> bool { + status.method == DeviceMethod::Fido + && fido::firmware_supports_legacy_fido_hardware_config(&status.info.firmware_version) + } + pub fn sync_from_device( &mut self, device: &DeviceConnectionState, @@ -544,14 +557,21 @@ impl ConfigView { cx.notify(); } - fn render_identity_card(&self, theme: &Theme) -> impl IntoElement { + fn render_identity_card( + &self, + theme: &Theme, + is_fido: bool, + hardware_config_disabled: bool, + ) -> impl IntoElement { let content = v_flex() .gap_4() .child( - v_flex() - .gap_2() - .child("Vendor Preset") - .child(Select::new(&self.vendor_select).bg(rgb(0x222225)).w_full()), + v_flex().gap_2().child("Vendor Preset").child( + Select::new(&self.vendor_select) + .bg(rgb(0x222225)) + .w_full() + .disabled(hardware_config_disabled), + ), ) .child( div() @@ -563,7 +583,7 @@ impl ConfigView { Input::new(&self.vid_input) .font_family("Mono") .bg(rgb(0x222225)) - .disabled(!self.is_custom_vendor), + .disabled(hardware_config_disabled || !self.is_custom_vendor), ), ) .child( @@ -571,16 +591,17 @@ impl ConfigView { Input::new(&self.pid_input) .font_family("Mono") .bg(rgb(0x222225)) - .disabled(!self.is_custom_vendor), + .disabled(hardware_config_disabled || !self.is_custom_vendor), ), ), ) .child(div().h_px().bg(theme.border)) .child( - v_flex() - .gap_2() - .child("Product Name") - .child(Input::new(&self.product_name_input).bg(rgb(0x222225))), + v_flex().gap_2().child("Product Name").child( + Input::new(&self.product_name_input) + .bg(rgb(0x222225)) + .disabled(is_fido), + ), ); Card::new() @@ -590,7 +611,12 @@ impl ConfigView { .child(content) } - fn render_led_card(&mut self, cx: &mut Context) -> impl IntoElement { + fn render_led_card( + &mut self, + cx: &mut Context, + is_fido: bool, + hardware_config_disabled: bool, + ) -> impl IntoElement { let dim_listener = cx.listener(|this, checked, _, cx| { this.led_dimmable = *checked; cx.notify(); @@ -608,16 +634,18 @@ impl ConfigView { let content = v_flex() .gap_4() .child( - v_flex() - .gap_2() - .child("LED GPIO Pin") - .child(Input::new(&self.led_gpio_input).bg(rgb(0x222225))), + v_flex().gap_2().child("LED GPIO Pin").child( + Input::new(&self.led_gpio_input) + .bg(rgb(0x222225)) + .disabled(hardware_config_disabled), + ), ) .child( v_flex().gap_2().child("LED Driver").child( Select::new(&self.led_driver_select) .w_full() - .bg(rgb(0x222225)), + .bg(rgb(0x222225)) + .disabled(is_fido), ), ) .child(div().h_px().bg(theme.border)) @@ -626,7 +654,11 @@ impl ConfigView { gpui_component::h_flex() .items_center() .gap_4() - .child(Slider::new(&self.led_brightness_slider).flex_1()) + .child( + Slider::new(&self.led_brightness_slider) + .flex_1() + .disabled(hardware_config_disabled), + ) .child( div() .text_xs() @@ -650,6 +682,7 @@ impl ConfigView { .child( Switch::new("led-dimmable") .checked(self.led_dimmable) + .disabled(hardware_config_disabled) .on_click(dim_listener), ), ) @@ -668,6 +701,7 @@ impl ConfigView { .child( Switch::new("led-steady") .checked(self.led_steady) + .disabled(hardware_config_disabled) .on_click(steady_listener), ), ); @@ -679,12 +713,13 @@ impl ConfigView { .child(content) } - fn render_touch_card(&self, _theme: &Theme) -> impl IntoElement { + fn render_touch_card(&self, _theme: &Theme, is_fido: bool) -> impl IntoElement { let content = v_flex().gap_4().child( - v_flex() - .gap_2() - .child("Touch Timeout (seconds)") - .child(Input::new(&self.touch_timeout_input).bg(rgb(0x222225))), + v_flex().gap_2().child("Touch Timeout (seconds)").child( + Input::new(&self.touch_timeout_input) + .bg(rgb(0x222225)) + .disabled(is_fido), + ), ); Card::new() @@ -694,7 +729,12 @@ impl ConfigView { .child(content) } - fn render_options_card(&mut self, cx: &mut Context) -> impl IntoElement { + fn render_options_card( + &mut self, + cx: &mut Context, + is_fido: bool, + hardware_config_disabled: bool, + ) -> impl IntoElement { let power_cycle_listener = cx.listener(|this, checked, _, cx| { this.power_cycle = *checked; cx.notify(); @@ -724,6 +764,7 @@ impl ConfigView { .child( Switch::new("power-cycle") .checked(self.power_cycle) + .disabled(hardware_config_disabled) .on_click(power_cycle_listener), ), ) @@ -742,6 +783,7 @@ impl ConfigView { .child( Switch::new("enable-secp") .checked(self.enable_secp256k1) + .disabled(is_fido) .on_click(secp_listener), ), ); @@ -781,13 +823,30 @@ impl Render for ConfigView { .into_any_element(); } - let led_card = self.render_led_card(cx).into_any_element(); - let options_card = self.render_options_card(cx).into_any_element(); + let status = self + .root + .upgrade() + .and_then(|r| r.read(cx).device.status.clone()); + let is_fido = status.as_ref().map(|s| s.method.clone()) == Some(DeviceMethod::Fido); + let supports_legacy_fido_config = status + .as_ref() + .map(Self::status_supports_legacy_fido_config) + .unwrap_or(false); + let hardware_config_disabled = is_fido && !supports_legacy_fido_config; + + let led_card = self + .render_led_card(cx, is_fido, hardware_config_disabled) + .into_any_element(); + let options_card = self + .render_options_card(cx, is_fido, hardware_config_disabled) + .into_any_element(); let theme = cx.theme(); - let identity_card = self.render_identity_card(theme).into_any_element(); - let touch_card = self.render_touch_card(theme).into_any_element(); + let identity_card = self + .render_identity_card(theme, is_fido, hardware_config_disabled) + .into_any_element(); + let touch_card = self.render_touch_card(theme, is_fido).into_any_element(); let is_wide = window.bounds().size.width > px(1100.0); let columns = if is_wide { 2 } else { 1 }; @@ -812,7 +871,7 @@ impl Render for ConfigView { Button::new("apply-changes") .icon(Icon::default().path("icons/save.svg")) .child("Apply Changes") - .disabled(self.loading) + .disabled(self.loading || hardware_config_disabled) .custom( ButtonCustomVariant::new(cx) .color(rgb(0xe3e3e6).into()) From d39dcbe5f2281c3b10136ea588c1a571c50ae98e Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 14 May 2026 20:02:09 +0300 Subject: [PATCH 5/7] style: cargo clippy and fmt --- src/device/fido/hid.rs | 2 +- src/device/fido/mod.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index c9987b6..213fddc 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -494,7 +494,7 @@ impl HidTransport { return Ok(b.clone()); } // Fall back to the first bytes value in the map - for (_, v) in &m { + for v in m.values() { if let Value::Bytes(b) = v { log::debug!("CSR found in map value ({} bytes)", b.len()); return Ok(b.clone()); diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index 2b1417c..4e52f69 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -632,16 +632,12 @@ fn parse_management_info(raw: &[u8]) -> Result { let val = &data[i..i + len]; match tag { 0x01 => info.usb_supported = parse_management_u16(val), - 0x02 => { - if val.len() == 4 { - info.serial = Some(hex::encode_upper(val)); - } + 0x02 if val.len() == 4 => { + info.serial = Some(hex::encode_upper(val)); } 0x03 => info.usb_enabled = parse_management_u16(val), - 0x05 => { - if val.len() >= 2 { - info.firmware_version = Some(format!("{}.{}", val[0], val[1])); - } + 0x05 if val.len() >= 2 => { + info.firmware_version = Some(format!("{}.{}", val[0], val[1])); } 0x0A => { if let Some(locked) = val.first() { From 4f583bd25c2925a74d75b755a3e226843a18c341 Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 14 May 2026 20:13:28 +0300 Subject: [PATCH 6/7] docs: add kralonur to credits --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.md b/CREDITS.md index a69acea..d360334 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -7,6 +7,7 @@ - [Lab-8916100448256](https://github.com/Lab-8916100448256): Contributed `shell.nix` file for dev env setup using nix package manager/nixos and worked on the backend of the application. - [jetcookies](https://github.com/jetcookies): Contributed `package.nix` file for nix packaging of the application and maintains the nix flake of the application. - [Sylvain Pelissier](https://github.com/sylvainpelissier): Worked on the Enterprise attestation features. +- [kralonur](https://github.com/kralonur): Improved Pico-FIDO 7.6 compatibility and preserved legacy FIDO hardware configuration support. **Third-party Libraries** From 3e3d1a72e8f5b4b857cbfb9e6e8019a68e22bdc9 Mon Sep 17 00:00:00 2001 From: kralonur Date: Thu, 14 May 2026 20:19:47 +0300 Subject: [PATCH 7/7] docs: update picofido firmware compatibility notes --- README.md | 8 +++++--- docs/Home.md | 2 +- docs/Troubleshooting.md | 12 +++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0410232..5ec8ca3 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,10 @@ > > Check application [Installation Wiki](https://github.com/librekeys/picoforge/wiki/Installation) for installation guide of the PicoForge app on your system. > -> PicoForge only supports v7.2 of the PICO FIDO series of firmwares. Support to v7.4 and above -> is WIP. +> PicoForge targets Pico FIDO firmware 7.6 for FIDO fallback reads and management. +> Hardware configuration from FIDO-only mode is firmware-dependent: 7.0/7.2 support +> a limited legacy configuration path, while 7.4/7.6 require rescue/PCSC mode for +> hardware configuration changes. ## About @@ -34,7 +36,7 @@ PicoForge is a modern desktop application for configuring and managing Pico FIDO - Real-time system logging and diagnostics - Support for multiple hardware variants and vendors -> **BETA Status**: This application is currently under active development and in beta stage. Users should expect bugs and are encouraged to report them. The app has been tested on Linux and Windows 10 with the official Raspberry Pi Pico2 & ESP32-S3 and, currently supports Pico FIDO firmware version 7.2 only. +> **BETA Status**: This application is currently under active development and in beta stage. Users should expect bugs and are encouraged to report them. The app has been tested on Linux and Windows 10 with the official Raspberry Pi Pico2 & ESP32-S3 and, currently targets Pico FIDO firmware version 7.6, with limited legacy FIDO-only configuration support for 7.0/7.2. ## Screenshots diff --git a/docs/Home.md b/docs/Home.md index 3867293..d65bf70 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -3,7 +3,7 @@ **PicoForge** is a modern desktop application for configuring and managing **Pico FIDO** security keys. Built with Rust, Tauri, and Svelte, it provides an intuitive interface. > [!WARNING] -> **Beta Status**: This application is currently under active development and in beta stage. Users should expect bugs and are encouraged to report them. The app has been tested on Linux and Windows 10/11 with the official Raspberry Pi Pico2, WaveShare RP2350 One & ESP32-S3 and, currently supports Pico FIDO firmware version 7.2 only. +> **Beta Status**: This application is currently under active development and in beta stage. Users should expect bugs and are encouraged to report them. The app has been tested on Linux and Windows 10/11 with the official Raspberry Pi Pico2, WaveShare RP2350 One & ESP32-S3 and, currently targets Pico FIDO firmware version 7.6. FIDO-only hardware configuration is available only for legacy 7.0/7.2 firmware; 7.4/7.6 require rescue/PCSC mode for hardware configuration. > > It does not support all the features exposed by the `pico-fido` firmware and `pico-hsm`. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 17c22cb..842f578 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -8,11 +8,13 @@ When this happens the device status on the lower left corner of the application To avoid this issue we plan to obtain a valid VID/PID that can freely be used by the open source community and to communicate it to pcsc-lite team so that they can include it in their CCID driver. But for now there are 3 ways to work around this issue. -### 1.1. Use the fido fallback implemented in picoforge to update the VID and PID -When connected to the key with the fido fallback there are some limitations. Only a limited set of configuration parameters can be read or written. -While in this mode it is possible to change the VID/PID to use the ones of a known vendor. -But note that **a security pin needs to be set** before being able to change the configuration when in fido only mode. -Then, after unplugging and re-plugging the key for the change to be taken into account, the key should be correctly detected by pcsc and you will be able to view and modify to the full set of configuration parameters. +### 1.1. Use the fido fallback implemented in picoforge +When connected to the key with the fido fallback there are some limitations. PicoForge can read FIDO/device information on newer firmware, but hardware configuration support depends on the pico-fido firmware version. + +- Firmware 7.0/7.2: PicoForge can write a limited hardware configuration set from FIDO-only mode: VID/PID, LED GPIO, LED brightness, LED dimmable/steady, and power-cycle/reset behavior. **A security pin needs to be set** before being able to change the configuration when in FIDO-only mode. +- Firmware 7.4/7.6: PicoForge does not write hardware configuration from FIDO-only mode. Use rescue/PCSC mode for VID/PID, product name, LED, touch timeout, LED driver, and curve settings. + +After changing VID/PID on firmware that supports legacy FIDO configuration, unplug and re-plug the key for the change to be taken into account. The key should then be correctly detected by pcsc and you will be able to view and modify the full set of configuration parameters. **Be mindfull of the legal implications when you change the VID:PID on a key that you plan to distribute to somebody else**. You will probably want to set it back to the generic VID:PID before you distribute it. ### 1.2. Flash a firmware that you built from source with USB VID/PID known by pcsc-lite