mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
docs: add documentation to src/device/module
This commit is contained in:
+546
-166
File diff suppressed because it is too large
Load Diff
+287
-3
@@ -1,3 +1,87 @@
|
||||
//! USB HID transport for CTAP2/FIDO2 communication.
|
||||
//!
|
||||
//! # What is HID?
|
||||
//!
|
||||
//! USB HID (Human Interface Device) is a standard USB device class for input
|
||||
//! devices like keyboards, mice, and gamepads. HID devices communicate through
|
||||
//! *reports* — fixed-size packets sent/received on USB endpoints. The OS
|
||||
//! auto-detects HID devices without requiring custom drivers, making it ideal
|
||||
//! for FIDO2 security keys that need to work across platforms.
|
||||
//!
|
||||
//! # What is CTAPHID?
|
||||
//!
|
||||
//! CTAPHID is the [CTAP2] transport binding for USB HID. It layers the CTAP2
|
||||
//! protocol on top of HID reports, allowing FIDO2 authenticators to
|
||||
//! communicate with hosts through the standard HID driver stack. The
|
||||
//! specification is defined in [CTAP2 §11.2](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#usb-human-interface-device-hid).
|
||||
//!
|
||||
//! # Framing protocol
|
||||
//!
|
||||
//! CTAPHID uses 64-byte HID reports. Messages that exceed 64 bytes are split
|
||||
//! across multiple packets:
|
||||
//!
|
||||
//! ```text
|
||||
//! Init Packet (64 bytes):
|
||||
//! CID(4) | CMD(1) | BCNT_HI(1) | BCNT_LO(1) | payload[..57]
|
||||
//!
|
||||
//! Continuation Packets:
|
||||
//! CID(4) | SEQ(1) | payload[..59]
|
||||
//! ```
|
||||
//!
|
||||
//! - **CID** (Channel ID): 4-byte identifier negotiated via `CTAPHID_INIT`.
|
||||
//! Multiplexes multiple logical channels on one HID device.
|
||||
//! - **CMD**: Command byte (e.g., `0x90` for CBOR, `0x86` for INIT).
|
||||
//! - **BCNT**: 16-bit big-endian payload length.
|
||||
//! - **SEQ**: Sequence number for continuation packets (starts at 0).
|
||||
//!
|
||||
//! # Channel initialization
|
||||
//!
|
||||
//! Before any CTAP2 command can be sent, the host must negotiate a Channel ID:
|
||||
//!
|
||||
//! 1. Host sends `CTAPHID_INIT` to the broadcast CID (`0xFFFFFFFF`) with a
|
||||
//! random 8-byte nonce.
|
||||
//! 2. Device responds with the same nonce and a newly allocated CID.
|
||||
//! 3. All subsequent communication uses this CID.
|
||||
//!
|
||||
//! This allows multiple CTAP2 sessions to coexist on one device (e.g., two
|
||||
//! browsers open simultaneously).
|
||||
//!
|
||||
//! # Cryptographic operations
|
||||
//!
|
||||
//! PIN operations require ECDH key agreement and AES-256-CBC encryption:
|
||||
//!
|
||||
//! ```text
|
||||
//! 1. Host → Device: GetKeyAgreement (returns device's P-256 public key)
|
||||
//! 2. Host generates ephemeral P-256 key pair
|
||||
//! 3. Host computes ECDH shared secret → SHA-256(shared_secret)
|
||||
//! 4. PIN hash encrypted with AES-256-CBC (key = shared_secret, IV = 0)
|
||||
//! 5. Token decrypted with same key
|
||||
//! ```
|
||||
//!
|
||||
//! The shared secret is derived as `SHA-256(ECDH_x_coordinate)`.
|
||||
//!
|
||||
//! # Firmware compatibility
|
||||
//!
|
||||
//! Both [pico-fido] and [RS-Key] implement CTAPHID. This module handles:
|
||||
//! - Standard CTAP2 commands (GetInfo, MakeCredential, GetAssertion, etc.)
|
||||
//! - Pico-fido vendor commands (`0xC1`, `0xC2`) for hardware config
|
||||
//! - RS-Key vendor command (`0x41`) for seed backup and attestation
|
||||
//!
|
||||
//! # File structure
|
||||
//!
|
||||
//! - [`HidTransport`] — main transport struct; opens HID device, negotiates
|
||||
//! CID, sends/receives CBOR payloads
|
||||
//! - [`EnumerateRpResponse`], [`EnumerateCredentialResponse`] — response
|
||||
//! types for credential management enumeration
|
||||
//! - PIN methods (`get_pin_token`, `set_pin`, `change_pin`) implement the
|
||||
//! full ECDH + AES-CBC flow per CTAP2 §11.5.4
|
||||
//! - Vendor methods (`send_vendor_config`, `get_enterprise_attestation_csr`)
|
||||
//! handle pico-fido/RS-Key specific extensions
|
||||
//!
|
||||
//! [CTAP2]: https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html
|
||||
//! [pico-fido]: https://github.com/polhenarejos/pico-fido
|
||||
//! [RS-Key]: https://github.com/TheMaxMur/RS-Key
|
||||
|
||||
use cbc::cipher::{Block, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::NoPadding};
|
||||
use rand::RngExt;
|
||||
use ring::{agreement, digest, hmac};
|
||||
@@ -8,22 +92,70 @@ use std::time::Duration;
|
||||
use crate::device::fido::constants::*;
|
||||
use crate::error::PFError;
|
||||
|
||||
// HID Transport Constants
|
||||
/// Size of a single USB HID report in bytes (CTAP2 §11.2 mandates 64-byte reports).
|
||||
const HID_REPORT_SIZE: usize = 64;
|
||||
|
||||
/// FIDO Alliance HID Usage Page identifier.
|
||||
///
|
||||
/// Devices advertising this usage page in their HID descriptor are identified
|
||||
/// as FIDO authenticators by the operating system's HID enumeration.
|
||||
const HID_USAGE_PAGE_FIDO: u16 = 0xF1D0;
|
||||
|
||||
/// Broadcast Channel ID used for the initial CTAPHID_INIT handshake.
|
||||
///
|
||||
/// The host sends an INIT command to this CID to request a unique Channel ID
|
||||
/// from the authenticator. All subsequent communication uses the negotiated CID.
|
||||
const CTAPHID_CID_BROADCAST: u32 = 0xFFFFFFFF;
|
||||
|
||||
/// CTAPHID INIT command byte (0x86).
|
||||
///
|
||||
/// Initiates channel negotiation. The host sends a random 8-byte nonce; the
|
||||
/// device responds with the same nonce and a newly allocated Channel ID.
|
||||
const CTAPHID_INIT: u8 = 0x86;
|
||||
|
||||
/// CTAPHID CBOR command byte (0x90).
|
||||
///
|
||||
/// Wraps a CTAP2 CBOR-encoded command or response payload. The payload is
|
||||
/// fragmented across one init packet and zero or more continuation packets.
|
||||
pub const CTAPHID_CBOR: u8 = 0x90;
|
||||
|
||||
/// CTAPHID ERROR response byte (0xBF).
|
||||
///
|
||||
/// Indicates the authenticator encountered an error processing the command.
|
||||
/// The next byte contains the CTAP2 error code.
|
||||
const CTAPHID_ERROR: u8 = 0xBF;
|
||||
|
||||
/// CTAPHID KEEPALIVE status byte (0xBB).
|
||||
///
|
||||
/// Sent by the authenticator while processing a long-running operation (e.g.,
|
||||
/// MakeCredential with user interaction). The host must continue reading
|
||||
/// until it receives the final CBOR or ERROR response.
|
||||
const CTAPHID_KEEPALIVE: u8 = 0xBB;
|
||||
|
||||
// Timeouts
|
||||
/// Default timeout in milliseconds for draining stale HID packets.
|
||||
const HID_READ_TIMEOUT_MS: i32 = 10;
|
||||
|
||||
/// Timeout in milliseconds for reading the CTAPHID_INIT response during channel negotiation.
|
||||
const HID_INIT_READ_TIMEOUT_MS: i32 = 100;
|
||||
|
||||
/// Timeout in milliseconds for reading a single HID response packet (excluding keepalives).
|
||||
const HID_RESP_READ_TIMEOUT_MS: i32 = 2000;
|
||||
|
||||
/// Timeout in milliseconds for reading CTAPHID continuation packets.
|
||||
const HID_CONT_READ_TIMEOUT_MS: i32 = 500;
|
||||
|
||||
/// Maximum total time in milliseconds allowed for a complete CBOR command/response exchange.
|
||||
const HID_TOTAL_TIMEOUT_MS: i32 = 5000;
|
||||
|
||||
/// USB HID transport for CTAP2/FIDO2 communication.
|
||||
///
|
||||
/// Wraps a `hidapi::HidDevice` and manages the CTAPHID framing layer:
|
||||
/// channel negotiation (INIT), multi-packet CBOR send/receive, keepalive
|
||||
/// handling, and all higher-level CTAP2 operations (PIN, credential management,
|
||||
/// vendor commands).
|
||||
///
|
||||
/// Created via [`HidTransport::open`], which scans for a device with the FIDO
|
||||
/// HID Usage Page (0xF1D0) and performs the INIT handshake to obtain a Channel ID.
|
||||
pub struct HidTransport {
|
||||
device: hidapi::HidDevice,
|
||||
cid: u32,
|
||||
@@ -32,6 +164,10 @@ pub struct HidTransport {
|
||||
pub product_name: String,
|
||||
}
|
||||
|
||||
/// Response from enumerating a Relying Party via credential management.
|
||||
///
|
||||
/// Returned by [`HidTransport::credential_management_enumerate_rps`]. Each entry
|
||||
/// represents one RP stored on the authenticator.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumerateRpResponse {
|
||||
pub rp: Value,
|
||||
@@ -40,6 +176,10 @@ pub struct EnumerateRpResponse {
|
||||
pub total_rps: Option<usize>,
|
||||
}
|
||||
|
||||
/// Response from enumerating a credential via credential management.
|
||||
///
|
||||
/// Returned by [`HidTransport::credential_management_enumerate_credentials`].
|
||||
/// Each entry represents one credential (public key) registered under an RP.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumerateCredentialResponse {
|
||||
pub user: Value,
|
||||
@@ -51,6 +191,11 @@ pub struct EnumerateCredentialResponse {
|
||||
}
|
||||
|
||||
impl HidTransport {
|
||||
/// Open the first available FIDO HID device and negotiate a Channel ID.
|
||||
///
|
||||
/// Scans for a device with HID Usage Page `0xF1D0`, opens it, and performs
|
||||
/// the CTAPHID_INIT handshake. Returns an error if no device is found or
|
||||
/// the INIT handshake times out.
|
||||
pub fn open() -> Result<Self, PFError> {
|
||||
log::info!("Attempting to open HID transport for FIDO device...");
|
||||
let api = hidapi::HidApi::new().map_err(|e| {
|
||||
@@ -101,6 +246,11 @@ impl HidTransport {
|
||||
})
|
||||
}
|
||||
|
||||
/// Negotiate a CTAPHID Channel ID via CTAPHID_INIT.
|
||||
///
|
||||
/// Sends an INIT command to the broadcast CID (`0xFFFFFFFF`) with a random
|
||||
/// 8-byte nonce, then reads the response to extract the allocated CID.
|
||||
/// Drains any stale packets before the handshake to avoid confusion.
|
||||
fn init_channel(device: &hidapi::HidDevice) -> Result<u32, PFError> {
|
||||
log::debug!("Initializing CTAPHID channel...");
|
||||
|
||||
@@ -162,10 +312,18 @@ impl HidTransport {
|
||||
))
|
||||
}
|
||||
|
||||
/// Send a CTAP2 CBOR command and wait for the response using the default timeout.
|
||||
///
|
||||
/// Convenience wrapper around [`send_cbor_with_timeout`](HidTransport::send_cbor_with_timeout).
|
||||
pub fn send_cbor(&self, cmd: u8, payload: &[u8]) -> Result<Vec<u8>, PFError> {
|
||||
self.send_cbor_with_timeout(cmd, payload, HID_TOTAL_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
/// Send a CTAP2 CBOR command and wait for the response with a custom timeout.
|
||||
///
|
||||
/// Fragments `payload` into CTAPHID init + continuation packets, then reads
|
||||
/// and reassembles the response. The `timeout_ms` parameter overrides the
|
||||
/// default for the read phase (useful for operations that require user interaction).
|
||||
pub fn send_cbor_with_timeout(
|
||||
&self,
|
||||
cmd: u8,
|
||||
@@ -176,11 +334,20 @@ impl HidTransport {
|
||||
self.read_cbor_response(cmd, timeout_ms)
|
||||
}
|
||||
|
||||
/// Send a CTAP2 CBOR command and return the raw HID response without status-byte parsing.
|
||||
///
|
||||
/// Unlike [`send_cbor`](HidTransport::send_cbor), this does not check the CTAP status byte
|
||||
/// or strip it from the response. Useful for vendor commands that return non-standard payloads.
|
||||
pub fn send_raw(&self, cmd: u8, payload: &[u8]) -> Result<Vec<u8>, PFError> {
|
||||
self.write_cbor_request(cmd, payload)?;
|
||||
self.read_hid_response(cmd, HID_TOTAL_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
/// Send the CTAP authenticatorReset command (0x07).
|
||||
///
|
||||
/// Resets the authenticator to its factory state: all credentials, PINs,
|
||||
/// and configuration are erased. Uses a 30-second timeout to allow for
|
||||
/// any required user interaction (e.g., touch confirmation).
|
||||
pub fn reset(&self) -> Result<(), PFError> {
|
||||
log::info!("Sending CTAP authenticatorReset (0x07)...");
|
||||
self.write_cbor_request(CTAPHID_CBOR, &[0x07])?;
|
||||
@@ -188,6 +355,11 @@ impl HidTransport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fragment and write a CTAPHID request to the device.
|
||||
///
|
||||
/// Encodes the command byte and payload into a CTAPHID init packet followed
|
||||
/// by zero or more continuation packets, then writes each 65-byte HID report
|
||||
/// (1 byte Report ID + 64 bytes payload) to the device.
|
||||
fn write_cbor_request(&self, cmd: u8, payload: &[u8]) -> Result<(), PFError> {
|
||||
log::debug!(
|
||||
"Sending CBOR Command: 0x{:02X}, Payload Size: {} bytes",
|
||||
@@ -254,6 +426,11 @@ impl HidTransport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a CTAPHID response and verify the CTAP status byte.
|
||||
///
|
||||
/// Delegates to [`read_hid_response`](HidTransport::read_hid_response) for packet
|
||||
/// reassembly, then checks the first byte for a non-zero CTAP status code and
|
||||
/// strips it before returning the payload.
|
||||
fn read_cbor_response(&self, cmd: u8, timeout_ms: i32) -> Result<Vec<u8>, PFError> {
|
||||
let response_data = self.read_hid_response(cmd, timeout_ms)?;
|
||||
|
||||
@@ -280,6 +457,13 @@ impl HidTransport {
|
||||
Ok(response_data[1..].to_vec())
|
||||
}
|
||||
|
||||
/// Read and reassemble a CTAPHID response from the device.
|
||||
///
|
||||
/// Handles the full CTAPHID receive flow:
|
||||
/// 1. Reads the init packet while skipping KEEPALIVE and mismatched-CID packets.
|
||||
/// 2. Validates the command byte matches the expected response.
|
||||
/// 3. Reads continuation packets in sequence order until the full payload is received.
|
||||
/// 4. Enforces the `timeout_ms` deadline across the entire read.
|
||||
fn read_hid_response(&self, cmd: u8, timeout_ms: i32) -> Result<Vec<u8>, PFError> {
|
||||
log::debug!("Waiting for response...");
|
||||
|
||||
@@ -396,6 +580,12 @@ impl HidTransport {
|
||||
Ok(response_data)
|
||||
}
|
||||
|
||||
/// Send a pico-fido vendor-specific authenticatorConfig command.
|
||||
///
|
||||
/// Wraps the vendor command ID and parameter into the VendorPrototype
|
||||
/// sub-command structure, signs it with the PIN token, and sends it as
|
||||
/// a CTAP Config command. The parameter value type (bytes, integer, or text)
|
||||
/// determines which CBOR key (0x02/0x03/0x04) is used.
|
||||
pub fn send_vendor_config(
|
||||
&self,
|
||||
pin_token: &[u8],
|
||||
@@ -537,6 +727,9 @@ impl HidTransport {
|
||||
/// CBOR map keys out of order (0x01, 0x03, 0x04, 0x02) instead of the required
|
||||
/// ascending order (0x01, 0x02, 0x03, 0x04). The pico-fido firmware strictly
|
||||
/// enforces canonical CBOR ordering per CTAP2 spec.
|
||||
///
|
||||
/// Builds the authenticatorConfig CBOR map with keys in ascending order, signs
|
||||
/// it with the PIN token, and sends it as a CTAP Config command.
|
||||
pub fn send_config(
|
||||
&self,
|
||||
sub_cmd: ConfigSubCommand,
|
||||
@@ -585,6 +778,10 @@ impl HidTransport {
|
||||
}
|
||||
|
||||
/// Send authenticatorConfig command to enable Enterprise attestation.
|
||||
///
|
||||
/// Calls the EnableEnterpriseAttestation sub-command (0x01) via [`send_config`](HidTransport::send_config).
|
||||
/// Enterprise attestation allows RPs to receive a per-device attestation certificate
|
||||
/// during MakeCredential, enabling enterprise device identification.
|
||||
pub fn send_config_enable_ea(&self, pin_token: &[u8]) -> Result<(), PFError> {
|
||||
log::debug!("Sending Enterprise Attestation enable config command...");
|
||||
match self.send_config(
|
||||
@@ -608,6 +805,10 @@ impl HidTransport {
|
||||
}
|
||||
|
||||
/// Send authenticatorConfig command to set minimum PIN length.
|
||||
///
|
||||
/// Calls the SetMinPinLength sub-command (0x03) via [`send_config`](HidTransport::send_config).
|
||||
/// The minimum PIN length can only be increased; attempting to decrease it returns
|
||||
/// `PIN_POLICY_VIOLATION` (0x37). A device reset is required to lower the minimum.
|
||||
pub fn send_config_set_min_pin_length(
|
||||
&self,
|
||||
pin_token: &[u8],
|
||||
@@ -653,6 +854,11 @@ impl HidTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the authenticator's P-256 ECDH public key for PIN protocol v1.
|
||||
///
|
||||
/// Sends a `getClientPin` command with `getKeyAgreement` sub-command (0x02).
|
||||
/// The returned COSE Key contains the authenticator's ephemeral public key
|
||||
/// (x and y coordinates) used for ECDH key agreement in PIN operations.
|
||||
pub fn get_key_agreement(&self) -> Result<Value, PFError> {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
@@ -685,6 +891,14 @@ impl HidTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain an encrypted PIN token using the standard getPinToken flow.
|
||||
///
|
||||
/// Implements the full CTAP2 §11.5.4 PIN token acquisition:
|
||||
/// 1. Fetches the authenticator's key agreement public key.
|
||||
/// 2. Generates an ephemeral P-256 key pair on the platform.
|
||||
/// 3. Performs ECDH and derives `SHA-256(shared_secret)`.
|
||||
/// 4. Encrypts the first 16 bytes of `SHA-256(pin)` with AES-256-CBC.
|
||||
/// 5. Sends getPinToken (sub-command 0x05) and decrypts the response token.
|
||||
pub fn get_pin_token(&self, pin: &str) -> Result<Vec<u8>, PFError> {
|
||||
log::info!("Starting custom get_pin_token (Subcommand 0x05)...");
|
||||
|
||||
@@ -789,6 +1003,12 @@ impl HidTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain a PIN token with specific permissions and optional RP ID scope.
|
||||
///
|
||||
/// Like [`get_pin_token`](HidTransport::get_pin_token) but uses the
|
||||
/// `getPinUvAuthTokenUsingPinWithPermissions` sub-command (0x09). This allows
|
||||
/// requesting only the permissions needed (e.g., `CREDENTIAL_MANAGEMENT` for
|
||||
/// enumeration/deletion), following the principle of least privilege.
|
||||
pub fn get_pin_token_with_permission(
|
||||
&self,
|
||||
pin: &str,
|
||||
@@ -911,6 +1131,16 @@ impl HidTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a new PIN on the authenticator (sub-command 0x03).
|
||||
///
|
||||
/// Implements the full CTAP2 setPin flow:
|
||||
/// 1. Performs ECDH key agreement to derive the shared secret.
|
||||
/// 2. Encrypts the new PIN (padded to 64 bytes) with AES-256-CBC.
|
||||
/// 3. Computes `HMAC-SHA-256(shared_secret, newPinEnc)[0..16]` as pinUvAuthParam.
|
||||
/// 4. Sends the SetPin command with the platform's public key, encrypted PIN, and HMAC.
|
||||
///
|
||||
/// The PIN must be 4–63 characters. Fails with `PIN_POLICY_VIOLATION` (0x37) if
|
||||
/// the PIN is too short.
|
||||
pub fn set_pin(&self, new_pin: &str) -> Result<(), PFError> {
|
||||
log::info!("Starting custom set_pin (Subcommand 0x03)...");
|
||||
|
||||
@@ -1030,6 +1260,18 @@ impl HidTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the authenticator PIN (sub-command 0x04).
|
||||
///
|
||||
/// Implements the full CTAP2 changePin flow:
|
||||
/// 1. Performs ECDH key agreement to derive the shared secret.
|
||||
/// 2. Encrypts `SHA-256(current_pin)[0..16]` with AES-256-CBC (pinHashEnc).
|
||||
/// 3. Encrypts the new PIN (padded to 64 bytes) with AES-256-CBC (newPinEnc).
|
||||
/// 4. Computes `HMAC-SHA-256(shared_secret, newPinEnc || pinHashEnc)[0..16]`.
|
||||
/// 5. Sends the ChangePin command.
|
||||
///
|
||||
/// Returns `CTAP2_ERR_PIN_AUTH_INVALID` (0x31) if the current PIN is wrong,
|
||||
/// `CTAP2_ERR_PIN_BLOCKED` (0x32) if the PIN is blocked, or
|
||||
/// `CTAP2_ERR_PIN_POLICY_VIOLATION` (0x37) if the new PIN violates policy.
|
||||
pub fn change_pin(&self, current_pin: &str, new_pin: &str) -> Result<(), PFError> {
|
||||
log::info!("Starting custom change_pin (Subcommand 0x04)...");
|
||||
|
||||
@@ -1173,7 +1415,11 @@ impl HidTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to sign the authenticatorConfig command
|
||||
/// Sign an authenticatorConfig command using HMAC-SHA-256.
|
||||
///
|
||||
/// Computes `HMAC-SHA-256(pin_token, 0x0d || subCommand || subCommandParams)[0..16]`
|
||||
/// per the CTAP2 authenticatorConfig signing specification. The 0x0d byte
|
||||
/// identifies the Config command category.
|
||||
fn sign_config_command(
|
||||
&self,
|
||||
pin_token: &[u8],
|
||||
@@ -1193,6 +1439,10 @@ impl HidTransport {
|
||||
sig.as_ref()[0..16].to_vec()
|
||||
}
|
||||
|
||||
/// Encode an uncompressed P-256 public key as a COSE_Key map.
|
||||
///
|
||||
/// Returns CBOR bytes for a map with keys: kty(1)=EC2(2), alg(3)=ES256(-7),
|
||||
/// crv(-1)=P-256(1), x(-2), y(-3). Used in PIN key agreement payloads.
|
||||
fn encode_cose_key(&self, x: &[u8], y: &[u8]) -> Vec<u8> {
|
||||
let mut bytes = vec![0xA5]; // Map(5)
|
||||
bytes.extend(to_vec(&Value::Integer(1)).unwrap());
|
||||
@@ -1208,6 +1458,11 @@ impl HidTransport {
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Build a CBOR map for ClientPin sub-command parameters.
|
||||
///
|
||||
/// Constructs the parameter map with `pinProtocol`, `subCommand`, `keyAgreement`,
|
||||
/// and `pinHashEnc`. Optionally includes `permissions` and `rpId` when the
|
||||
/// `getPinUvAuthTokenUsingPinWithPermissions` sub-command is used.
|
||||
fn encode_client_pin_params(
|
||||
&self,
|
||||
sub_cmd: ClientPinSubCommand,
|
||||
@@ -1243,6 +1498,14 @@ impl HidTransport {
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Enumerate all Relying Parties stored on the authenticator.
|
||||
///
|
||||
/// Performs the CTAP2 credential management enumeration flow:
|
||||
/// 1. Obtains a PIN token with `CREDENTIAL_MANAGEMENT` permission.
|
||||
/// 2. Sends `EnumerateRpsBegin` (sub-command 0x02) to get the first RP.
|
||||
/// 3. Iterates with `EnumerateRpsGetNextRp` (sub-command 0x03) until all RPs are returned.
|
||||
///
|
||||
/// Returns an empty vector if no credentials exist on the device.
|
||||
pub fn credential_management_enumerate_rps(
|
||||
&self,
|
||||
pin: &str,
|
||||
@@ -1382,6 +1645,14 @@ impl HidTransport {
|
||||
Ok(all_rps)
|
||||
}
|
||||
|
||||
/// Enumerate all credentials registered under a specific Relying Party.
|
||||
///
|
||||
/// Given an `rp_id_hash` (SHA-256 of the RP's ID), performs:
|
||||
/// 1. Obtains a PIN token with `CREDENTIAL_MANAGEMENT` permission.
|
||||
/// 2. Sends `EnumerateCredentialsBegin` (sub-command 0x04) with the RP ID hash.
|
||||
/// 3. Iterates with `EnumerateCredentialsGetNextCredential` (sub-command 0x05).
|
||||
///
|
||||
/// Returns user info, credential ID, and public key for each credential.
|
||||
pub fn credential_management_enumerate_credentials(
|
||||
&self,
|
||||
pin: &str,
|
||||
@@ -1553,6 +1824,12 @@ impl HidTransport {
|
||||
Ok(all_creds)
|
||||
}
|
||||
|
||||
/// Delete a specific credential from the authenticator.
|
||||
///
|
||||
/// Obtains a PIN token with `CREDENTIAL_MANAGEMENT` permission, then sends
|
||||
/// the `DeleteCredential` command (sub-command 0x06) with the credential ID
|
||||
/// descriptor map. The `credential_id_map` must be a CBOR map with key 0x02
|
||||
/// containing the credential ID.
|
||||
pub fn credential_management_delete_credential(
|
||||
&self,
|
||||
pin: &str,
|
||||
@@ -1607,6 +1884,13 @@ impl HidTransport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sign a credential management command using HMAC-SHA-256.
|
||||
///
|
||||
/// Uses pico-fido's non-standard signing scheme: for sub-commands 0x01
|
||||
/// (GetCredsMetadata) and 0x02 (EnumerateRpsBegin), only the sub-command
|
||||
/// byte is signed. For all others, the sub-command byte followed by the
|
||||
/// CBOR-encoded SubCommandParams is signed. Returns the first 16 bytes
|
||||
/// of the HMAC digest.
|
||||
fn sign_credential_mgmt_command(
|
||||
&self,
|
||||
pin_token: &[u8],
|
||||
|
||||
@@ -1,3 +1,59 @@
|
||||
//! FIDO2 / CTAP2 protocol implementation for pico-fido and RS-Key firmware.
|
||||
//!
|
||||
//! ```text
|
||||
//! fido/
|
||||
//! ├── mod.rs — high-level FIDO2 operations (info, PIN, credentials, config)
|
||||
//! ├── constants.rs — CTAP2 command codes, CBOR map keys, COSE algorithms, bitflags
|
||||
//! └── hid.rs — USB HID transport (CTAPHID framing, channel init, CBOR exchange)
|
||||
//! ```
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Communication flows top-down:
|
||||
//!
|
||||
//! ```text
|
||||
//! io::read_device_details()
|
||||
//! │
|
||||
//! ▼
|
||||
//! fido::read_device_details() ← this file
|
||||
//! │
|
||||
//! ▼
|
||||
//! HidTransport::open() ← hid.rs
|
||||
//! │
|
||||
//! ▼
|
||||
//! USB HID (CTAPHID protocol)
|
||||
//! ```
|
||||
//!
|
||||
//! [`constants`] is imported by both `mod.rs` and `hid.rs` and should be the
|
||||
//! single source of truth for every CTAP2-defined byte value. If you need to
|
||||
//! add a new command, sub-command, or CBOR key, put it there.
|
||||
//!
|
||||
//! [`hid`] owns the raw byte-level exchange: channel ID negotiation, packet
|
||||
//! framing (init + continuation packets), PIN token acquisition, ECDH key
|
||||
//! agreement, and CBOR serialization. It exposes [`HidTransport`] which the
|
||||
//! rest of the module uses for all device I/O.
|
||||
//!
|
||||
//! [`mod.rs`] contains the public functions called from [`super::io`].
|
||||
//! Each function opens an [`HidTransport`], performs the CTAP2 operation,
|
||||
//! and parses the CBOR response into the structs defined in [`super::types`].
|
||||
//!
|
||||
//! # Vendor extensions
|
||||
//!
|
||||
//! Pico-fido firmware exposes vendor-specific CTAP commands (`0xC1`, `0xC2`)
|
||||
//! for hardware configuration (VID/PID, LED, memory stats). These are handled
|
||||
//! through [`HidTransport::send_vendor_config`] and the
|
||||
//! [`VendorConfigCommand`] enum in constants. Legacy firmware (≤7.2) uses a
|
||||
//! different physical-options encoding; see `firmware_supports_legacy_fido_hardware_config`.
|
||||
//!
|
||||
//! # Adding a new FIDO2 operation
|
||||
//!
|
||||
//! 1. Add any new command/sub-command enums to [`constants`].
|
||||
//! 2. Implement the CBOR encoding and transport call in [`hid`] (if it
|
||||
//! requires new framing or PIN token logic).
|
||||
//! 3. Add the high-level function in this file, following the pattern:
|
||||
//! open transport → build CBOR payload → send → parse response → return.
|
||||
//! 4. Expose it through [`super::io`].
|
||||
|
||||
pub mod constants;
|
||||
pub mod hid;
|
||||
|
||||
|
||||
+63
-31
@@ -1,8 +1,20 @@
|
||||
//! Tauri Commands to interact with the pico-fido firmware via rescue and fido protocols.
|
||||
//! Device I/O layer bridging rescue (pcsc) and FIDO2 protocols.
|
||||
//!
|
||||
//! High-level entry points for reading/writing device configuration,
|
||||
//! managing credentials, and controlling LED/boot behavior.
|
||||
//!
|
||||
//! Functions are grouped by the protocol they use:
|
||||
//! - Functions that use both rescue and FIDO (fallback/dispatch logic)
|
||||
//! - Functions that communicate exclusively over the rescue (PC/SC) channel
|
||||
//! - Functions that communicate exclusively over the FIDO2 channel
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{device::fido, device::rescue, device::types::*, error::PFError};
|
||||
|
||||
// ── Shared: functions that use both rescue and FIDO ─────────────────────────
|
||||
|
||||
/// Read full device status. Tries rescue first, falls back to FIDO on failure.
|
||||
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
|
||||
match rescue::read_device_details() {
|
||||
Ok(status) => Ok(status),
|
||||
@@ -13,6 +25,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write app config. Dispatches to rescue or FIDO based on `method`.
|
||||
pub fn write_config(
|
||||
config: AppConfigInput,
|
||||
method: DeviceMethod,
|
||||
@@ -25,48 +38,24 @@ pub fn write_config(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rescue protocol (PC/SC) ─────────────────────────────────────────────────
|
||||
|
||||
/// Lock or unlock secure boot via rescue.
|
||||
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
|
||||
rescue::enable_secure_boot(lock)
|
||||
}
|
||||
|
||||
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
|
||||
fido::get_fido_info()
|
||||
}
|
||||
|
||||
pub(crate) fn change_fido_pin(
|
||||
current_pin: Option<String>,
|
||||
new_pin: String,
|
||||
) -> Result<String, String> {
|
||||
fido::change_fido_pin(current_pin, new_pin)
|
||||
}
|
||||
|
||||
pub(crate) fn set_min_pin_length(
|
||||
current_pin: String,
|
||||
min_pin_length: u8,
|
||||
) -> Result<String, String> {
|
||||
fido::set_min_pin_length(current_pin, min_pin_length)
|
||||
}
|
||||
|
||||
/// Reboot the device. Pass `true` to enter BOOTSEL mode.
|
||||
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
|
||||
rescue::reboot_device(to_bootsel)
|
||||
}
|
||||
|
||||
pub fn get_credentials(pin: String) -> Result<Vec<StoredCredential>, String> {
|
||||
fido::get_credentials(pin)
|
||||
}
|
||||
|
||||
pub fn delete_credential(pin: String, credential_id: String) -> Result<String, String> {
|
||||
fido::delete_credential(pin, credential_id)
|
||||
}
|
||||
|
||||
pub fn reset_device() -> Result<String, String> {
|
||||
fido::reset_device()
|
||||
}
|
||||
|
||||
/// Read current LED status config via rescue.
|
||||
pub fn read_led_config() -> Result<LedStatusConfig, PFError> {
|
||||
rescue::read_led_config()
|
||||
}
|
||||
|
||||
/// Write LED status (on/off, color, brightness, steady/blinking).
|
||||
pub fn write_led_status(
|
||||
status: u8,
|
||||
color: u8,
|
||||
@@ -76,22 +65,65 @@ pub fn write_led_status(
|
||||
rescue::write_led_status(status, color, brightness, steady)
|
||||
}
|
||||
|
||||
/// Read management app config via rescue.
|
||||
pub fn read_management_config() -> Result<ManagementAppConfig, PFError> {
|
||||
rescue::read_management_config()
|
||||
}
|
||||
|
||||
/// Write management app enabled-mask via rescue.
|
||||
pub fn write_management_config(enabled_mask: u16) -> Result<String, PFError> {
|
||||
rescue::write_management_config(enabled_mask)
|
||||
}
|
||||
|
||||
// ── FIDO2 protocol ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Query basic FIDO device info (AAGUID, version, etc.).
|
||||
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
|
||||
fido::get_fido_info()
|
||||
}
|
||||
|
||||
/// Change the FIDO user PIN.
|
||||
pub(crate) fn change_fido_pin(
|
||||
current_pin: Option<String>,
|
||||
new_pin: String,
|
||||
) -> Result<String, String> {
|
||||
fido::change_fido_pin(current_pin, new_pin)
|
||||
}
|
||||
|
||||
/// Set the minimum PIN length requirement.
|
||||
pub(crate) fn set_min_pin_length(
|
||||
current_pin: String,
|
||||
min_pin_length: u8,
|
||||
) -> Result<String, String> {
|
||||
fido::set_min_pin_length(current_pin, min_pin_length)
|
||||
}
|
||||
|
||||
/// List stored credentials for the given PIN.
|
||||
pub fn get_credentials(pin: String) -> Result<Vec<StoredCredential>, String> {
|
||||
fido::get_credentials(pin)
|
||||
}
|
||||
|
||||
/// Delete a single credential by its ID.
|
||||
pub fn delete_credential(pin: String, credential_id: String) -> Result<String, String> {
|
||||
fido::delete_credential(pin, credential_id)
|
||||
}
|
||||
|
||||
/// Factory-reset the device, wiping all credentials and settings.
|
||||
pub fn reset_device() -> Result<String, String> {
|
||||
fido::reset_device()
|
||||
}
|
||||
|
||||
/// Enable enterprise attestation for the device.
|
||||
pub fn enable_enterprise_attestation(pin: String) -> Result<String, String> {
|
||||
fido::enable_enterprise_attestation(pin)
|
||||
}
|
||||
|
||||
/// Retrieve the enterprise attestation CSR.
|
||||
pub fn get_enterprise_attestation_csr() -> Result<String, String> {
|
||||
fido::get_enterprise_attestation_csr()
|
||||
}
|
||||
|
||||
/// Upload a signed enterprise attestation certificate.
|
||||
pub fn upload_enterprise_attestation_cert(
|
||||
pin: String,
|
||||
cert_path: String,
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
//! Device communication layer for pico-forge.
|
||||
//!
|
||||
//! ```text
|
||||
//! device/
|
||||
//! ├── mod.rs — module root, re-exports submodules
|
||||
//! ├── io.rs — high-level entry points (both protocols)
|
||||
//! ├── rescue.rs — rescue / PC/SC protocol implementation
|
||||
//! ├── fido.rs — FIDO2 / CTAP2 protocol implementation
|
||||
//! └── types.rs — shared structs, enums, and constants
|
||||
//! ```
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! The `device` module is the only place that talks to the hardware token.
|
||||
//! Everything above it (UI state, gpui-component views) depends on the
|
||||
//! public functions exported here; nothing below it should know about the
|
||||
//! communication details.
|
||||
//!
|
||||
//! Two protocols are used:
|
||||
//!
|
||||
//! - **Rescue (PC/SC)** — low-level APDU channel for firmware-level
|
||||
//! configuration: secure boot, LED status, USB applet management, and
|
||||
//! device reboot. Implemented in [`rescue`].
|
||||
//!
|
||||
//! - **FIDO2 (CTAP2)** — standard authenticator protocol for credential
|
||||
//! management, PIN operations, and enterprise attestation.
|
||||
//! Implemented in [`fido`].
|
||||
//!
|
||||
//! [`io`] sits on top of both and exposes a single function per device
|
||||
//! operation. Some functions dispatch to one protocol or the other based
|
||||
//! on a [`types::DeviceMethod`] flag; others try rescue first and fall back
|
||||
//! to FIDO on failure.
|
||||
//!
|
||||
//! # Adding a new device operation
|
||||
//!
|
||||
//! 1. Add any new structs/enums to [`types`].
|
||||
//! 2. Implement the raw protocol call in [`rescue`] or [`fido`].
|
||||
//! 3. Expose a high-level wrapper in [`io`] that picks the right protocol
|
||||
//! and converts errors to the caller's expected type.
|
||||
//! 4. Wire the wrapper into a gpui-component view or action handler.
|
||||
|
||||
pub mod fido;
|
||||
pub mod io;
|
||||
pub mod rescue;
|
||||
|
||||
+452
-23
File diff suppressed because it is too large
Load Diff
+260
-4
@@ -1,6 +1,161 @@
|
||||
//! Implements communication with the pico-fido firmware via the `Rescue API`.
|
||||
//! Rescue applet implementation for pico-fido and RS-Key firmware.
|
||||
//!
|
||||
//! For more details checkout the [pico-key-sdk](https://github.com/polhenarejos/pico-keys-sdk/blob/main/src/rescue.c)
|
||||
//! ```text
|
||||
//! rescue/
|
||||
//! ├── mod.rs — high-level rescue operations (read/write config, reboot, LED, management)
|
||||
//! └── constants.rs — ISO 7816-4 constants, rescue instructions, PHY tags, vendor applets
|
||||
//! ```
|
||||
//!
|
||||
//! # What is the Rescue Applet?
|
||||
//!
|
||||
//! The Rescue applet is a low-level firmware recovery and hardware configuration
|
||||
//! interface that operates independently of the FIDO2/CTAP2 stack. It provides
|
||||
//! direct access to device hardware settings, flash memory, and security features
|
||||
//! through a proprietary APDU-based protocol.
|
||||
//!
|
||||
//! Both [pico-fido](https://github.com/polhenarejos/pico-fido) (C) and
|
||||
//! [RS-Key](https://github.com/TheMaxMur/RS-Key) (Rust) firmware implement
|
||||
//! this applet with the same AID and command set.
|
||||
//!
|
||||
//! # Why is Rescue Mode Needed?
|
||||
//!
|
||||
//! FIDO2 devices expose a standardized interface (CTAP2) that abstracts away
|
||||
//! hardware details. However, there are scenarios where direct hardware access
|
||||
//! is required:
|
||||
//!
|
||||
//! - **Firmware recovery**: When FIDO mode is unresponsive or corrupted
|
||||
//! - **Hardware configuration**: Changing USB VID/PID, LED settings, touch timeout
|
||||
//! without requiring FIDO PIN authentication
|
||||
//! - **Secure boot management**: Enabling/disabling secure boot, reading OTP status
|
||||
//! - **Device provisioning**: Uploading attestation certificates, setting serial numbers
|
||||
//! - **Firmware updates**: Rebooting into bootloader (BOOTSEL) mode for flashing
|
||||
//!
|
||||
//! The Rescue applet runs on the CCID (smart card) USB interface, which is always
|
||||
//! available even when FIDO functionality is disabled or misconfigured.
|
||||
//!
|
||||
//! # Communication Protocol: PC/SC
|
||||
//!
|
||||
//! Unlike FIDO2 which uses USB HID (CTAPHID), the Rescue applet communicates via
|
||||
//! **PC/SC** (Personal Computer/Smart Card) — the standard protocol for interacting
|
||||
//! with smart card readers and ICCs (Integrated Circuit Cards).
|
||||
//!
|
||||
//! ```text
|
||||
//! Host Application
|
||||
//! │
|
||||
//! ▼
|
||||
//! pcsc-lite daemon (pcscd) ← Linux/macOS daemon
|
||||
//! │
|
||||
//! ▼
|
||||
//! USB CCID Class Driver ← Smart card reader driver
|
||||
//! │
|
||||
//! ▼
|
||||
//! Device CCID Interface ← Composite USB device
|
||||
//! │
|
||||
//! ▼
|
||||
//! Rescue Applet (APDU commands) ← Firmware
|
||||
//! ```
|
||||
//!
|
||||
//! ## PC/SC Architecture
|
||||
//!
|
||||
//! The PC/SC specification defines a standard API for communicating with smart
|
||||
//! cards. In our case, the RP2040/RP2350 device emulates a CCID-compliant smart
|
||||
//! card reader with an embedded ICC.
|
||||
//!
|
||||
//! Key concepts:
|
||||
//! - **Context**: A connection to the PC/SC daemon (establishes resource manager)
|
||||
//! - **Reader**: A physical or virtual smart card reader (our device appears as one)
|
||||
//! - **Card**: A connection to a specific card in a reader
|
||||
//! - **APDU**: Application Protocol Data Unit — the command/response format
|
||||
//!
|
||||
//! ## APDU Command Structure
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌─────┬─────┬─────┬─────┬─────┬─────────────┐
|
||||
//! │ CLA │ INS │ P1 │ P2 │ Lc │ Data │
|
||||
//! └─────┴─────┴─────┴─────┴─────┴─────────────┘
|
||||
//! 1B 1B 1B 1B 0-1B 0-255 bytes
|
||||
//! ```
|
||||
//!
|
||||
//! - **CLA** (0x80 for Rescue): Command class — proprietary extension
|
||||
//! - **INS**: Instruction code (e.g., 0x1E for READ, 0x1C for WRITE)
|
||||
//! - **P1/P2**: Parameters (sub-command selectors)
|
||||
//! - **Lc**: Length of data field
|
||||
//! - **Data**: Command payload
|
||||
//!
|
||||
//! Response ends with Status Words (SW1 SW2):
|
||||
//! - `0x90 0x00`: Success
|
||||
//! - `0x6A 0x82`: File/application not found
|
||||
//! - `0x69 0x82`: Security status not satisfied
|
||||
//!
|
||||
//! # Data Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! io::read_device_details()
|
||||
//! │
|
||||
//! ▼
|
||||
//! rescue::read_device_details() ← this file
|
||||
//! │
|
||||
//! ▼
|
||||
//! connect_and_select() ← PC/SC connection + applet selection
|
||||
//! │
|
||||
//! ▼
|
||||
//! card.transmit(apdu) ← ISO 7816-4 APDU exchange
|
||||
//! │
|
||||
//! ▼
|
||||
//! PC/SC (CCID USB interface)
|
||||
//! ```
|
||||
//!
|
||||
//! ## Applet Selection
|
||||
//!
|
||||
//! Every session begins with applet selection:
|
||||
//!
|
||||
//! ```text
|
||||
//! APDU: 00 A4 04 04 08 A0 58 3F C1 9B 7E 4F 21
|
||||
//! ── ── ── ── ── ─────────────────────────
|
||||
//! CLA INS P1 P2 Len AID (Rescue Applet)
|
||||
//! ```
|
||||
//!
|
||||
//! The SELECT response contains device identity:
|
||||
//! - Byte 0: MCU type (1=RP2350, 2=ESP32-S3, etc.)
|
||||
//! - Byte 1: Product type (2=FIDO)
|
||||
//! - Byte 2: SDK version major
|
||||
//! - Byte 3: SDK version minor
|
||||
//! - Bytes 4-11: Serial number (8 bytes)
|
||||
//!
|
||||
//! # Module Structure
|
||||
//!
|
||||
//! [`constants`] defines all protocol constants shared between pico-fido and RS-Key:
|
||||
//! - ISO 7816-4 command bytes (CLA, INS, P1, P2, SW)
|
||||
//! - Rescue instruction codes and parameters
|
||||
//! - PHY configuration tags and bitflags
|
||||
//! - Vendor applet AIDs and instructions (LED, Management)
|
||||
//!
|
||||
//! [`mod.rs`] contains the public functions called from [`super::io`]:
|
||||
//! - `read_device_details()`: Reads full device status via Rescue
|
||||
//! - `write_config()`: Writes PHY configuration (VID/PID, LED, curves, etc.)
|
||||
//! - `reboot_device()`: Reboots device (normal or BOOTSEL mode)
|
||||
//! - `enable_secure_boot()`: Enables secure boot (WIP)
|
||||
//! - `read_led_config()` / `write_led_status()`: LED color configuration (RS-Key)
|
||||
//! - `read_management_config()` / `write_management_config()`: USB interface config (RS-Key)
|
||||
//!
|
||||
//! # Firmware Differences
|
||||
//!
|
||||
//! | Feature | pico-fido | RS-Key |
|
||||
//! |---------|-----------|--------|
|
||||
//! | Language | C | Rust |
|
||||
//! | Rescue AID | `A0 58 3F C1 9B 7E 4F 21` | Same |
|
||||
//! | Secure Boot | `INS_SECURE` (0x1D) | `INS_OTP_LOCK` (0x1B) — irreversible |
|
||||
//! | LED Applet | Not available | Available (AID: `F0 00 00 00 01`) |
|
||||
//! | Management | Not available | Available (Yubico-compatible) |
|
||||
//! | Anti-rollback | Not available | Available (OTP fuses) |
|
||||
//!
|
||||
//! # References
|
||||
//!
|
||||
//! - [pico-fido Rescue](https://github.com/polhenarejos/pico-fido/blob/main/src/rescue.c)
|
||||
//! - [RS-Key Rescue](https://github.com/TheMaxMur/RS-Key/blob/main/crates/rsk-rescue/src/lib.rs)
|
||||
//! - [PC/SC Specification](https://pcsc1groupwg.readthedocs.io/)
|
||||
//! - [ISO 7816-4](https://www.iso.org/standard/74873.html)
|
||||
//! - [CCID Specification](https://www.usb.org/document-library/class-specification-12-chip-smart-card-interface)
|
||||
|
||||
pub mod constants;
|
||||
|
||||
@@ -10,7 +165,21 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use pcsc::{Context, Protocols, Scope, ShareMode};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Connects to the first available reader and selects the Rescue Applet
|
||||
/// Establishes a PC/SC connection to the first available smart card reader and selects the Rescue Applet.
|
||||
///
|
||||
/// Sends a SELECT APDU (`00 A4 04 04 08 A0 58 3F C1 9B 7E 4F 21`) to the device via the CCID interface.
|
||||
/// The response contains device identity data (MCU type, product type, firmware version, serial number).
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of `(Card, SelectResponse, FirmwareType)` where:
|
||||
/// - `Card` is the active PC/SC card handle for subsequent APDU exchanges
|
||||
/// - `SelectResponse` is the raw FCI/identity data from the SELECT command
|
||||
/// - `FirmwareType` is detected as `RSKey`, `PicoFido`, or `Unknown`
|
||||
///
|
||||
/// # Errors
|
||||
/// - `PFError::NoDevice` if no smart card reader is found
|
||||
/// - `PFError::Pcsc` if the PC/SC context cannot be established
|
||||
/// - `PFError::Device` if the Rescue Applet is not found (wrong AID or device in wrong mode)
|
||||
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>, FirmwareType), PFError> {
|
||||
let ctx = Context::establish(Scope::User).map_err(|e| {
|
||||
log::error!("Failed to establish PCSC context: {}", e);
|
||||
@@ -73,6 +242,20 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>, FirmwareType), PFError>
|
||||
Ok((card, data, fw_type))
|
||||
}
|
||||
|
||||
/// Reads comprehensive device details including identity, flash usage, secure boot status, and PHY configuration.
|
||||
///
|
||||
/// Performs three sequential APDU operations after applet selection:
|
||||
/// 1. SELECT response is parsed for MCU type, firmware version, and serial number
|
||||
/// 2. `READ(FlashInfo)` — reads flash usage statistics (free, used, total)
|
||||
/// 3. `READ(SecureBootStatus)` — reads secure boot enable/lock state
|
||||
/// 4. `READ(PhyConfig)` — reads TLV-encoded hardware configuration (VID/PID, LED, curves, etc.)
|
||||
///
|
||||
/// # Returns
|
||||
/// A `FullDeviceStatus` struct containing device info, parsed PHY config, and secure boot state.
|
||||
///
|
||||
/// # Errors
|
||||
/// - `PFError::Device` if the SELECT response is malformed or any READ command fails
|
||||
/// - `PFError::NoDevice` if no reader is available
|
||||
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
|
||||
log::info!("Reading full device details");
|
||||
let (card, select_resp, fw_type) = connect_and_select()?;
|
||||
@@ -269,6 +452,28 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes PHY configuration to the device via the Rescue Applet's WRITE command.
|
||||
///
|
||||
/// Constructs a TLV (Tag-Length-Value) blob from the provided `AppConfigInput` fields and sends
|
||||
/// it as a single APDU: `80 1C 01 00 [Lc] [TLV Data]`. Supported tags include:
|
||||
/// - `0x00`: VID:PID (4 bytes, big-endian)
|
||||
/// - `0x04`: LED GPIO pin
|
||||
/// - `0x05`: LED brightness
|
||||
/// - `0x08`: Touch/presence timeout
|
||||
/// - `0x06`: Options bitmask (LED_DIMMABLE, DISABLE_POWER_RESET, LED_STEADY)
|
||||
/// - `0x07`: Elliptic curves bitmask (SECP256K1, etc.)
|
||||
/// - `0x0C`: LED driver selection
|
||||
/// - `0x09`: USB product name (null-terminated)
|
||||
/// - `0x0D`: LED order (RS-Key extension)
|
||||
/// - `0x0B`: Enabled USB interfaces (CCID bit is always forced on for safety)
|
||||
///
|
||||
/// # Returns
|
||||
/// A success message string on `SW 9000`.
|
||||
///
|
||||
/// # Errors
|
||||
/// - `PFError::Io` if VID/PID are not valid hex strings
|
||||
/// - `PFError::Device` if the WRITE APDU fails or returns a non-success status
|
||||
/// - `PFError::Io` if the product name exceeds 32 bytes
|
||||
pub fn write_config(config: AppConfigInput) -> Result<String, PFError> {
|
||||
log::info!("Writing configuration to device");
|
||||
log::debug!("Config input: {:?}", config);
|
||||
@@ -415,6 +620,21 @@ pub fn write_config(config: AppConfigInput) -> Result<String, PFError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reboots the device, optionally entering BOOTSEL (mass storage) mode for firmware updates.
|
||||
///
|
||||
/// Sends a REBOOT APDU: `80 1B [P1] 00 00` where:
|
||||
/// - `P1 = 0x00` (`RebootParam::Normal`): Reboots into normal FIDO mode
|
||||
/// - `P1 = 0x01` (`RebootParam::Bootsel`): Reboots into BOOTSEL/UF2 bootloader mode
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `to_bootsel` - If `true`, device enters UF2 bootloader mode for firmware flashing.
|
||||
/// If `false`, device performs a normal reboot into FIDO mode.
|
||||
///
|
||||
/// # Returns
|
||||
/// A confirmation string if the reboot command was accepted.
|
||||
///
|
||||
/// # Errors
|
||||
/// - `PFError::Device` if the APDU fails or returns a non-success status
|
||||
pub fn reboot_device(to_bootsel: bool) -> Result<String, PFError> {
|
||||
let (card, _, _) = connect_and_select()?;
|
||||
|
||||
@@ -442,7 +662,27 @@ pub fn reboot_device(to_bootsel: bool) -> Result<String, PFError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// UNSTABLE! (WIP)
|
||||
/// Enables or disables secure boot on the device. **UNSTABLE — work in progress.**
|
||||
///
|
||||
/// Sends a SECURE APDU: `80 1D 00 [LockBool] 00` where:
|
||||
/// - `LockBool = 0x01`: Enable and lock secure boot (irreversible on some firmware)
|
||||
/// - `LockBool = 0x00`: Disable secure boot
|
||||
///
|
||||
/// Uses pico-fido instruction `INS_SECURE` (0x1D). RS-Key uses `INS_OTP_LOCK` (0x1B)
|
||||
/// for OTP fuse locking, which is a different operation.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `lock` - If `true`, enables secure boot with lock (may be irreversible).
|
||||
///
|
||||
/// # Returns
|
||||
/// A confirmation string if the secure boot command was accepted.
|
||||
///
|
||||
/// # Errors
|
||||
/// - `PFError::Device` if the APDU fails or returns a non-success status
|
||||
///
|
||||
/// # Warning
|
||||
/// This function is unstable and may change. Locking secure boot can permanently
|
||||
/// prevent firmware downgrades. Use with caution.
|
||||
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
|
||||
let (card, _, _) = connect_and_select()?;
|
||||
|
||||
@@ -470,6 +710,22 @@ pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
|
||||
|
||||
// --- Vendor/LED Applet (RS-Key) ---
|
||||
|
||||
/// Establishes a PC/SC connection and selects a specific vendor applet by AID.
|
||||
///
|
||||
/// Unlike [`connect_and_select`] which selects the Rescue Applet, this function
|
||||
/// selects an arbitrary applet (e.g., LED applet `F0 00 00 00 01` or Management applet).
|
||||
/// Sends a SELECT APDU: `00 A4 04 00 [Len] [AID] 00`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `aid` - The Application Identifier of the target applet (e.g., `VENDOR_LED_AID`, `MANAGEMENT_AID`)
|
||||
///
|
||||
/// # Returns
|
||||
/// An active `pcsc::Card` handle ready for APDU exchange with the selected applet.
|
||||
///
|
||||
/// # Errors
|
||||
/// - `PFError::NoDevice` if no smart card reader is found
|
||||
/// - `PFError::Pcsc` if the PC/SC context cannot be established
|
||||
/// - `PFError::Device` if the applet is not found (AID not recognized by firmware)
|
||||
fn connect_and_select_aid(aid: &[u8]) -> Result<pcsc::Card, PFError> {
|
||||
let ctx = Context::establish(Scope::User).map_err(|e| {
|
||||
log::error!("Failed to establish PCSC context: {}", e);
|
||||
|
||||
+34
-13
@@ -1,12 +1,23 @@
|
||||
//! Shared types for device communication.
|
||||
//!
|
||||
//! Organized into three groups:
|
||||
//! - Application-level types: device info, config, and status used across both protocols
|
||||
//! - Rescue (PC/SC) types: LED and USB applet configuration read/written over PC/SC
|
||||
//! - FIDO2 types: credential and authenticator info from CTAP2
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
// ── Application-level types ─────────────────────────────────────────────────
|
||||
|
||||
/// Internal application state holding device info for the current session.
|
||||
struct PForgeState {
|
||||
device_info: DeviceInfo,
|
||||
}
|
||||
|
||||
/// Basic device identity and flash usage reported by the firmware.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeviceInfo {
|
||||
@@ -16,6 +27,7 @@ pub struct DeviceInfo {
|
||||
pub firmware_version: String,
|
||||
}
|
||||
|
||||
/// Full device configuration (USB descriptors, LED, touch, crypto options).
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppConfig {
|
||||
@@ -39,6 +51,7 @@ pub struct AppConfig {
|
||||
pub enabled_usb_itf: Option<u8>,
|
||||
}
|
||||
|
||||
/// Partial config update; `None` fields are left unchanged on the device.
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppConfigInput {
|
||||
@@ -58,6 +71,7 @@ pub struct AppConfigInput {
|
||||
pub enabled_usb_itf: Option<u8>,
|
||||
}
|
||||
|
||||
/// Aggregated snapshot of device info, config, and security state.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullDeviceStatus {
|
||||
@@ -69,6 +83,7 @@ pub struct FullDeviceStatus {
|
||||
pub firmware_type: FirmwareType,
|
||||
}
|
||||
|
||||
/// Protocol channel used to communicate with the device.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum DeviceMethod {
|
||||
#[serde(rename = "FIDO")]
|
||||
@@ -76,8 +91,8 @@ pub enum DeviceMethod {
|
||||
Rescue,
|
||||
}
|
||||
|
||||
/// Represents the recognized firmware variants running on the connected hardware token.
|
||||
/// Used extensively to gate UI features, connection methods, and compatibility checks.
|
||||
/// Recognized firmware variants. Gates UI features, connection methods, and
|
||||
/// compatibility checks throughout the application.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
pub enum FirmwareType {
|
||||
PicoFido,
|
||||
@@ -96,31 +111,29 @@ impl fmt::Display for FirmwareType {
|
||||
}
|
||||
}
|
||||
|
||||
/// The globally unique Authenticator Attestation GUID (AAGUID) assigned to RS-Key hardware.
|
||||
pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7";
|
||||
/// The globally unique Authenticator Attestation GUID (AAGUID) assigned to Pico-Fido hardware.
|
||||
pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145";
|
||||
// ── Rescue (PC/SC) types ────────────────────────────────────────────────────
|
||||
|
||||
/// Aggregates the LED status configurations read from the RS-Key Vendor/LED applet.
|
||||
/// Contains the global steady flag and a fixed array of `(color_code, brightness)` pairs
|
||||
/// mapped chronologically to device statuses: [Idle, Processing, Touch, Boot].
|
||||
/// LED status configuration read from the Vendor/LED applet.
|
||||
/// `statuses` is a fixed array of `(color, brightness)` pairs indexed by
|
||||
/// device status: Idle, Processing, Touch, Boot.
|
||||
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LedStatusConfig {
|
||||
pub steady: bool,
|
||||
pub statuses: [(u8, u8); 4],
|
||||
}
|
||||
|
||||
/// Encapsulates the bitmasks defining USB application endpoints on the device.
|
||||
/// The `usb_supported` mask indicates which applets the firmware is capable of running,
|
||||
/// while `usb_enabled` reflects the active endpoints the device will enumerate on next boot.
|
||||
/// USB application endpoint bitmasks from the Management applet.
|
||||
/// `usb_supported` lists applets the firmware can run;
|
||||
/// `usb_enabled` lists those active on next boot.
|
||||
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct ManagementAppConfig {
|
||||
pub usb_supported: u16,
|
||||
pub usb_enabled: u16,
|
||||
}
|
||||
|
||||
// Fido stuff:
|
||||
// ── FIDO2 types ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Authenticator metadata from CTAP2 GetInfo.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FidoDeviceInfo {
|
||||
@@ -145,6 +158,7 @@ pub struct FidoDeviceInfo {
|
||||
pub max_cred_blob_length: Option<i128>,
|
||||
}
|
||||
|
||||
/// A single FIDO2 credential stored on the device.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StoredCredential {
|
||||
@@ -155,3 +169,10 @@ pub struct StoredCredential {
|
||||
pub user_id: String,
|
||||
pub credential_id: String,
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// AAGUID assigned to RS-Key hardware.
|
||||
pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7";
|
||||
/// AAGUID assigned to Pico-Fido hardware.
|
||||
pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145";
|
||||
|
||||
Reference in New Issue
Block a user