Merge pull request #94 from kralonur/feat/picofido-7.6-support

Picofido 7.6 support
This commit is contained in:
Suyog Tandel
2026-06-04 19:57:23 +05:30
committed by GitHub
8 changed files with 662 additions and 376 deletions
+1
View File
@@ -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**
+5 -3
View File
@@ -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
+1 -1
View File
@@ -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`.
+7 -5
View File
@@ -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
+39 -24
View File
@@ -168,6 +168,11 @@ impl HidTransport {
self.read_cbor_response(cmd)
}
pub fn send_raw(&self, cmd: u8, payload: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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(
@@ -483,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());
@@ -518,7 +529,7 @@ impl HidTransport {
sub_params: Option<Value>,
) -> Result<Vec<u8>, PFError> {
let mut sub_params_bytes: Vec<u8> = Vec::new();
if let Some(ref params) = sub_params {
sub_params_bytes = to_vec(&params).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 {}",
+507 -300
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -79,9 +79,9 @@ pub struct FidoDeviceInfo {
pub remaining_discoverable_credentials: Option<i128>,
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<String>,
/// Device certifications, parsed from CTAP GetInfo key 0x15
/// Device certifications when firmware exposes them separately from vendor commands.
pub certifications: std::collections::HashMap<String, bool>,
pub max_credential_count_in_list: Option<i128>,
pub max_credential_id_length: Option<i128>,
+100 -41
View File
@@ -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<Self>) -> impl IntoElement {
fn render_led_card(
&mut self,
cx: &mut Context<Self>,
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<Self>) -> impl IntoElement {
fn render_options_card(
&mut self,
cx: &mut Context<Self>,
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())