From e3dc8fc1c6f6fe8b917af4edfdd3e28332cbc746 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 21:53:00 +0530 Subject: [PATCH] docs: add documentation to src/device/module --- src/device/fido/constants.rs | 712 +++++++++++++++++++++++++-------- src/device/fido/hid.rs | 290 +++++++++++++- src/device/fido/mod.rs | 56 +++ src/device/io.rs | 94 +++-- src/device/mod.rs | 41 ++ src/device/rescue/constants.rs | 475 ++++++++++++++++++++-- src/device/rescue/mod.rs | 264 +++++++++++- src/device/types.rs | 47 ++- 8 files changed, 1739 insertions(+), 240 deletions(-) diff --git a/src/device/fido/constants.rs b/src/device/fido/constants.rs index d2797f3..bf34356 100644 --- a/src/device/fido/constants.rs +++ b/src/device/fido/constants.rs @@ -1,375 +1,398 @@ -//! Constants, enums, bitflags and data structures for FIDO2 protocol for pico-fido firmware. +//! CTAP2 / FIDO2 protocol constants for pico-fido and RS-Key firmware. +//! +//! This file is the single source of truth for every byte value, error code, +//! and CBOR map key used by this codebase. Values are organized into three +//! categories: +//! +//! 1. **CTAP2 standard** — defined by the [FIDO CTAP2 spec], used by any +//! CTAP2-compliant authenticator. +//! 2. **Pico-fido vendor extensions** — custom commands/IDs for the +//! [pico-fido] firmware (vendor commands `0xC1`/`0xC2`, 64-bit config IDs). +//! 3. **RS-Key extensions** — additions specific to [RS-Key] firmware +//! (rescue applet AIDs, phy record tags, CTAPHID `0x41` vendor command). +//! +//! All enums use `#[repr(u8)]` or `#[repr(u64)]` so their numeric values +//! match the wire format exactly. +//! +//! # Reference +//! +//! - CTAP2 values: [CTAP2 v2.3 spec §8.1](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html) +//! - Pico-fido vendor commands: [pico-fido source](https://github.com/polhenarejos/pico-fido) +//! - RS-Key protocol: [RS-Key Host Protocol Docs](https://themaxmur.github.io/RS-Key/develop/protocol.html) +//! +//! [FIDO CTAP2 spec]: 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 #![allow(unused)] use std::fmt; +// ══════════════════════════════════════════════════════════════════════════════ +// CTAP2 STANDARD — FIDO Alliance specification §8.1 +// ══════════════════════════════════════════════════════════════════════════════ + +// ── CTAP2 command codes (§8.1) ────────────────────────────────────────────── + +/// CTAP2 CBOR command codes (CTAP2 spec §8.1). +/// +/// These are the opcodes sent as the first byte of a `CTAPHID_CBOR` payload. +/// The authenticator dispatches to the corresponding handler based on this byte. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CtapCommand { + /// Create a new credential (§11.5.1). MakeCredential = 0x01, + /// Generate an authentication assertion (§11.5.2). GetAssertion = 0x02, + /// Return authenticator metadata (§11.5.3). GetInfo = 0x04, + /// PIN/UV token management (§11.5.4). ClientPin = 0x06, + /// Factory-reset all credentials and PIN (§11.5.5). Reset = 0x07, + /// Get the next assertion when multiple credentials match (§11.5.6). GetNextAssertion = 0x08, + /// Credential management operations (§11.5.8). CredentialMgmt = 0x0A, + /// Put the authenticator into a discoverable state (§11.5.7). Selection = 0x0B, + /// Read/write large blob storage (§11.5.9). LargeBlobs = 0x0C, + /// Authenticator configuration (enterprise attestation, min PIN, etc.) (§11.5.10). Config = 0x0D, } +/// CTAP1/U2F command codes (legacy protocol, U2F Raw Messages spec). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum U2fCommand { + /// U2F Register command. Register = 0x01, + /// U2F Authenticate command. Authenticate = 0x02, + /// U2F Version inquiry. Version = 0x03, } -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorCommand { - Backup = 0x01, - ManageSecurityEnvironment = 0x02, - Unlock = 0x03, - EnterpriseAttestation = 0x04, - PhysicalOptions = 0x05, - Memory = 0x06, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AuthenticateControl { - EnforceUserPresence = 0x03, - CheckOnly = 0x07, -} +// ── CBOR map key enums (§11.5.x) ─────────────────────────────────────────── +// +// Each CTAP2 command encodes its parameters as a CBOR map with integer keys. +// These enums map human-readable names to the wire-format key bytes. +/// CBOR map keys for `authenticatorClientPIN` sub-commands (§11.5.4). +/// +/// The `subCommand` field selects which PIN operation to perform. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientPinSubCommand { + /// Get remaining PIN attempts. GetPinRetries = 0x01, + /// Get ECDH key agreement public key. GetKeyAgreement = 0x02, + /// Set a new PIN (first-time setup). SetPin = 0x03, + /// Change an existing PIN. ChangePin = 0x04, + /// Get a PIN token for permission-gated operations. GetPinToken = 0x05, + /// Get a UV auth token using biometric/other UV (§11.5.4.1). GetPinUvAuthTokenUsingUvWithPermissions = 0x06, + /// Get remaining UV attempts. GetUvRetries = 0x07, - GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // TODO: per fido spec, this should be 0x08? Needs to confirm and fix the firmware if true. + /// Get a PIN auth token with specific permissions. + /// + /// **Pico-fido note:** The CTAP2 spec defines this as `0x08`, but pico-fido + /// firmware uses `0x09`. This discrepancy is documented here for clarity. + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } +/// CBOR map keys for `authenticatorMakeCredential` (§11.5.1). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MakeCredentialParam { + /// SHA-256 hash of the client data (required). ClientDataHash = 0x01, + /// Relying party object `{ id, name, icon }` (required). Rp = 0x02, + /// User object `{ id, name, displayName, icon }` (required). User = 0x03, + /// Supported credential algorithms, preferred-first (required). PubKeyCredParams = 0x04, + /// Credentials to exclude (prevents duplication). ExcludeList = 0x05, + /// Extension inputs. Extensions = 0x06, + /// Options like `rk`, `up`, `uv`. Options = 0x07, + /// HMAC from PIN/UV token for user verification. PinUvAuthParam = 0x08, + /// PIN/UV protocol version (currently 1). PinUvAuthProtocol = 0x09, + /// Enterprise attestation mode (0=off, 1=permissive, 2=strict). EnterpriseAttestation = 0x0A, } +/// CBOR map keys for `authenticatorGetAssertion` (§11.5.2). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GetAssertionParam { + /// Relying party identifier (required). RpId = 0x01, + /// SHA-256 hash of the client data (required). ClientDataHash = 0x02, + /// Allowed credentials; if present, only these may be used. AllowList = 0x03, + /// Extension inputs. Extensions = 0x04, + /// Options like `up`, `uv`, `pin`. Options = 0x05, + /// HMAC from PIN/UV token. PinUvAuthParam = 0x06, + /// PIN/UV protocol version. PinUvAuthProtocol = 0x07, } +/// CBOR map keys for `authenticatorClientPIN` request body (§11.5.4). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientPinParam { + /// PIN/UV protocol version (must be 1). PinUvAuthProtocol = 0x01, + /// Sub-command to execute (see [`ClientPinSubCommand`]). SubCommand = 0x02, + /// Platform's ECDH public key (COSE_Key). KeyAgreement = 0x03, + /// HMAC of the encrypted PIN or client data. PinUvAuthParam = 0x04, + /// AES-256-CBC encrypted new PIN. NewPinEnc = 0x05, + /// AES-256-CBC encrypted first 16 bytes of PIN hash. PinHashEnc = 0x06, + /// Permission bits for `getPinUvAuthTokenUsingPinWithPermissions`. Permissions = 0x09, + /// RP ID scope for the requested permissions. PermissionsRpId = 0x0A, } +/// CBOR map keys for `authenticatorClientPIN` response body (§11.5.4). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientPinResponseParam { + /// Authenticator's ECDH public key (COSE_Key). KeyAgreement = 0x01, + /// Encrypted PIN/UV auth token. PinToken = 0x02, + /// Remaining PIN attempts. PinRetries = 0x03, + /// Continuation message for large payloads. NextMsg = 0x04, + /// Remaining UV attempts. UvRetries = 0x05, } +/// CBOR map keys for `authenticatorConfig` (§11.5.10). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigParam { + /// Config sub-command (see [`ConfigSubCommand`]). SubCommand = 0x01, + /// Sub-command parameters (CBOR map). SubCommandParams = 0x02, + /// PIN/UV protocol version. PinUvAuthProtocol = 0x03, + /// HMAC of the sub-command parameters. PinUvAuthParam = 0x04, } +/// Sub-commands for `authenticatorConfig` (§11.5.10). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigSubCommand { + /// Enable enterprise attestation for this authenticator. EnableEnterpriseAttestation = 0x01, + /// Toggle "always UV" policy (requires re-setting PIN). ToggleAlwaysUv = 0x02, + /// Set the minimum PIN length requirement. SetMinPinLength = 0x03, + /// Vendor-defined prototype config command (pico-fido/RS-Key extension). VendorPrototype = 0xFF, } -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorParam { - VendorCommand = 0x01, - VendorSubParams = 0x02, - PinUvAuthProtocol = 0x03, - PinUvAuthParam = 0x04, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorSubParam { - VendorParam = 0x01, - CoseKey = 0x02, - VendorParamInt = 0x03, - VendorParamText = 0x04, -} - +/// Sub-commands for `authenticatorCredentialManagement` (§11.5.8). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialMgmtSubCommand { + /// Get total credential/RP counts and remaining space. GetCredsMetadata = 0x01, + /// Begin enumerating Relying Parties. EnumerateRpsBegin = 0x02, + /// Get the next RP in the enumeration. EnumerateRpsGetNextRp = 0x03, + /// Begin enumerating credentials for a given RP. EnumerateCredentialsBegin = 0x04, + /// Get the next credential in the enumeration. EnumerateCredentialsGetNextCredential = 0x05, + /// Delete a stored credential. DeleteCredential = 0x06, + /// Update user information for a credential. UpdateUserInformation = 0x07, } +/// CBOR map keys for `authenticatorCredentialManagement` requests (§11.5.8). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialMgmtParam { + /// Sub-command to execute (see [`CredentialMgmtSubCommand`]). SubCommand = 0x01, + /// Sub-command parameters (CBOR map). SubCommandParams = 0x02, + /// PIN/UV protocol version. PinUvAuthProtocol = 0x03, + /// HMAC for authentication. PinUvAuthParam = 0x04, } +/// CBOR map keys for `authenticatorCredentialManagement` responses (§11.5.8). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialMgmtResponseParam { + /// Relying party object. Rp = 0x03, + /// SHA-256 hash of the RP ID. RpIdHash = 0x04, + /// Total number of RPs stored. TotalRps = 0x05, + /// User object. User = 0x06, + /// Credential descriptor. CredentialId = 0x07, + /// Credential public key (COSE_Key). PublicKey = 0x08, + /// Total credentials for the current RP. TotalCredentials = 0x09, } +/// Sub-command parameters for `authenticatorConfig` (§11.5.10). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigSubCommandParam { + /// New minimum PIN length. NewMinPinLength = 0x01, + /// RP IDs allowed to read the minimum PIN length. MinPinLengthRPIDs = 0x02, + /// Force PIN change on next use. ForceChangePin = 0x03, } -#[repr(u64)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorConfigCommand { - AuthEncryptionEnable = 0x03e43f56b34285e2, - AuthEncryptionDisable = 0x1831a40f04a25ed9, - EnterpriseAttestationUpload = 0x66f2a674c29a8dcf, - PinComplexityPolicy = 0x6c07d70fe96c3897, - PhysicalVidPid = 0x6fcb19b0cbe3acfa, - PhysicalLedBrightness = 0x76a85945985d02fd, - PhysicalLedGpio = 0x7b392a394de9f948, - PhysicalOptions = 0x269f3b09eceb805f, -} - -impl VendorConfigCommand { - pub fn from_u64(val: u64) -> Option { - match val { - 0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable), - 0x1831a40f04a25ed9 => Some(Self::AuthEncryptionDisable), - 0x66f2a674c29a8dcf => Some(Self::EnterpriseAttestationUpload), - 0x6c07d70fe96c3897 => Some(Self::PinComplexityPolicy), - 0x6fcb19b0cbe3acfa => Some(Self::PhysicalVidPid), - 0x76a85945985d02fd => Some(Self::PhysicalLedBrightness), - 0x7b392a394de9f948 => Some(Self::PhysicalLedGpio), - 0x269f3b09eceb805f => Some(Self::PhysicalOptions), - _ => None, - } - } -} - -#[repr(u64)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FidoCertification { - AuthEncryption = 0x03E43F56B34285E2, - AuthEncryptionLock = 0x1831A40F04A25ED9, - EnterpriseAttestation = 0x66F2A674C29A8DCF, - PinComplexity = 0x6C07D70FE96C3897, - PhysicalVidPid = 0x6FCB19B0CBE3ACFA, - LedBrightness = 0x76A85945985D02FD, - LedGpio = 0x7B392A394DE9F948, - PhysicalOptions = 0x269F3B09ECEB805F, -} - -impl FidoCertification { - pub fn from_u64(val: u64) -> Option { - match val { - 0x03E43F56B34285E2 => Some(Self::AuthEncryption), - 0x1831A40F04A25ED9 => Some(Self::AuthEncryptionLock), - 0x66F2A674C29A8DCF => Some(Self::EnterpriseAttestation), - 0x6C07D70FE96C3897 => Some(Self::PinComplexity), - 0x6FCB19B0CBE3ACFA => Some(Self::PhysicalVidPid), - 0x76A85945985D02FD => Some(Self::LedBrightness), - 0x7B392A394DE9F948 => Some(Self::LedGpio), - 0x269F3B09ECEB805F => Some(Self::PhysicalOptions), - _ => None, - } - } - - pub fn from_str(val: &str) -> Option { - let val = val.strip_prefix("0x").unwrap_or(val); - u64::from_str_radix(val, 16).ok().and_then(Self::from_u64) - } -} - -impl fmt::Display for FidoCertification { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AuthEncryption => write!(f, "Auth Encryption"), - Self::AuthEncryptionLock => write!(f, "Auth Encryption (Lock)"), - Self::EnterpriseAttestation => write!(f, "Enterprise Attestation"), - Self::PinComplexity => write!(f, "PIN Complexity"), - Self::PhysicalVidPid => write!(f, "Physical VID/PID"), - Self::LedBrightness => write!(f, "LED Brightness"), - Self::LedGpio => write!(f, "LED GPIO"), - Self::PhysicalOptions => write!(f, "Physical Options"), - } - } -} - -impl fmt::Display for VendorConfigCommand { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AuthEncryptionEnable => write!(f, "AuthEncryptionEnable"), - Self::AuthEncryptionDisable => write!(f, "AuthEncryptionDisable"), - Self::EnterpriseAttestationUpload => write!(f, "EnterpriseAttestationUpload"), - Self::PinComplexityPolicy => write!(f, "PinComplexityPolicy"), - Self::PhysicalVidPid => write!(f, "PhysicalVidPid"), - Self::PhysicalLedBrightness => write!(f, "PhysicalLedBrightness"), - Self::PhysicalLedGpio => write!(f, "PhysicalLedGpio"), - Self::PhysicalOptions => write!(f, "PhysicalOptions"), - } - } -} - +/// Control byte for U2F Authenticate (check-only vs. enforce presence). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BackupSubCommand { - GetEncryptedBackup = 0x01, - RestoreEncryptedBackup = 0x02, +pub enum AuthenticateControl { + /// Require user presence test. + EnforceUserPresence = 0x03, + /// Check if key handle is valid (no user interaction). + CheckOnly = 0x07, } -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MseSubCommand { - KeyAgreement = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EnterpriseAttestationSubCommand { - GenerateCsr = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PhysicalOptionsSubCommand { - GetOptions = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MemorySubCommand { - GetStats = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MemoryResponseKey { - FreeSpace = 0x01, - UsedSpace = 0x02, - TotalSpace = 0x03, - NumFiles = 0x04, - FlashSize = 0x05, -} +// ── Bitflags (§11.3.2, §11.5.x) ──────────────────────────────────────────── +/// Permission bits for `getPinUvAuthTokenUsingPinWithPermissions` (§11.5.4.1). +/// +/// The platform requests specific permissions when obtaining a PIN/UV token. +/// The authenticator gates access to sensitive operations behind these flags. bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PinUvAuthTokenPermissions: u8 { + /// Permission to create credentials (MakeCredential). const MAKE_CREDENTIAL = 0x01; + /// Permission to generate assertions (GetAssertion). const GET_ASSERTION = 0x02; + /// Permission to enumerate/delete credentials. const CREDENTIAL_MANAGEMENT = 0x04; + /// Permission for biometric enrollment. const BIO_ENROLLMENT = 0x08; + /// Permission to write to large blob storage. const LARGE_BLOB_WRITE = 0x10; + /// Permission to modify authenticator config (enterprise attestation, min PIN). const AUTHENTICATOR_CONFIG = 0x20; + /// Read-only credential management (no delete/update). const PER_CREDENTIAL_MGMT_READONLY = 0x40; } } +/// Flags byte in CTAP2 response messages (§11.3.2). +/// +/// Indicates the authenticator's state after processing a command. bitflags::bitflags! { pub struct AuthenticatorFlags: u8 { + /// User presence was tested and confirmed. const USER_PRESENT = 0x01; + /// User verification (biometric or PIN) was performed. const USER_VERIFIED = 0x04; + /// Response includes attested credential data. const ATTESTED_CREDENTIAL_DATA = 0x40; + /// Response includes extension output data. const EXTENSION_DATA = 0x80; } } +/// Options that can be passed in `MakeCredential` or `GetAssertion` (§11.5.1/2). bitflags::bitflags! { pub struct AuthenticatorOptions: u8 { + /// Request enterprise attestation (MakeCredential only). const ENTERPRISE_ATTESTATION = 0x01; + /// Require user verification (PIN or biometric). const USER_VERIFICATION = 0x02; } } +// ── COSE key types (RFC 8152) ─────────────────────────────────────────────── + +/// COSE algorithm identifiers (IANA COSE Algorithms registry). +/// +/// Used in `pubKeyCredParams` to specify which signature algorithms +/// the platform supports. The authenticator picks the first match. #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseAlgorithm { + /// ECDSA with P-256 and SHA-256 (most common for WebAuthn). ES256 = -7, + /// EdDSA with Ed25519. EdDSA = -8, + /// ECDSA with P-256 (alternate ID, same as ES256). ESP256 = -9, + /// EdDSA with Ed25519 (alternate ID). Ed25519 = -19, + /// ECDH-ES with HKDF-256 key agreement. EcdhEsHkdf256 = -25, + /// ECDSA with P-384 and SHA-384. ES384 = -35, + /// ECDSA with P-521 and SHA-512. ES512 = -36, + /// ECDSA with secp256k1 and SHA-256 (Bitcoin curve). ES256K = -47, + /// ECDSA with P-384 (alternate ID). ESP384 = -51, + /// ECDSA with P-521 (alternate ID). ESP512 = -52, + /// EdDSA with Ed448. Ed448 = -53, + /// RSASSA-PKCS1-v1_5 with SHA-256. RS256 = -257, + /// RSASSA-PKCS1-v1_5 with SHA-384. RS384 = -258, + /// RSASSA-PKCS1-v1_5 with SHA-512. RS512 = -259, + /// ECDSA with brainpool256r1 and SHA-256. ESB256 = -265, + /// ECDSA with brainpool384r1 and SHA-384. ESB384 = -267, + /// ECDSA with brainpool512r1 and SHA-512. ESB512 = -268, } impl CoseAlgorithm { + /// Convert a raw i128 (from CBOR) to a [`CoseAlgorithm`]. pub fn from_i128(val: i128) -> Option { match val as i32 { -7 => Some(Self::ES256), @@ -418,99 +441,456 @@ impl fmt::Display for CoseAlgorithm { } } +/// COSE elliptic curve identifiers (RFC 8152 §13.1.1). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseCurve { + /// NIST P-256 (secp256r1, prime256v1). P256 = 1, + /// NIST P-384 (secp384r1). P384 = 2, + /// NIST P-521 (secp521r1). P521 = 3, + /// X25519 for key agreement. X25519 = 4, + /// X448 for key agreement. X448 = 5, + /// Ed25519 for signing. Ed25519 = 6, + /// Ed448 for signing. Ed448 = 7, + /// secp256k1 (Bitcoin/Ethereum curve). P256K1 = 8, + /// BrainpoolP256R1. BP256R1 = 9, + /// BrainpoolP384R1. BP384R1 = 10, + /// BrainpoolP512R1. BP512R1 = 11, } +/// COSE key parameter identifiers (RFC 8152 §7.1). #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseKeyParam { + /// Key type (OKP, EC2, RSA, etc.). Kty = 1, + /// Key identifier. Kid = 2, + /// Algorithm identifier. Alg = 3, + /// Key operations (sign, verify, encrypt, etc.). KeyOps = 4, + /// Base IV for symmetric operations. BaseIV = 5, + /// Elliptic curve identifier. Crv = -1, + /// X coordinate (EC2) or public key bytes (OKP). X = -2, + /// Y coordinate (EC2). Y = -3, + /// Private key (EC2 or OKP). D = -4, } +// ── CTAP2 errors (§8.2) ──────────────────────────────────────────────────── + +/// CTAP2 error codes (§8.2). +/// +/// Returned as the first byte of a `CTAPHID_CBOR` response when the +/// status code is non-zero. Negative status indicates an error. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Ctap2Error { + /// Operation completed successfully. Success = 0x00, + /// CBOR value has an unexpected type. CborUnexpectedType = 0x11, + /// CBOR structure is malformed. InvalidCbor = 0x12, + /// A required parameter is missing. MissingParameter = 0x14, + /// A limit has been exceeded (e.g., too many credentials). LimitExceeded = 0x15, + /// Internal fingerprint database is full. FpDatabaseFull = 0x17, + /// Large blob storage is full. LargeBlobStorageFull = 0x18, + /// Credential already exists (exclusion list match). CredentialExcluded = 0x19, + /// Operation is still processing. Processing = 0x21, + /// Credential ID is invalid or not found. InvalidCredential = 0x22, + /// User action (touch) is pending. UserActionPending = 0x23, + /// Another operation is in progress. OperationPending = 0x24, + /// No more operations to process. NoOperations = 0x25, + /// Algorithm not supported by the authenticator. UnsupportedAlgorithm = 0x26, + /// Operation was denied (user declined or policy). OperationDenied = 0x27, + /// Key store is full. KeyStoreFull = 0x28, + /// Option not recognized or not supported. UnsupportedOption = 0x2B, + /// Option value is invalid. InvalidOption = 0x2C, + /// Keepalive was cancelled. KeepaliveCancel = 0x2D, + /// No matching credentials found. NoCredentials = 0x2E, + /// User action timed out. UserActionTimeout = 0x2F, + /// Operation not allowed (e.g., reset not within power cycle). NotAllowed = 0x30, + /// PIN is invalid. PinInvalid = 0x31, + /// PIN is blocked (too many failed attempts). PinBlocked = 0x32, + /// PIN authentication token is invalid. PinAuthInvalid = 0x33, + /// PIN authentication is blocked. PinAuthBlocked = 0x34, + /// PIN has not been set. PinNotSet = 0x35, + /// PIN/UV auth token required but not provided. PuatRequired = 0x36, + /// PIN policy violation (e.g., min length not met). PinPolicyViolation = 0x37, + /// Request payload is too large. RequestTooLarge = 0x39, + /// Action timed out. ActionTimeout = 0x3A, + /// User presence (touch) required. UpRequired = 0x3B, + /// User verification is blocked. UvBlocked = 0x3C, + /// Cryptographic integrity check failed. IntegrityFailure = 0x3D, + /// Sub-command not recognized. InvalidSubcommand = 0x3E, + /// User verification is invalid. UvInvalid = 0x3F, + /// Requested permission not authorized. UnauthorizedPermission = 0x40, } +// ══════════════════════════════════════════════════════════════════════════════ +// PICO-FIDO VENDOR EXTENSIONS +// ══════════════════════════════════════════════════════════════════════════════ +// +// The following types are NOT part of the CTAP2 standard. They are custom +// extensions used by the pico-fido firmware (and RS-Key, which shares the +// same vendor command surface). + +/// Pico-fido vendor commands sent via `CTAPHID_MSG` (not CBOR). +/// +/// These are raw byte commands, not CTAP2-standard. Each sub-command +/// carries its own TLV or binary payload. RS-Key implements the same +/// command set for compatibility. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorCommand { + /// Encrypted backup / restore operations. + Backup = 0x01, + /// Manage security environment (key agreement). + ManageSecurityEnvironment = 0x02, + /// Unlock a locked device. + Unlock = 0x03, + /// Enterprise attestation CSR generation. + EnterpriseAttestation = 0x04, + /// Physical options (LED, power, etc.) — legacy TLV encoding. + PhysicalOptions = 0x05, + /// Flash memory statistics (free/used/total). + Memory = 0x06, +} + +/// Pico-fido vendor config command IDs (64-bit). +/// +/// These are sent via `authenticatorConfig` → `VendorPrototype` (0xFF) +/// sub-command. Each ID identifies a specific hardware configuration +/// operation (LED, VID/PID, encryption, etc.). +/// +/// RS-Key uses the same command IDs for compatibility. +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorConfigCommand { + /// Enable authenticated encryption for secure communication. + AuthEncryptionEnable = 0x03e43f56b34285e2, + /// Disable authenticated encryption. + AuthEncryptionDisable = 0x1831a40f04a25ed9, + /// Upload enterprise attestation certificate. + EnterpriseAttestationUpload = 0x66f2a674c29a8dcf, + /// Configure PIN complexity policy. + PinComplexityPolicy = 0x6c07d70fe96c3897, + /// Set USB Vendor ID and Product ID. + PhysicalVidPid = 0x6fcb19b0cbe3acfa, + /// Set LED brightness level. + PhysicalLedBrightness = 0x76a85945985d02fd, + /// Set LED GPIO pin assignment. + PhysicalLedGpio = 0x7b392a394de9f948, + /// Physical options bitmask (dimmable, power-reset, steady LED). + PhysicalOptions = 0x269f3b09eceb805f, +} + +impl VendorConfigCommand { + /// Convert a raw 64-bit value to a [`VendorConfigCommand`]. + pub fn from_u64(val: u64) -> Option { + match val { + 0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable), + 0x1831a40f04a25ed9 => Some(Self::AuthEncryptionDisable), + 0x66f2a674c29a8dcf => Some(Self::EnterpriseAttestationUpload), + 0x6c07d70fe96c3897 => Some(Self::PinComplexityPolicy), + 0x6fcb19b0cbe3acfa => Some(Self::PhysicalVidPid), + 0x76a85945985d02fd => Some(Self::PhysicalLedBrightness), + 0x7b392a394de9f948 => Some(Self::PhysicalLedGpio), + 0x269f3b09eceb805f => Some(Self::PhysicalOptions), + _ => None, + } + } +} + +/// Certification identifiers reported by pico-fido/RS-Key firmware. +/// +/// These share the same 64-bit IDs as [`VendorConfigCommand`] but are +/// used in the `GetInfo` certifications map to indicate which features +/// the device has been certified for. +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FidoCertification { + /// Authenticated encryption enabled and certified. + AuthEncryption = 0x03E43F56B34285E2, + /// Authenticated encryption locked (cannot be disabled). + AuthEncryptionLock = 0x1831A40F04A25ED9, + /// Enterprise attestation certified. + EnterpriseAttestation = 0x66F2A674C29A8DCF, + /// PIN complexity policy enforced. + PinComplexity = 0x6C07D70FE96C3897, + /// Physical VID/PID configuration certified. + PhysicalVidPid = 0x6FCB19B0CBE3ACFA, + /// LED brightness control certified. + LedBrightness = 0x76A85945985D02FD, + /// LED GPIO assignment certified. + LedGpio = 0x7B392A394DE9F948, + /// Physical options (dimmable, power-reset, steady) certified. + PhysicalOptions = 0x269F3B09ECEB805F, +} + +impl FidoCertification { + /// Convert a raw 64-bit value to a [`FidoCertification`]. + pub fn from_u64(val: u64) -> Option { + match val { + 0x03E43F56B34285E2 => Some(Self::AuthEncryption), + 0x1831A40F04A25ED9 => Some(Self::AuthEncryptionLock), + 0x66F2A674C29A8DCF => Some(Self::EnterpriseAttestation), + 0x6C07D70FE96C3897 => Some(Self::PinComplexity), + 0x6FCB19B0CBE3ACFA => Some(Self::PhysicalVidPid), + 0x76A85945985D02FD => Some(Self::LedBrightness), + 0x7B392A394DE9F948 => Some(Self::LedGpio), + 0x269F3B09ECEB805F => Some(Self::PhysicalOptions), + _ => None, + } + } + + /// Parse a hex string (with or without `0x` prefix) to a [`FidoCertification`]. + pub fn from_str(val: &str) -> Option { + let val = val.strip_prefix("0x").unwrap_or(val); + u64::from_str_radix(val, 16).ok().and_then(Self::from_u64) + } +} + +impl fmt::Display for FidoCertification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthEncryption => write!(f, "Auth Encryption"), + Self::AuthEncryptionLock => write!(f, "Auth Encryption (Lock)"), + Self::EnterpriseAttestation => write!(f, "Enterprise Attestation"), + Self::PinComplexity => write!(f, "PIN Complexity"), + Self::PhysicalVidPid => write!(f, "Physical VID/PID"), + Self::LedBrightness => write!(f, "LED Brightness"), + Self::LedGpio => write!(f, "LED GPIO"), + Self::PhysicalOptions => write!(f, "Physical Options"), + } + } +} + +impl fmt::Display for VendorConfigCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthEncryptionEnable => write!(f, "AuthEncryptionEnable"), + Self::AuthEncryptionDisable => write!(f, "AuthEncryptionDisable"), + Self::EnterpriseAttestationUpload => write!(f, "EnterpriseAttestationUpload"), + Self::PinComplexityPolicy => write!(f, "PinComplexityPolicy"), + Self::PhysicalVidPid => write!(f, "PhysicalVidPid"), + Self::PhysicalLedBrightness => write!(f, "PhysicalLedBrightness"), + Self::PhysicalLedGpio => write!(f, "PhysicalLedGpio"), + Self::PhysicalOptions => write!(f, "PhysicalOptions"), + } + } +} + +// ── Vendor sub-commands ───────────────────────────────────────────────────── + +/// CBOR map keys for pico-fido vendor prototype commands. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorParam { + /// Vendor command identifier (64-bit). + VendorCommand = 0x01, + /// Nested vendor sub-parameters. + VendorSubParams = 0x02, + /// PIN/UV protocol version. + PinUvAuthProtocol = 0x03, + /// HMAC for authentication. + PinUvAuthParam = 0x04, +} + +/// Sub-parameter keys inside vendor prototype payloads. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorSubParam { + /// Raw vendor parameter value. + VendorParam = 0x01, + /// COSE-encoded public key. + CoseKey = 0x02, + /// Integer vendor parameter. + VendorParamInt = 0x03, + /// Text vendor parameter. + VendorParamText = 0x04, +} + +/// Backup sub-commands (encrypted backup/restore). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BackupSubCommand { + /// Export an encrypted backup blob. + GetEncryptedBackup = 0x01, + /// Import and restore an encrypted backup. + RestoreEncryptedBackup = 0x02, +} + +/// Manage Security Environment sub-commands. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MseSubCommand { + /// Perform ECDH key agreement for secure channel setup. + KeyAgreement = 0x01, +} + +/// Enterprise attestation sub-commands. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnterpriseAttestationSubCommand { + /// Generate a Certificate Signing Request. + GenerateCsr = 0x01, +} + +/// Physical options sub-commands (legacy vendor command 0x05). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PhysicalOptionsSubCommand { + /// Read the current physical options bitmask. + GetOptions = 0x01, +} + +/// Memory sub-commands (vendor command 0x06). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemorySubCommand { + /// Get flash memory usage statistics. + GetStats = 0x01, +} + +/// Response keys for `Memory::GetStats`. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryResponseKey { + /// Free space in bytes. + FreeSpace = 0x01, + /// Used space in bytes. + UsedSpace = 0x02, + /// Total flash capacity in bytes. + TotalSpace = 0x03, + /// Number of stored files/credentials. + NumFiles = 0x04, + /// Raw flash chip size. + FlashSize = 0x05, +} + +// ══════════════════════════════════════════════════════════════════════════════ +// RS-KEY SPECIFIC EXTENSIONS +// ══════════════════════════════════════════════════════════════════════════════ +// +// The following constants are specific to RS-Key firmware. Some overlap with +// pico-fido (RS-Key is a rust rewrite of pico-fido), but the Rescue applet AIDs and CTAPHID 0x41 +// vendor command are RS-Key additions. + +/// Vendor CBOR command opcode (pico-fido/RS-Key extension). +/// +/// Sent as the first byte of a `CTAPHID_CBOR` payload to invoke +/// vendor-specific CBOR-encoded commands. pub const CTAP_VENDOR_CBOR_CMD: u8 = 0xC1; + +/// Vendor config command opcode (pico-fido/RS-Key extension). +/// +/// Sent as the first byte of a `CTAPHID_CBOR` payload for +/// `authenticatorConfig` vendor prototype commands. pub const CTAP_VENDOR_CONFIG_CMD: u8 = 0xC2; +/// RS-Key CTAPHID vendor command (0x41). +/// +/// Carries CBOR-encoded sub-commands for seed backup, attestation, +/// and audit operations. This is RS-Key specific and not part of pico-fido. +/// +/// See [RS-Key protocol §9](https://themaxmur.github.io/RS-Key/develop/) for details. +pub const RSKEY_CTAPHID_VENDOR_CMD: u8 = 0x41; + +// ══════════════════════════════════════════════════════════════════════════════ +// SHARED PROTOCOL CONSTANTS +// ══════════════════════════════════════════════════════════════════════════════ + +/// Size of the relying party identifier hash (SHA-256). pub const CTAP_APPID_SIZE: usize = 32; +/// Size of the challenge hash (SHA-256). pub const CTAP_CHAL_SIZE: usize = 32; +/// Size of an EC public key coordinate (P-256). pub const CTAP_EC_KEY_SIZE: usize = 32; +/// Size of an uncompressed EC public key point (0x04 + X + Y). pub const CTAP_EC_POINT_SIZE: usize = 65; +/// Maximum key handle size stored on the device. pub const CTAP_MAX_KH_SIZE: usize = 128; +/// Default key handle length for credential serialization. pub const KEY_HANDLE_LEN: usize = 64; +/// Maximum EC signature size (DER-encoded). pub const CTAP_MAX_EC_SIG_SIZE: usize = 72; +/// Size of the transaction counter field. pub const CTAP_CTR_SIZE: usize = 4; +/// Maximum number of PIN entry attempts before lockout. pub const MAX_PIN_RETRIES: u8 = 8; +/// Maximum credentials returned in a single `GetAssertion` response. pub const MAX_CREDENTIAL_COUNT_IN_LIST: usize = 16; +/// Maximum credential ID length in bytes. pub const MAX_CRED_ID_LENGTH: usize = 1024; +/// Maximum number of discoverable (resident) credentials. pub const MAX_RESIDENT_CREDENTIALS: usize = 256; +/// Maximum length of a credential blob extension. pub const MAX_CREDBLOB_LENGTH: usize = 128; +/// Maximum CTAP2 message size in bytes. pub const MAX_MSG_SIZE: usize = 1024; +/// Maximum fragment size (message size minus CTAPHID header). pub const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64; +/// Maximum large blob array size in bytes. pub const MAX_LARGE_BLOB_SIZE: usize = 2048; +/// Default AAGUID for pico-fido firmware. +/// +/// Used to identify the authenticator model. Compare against +/// [`super::super::types::PICOFIDO_AAGUID`] or +/// [`super::super::types::RSKEY_AAGUID`] to determine firmware type. pub const AAGUID: [u8; 16] = [ 0x89, 0xFB, 0x94, 0xB7, 0x06, 0xC9, 0x36, 0x73, 0x9B, 0x7E, 0x30, 0x52, 0x6D, 0x96, 0x81, 0x45, ]; diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 9f1599c..1f08303 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -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, } +/// 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 { 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 { 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, 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, 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, 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, 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 { 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, 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 { 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], diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index d78cc46..d0b3e6c 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -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; diff --git a/src/device/io.rs b/src/device/io.rs index c52b166..79897e7 100644 --- a/src/device/io.rs +++ b/src/device/io.rs @@ -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 { match rescue::read_device_details() { Ok(status) => Ok(status), @@ -13,6 +25,7 @@ pub fn read_device_details() -> Result { } } +/// 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 { rescue::enable_secure_boot(lock) } -pub(crate) fn get_fido_info() -> Result { - fido::get_fido_info() -} - -pub(crate) fn change_fido_pin( - current_pin: Option, - new_pin: String, -) -> Result { - fido::change_fido_pin(current_pin, new_pin) -} - -pub(crate) fn set_min_pin_length( - current_pin: String, - min_pin_length: u8, -) -> Result { - 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 { rescue::reboot_device(to_bootsel) } -pub fn get_credentials(pin: String) -> Result, String> { - fido::get_credentials(pin) -} - -pub fn delete_credential(pin: String, credential_id: String) -> Result { - fido::delete_credential(pin, credential_id) -} - -pub fn reset_device() -> Result { - fido::reset_device() -} - +/// Read current LED status config via rescue. pub fn read_led_config() -> Result { 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 { rescue::read_management_config() } +/// Write management app enabled-mask via rescue. pub fn write_management_config(enabled_mask: u16) -> Result { rescue::write_management_config(enabled_mask) } +// ── FIDO2 protocol ────────────────────────────────────────────────────────── + +/// Query basic FIDO device info (AAGUID, version, etc.). +pub(crate) fn get_fido_info() -> Result { + fido::get_fido_info() +} + +/// Change the FIDO user PIN. +pub(crate) fn change_fido_pin( + current_pin: Option, + new_pin: String, +) -> Result { + 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 { + fido::set_min_pin_length(current_pin, min_pin_length) +} + +/// List stored credentials for the given PIN. +pub fn get_credentials(pin: String) -> Result, String> { + fido::get_credentials(pin) +} + +/// Delete a single credential by its ID. +pub fn delete_credential(pin: String, credential_id: String) -> Result { + fido::delete_credential(pin, credential_id) +} + +/// Factory-reset the device, wiping all credentials and settings. +pub fn reset_device() -> Result { + fido::reset_device() +} + +/// Enable enterprise attestation for the device. pub fn enable_enterprise_attestation(pin: String) -> Result { fido::enable_enterprise_attestation(pin) } +/// Retrieve the enterprise attestation CSR. pub fn get_enterprise_attestation_csr() -> Result { fido::get_enterprise_attestation_csr() } +/// Upload a signed enterprise attestation certificate. pub fn upload_enterprise_attestation_cert( pin: String, cert_path: String, diff --git a/src/device/mod.rs b/src/device/mod.rs index 3b591e9..96ec7a0 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -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; diff --git a/src/device/rescue/constants.rs b/src/device/rescue/constants.rs index 66ac39f..0a34d2d 100644 --- a/src/device/rescue/constants.rs +++ b/src/device/rescue/constants.rs @@ -1,4 +1,53 @@ -//! Constants, enums, bitflags and data structures for Rescue and vendor applets. +//! Rescue and vendor applet constants for pico-fido and RS-Key firmware. +//! +//! This file defines constants, enums, bitflags, and data structures used by the +//! Rescue applet and vendor-specific applets (LED, Management) across both +//! [pico-fido](https://github.com/polhenarejos/pico-fido) and +//! [RS-Key](https://github.com/TheMaxMur/RS-Key) firmware. +//! +//! # Organization +//! +//! - **ISO 7816-4 Standard Constants**: APDU command structure constants (CLA, INS, P1, P2, SW) +//! - **Rescue Applet Constants**: Commands and parameters for the Rescue applet +//! - **PHY Configuration Tags & Flags**: Hardware configuration tags and option bitflags +//! - **Vendor/LED Applet**: RS-Key specific LED control commands +//! - **Management Applet**: Yubico-compatible management interface for configuration +//! +//! # Rescue Applet +//! +//! The Rescue applet (AID: `A0 58 3F C1 9B 7E 4F 21`) provides low-level device access +//! for firmware recovery and hardware configuration. It uses ISO 7816-4 APDU commands +//! with a proprietary CLA byte (0x80). +//! +//! This applet is implemented in both firmware variants: +//! - **pico-fido**: C implementation in `pico-keys-sdk/src/rescue.c` +//! - **RS-Key**: Rust reimplementation in `crates/rsk-rescue/src/lib.rs` +//! +//! Key operations: +//! - `KeyDevSign` (0x10): Cryptographic operations (sign, get public key, upload cert) +//! - `Write` (0x1C): Write hardware configuration (PHY tags), set RTC time +//! - `Read` (0x1E): Read hardware configuration, flash info, secure boot status, time +//! - `Reboot` (0x1F): Reboot device (normal or bootloader mode) +//! +//! # PHY Configuration Tags +//! +//! PHY tags define hardware-specific parameters stored in the device's flash memory. +//! These tags control USB identifiers, LED behavior, cryptographic curves, and +//! interface enablement. +//! +//! PHY configuration is shared between pico-fido and RS-Key, with RS-Key adding +//! additional tags like `LedOrder` for RGB LED support. +//! +//! References: +//! - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +//! - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` +//! +//! # Vendor Applets +//! +//! - **LED Applet** (AID: `F0 00 00 00 01`): RS-Key specific LED color control +//! - **Management Applet** (Yubico-compatible): Read/write device configuration +//! +//! These applets are only available in RS-Key firmware, not in pico-fido. #![allow(unused)] // use serde::{Deserialize, Serialize}; @@ -6,101 +55,266 @@ // --- 1. ISO 7816-4 Standard Constants --- -/// Class Byte (CLA) -pub const APDU_CLA_ISO: u8 = 0x00; // Standard ISO commands -pub const APDU_CLA_PROPRIETARY: u8 = 0x80; // Custom/Rescue commands +/// ISO 7816-4 Class Byte (CLA) for standard commands. +/// +/// CLA byte indicates the class of the command. Value 0x00 indicates standard +/// ISO commands that follow the specification exactly. +pub const APDU_CLA_ISO: u8 = 0x00; -/// Instruction (INS) for Selection +/// ISO 7816-4 Class Byte (CLA) for proprietary commands. +/// +/// CLA byte 0x80 indicates proprietary commands that extend beyond the +/// standard ISO 7816-4 specification. Used by the Rescue applet. +pub const APDU_CLA_PROPRIETARY: u8 = 0x80; + +/// ISO 7816-4 SELECT instruction (INS) byte. +/// +/// The SELECT command (INS 0xA4) is used to select an application or file +/// on the device by its Application Identifier (AID). pub const APDU_INS_SELECT: u8 = 0xA4; -/// Selection Parameters (P1, P2) +/// SELECT P1 parameter: Select by DF name (Application Identifier). +/// +/// When P1 = 0x04, the command selects an application using its AID +/// (Application Identifier) in the data field. pub const APDU_P1_SELECT_BY_DF_NAME: u8 = 0x04; -pub const APDU_P2_RETURN_FCI: u8 = 0x04; // Return File Control Info -/// Status Words (SW1 SW2) +/// SELECT P2 parameter: Return File Control Information (FCI). +/// +/// When P2 = 0x04, the device returns FCI template containing +/// application metadata (AID, label, etc.) after selection. +pub const APDU_P2_RETURN_FCI: u8 = 0x04; + +/// ISO 7816-4 status word for successful command execution. +/// +/// SW1=0x90, SW2=0x00 indicates the command completed successfully +/// with no errors. pub const SW_SUCCESS: [u8; 2] = [0x90, 0x00]; // --- 2. Rescue Applet Constants --- -// The Rescue Application ID (AID) from src/rescue.c +/// Rescue Applet Application Identifier (AID). +/// +/// The Rescue applet is selected using this AID. The applet provides low-level +/// device access for firmware recovery and hardware configuration. +/// +/// This AID is shared between pico-fido and RS-Key firmware: +/// - **pico-fido**: Defined in `src/rescue.c` (C implementation) +/// - **RS-Key**: Defined in `crates/rsk-rescue/src/lib.rs` (Rust reimplementation) +/// +/// Byte sequence: `A0 58 3F C1 9B 7E 4F 21` pub const RESCUE_AID: &[u8] = &[0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21]; -// APDU Instructions +/// Rescue applet instruction codes. +/// +/// These instructions define the operations supported by the Rescue applet. +/// Each instruction has specific P1/P2 parameters and data field requirements. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RescueInstruction { + /// Cryptographic operations: sign data, get public key, upload certificate. + /// + /// P1 parameter determines the operation type (SignData, GetPublicKey, UploadCert). + /// P2 is typically 0x00. Data field contains operation-specific payload. KeyDevSign = 0x10, + + /// Write hardware configuration to flash memory. + /// + /// P1 parameter determines which PHY tag to write (e.g., PhyConfig for 0x01). + /// Data field contains the tag value to write. Write = 0x1C, + + /// Lock or unlock device access. + /// + /// P2 parameter determines lock state (0x00=Unlock, 0x01=Lock). + /// When locked, PHY configuration commands are rejected. Secure = 0x1D, + + /// Read hardware configuration from flash memory. + /// + /// P1 parameter determines which PHY tag to read (e.g., PhyConfig for 0x01). + /// Response contains the tag value. Read = 0x1E, + + /// Reboot the device. + /// + /// P2 parameter determines reboot mode (0x00=Normal, 0x01=Bootsel). + /// Normal reboot restarts the firmware; bootsel enters bootloader mode. Reboot = 0x1F, } -/// P1 Parameters for RescueInstruction::Read (0x1E) +/// P1 parameters for `RescueInstruction::Read` (0x1E). +/// +/// These parameters determine which hardware configuration to read. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReadParam { + /// Read full PHY configuration (VID/PID, LED settings, curves, etc.). PhyConfig = 0x01, + + /// Read flash memory information (size, used, free). FlashInfo = 0x02, + + /// Read secure boot status and verification result. SecureBootStatus = 0x03, } -/// P1 Parameters for WRITE (0x1C) +/// P1 parameters for `RescueInstruction::Write` (0x1C). +/// +/// These parameters determine which hardware configuration to write. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WriteParam { + /// Write full PHY configuration (VID/PID, LED settings, curves, etc.). PhyConfig = 0x01, } -/// P1 Parameters for RescueInstruction::KeyDevSign (0x10) +/// P1 parameters for `RescueInstruction::KeyDevSign` (0x10). +/// +/// These parameters determine the cryptographic operation to perform. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SignParam { + /// Sign data using the device's attestation key. + /// + /// Data field contains the data to sign. Response contains the signature. SignData = 0x01, + + /// Get the device's attestation public key. + /// + /// No data field required. Response contains the COSE-encoded public key. GetPublicKey = 0x02, + + /// Upload a certificate for the attestation key. + /// + /// Data field contains the DER-encoded certificate. Device stores it + /// for later retrieval during attestation. UploadCert = 0x03, } -/// P1 Parameters for RescueInstruction::Reboot (0x1F) +/// P1 parameters for `RescueInstruction::Reboot` (0x1F). +/// +/// These parameters determine the reboot mode. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RebootParam { + /// Normal reboot: restart the current firmware. Normal = 0x00, + + /// Bootsel reboot: enter RP2040 bootloader for firmware update. + /// + /// This mode allows flashing new firmware via USB mass storage. Bootsel = 0x01, } -/// P2 Parameters for SECURE (0x1D) +/// P2 parameters for `RescueInstruction::Secure` (0x1D). +/// +/// These parameters determine the lock state. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum SecureLockParam { + /// Unlock the device: allow PHY configuration commands. #[default] Unlock = 0x00, + + /// Lock the device: reject PHY configuration commands. + /// + /// When locked, Write and some Read operations are rejected + /// to prevent unauthorized configuration changes. Lock = 0x01, } -/// Default P2 value when not used +/// Default P2 value when not used in APDU commands. +/// +/// Some commands don't use the P2 parameter. This constant provides +/// a consistent value (0x00) for such cases. pub const P2_UNUSED: u8 = 0x00; // --- 3. PHY Configuration Tags & Flags --- -// PHY Tags from src/fs/phy.h +/// PHY configuration tag identifiers. +/// +/// These tags define the hardware parameters stored in the device's flash memory. +/// Each tag has a specific format and purpose for configuring the device. +/// +/// Tags are used with `RescueInstruction::Read` and `RescueInstruction::Write` +/// commands to access hardware configuration. +/// +/// PHY configuration is shared between pico-fido and RS-Key, with RS-Key adding +/// additional tags like `LedOrder` for RGB LED support. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PhyTag { + /// USB Vendor ID and Product ID. + /// + /// Data format: `[VID_LSB, VID_MSB, PID_LSB, PID_MSB]` (4 bytes). + /// Used to identify the device to the host system. VidPid = 0x00, + + /// LED GPIO pin configuration. + /// + /// Data format: `[GPIO_PIN]` (1 byte) or `[R_PIN, G_PIN, B_PIN]` (3 bytes). + /// Defines which GPIO pins control the status LED(s). LedGpio = 0x04, + + /// LED brightness level. + /// + /// Data format: `[BRIGHTNESS]` (1 byte, 0-255). + /// Controls the default brightness of the status LED. LedBrightness = 0x05, + + /// Device configuration options (bitflags). + /// + /// Data format: `[OPTIONS_LSB, OPTIONS_MSB]` (2 bytes). + /// See `RescueOptions` bitflags for individual option bits. Opts = 0x06, + + /// Touch presence timeout. + /// + /// Data format: `[TIMEOUT_MS_LSB, TIMEOUT_MS_MSB]` (2 bytes). + /// Timeout in milliseconds for user presence verification. PresenceTimeout = 0x08, + + /// USB product string. + /// + /// Data format: UTF-8 string bytes. + /// The product name displayed to the host system. UsbProduct = 0x09, + + /// Enabled cryptographic curves (bitflags). + /// + /// Data format: `[CURVES_LSB, CURVES_MSB, CURVES_3, CURVES_MSB]` (4 bytes). + /// See `RescueCurves` bitflags for supported curves. Curves = 0x0A, - LedDriver = 0x0C, - LedOrder = 0x0D, + + /// Enabled USB interfaces (bitflags). + /// + /// Data format: `[INTERFACES]` (1 byte). + /// See `UsbInterfaces` bitflags for available interfaces. EnabledUsbItf = 0x0B, + + /// LED driver type. + /// + /// Data format: `[DRIVER_TYPE]` (1 byte). + /// Specifies the LED driver hardware (e.g., PWM, I2C, etc.). + LedDriver = 0x0C, + + /// LED color order for RGB LEDs. + /// + /// Data format: `[ORDER]` (1 byte). + /// RS-Key specific tag for configuring LED color channel order. + LedOrder = 0x0D, } impl PhyTag { - /// Helper to convert raw u8 from device back to Enum + /// Convert a raw u8 value to a PhyTag enum variant. + /// + /// Returns `None` if the value doesn't match any known tag. + /// Used when parsing device responses that contain raw tag bytes. pub fn from_u8(val: u8) -> Option { match val { 0x00 => Some(Self::VidPid), @@ -118,58 +332,173 @@ impl PhyTag { } } +/// Device configuration options bitflags. +/// +/// These flags control device behavior and capabilities. They are stored +/// in the PHY configuration under tag `0x06` (Opts). +/// +/// This bitflags type is shared between pico-fido and RS-Key firmware. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { - /// Configuration options for TAG_OPTS (Tag 0x06) pub struct RescueOptions: u16 { + /// LED supports dimming (PWM control). + /// + /// When set, the LED brightness can be adjusted. When clear, + /// the LED is only on/off. const LED_DIMMABLE = 0x02; + + /// Disable power-on reset detection. + /// + /// When set, the device doesn't reset on power-on events. + /// Useful for devices with unstable power supply. const DISABLE_POWER_RESET = 0x04; + + /// LED stays steady (no blinking). + /// + /// When set, the LED remains solid when active. + /// When clear, the LED blinks to indicate activity. const LED_STEADY = 0x08; } } +/// Enabled cryptographic curves bitflags. +/// +/// These flags define which elliptic curves are available for +/// cryptographic operations (ECDH, ECDSA, etc.). +/// +/// This bitflags type is shared between pico-fido and RS-Key firmware. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { - /// Enabled curves for TAG_CURVES (Tag 0x0A) pub struct RescueCurves: u32 { + /// SECP256K1 curve (Bitcoin/Ethereum). + /// + /// Used by cryptocurrency wallets and some FIDO2 implementations. + /// Curve OID: 1.3.132.0.10 const SECP256K1 = 0x08; } } +/// Enabled USB interfaces bitflags. +/// +/// These flags define which USB interfaces are active on the device. +/// Multiple interfaces can be enabled simultaneously. +/// +/// This bitflags type is shared between pico-fido and RS-Key firmware. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { - /// Enabled USB interfaces for TAG 0x0B (EnabledUsbItf) pub struct UsbInterfaces: u8 { + /// CCID interface (smart card reader). + /// + /// Implements USB CCID class for smart card operations. + /// Used by some enterprise security solutions. const CCID = 0x01; + + /// WCID interface (Windows Compatible ID). + /// + /// Provides Windows-compatible device identification + /// without requiring custom drivers. const WCID = 0x02; + + /// HID interface (FIDO2/CTAP2). + /// + /// Implements USB HID class for CTAP2/FIDO2 communication. + /// This is the primary interface for security key operations. const HID = 0x04; + + /// Keyboard interface (HID keyboard). + /// + /// Emulates a USB keyboard for TOTP code entry or other + /// keyboard-based interactions. const KB = 0x08; + + /// LWIP interface (TCP/IP stack). + /// + /// Enables the lightweight TCP/IP stack for network + /// communication (if supported by hardware). const LWIP = 0x10; } } // --- 4. Vendor/LED Applet (RS-Key specific) --- +/// Vendor LED applet Application Identifier (AID). +/// +/// This AID selects the RS-Key specific LED control applet. +/// The applet provides commands to control the device's RGB LED +/// for status indication and user feedback. +/// +/// Byte sequence: `F0 00 00 00 01` +/// +/// **Note**: This applet is only available in RS-Key firmware, not in pico-fido. pub const VENDOR_LED_AID: &[u8] = &[0xF0, 0x00, 0x00, 0x00, 0x01]; +/// Vendor LED applet instruction codes. +/// +/// These instructions control the device's RGB LED. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VendorLedInstruction { + /// Set LED color for a specific status indicator. + /// + /// P1: Status indicator (LedStatus). + /// P2: LED color (LedColor). + /// Data: None. SetLed = 0x10, + + /// Get current LED color for a status indicator. + /// + /// P1: Status indicator (LedStatus). + /// P2: 0x00. + /// Data: None. + /// Response: LED color byte. GetLed = 0x11, } +/// LED color definitions for the Vendor LED applet. +/// +/// Each color corresponds to a specific RGB value that can be +/// displayed on the device's status LED. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LedColor { + /// LED off (no light). Off = 0, + + /// Red LED. Red = 1, + + /// Green LED. Green = 2, + + /// Blue LED. Blue = 3, + + /// Yellow LED (Red + Green). Yellow = 4, + + /// Magenta LED (Red + Blue). Magenta = 5, + + /// Cyan LED (Green + Blue). Cyan = 6, + + /// White LED (Red + Green + Blue). White = 7, } impl LedColor { + /// Convert a raw u8 value to a LedColor enum variant. + /// + /// Returns `None` if the value doesn't match any known color. pub fn from_u8(val: u8) -> Option { match val { 0 => Some(Self::Off), @@ -184,6 +513,10 @@ impl LedColor { } } + /// Get a human-readable label for the color. + /// + /// Returns a static string like "Red", "Green", etc. + /// Used for display in the UI. pub fn label(&self) -> &'static str { match self { Self::Off => "Off", @@ -197,6 +530,10 @@ impl LedColor { } } + /// Get all available LED colors. + /// + /// Returns a slice of all `LedColor` variants in order. + /// Useful for populating UI dropdowns or color pickers. pub fn all() -> &'static [Self] { &[ Self::Off, @@ -211,16 +548,43 @@ impl LedColor { } } +/// LED status indicator definitions. +/// +/// These indicate the device state for which the LED color is configured. +/// Each status can have a different color for visual feedback. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LedStatus { + /// Idle state: device is waiting for user interaction. + /// + /// Typically shows a dim or pulsing light to indicate the device + /// is ready but not actively processing. Idle = 0, + + /// Processing state: device is performing an operation. + /// + /// Shows a bright or blinking light to indicate the device + /// is busy (e.g., during cryptographic operations). Processing = 1, + + /// Touch required: device is waiting for user touch. + /// + /// Shows a specific color pattern to prompt the user to + /// touch the device's capacitive sensor. Touch = 2, + + /// Boot state: device is starting up. + /// + /// Shows a brief color pattern during the boot sequence + /// to indicate successful initialization. Boot = 3, } impl LedStatus { + /// Get a human-readable label for the status. + /// + /// Returns a static string like "Idle", "Processing", etc. + /// Used for display in the UI. pub fn label(&self) -> &'static str { match self { Self::Idle => "Idle", @@ -230,6 +594,10 @@ impl LedStatus { } } + /// Get all available LED statuses. + /// + /// Returns a slice of all `LedStatus` variants in order. + /// Useful for populating UI dropdowns or status lists. pub fn all() -> &'static [Self] { &[Self::Idle, Self::Processing, Self::Touch, Self::Boot] } @@ -237,26 +605,87 @@ impl LedStatus { // --- 5. Management Applet (Yubico-compatible, RS-Key) --- +/// Management applet Application Identifier (AID). +/// +/// This AID selects the Yubico-compatible management applet in RS-Key firmware. +/// The applet provides configuration read/write operations similar to Yubico's +/// management interface. +/// +/// Byte sequence: `A0 00 00 05 27 47 11 17` +/// +/// **Note**: This applet is only available in RS-Key firmware, not in pico-fido. pub const MANAGEMENT_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]; +/// Management applet instruction codes. +/// +/// These instructions read and write device configuration using +/// a TLV (Tag-Length-Value) format. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ManagementInstruction { + /// Read device configuration. + /// + /// Returns a TLV-encoded map of device settings. + /// Use `MGMT_TAG_*` constants to parse the response. ReadConfig = 0x1D, + + /// Write device configuration. + /// + /// Accepts a TLV-encoded map of settings to write. + /// Use `MGMT_TAG_*` constants to build the request. WriteConfig = 0x1C, } +/// TLV tag for USB support flag. +/// +/// Value: `true` if the device supports USB, `false` otherwise. pub const MGMT_TAG_USB_SUPPORTED: u8 = 0x01; + +/// TLV tag for device serial number. +/// +/// Value: 32-bit unsigned integer representing the device serial. pub const MGMT_TAG_SERIAL: u8 = 0x02; + +/// TLV tag for enabled USB interfaces. +/// +/// Value: Bitmask of enabled USB interfaces (see `USB_CAP_*` constants). pub const MGMT_TAG_USB_ENABLED: u8 = 0x03; + +/// TLV tag for device form factor. +/// +/// Value: Device form factor identifier (e.g., USB key, NFC, etc.). pub const MGMT_TAG_FORM_FACTOR: u8 = 0x04; + +/// TLV tag for firmware version. +/// +/// Value: Firmware version number (major.minor.patch encoded as integer). pub const MGMT_TAG_VERSION: u8 = 0x05; + +/// TLV tag for device flags. +/// +/// Value: Bitmask of device flags (e.g., FIPS compliance, etc.). pub const MGMT_TAG_DEVICE_FLAGS: u8 = 0x08; + +/// TLV tag for configuration lock state. +/// +/// Value: `true` if configuration is locked, `false` otherwise. +/// When locked, `WriteConfig` commands are rejected. pub const MGMT_TAG_CONFIG_LOCK: u8 = 0x0A; +/// USB capability: OTP (One-Time Password) interface. pub const USB_CAP_OTP: u16 = 0x0001; + +/// USB capability: U2F (Universal 2nd Factor) interface. pub const USB_CAP_U2F: u16 = 0x0002; + +/// USB capability: OpenPGP card interface. pub const USB_CAP_OPENPGP: u16 = 0x0008; + +/// USB capability: PIV (Personal Identity Verification) interface. pub const USB_CAP_PIV: u16 = 0x0010; + +/// USB capability: OATH (One-Time Auth) interface. pub const USB_CAP_OATH: u16 = 0x0020; + +/// USB capability: FIDO2/CTAP2 interface. pub const USB_CAP_FIDO2: u16 = 0x0200; diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index cbd6b4d..75e5e70 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -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, 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, 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 { 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 { }) } +/// 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 { log::info!("Writing configuration to device"); log::debug!("Config input: {:?}", config); @@ -415,6 +620,21 @@ pub fn write_config(config: AppConfigInput) -> Result { } } +/// 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 { let (card, _, _) = connect_and_select()?; @@ -442,7 +662,27 @@ pub fn reboot_device(to_bootsel: bool) -> Result { } } -/// 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 { let (card, _, _) = connect_and_select()?; @@ -470,6 +710,22 @@ pub fn enable_secure_boot(lock: bool) -> Result { // --- 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 { let ctx = Context::establish(Scope::User).map_err(|e| { log::error!("Failed to establish PCSC context: {}", e); diff --git a/src/device/types.rs b/src/device/types.rs index 7138eab..d827585 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -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, } +/// 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, } +/// 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, } +/// 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";