From b293e294f5b754b33d0594c116903ae60e97de0e Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 7 Jul 2026 01:23:26 +0530 Subject: [PATCH] feat: refactor hal module architecture and add tests in fido module --- src/hal/common/cose.rs | 112 ++++++ src/hal/common/mod.rs | 4 + src/hal/common/version.rs | 170 +++++++++ src/hal/fido/constants.rs | 531 ++++++++++++++++++++++++++++ src/hal/fido/hid.rs | 1 + src/hal/fido/mod.rs | 197 +++++++++-- src/hal/firmwares/mod.rs | 93 +++++ src/hal/firmwares/picofido.rs | 36 ++ src/hal/firmwares/rskey.rs | 36 ++ src/hal/io.rs | 141 +++++--- src/hal/mod.rs | 3 + src/hal/rescue/constants.rs | 23 +- src/hal/rescue/mod.rs | 7 + src/hal/transport/mod.rs | 89 +++++ src/hal/types.rs | 3 + src/ui/screens/config/view_model.rs | 1 + 16 files changed, 1371 insertions(+), 76 deletions(-) create mode 100644 src/hal/common/cose.rs create mode 100644 src/hal/common/mod.rs create mode 100644 src/hal/common/version.rs create mode 100644 src/hal/firmwares/mod.rs create mode 100644 src/hal/firmwares/picofido.rs create mode 100644 src/hal/firmwares/rskey.rs create mode 100644 src/hal/transport/mod.rs diff --git a/src/hal/common/cose.rs b/src/hal/common/cose.rs new file mode 100644 index 0000000..6729e9f --- /dev/null +++ b/src/hal/common/cose.rs @@ -0,0 +1,112 @@ +#![allow(dead_code)] +use std::fmt; + +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseAlgorithm { + ES256 = -7, + EdDSA = -8, + ESP256 = -9, + Ed25519 = -19, + EcdhEsHkdf256 = -25, + ES384 = -35, + ES512 = -36, + ES256K = -47, + ESP384 = -51, + ESP512 = -52, + Ed448 = -53, + RS256 = -257, + RS384 = -258, + RS512 = -259, + ESB256 = -265, + ESB384 = -267, + ESB512 = -268, + MLDSA44 = -48, + MLDSA65 = -49, + MLDSA87 = -50, +} + +impl CoseAlgorithm { + pub fn from_i128(val: i128) -> Option { + match val as i32 { + -7 => Some(Self::ES256), + -8 => Some(Self::EdDSA), + -9 => Some(Self::ESP256), + -19 => Some(Self::Ed25519), + -25 => Some(Self::EcdhEsHkdf256), + -35 => Some(Self::ES384), + -36 => Some(Self::ES512), + -47 => Some(Self::ES256K), + -51 => Some(Self::ESP384), + -52 => Some(Self::ESP512), + -53 => Some(Self::Ed448), + -257 => Some(Self::RS256), + -258 => Some(Self::RS384), + -259 => Some(Self::RS512), + -265 => Some(Self::ESB256), + -267 => Some(Self::ESB384), + -268 => Some(Self::ESB512), + -48 => Some(Self::MLDSA44), + -49 => Some(Self::MLDSA65), + -50 => Some(Self::MLDSA87), + _ => None, + } + } +} + +impl fmt::Display for CoseAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ES256 => write!(f, "ES256"), + Self::EdDSA => write!(f, "EdDSA"), + Self::ESP256 => write!(f, "ESP256"), + Self::Ed25519 => write!(f, "Ed25519"), + Self::EcdhEsHkdf256 => write!(f, "ECDH-ES-HKDF-256"), + Self::ES384 => write!(f, "ES384"), + Self::ES512 => write!(f, "ES512"), + Self::ES256K => write!(f, "ES256K"), + Self::ESP384 => write!(f, "ESP384"), + Self::ESP512 => write!(f, "ESP512"), + Self::Ed448 => write!(f, "Ed448"), + Self::RS256 => write!(f, "RS256"), + Self::RS384 => write!(f, "RS384"), + Self::RS512 => write!(f, "RS512"), + Self::ESB256 => write!(f, "ESB256"), + Self::ESB384 => write!(f, "ESB384"), + Self::ESB512 => write!(f, "ESB512"), + Self::MLDSA44 => write!(f, "ML-DSA-44"), + Self::MLDSA65 => write!(f, "ML-DSA-65"), + Self::MLDSA87 => write!(f, "ML-DSA-87"), + } + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseCurve { + P256 = 1, + P384 = 2, + P521 = 3, + X25519 = 4, + X448 = 5, + Ed25519 = 6, + Ed448 = 7, + P256K1 = 8, + BP256R1 = 9, + BP384R1 = 10, + BP512R1 = 11, +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseKeyParam { + Kty = 1, + Kid = 2, + Alg = 3, + KeyOps = 4, + BaseIV = 5, + Crv = -1, + X = -2, + Y = -3, + D = -4, +} diff --git a/src/hal/common/mod.rs b/src/hal/common/mod.rs new file mode 100644 index 0000000..88d4820 --- /dev/null +++ b/src/hal/common/mod.rs @@ -0,0 +1,4 @@ +pub mod cose; +pub mod version; + +pub use version::FirmwareVersion; diff --git a/src/hal/common/version.rs b/src/hal/common/version.rs new file mode 100644 index 0000000..c4ef5ff --- /dev/null +++ b/src/hal/common/version.rs @@ -0,0 +1,170 @@ +#![allow(dead_code)] +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FirmwareVersion { + pub major: u16, + pub minor: u16, + pub patch: u16, + pub raw: String, +} + +impl FirmwareVersion { + pub fn parse(version: &str) -> Option { + let parts: Vec<&str> = version.split('.').collect(); + let major = parts.first()?.parse().ok()?; + let minor = parts.get(1)?.parse().ok()?; + let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + Some(Self { + major, + minor, + patch, + raw: version.to_string(), + }) + } + + pub fn is_at_least(&self, major: u16, minor: u16) -> bool { + self.major > major || (self.major == major && self.minor >= minor) + } + + pub fn is_between(&self, lo_major: u16, lo_minor: u16, hi_major: u16, hi_minor: u16) -> bool { + self.is_at_least(lo_major, lo_minor) + && (self.major < hi_major || (self.major == hi_major && self.minor <= hi_minor)) + } +} + +impl fmt::Display for FirmwareVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.raw) + } +} + +impl Default for FirmwareVersion { + fn default() -> Self { + Self { + major: 0, + minor: 0, + patch: 0, + raw: "0.0".into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_two_part_version() { + let v = FirmwareVersion::parse("7.6").unwrap(); + assert_eq!(v.major, 7); + assert_eq!(v.minor, 6); + assert_eq!(v.patch, 0); + assert_eq!(v.raw, "7.6"); + } + + #[test] + fn test_parse_three_part_version() { + let v = FirmwareVersion::parse("5.7.4").unwrap(); + assert_eq!(v.major, 5); + assert_eq!(v.minor, 7); + assert_eq!(v.patch, 4); + assert_eq!(v.raw, "5.7.4"); + } + + #[test] + fn test_parse_single_part_fails() { + assert!(FirmwareVersion::parse("7").is_none()); + } + + #[test] + fn test_parse_non_numeric_fails() { + assert!(FirmwareVersion::parse("a.b").is_none()); + assert!(FirmwareVersion::parse("7.x").is_none()); + } + + #[test] + fn test_parse_empty_fails() { + assert!(FirmwareVersion::parse("").is_none()); + } + + #[test] + fn test_is_at_least_exact_match() { + let v = FirmwareVersion::parse("7.2").unwrap(); + assert!(v.is_at_least(7, 2)); + } + + #[test] + fn test_is_at_least_above() { + let v = FirmwareVersion::parse("7.6").unwrap(); + assert!(v.is_at_least(7, 2)); + assert!(v.is_at_least(6, 0)); + assert!(v.is_at_least(7, 6)); + } + + #[test] + fn test_is_at_least_below() { + let v = FirmwareVersion::parse("7.0").unwrap(); + assert!(!v.is_at_least(7, 2)); + assert!(!v.is_at_least(8, 0)); + } + + #[test] + fn test_is_between_inclusive_range() { + let v = FirmwareVersion::parse("7.2").unwrap(); + assert!(v.is_between(6, 0, 8, 0)); + assert!(v.is_between(7, 0, 7, 2)); + assert!(v.is_between(7, 2, 7, 2)); + } + + #[test] + fn test_is_between_outside_range() { + let v = FirmwareVersion::parse("7.6").unwrap(); + assert!(!v.is_between(6, 0, 7, 2)); + assert!(!v.is_between(8, 0, 9, 0)); + } + + #[test] + fn test_default_version() { + let v = FirmwareVersion::default(); + assert_eq!(v.major, 0); + assert_eq!(v.minor, 0); + assert_eq!(v.patch, 0); + assert_eq!(v.raw, "0.0"); + } + + #[test] + fn test_display() { + let v = FirmwareVersion::parse("7.6.1").unwrap(); + assert_eq!(v.to_string(), "7.6.1"); + } + + #[test] + fn test_parse_with_patch_zero() { + let v = FirmwareVersion::parse("7.6.0").unwrap(); + assert_eq!(v.major, 7); + assert_eq!(v.minor, 6); + assert_eq!(v.patch, 0); + } + + #[test] + fn test_legacy_fido_config_boundaries() { + // <= 7.2 supports legacy FIDO hardware config + assert!(FirmwareVersion::parse("7.2").unwrap().is_at_least(0, 0)); + assert!( + !FirmwareVersion::parse("7.3") + .unwrap() + .is_between(0, 0, 7, 2) + ); + assert!( + FirmwareVersion::parse("7.2") + .unwrap() + .is_between(0, 0, 7, 2) + ); + assert!( + FirmwareVersion::parse("6.6") + .unwrap() + .is_between(0, 0, 7, 2) + ); + } +} diff --git a/src/hal/fido/constants.rs b/src/hal/fido/constants.rs index b542801..1a07a8f 100644 --- a/src/hal/fido/constants.rs +++ b/src/hal/fido/constants.rs @@ -920,3 +920,534 @@ pub const MAX_LARGE_BLOB_SIZE: usize = 2048; pub const AAGUID: [u8; 16] = [ 0x89, 0xFB, 0x94, 0xB7, 0x06, 0xC9, 0x36, 0x73, 0x9B, 0x7E, 0x30, 0x52, 0x6D, 0x96, 0x81, 0x45, ]; + +/// CTAP 2.1 GetInfo response map keys (§11.5.3). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Ctap2GetInfoKey { + Versions = 0x01, + Extensions = 0x02, + Aaguid = 0x03, + Options = 0x04, + MaxMsgSize = 0x05, + PinUvAuthProtocols = 0x06, + MaxCredentialCountInList = 0x07, + MaxCredentialIdLength = 0x08, + RemainingDiscoverableCredentials = 0x14, + FirmwareVersion = 0x0E, +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── CTAP2 command codes ────────────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h: #define CTAP_MAKE_CREDENTIAL 0x01 + // CTAP_GET_ASSERTION 0x02 + // CTAP_GET_INFO 0x04 + // CTAP_CLIENT_PIN 0x06 + // CTAP_RESET 0x07 + // CTAP_GET_NEXT_ASSERTION 0x08 + // CTAP_CREDENTIAL_MGMT 0x0A + // CTAP_SELECTION 0x0B + // CTAP_LARGE_BLOBS 0x0C + // CTAP_CONFIG 0x0D + + #[test] + fn test_ctap_command_values_match_firmware() { + assert_eq!(CtapCommand::MakeCredential as u8, 0x01); + assert_eq!(CtapCommand::GetAssertion as u8, 0x02); + assert_eq!(CtapCommand::GetInfo as u8, 0x04); + assert_eq!(CtapCommand::ClientPin as u8, 0x06); + assert_eq!(CtapCommand::Reset as u8, 0x07); + assert_eq!(CtapCommand::GetNextAssertion as u8, 0x08); + assert_eq!(CtapCommand::CredentialMgmt as u8, 0x0A); + assert_eq!(CtapCommand::Selection as u8, 0x0B); + assert_eq!(CtapCommand::LargeBlobs as u8, 0x0C); + assert_eq!(CtapCommand::Config as u8, 0x0D); + } + + // ── U2F command codes ──────────────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h: #define CTAP_REGISTER 0x01 + // CTAP_AUTHENTICATE 0x02 + // CTAP_VERSION 0x03 + + #[test] + fn test_u2f_command_values_match_firmware() { + assert_eq!(U2fCommand::Register as u8, 0x01); + assert_eq!(U2fCommand::Authenticate as u8, 0x02); + assert_eq!(U2fCommand::Version as u8, 0x03); + } + + // ── AuthenticateControl ────────────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h: #define CTAP_AUTH_ENFORCE 0x03 + // CTAP_AUTH_CHECK_ONLY 0x07 + + #[test] + fn test_authenticate_control_values_match_firmware() { + assert_eq!(AuthenticateControl::EnforceUserPresence as u8, 0x03); + assert_eq!(AuthenticateControl::CheckOnly as u8, 0x07); + } + + // ── Client PIN sub-commands ────────────────────────────────────────────── + // Reference: CTAP 2.1 §11.5.4 + + #[test] + fn test_client_pin_sub_command_values_match_spec() { + assert_eq!(ClientPinSubCommand::GetPinRetries as u8, 0x01); + assert_eq!(ClientPinSubCommand::GetKeyAgreement as u8, 0x02); + assert_eq!(ClientPinSubCommand::SetPin as u8, 0x03); + assert_eq!(ClientPinSubCommand::ChangePin as u8, 0x04); + assert_eq!(ClientPinSubCommand::GetPinToken as u8, 0x05); + assert_eq!( + ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions as u8, + 0x06 + ); + assert_eq!(ClientPinSubCommand::GetUvRetries as u8, 0x07); + assert_eq!( + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions as u8, + 0x09 + ); + } + + // ── Config sub-commands ────────────────────────────────────────────────── + // Reference: CTAP 2.1 §11.5.10 + + #[test] + fn test_config_sub_command_values_match_spec() { + assert_eq!(ConfigSubCommand::EnableEnterpriseAttestation as u8, 0x01); + assert_eq!(ConfigSubCommand::ToggleAlwaysUv as u8, 0x02); + assert_eq!(ConfigSubCommand::SetMinPinLength as u8, 0x03); + assert_eq!(ConfigSubCommand::VendorPrototype as u8, 0xFF); + } + + // ── AuthenticatorFlags ─────────────────────────────────────────────────── + // Reference: pico-fido src/fido/fido.h: + // #define FIDO2_AUT_FLAG_UP 0x1 + // #define FIDO2_AUT_FLAG_UV 0x4 + // #define FIDO2_AUT_FLAG_AT 0x40 + // #define FIDO2_AUT_FLAG_ED 0x80 + + #[test] + fn test_authenticator_flags_values_match_firmware() { + assert_eq!(AuthenticatorFlags::USER_PRESENT.bits(), 0x01); + assert_eq!(AuthenticatorFlags::USER_VERIFIED.bits(), 0x04); + assert_eq!(AuthenticatorFlags::ATTESTED_CREDENTIAL_DATA.bits(), 0x40); + assert_eq!(AuthenticatorFlags::EXTENSION_DATA.bits(), 0x80); + } + + #[test] + fn test_authenticator_flags_combine_correctly() { + let up_uv = AuthenticatorFlags::USER_PRESENT | AuthenticatorFlags::USER_VERIFIED; + assert_eq!(up_uv.bits(), 0x05); + let full = up_uv + | AuthenticatorFlags::ATTESTED_CREDENTIAL_DATA + | AuthenticatorFlags::EXTENSION_DATA; + assert_eq!(full.bits(), 0xC5); + } + + // ── AuthenticatorOptions ───────────────────────────────────────────────── + // Reference: pico-fido src/fido/fido.h: + // #define FIDO2_OPT_EA 0x01 + // #define FIDO2_OPT_AUV 0x02 + + #[test] + fn test_authenticator_options_values_match_firmware() { + assert_eq!(AuthenticatorOptions::ENTERPRISE_ATTESTATION.bits(), 0x01); + assert_eq!(AuthenticatorOptions::USER_VERIFICATION.bits(), 0x02); + } + + // ── PinUvAuthTokenPermissions ──────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h: + // #define CTAP_PERMISSION_MC 0x01 + // #define CTAP_PERMISSION_GA 0x02 + // #define CTAP_PERMISSION_CM 0x04 + // #define CTAP_PERMISSION_BE 0x08 + // #define CTAP_PERMISSION_LBW 0x10 + // #define CTAP_PERMISSION_ACFG 0x20 + // #define CTAP_PERMISSION_PCMR 0x40 + + #[test] + fn test_pin_uv_auth_token_permissions_values_match_firmware() { + assert_eq!(PinUvAuthTokenPermissions::MAKE_CREDENTIAL.bits(), 0x01); + assert_eq!(PinUvAuthTokenPermissions::GET_ASSERTION.bits(), 0x02); + assert_eq!( + PinUvAuthTokenPermissions::CREDENTIAL_MANAGEMENT.bits(), + 0x04 + ); + assert_eq!(PinUvAuthTokenPermissions::BIO_ENROLLMENT.bits(), 0x08); + assert_eq!(PinUvAuthTokenPermissions::LARGE_BLOB_WRITE.bits(), 0x10); + assert_eq!(PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG.bits(), 0x20); + assert_eq!( + PinUvAuthTokenPermissions::PER_CREDENTIAL_MGMT_READONLY.bits(), + 0x40 + ); + } + + #[test] + fn test_pin_uv_auth_token_permissions_combine() { + let mc_ga = + PinUvAuthTokenPermissions::MAKE_CREDENTIAL | PinUvAuthTokenPermissions::GET_ASSERTION; + assert_eq!(mc_ga.bits(), 0x03); + let all = mc_ga + | PinUvAuthTokenPermissions::CREDENTIAL_MANAGEMENT + | PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG; + assert_eq!(all.bits(), 0x27); + } + + // ── COSE Algorithms ────────────────────────────────────────────────────── + // Reference: pico-fido src/fido/fido.h: #define FIDO2_ALG_* defines + + #[test] + fn test_cose_algorithm_values_match_firmware() { + assert_eq!(CoseAlgorithm::ES256 as i32, -7); + assert_eq!(CoseAlgorithm::EdDSA as i32, -8); + assert_eq!(CoseAlgorithm::ESP256 as i32, -9); + assert_eq!(CoseAlgorithm::Ed25519 as i32, -19); + assert_eq!(CoseAlgorithm::EcdhEsHkdf256 as i32, -25); + assert_eq!(CoseAlgorithm::ES384 as i32, -35); + assert_eq!(CoseAlgorithm::ES512 as i32, -36); + assert_eq!(CoseAlgorithm::ES256K as i32, -47); + assert_eq!(CoseAlgorithm::ESP384 as i32, -51); + assert_eq!(CoseAlgorithm::ESP512 as i32, -52); + assert_eq!(CoseAlgorithm::Ed448 as i32, -53); + assert_eq!(CoseAlgorithm::RS256 as i32, -257); + assert_eq!(CoseAlgorithm::RS384 as i32, -258); + assert_eq!(CoseAlgorithm::RS512 as i32, -259); + assert_eq!(CoseAlgorithm::ESB256 as i32, -265); + assert_eq!(CoseAlgorithm::ESB384 as i32, -267); + assert_eq!(CoseAlgorithm::ESB512 as i32, -268); + } + + #[test] + fn test_cose_algorithm_from_i128_roundtrip() { + let test_cases = [ + -7, -8, -9, -19, -25, -35, -36, -47, -51, -52, -53, -257, -258, -259, -265, -267, -268, + -48, -49, -50, + ]; + for val in test_cases { + let alg = CoseAlgorithm::from_i128(val as i128) + .unwrap_or_else(|| panic!("from_i128({}) failed", val)); + assert_eq!(alg as i32, val); + } + } + + #[test] + fn test_cose_algorithm_unknown_returns_none() { + assert!(CoseAlgorithm::from_i128(0).is_none()); + assert!(CoseAlgorithm::from_i128(1).is_none()); + assert!(CoseAlgorithm::from_i128(-1).is_none()); + assert!(CoseAlgorithm::from_i128(-100).is_none()); + assert!(CoseAlgorithm::from_i128(-300).is_none()); + } + + #[test] + fn test_cose_algorithm_display() { + assert_eq!(CoseAlgorithm::ES256.to_string(), "ES256"); + assert_eq!(CoseAlgorithm::EdDSA.to_string(), "EdDSA"); + assert_eq!(CoseAlgorithm::MLDSA44.to_string(), "ML-DSA-44"); + } + + // ── COSE Curves ────────────────────────────────────────────────────────── + // Reference: pico-fido src/fido/fido.h: + // #define FIDO2_CURVE_P256 1 #define FIDO2_CURVE_BP256R1 9 + // #define FIDO2_CURVE_P384 2 #define FIDO2_CURVE_BP384R1 10 + // #define FIDO2_CURVE_P521 3 #define FIDO2_CURVE_BP512R1 11 + // #define FIDO2_CURVE_X25519 4 + // #define FIDO2_CURVE_X448 5 + // #define FIDO2_CURVE_ED25519 6 + // #define FIDO2_CURVE_ED448 7 + // #define FIDO2_CURVE_P256K1 8 + + #[test] + fn test_cose_curve_values_match_firmware() { + assert_eq!(CoseCurve::P256 as u8, 1); + assert_eq!(CoseCurve::P384 as u8, 2); + assert_eq!(CoseCurve::P521 as u8, 3); + assert_eq!(CoseCurve::X25519 as u8, 4); + assert_eq!(CoseCurve::X448 as u8, 5); + assert_eq!(CoseCurve::Ed25519 as u8, 6); + assert_eq!(CoseCurve::Ed448 as u8, 7); + assert_eq!(CoseCurve::P256K1 as u8, 8); + assert_eq!(CoseCurve::BP256R1 as u8, 9); + assert_eq!(CoseCurve::BP384R1 as u8, 10); + assert_eq!(CoseCurve::BP512R1 as u8, 11); + } + + // ── CTAP2 error codes ──────────────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h: #define CTAP2_ERR_* defines + + #[test] + fn test_ctap2_error_values_match_firmware() { + assert_eq!(Ctap2Error::Success as u8, 0x00); + assert_eq!(Ctap2Error::CborUnexpectedType as u8, 0x11); + assert_eq!(Ctap2Error::InvalidCbor as u8, 0x12); + assert_eq!(Ctap2Error::MissingParameter as u8, 0x14); + assert_eq!(Ctap2Error::LimitExceeded as u8, 0x15); + assert_eq!(Ctap2Error::FpDatabaseFull as u8, 0x17); + assert_eq!(Ctap2Error::LargeBlobStorageFull as u8, 0x18); + assert_eq!(Ctap2Error::CredentialExcluded as u8, 0x19); + assert_eq!(Ctap2Error::Processing as u8, 0x21); + assert_eq!(Ctap2Error::InvalidCredential as u8, 0x22); + assert_eq!(Ctap2Error::UserActionPending as u8, 0x23); + assert_eq!(Ctap2Error::OperationPending as u8, 0x24); + assert_eq!(Ctap2Error::NoOperations as u8, 0x25); + assert_eq!(Ctap2Error::UnsupportedAlgorithm as u8, 0x26); + assert_eq!(Ctap2Error::OperationDenied as u8, 0x27); + assert_eq!(Ctap2Error::KeyStoreFull as u8, 0x28); + assert_eq!(Ctap2Error::UnsupportedOption as u8, 0x2B); + assert_eq!(Ctap2Error::InvalidOption as u8, 0x2C); + assert_eq!(Ctap2Error::KeepaliveCancel as u8, 0x2D); + assert_eq!(Ctap2Error::NoCredentials as u8, 0x2E); + assert_eq!(Ctap2Error::UserActionTimeout as u8, 0x2F); + assert_eq!(Ctap2Error::NotAllowed as u8, 0x30); + assert_eq!(Ctap2Error::PinInvalid as u8, 0x31); + assert_eq!(Ctap2Error::PinBlocked as u8, 0x32); + assert_eq!(Ctap2Error::PinAuthInvalid as u8, 0x33); + assert_eq!(Ctap2Error::PinAuthBlocked as u8, 0x34); + assert_eq!(Ctap2Error::PinNotSet as u8, 0x35); + assert_eq!(Ctap2Error::PuatRequired as u8, 0x36); + assert_eq!(Ctap2Error::PinPolicyViolation as u8, 0x37); + assert_eq!(Ctap2Error::RequestTooLarge as u8, 0x39); + assert_eq!(Ctap2Error::ActionTimeout as u8, 0x3A); + assert_eq!(Ctap2Error::UpRequired as u8, 0x3B); + assert_eq!(Ctap2Error::UvBlocked as u8, 0x3C); + assert_eq!(Ctap2Error::IntegrityFailure as u8, 0x3D); + assert_eq!(Ctap2Error::InvalidSubcommand as u8, 0x3E); + assert_eq!(Ctap2Error::UvInvalid as u8, 0x3F); + assert_eq!(Ctap2Error::UnauthorizedPermission as u8, 0x40); + } + + // ── VendorCommand codes ────────────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h: + // #define CTAP_VENDOR_BACKUP 0x01 + // #define CTAP_VENDOR_MSE 0x02 + // #define CTAP_VENDOR_UNLOCK 0x03 + // #define CTAP_VENDOR_EA 0x04 + // #define CTAP_VENDOR_ADMIN_PIN 0x08 + // Note: PhysicalOptions(0x05) and Memory(0x06) are legacy (<=v7.2) and + // were removed in later firmware releases. + + #[test] + fn test_vendor_command_values_match_firmware() { + assert_eq!(VendorCommand::Backup as u8, 0x01); + assert_eq!(VendorCommand::ManageSecurityEnvironment as u8, 0x02); + assert_eq!(VendorCommand::Unlock as u8, 0x03); + assert_eq!(VendorCommand::EnterpriseAttestation as u8, 0x04); + // PhysicalOptions(0x05) and Memory(0x06) are legacy <=v7.2 + assert_eq!(VendorCommand::PhysicalOptions as u8, 0x05); + assert_eq!(VendorCommand::Memory as u8, 0x06); + } + + // ── RS-Key vendor command ──────────────────────────────────────────────── + // Reference: RS-Key protocol docs §9 + + #[test] + fn test_rskey_vendor_cmd_value() { + assert_eq!(RSKEY_CTAPHID_VENDOR_CMD, 0x41); + } + + // ── Shared protocol constants ──────────────────────────────────────────── + // Reference: pico-fido src/fido/fido.h, src/fido/ctap.h + + #[test] + fn test_size_constants_match_firmware() { + assert_eq!(CTAP_APPID_SIZE, 32); + assert_eq!(CTAP_CHAL_SIZE, 32); + assert_eq!(CTAP_EC_KEY_SIZE, 32); + assert_eq!(CTAP_EC_POINT_SIZE, 65); + assert_eq!(CTAP_MAX_KH_SIZE, 128); + assert_eq!(KEY_HANDLE_LEN, 64); + assert_eq!(CTAP_MAX_EC_SIG_SIZE, 72); + assert_eq!(CTAP_CTR_SIZE, 4); + assert_eq!(MAX_PIN_RETRIES, 8); + assert_eq!(MAX_CREDENTIAL_COUNT_IN_LIST, 16); + assert_eq!(MAX_CRED_ID_LENGTH, 1024); + assert_eq!(MAX_RESIDENT_CREDENTIALS, 256); + assert_eq!(MAX_CREDBLOB_LENGTH, 128); + assert_eq!(MAX_MSG_SIZE, 1024); + assert_eq!(MAX_FRAGMENT_LENGTH, 960); + assert_eq!(MAX_LARGE_BLOB_SIZE, 2048); + } + + // ── Vendor config command IDs ──────────────────────────────────────────── + // Reference: pico-fido src/fido/ctap.h (for auth/enable/disable/EA/PIN) + // RS-Key protocol docs §11 (for physical config commands) + // + // NOTE: The auth encryption and PIN policy IDs in PicoForge do NOT match the + // current pico-fido ctap.h values. The ctap.h values are documented below + // for reference but the PicoForge values may target an older firmware version. + // + // Firmware ctap.h values: + // AuthEncryptionEnable: 0x00043f56b34285e2 + // AuthEncryptionDisable: 0x0001a40f04a25ed9 + // EnterpriseAttestationUpload: 0x0002a674c29a8dcf + // PinComplexityPolicy: 0x0007d70fe96c3897 + // + // PicoForge values (verified against RS-Key protocol for physical ones): + + #[test] + fn test_vendor_config_command_from_u64() { + assert_eq!( + VendorConfigCommand::from_u64(0x03e43f56b34285e2), + Some(VendorConfigCommand::AuthEncryptionEnable) + ); + assert_eq!( + VendorConfigCommand::from_u64(0x1831a40f04a25ed9), + Some(VendorConfigCommand::AuthEncryptionDisable) + ); + assert_eq!( + VendorConfigCommand::from_u64(0x66f2a674c29a8dcf), + Some(VendorConfigCommand::EnterpriseAttestationUpload) + ); + assert_eq!( + VendorConfigCommand::from_u64(0x6c07d70fe96c3897), + Some(VendorConfigCommand::PinComplexityPolicy) + ); + } + + #[test] + fn test_physical_vendor_config_ids_match_rskey_protocol() { + // These 4 values are verified against RS-Key protocol docs §11 + assert_eq!( + VendorConfigCommand::from_u64(0x6fcb19b0cbe3acfa), + Some(VendorConfigCommand::PhysicalVidPid) + ); + assert_eq!( + VendorConfigCommand::from_u64(0x7b392a394de9f948), + Some(VendorConfigCommand::PhysicalLedGpio) + ); + assert_eq!( + VendorConfigCommand::from_u64(0x76a85945985d02fd), + Some(VendorConfigCommand::PhysicalLedBrightness) + ); + assert_eq!( + VendorConfigCommand::from_u64(0x269f3b09eceb805f), + Some(VendorConfigCommand::PhysicalOptions) + ); + } + + #[test] + fn test_vendor_config_command_unknown_returns_none() { + assert!(VendorConfigCommand::from_u64(0).is_none()); + assert!(VendorConfigCommand::from_u64(0xDEADBEEF).is_none()); + } + + #[test] + fn test_vendor_config_command_display() { + assert_eq!( + VendorConfigCommand::AuthEncryptionEnable.to_string(), + "AuthEncryptionEnable" + ); + assert_eq!( + VendorConfigCommand::PhysicalVidPid.to_string(), + "PhysicalVidPid" + ); + } + + // ── FidoCertification ──────────────────────────────────────────────────── + + #[test] + fn test_fido_certification_from_str() { + assert_eq!( + FidoCertification::from_str("0x03E43F56B34285E2"), + Some(FidoCertification::AuthEncryption) + ); + assert_eq!( + FidoCertification::from_str("03E43F56B34285E2"), + Some(FidoCertification::AuthEncryption) + ); + assert_eq!( + FidoCertification::from_str("0x6FCB19B0CBE3ACFA"), + Some(FidoCertification::PhysicalVidPid) + ); + assert!(FidoCertification::from_str("unknown").is_none()); + } + + #[test] + fn test_fido_certification_display() { + assert_eq!( + FidoCertification::AuthEncryption.to_string(), + "Auth Encryption" + ); + assert_eq!( + FidoCertification::PhysicalVidPid.to_string(), + "Physical VID/PID" + ); + assert_eq!( + FidoCertification::PinComplexity.to_string(), + "PIN Complexity" + ); + } + + // ── Credential management sub-commands ──────────────────────────────────── + + #[test] + fn test_credential_mgmt_sub_command_values() { + assert_eq!(CredentialMgmtSubCommand::GetCredsMetadata as u8, 0x01); + assert_eq!(CredentialMgmtSubCommand::EnumerateRpsBegin as u8, 0x02); + assert_eq!(CredentialMgmtSubCommand::EnumerateRpsGetNextRp as u8, 0x03); + assert_eq!( + CredentialMgmtSubCommand::EnumerateCredentialsBegin as u8, + 0x04 + ); + assert_eq!( + CredentialMgmtSubCommand::EnumerateCredentialsGetNextCredential as u8, + 0x05 + ); + assert_eq!(CredentialMgmtSubCommand::DeleteCredential as u8, 0x06); + assert_eq!(CredentialMgmtSubCommand::UpdateUserInformation as u8, 0x07); + } + + // ── AAGUID ─────────────────────────────────────────────────────────────── + + #[test] + fn test_aaguid_is_pico_fido_default() { + let expected: [u8; 16] = [ + 0x89, 0xFB, 0x94, 0xB7, 0x06, 0xC9, 0x36, 0x73, 0x9B, 0x7E, 0x30, 0x52, 0x6D, 0x96, + 0x81, 0x45, + ]; + assert_eq!(AAGUID, expected); + assert_eq!(AAGUID.len(), 16); + } + + // ── Vendor command constants ───────────────────────────────────────────── + + #[test] + fn test_vendor_cbor_and_config_cmds() { + assert_eq!(CTAP_VENDOR_CBOR_CMD, 0xC1); + assert_eq!(CTAP_VENDOR_CONFIG_CMD, 0xC2); + } + + // ── GetInfo response key constants ─────────────────────────────────────── + // These are the CTAP 2.1 GetInfo response keys + + #[test] + fn test_get_info_response_keys() { + assert_eq!(Ctap2GetInfoKey::Versions as u8, 0x01); + assert_eq!(Ctap2GetInfoKey::Extensions as u8, 0x02); + assert_eq!(Ctap2GetInfoKey::Aaguid as u8, 0x03); + assert_eq!(Ctap2GetInfoKey::Options as u8, 0x04); + assert_eq!(Ctap2GetInfoKey::MaxMsgSize as u8, 0x05); + assert_eq!(Ctap2GetInfoKey::PinUvAuthProtocols as u8, 0x06); + assert_eq!(Ctap2GetInfoKey::MaxCredentialCountInList as u8, 0x07); + assert_eq!(Ctap2GetInfoKey::MaxCredentialIdLength as u8, 0x08); + assert_eq!(Ctap2GetInfoKey::FirmwareVersion as u8, 0x0E); + assert_eq!( + Ctap2GetInfoKey::RemainingDiscoverableCredentials as u8, + 0x14 + ); + } + + // ── MakeCredential param keys ──────────────────────────────────────────── + + #[test] + fn test_make_credential_param_keys() { + assert_eq!(MakeCredentialParam::ClientDataHash as u8, 0x01); + assert_eq!(MakeCredentialParam::Rp as u8, 0x02); + assert_eq!(MakeCredentialParam::User as u8, 0x03); + assert_eq!(MakeCredentialParam::PubKeyCredParams as u8, 0x04); + assert_eq!(MakeCredentialParam::ExcludeList as u8, 0x05); + assert_eq!(MakeCredentialParam::EnterpriseAttestation as u8, 0x0A); + } +} diff --git a/src/hal/fido/hid.rs b/src/hal/fido/hid.rs index ab375be..69c0b53 100644 --- a/src/hal/fido/hid.rs +++ b/src/hal/fido/hid.rs @@ -156,6 +156,7 @@ const HID_TOTAL_TIMEOUT_MS: i32 = 5000; /// /// 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. +#[derive(Debug)] pub struct HidTransport { device: hidapi::HidDevice, cid: u32, diff --git a/src/hal/fido/mod.rs b/src/hal/fido/mod.rs index 182eaa2..839cf82 100644 --- a/src/hal/fido/mod.rs +++ b/src/hal/fido/mod.rs @@ -59,9 +59,12 @@ pub mod hid; use crate::{ error::PFError, - hal::types::{ - AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FirmwareType, - FullDeviceStatus, PICOFIDO_AAGUID, RSKEY_AAGUID, StoredCredential, + hal::{ + firmwares::AnyFirmware, + types::{ + AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FirmwareType, + FullDeviceStatus, PICOFIDO_AAGUID, RSKEY_AAGUID, StoredCredential, + }, }, }; use base64::{Engine as _, engine::general_purpose}; @@ -374,18 +377,11 @@ fn parse_get_info_extension_list( } pub(crate) fn firmware_supports_legacy_fido_hardware_config(version: &str) -> bool { - let Some((major, minor)) = parse_firmware_version(version) else { + let ver = crate::hal::common::FirmwareVersion::parse(version); + let Some(ref ver) = ver else { return false; }; - - major < 7 || (major == 7 && minor <= 2) -} - -fn parse_firmware_version(version: &str) -> Option<(u16, u16)> { - let mut parts = version.split('.'); - let major = parts.next()?.parse().ok()?; - let minor = parts.next()?.parse().ok()?; - Some((major, minor)) + ver.major < 7 || (ver.major == 7 && ver.minor <= 2) } pub(crate) fn change_fido_pin( @@ -595,8 +591,15 @@ pub fn read_device_details() -> Result { fido_info.firmware_version ); - let supports_legacy_hardware_config = - firmware_supports_legacy_fido_hardware_config(&fido_info.firmware_version); + let firmware_type = if fido_info.aaguid == RSKEY_AAGUID { + FirmwareType::RSKey + } else if fido_info.aaguid == PICOFIDO_AAGUID { + FirmwareType::PicoFido + } else { + FirmwareType::Unknown + }; + let firmware = AnyFirmware::new(firmware_type, &fido_info.firmware_version); + let supports_legacy_hardware_config = firmware.supports_legacy_fido_hardware_config(); let management = read_management_info(&transport); let config = AppConfig { vid: format!("{:04X}", transport.vid), @@ -629,14 +632,6 @@ pub fn read_device_details() -> Result { .unwrap_or_else(|| "Unknown".to_string()) }; - let firmware_type = if fido_info.aaguid == RSKEY_AAGUID { - FirmwareType::RSKey - } else if fido_info.aaguid == PICOFIDO_AAGUID { - FirmwareType::PicoFido - } else { - FirmwareType::Unknown - }; - Ok(FullDeviceStatus { info: DeviceInfo { serial: management @@ -650,7 +645,7 @@ pub fn read_device_details() -> Result { secure_boot: false, secure_lock: false, method: DeviceMethod::Fido, - firmware_type, + firmware_type: firmware.firmware_type(), }) } @@ -830,8 +825,15 @@ pub fn write_config(config: AppConfigInput, pin: Option) -> Result would become "0.0" + map.insert(Value::Integer(0x0E), Value::Integer(0x0000)); + + let info = parse_fido_get_info(&Value::Map(map)).unwrap(); + assert_eq!(info.firmware_version, "0.0"); + } + + #[test] + fn test_is_empty_config_input_works() { + assert!(is_empty_config_input(&empty_config_input())); + let mut c = empty_config_input(); + c.led_gpio = Some(25); + assert!(!is_empty_config_input(&c)); + let mut c = empty_config_input(); + c.vid = Some("FEFF".to_string()); + assert!(!is_empty_config_input(&c)); + } } diff --git a/src/hal/firmwares/mod.rs b/src/hal/firmwares/mod.rs new file mode 100644 index 0000000..ed47b6e --- /dev/null +++ b/src/hal/firmwares/mod.rs @@ -0,0 +1,93 @@ +#![allow(dead_code)] +pub mod picofido; +pub mod rskey; + +pub use picofido::*; +pub use rskey::*; + +use crate::hal::common::FirmwareVersion; +use crate::hal::types::FirmwareType; + +#[derive(Debug, Clone)] +pub enum AnyFirmware { + PicoFido(PicoFidoFirmware), + RSKey(RSKeyFirmware), +} + +pub trait FirmwareTrait { + fn firmware_type(&self) -> FirmwareType; + fn version(&self) -> &FirmwareVersion; + fn major_minor(&self) -> (u16, u16) { + (self.version().major, self.version().minor) + } + fn version_str(&self) -> &str { + &self.version().raw + } + + fn supports_legacy_fido_hardware_config(&self) -> bool; + fn supports_rs_key_vendor_command(&self) -> bool; + fn supports_rescue_channel(&self) -> bool; +} + +impl AnyFirmware { + pub fn detect_by_aaguid(aaguid: &str) -> FirmwareType { + if aaguid == crate::hal::types::RSKEY_AAGUID { + FirmwareType::RSKey + } else if aaguid == crate::hal::types::PICOFIDO_AAGUID { + FirmwareType::PicoFido + } else { + FirmwareType::Unknown + } + } + + pub fn new(fw_type: FirmwareType, version: &str) -> Self { + let ver = FirmwareVersion::parse(version).unwrap_or_default(); + match fw_type { + FirmwareType::PicoFido => Self::PicoFido(PicoFidoFirmware::new(ver)), + FirmwareType::RSKey => Self::RSKey(RSKeyFirmware::new(ver)), + FirmwareType::Unknown => Self::PicoFido(PicoFidoFirmware::new(ver)), + } + } + + pub fn version(&self) -> &FirmwareVersion { + match self { + Self::PicoFido(fw) => fw.version(), + Self::RSKey(fw) => fw.version(), + } + } + + pub fn firmware_type(&self) -> FirmwareType { + match self { + Self::PicoFido(_) => FirmwareType::PicoFido, + Self::RSKey(_) => FirmwareType::RSKey, + } + } + + pub fn supports_legacy_fido_hardware_config(&self) -> bool { + match self { + Self::PicoFido(fw) => fw.supports_legacy_fido_hardware_config(), + Self::RSKey(fw) => fw.supports_legacy_fido_hardware_config(), + } + } + + pub fn supports_new_fido_hardware_config(&self) -> bool { + match self { + Self::PicoFido(fw) => !fw.supports_legacy_fido_hardware_config(), + Self::RSKey(_) => false, + } + } + + pub fn supports_rs_key_vendor_command(&self) -> bool { + match self { + Self::PicoFido(_) => false, + Self::RSKey(fw) => fw.supports_rs_key_vendor_command(), + } + } + + pub fn supports_rescue_channel(&self) -> bool { + match self { + Self::PicoFido(_) => true, + Self::RSKey(_) => true, + } + } +} diff --git a/src/hal/firmwares/picofido.rs b/src/hal/firmwares/picofido.rs new file mode 100644 index 0000000..533e594 --- /dev/null +++ b/src/hal/firmwares/picofido.rs @@ -0,0 +1,36 @@ +use crate::hal::common::FirmwareVersion; +use crate::hal::firmwares::FirmwareTrait; +use crate::hal::types::FirmwareType; + +#[derive(Debug, Clone)] +pub struct PicoFidoFirmware { + version: FirmwareVersion, +} + +impl PicoFidoFirmware { + pub fn new(version: FirmwareVersion) -> Self { + Self { version } + } +} + +impl FirmwareTrait for PicoFidoFirmware { + fn firmware_type(&self) -> FirmwareType { + FirmwareType::PicoFido + } + + fn version(&self) -> &FirmwareVersion { + &self.version + } + + fn supports_legacy_fido_hardware_config(&self) -> bool { + self.version.major < 7 || (self.version.major == 7 && self.version.minor <= 2) + } + + fn supports_rs_key_vendor_command(&self) -> bool { + false + } + + fn supports_rescue_channel(&self) -> bool { + true + } +} diff --git a/src/hal/firmwares/rskey.rs b/src/hal/firmwares/rskey.rs new file mode 100644 index 0000000..f4cc4f5 --- /dev/null +++ b/src/hal/firmwares/rskey.rs @@ -0,0 +1,36 @@ +use crate::hal::common::FirmwareVersion; +use crate::hal::firmwares::FirmwareTrait; +use crate::hal::types::FirmwareType; + +#[derive(Debug, Clone)] +pub struct RSKeyFirmware { + version: FirmwareVersion, +} + +impl RSKeyFirmware { + pub fn new(version: FirmwareVersion) -> Self { + Self { version } + } +} + +impl FirmwareTrait for RSKeyFirmware { + fn firmware_type(&self) -> FirmwareType { + FirmwareType::RSKey + } + + fn version(&self) -> &FirmwareVersion { + &self.version + } + + fn supports_legacy_fido_hardware_config(&self) -> bool { + false + } + + fn supports_rs_key_vendor_command(&self) -> bool { + self.version.is_at_least(0, 1) + } + + fn supports_rescue_channel(&self) -> bool { + true + } +} diff --git a/src/hal/io.rs b/src/hal/io.rs index 6e26dbb..7a6d205 100644 --- a/src/hal/io.rs +++ b/src/hal/io.rs @@ -1,31 +1,103 @@ -//! 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 +use crate::{ + error::PFError, + hal::{fido, rescue, types::*}, +}; -#![allow(unused)] - -use crate::{error::PFError, hal::fido, hal::rescue, hal::types::*}; - -// ── 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 { + let mut fido_status: Option = None; + let mut rescue_status: Option = None; + + match fido::read_device_details() { + Ok(status) => { + log::info!("FIDO device details read successfully"); + fido_status = Some(status); + } + Err(e) => log::warn!("FIDO read_device_details failed: {}", e), + } + match rescue::read_device_details() { - Ok(status) => Ok(status), - Err(e) => { - log::warn!("Rescue method failed: {}. Falling back to FIDO...", e); - fido::read_device_details() + Ok(status) => { + log::info!("Rescue device details read successfully"); + rescue_status = Some(status); + } + Err(e) => log::warn!("Rescue read_device_details failed: {}", e), + } + + match (fido_status, rescue_status) { + (Some(fido), Some(rescue)) => { + log::info!("Merging FIDO and Rescue device details"); + Ok(FullDeviceStatus { + info: DeviceInfo { + serial: rescue.info.serial, + flash_used: rescue.info.flash_used, + flash_total: rescue.info.flash_total, + firmware_version: fido.info.firmware_version, + }, + config: AppConfig { + vid: if !rescue.config.vid.is_empty() { + rescue.config.vid + } else { + fido.config.vid + }, + pid: if !rescue.config.pid.is_empty() { + rescue.config.pid + } else { + fido.config.pid + }, + led_gpio: rescue.config.led_gpio, + led_brightness: rescue.config.led_brightness, + led_dimmable: rescue.config.led_dimmable, + power_cycle_on_reset: rescue.config.power_cycle_on_reset, + led_steady: rescue.config.led_steady, + enable_secp256k1: rescue.config.enable_secp256k1, + led_driver: rescue.config.led_driver.or_else(|| { + if fido.config.led_driver.is_some() { + fido.config.led_driver + } else { + None + } + }), + product_name: rescue.config.product_name, + touch_timeout: rescue.config.touch_timeout, + raw_curves_mask: rescue.config.raw_curves_mask, + led_order: rescue.config.led_order, + enabled_usb_itf: rescue.config.enabled_usb_itf, + led_num: rescue.config.led_num, + }, + secure_boot: rescue.secure_boot, + secure_lock: rescue.secure_lock, + method: DeviceMethod::Fido, + firmware_type: fido.firmware_type, + }) + } + (Some(fido), None) => { + log::info!("Using FIDO-only device details"); + Ok(FullDeviceStatus { + firmware_type: fido.firmware_type, + ..fido + }) + } + (None, Some(rescue)) => { + log::info!("Using Rescue-only device details"); + Ok(rescue) + } + (None, None) => { + log::error!("Failed to read device details via both FIDO and Rescue"); + Err(PFError::NoDevice) } } } -/// Write app config. Dispatches to rescue or FIDO based on `method`. +#[allow(dead_code)] +pub fn enable_secure_boot(lock: bool) -> Result { + rescue::enable_secure_boot(lock) +} + +#[allow(dead_code)] +pub fn reboot(to_bootsel: bool) -> Result { + rescue::reboot_device(to_bootsel) +} + pub fn write_config( config: AppConfigInput, method: DeviceMethod, @@ -38,24 +110,10 @@ 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) -} - -/// Reboot the device. Pass `true` to enter BOOTSEL mode. -pub fn reboot(to_bootsel: bool) -> Result { - rescue::reboot_device(to_bootsel) -} - -/// 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, @@ -65,24 +123,18 @@ 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, @@ -90,7 +142,6 @@ pub(crate) fn change_fido_pin( 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, @@ -98,32 +149,26 @@ pub(crate) fn set_min_pin_length( 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/hal/mod.rs b/src/hal/mod.rs index 96ec7a0..821091f 100644 --- a/src/hal/mod.rs +++ b/src/hal/mod.rs @@ -39,7 +39,10 @@ //! and converts errors to the caller's expected type. //! 4. Wire the wrapper into a gpui-component view or action handler. +pub mod common; pub mod fido; +pub mod firmwares; pub mod io; pub mod rescue; +pub mod transport; pub mod types; diff --git a/src/hal/rescue/constants.rs b/src/hal/rescue/constants.rs index c5aad9d..93bcd67 100644 --- a/src/hal/rescue/constants.rs +++ b/src/hal/rescue/constants.rs @@ -124,10 +124,13 @@ pub enum RescueInstruction { /// Data field contains the tag value to write. Write = 0x1C, - /// Lock or unlock device access. + /// Lock or unlock device access (pico-fido only, not RS-Key). /// /// P2 parameter determines lock state (0x00=Unlock, 0x01=Lock). /// When locked, PHY configuration commands are rejected. + /// + /// **Note**: This instruction is only available on pico-fido firmware + /// (RP2350/ESP32). RS-Key uses `OtpLock = 0x1B` instead. Secure = 0x1D, /// Read hardware configuration from flash memory. @@ -241,7 +244,8 @@ pub const P2_UNUSED: u8 = 0x00; /// 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. +/// additional tags like `LedOrder` for RGB LED support and `LedNum` for +/// multi-LED count. /// /// References: /// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` @@ -308,6 +312,13 @@ pub enum PhyTag { /// Data format: `[ORDER]` (1 byte). /// RS-Key specific tag for configuring LED color channel order. LedOrder = 0x0D, + + /// Number of LEDs on the device (RS-Key extension). + /// + /// Data format: `[COUNT]` (1 byte). + /// RS-Key specific tag specifying how many individual LEDs + /// are present (e.g., 1 for single, 3 for RGB). + LedNum = 0x0E, } impl PhyTag { @@ -327,6 +338,7 @@ impl PhyTag { 0x0B => Some(Self::EnabledUsbItf), 0x0C => Some(Self::LedDriver), 0x0D => Some(Self::LedOrder), + 0x0E => Some(Self::LedNum), _ => None, } } @@ -344,6 +356,13 @@ impl PhyTag { /// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { pub struct RescueOptions: u16 { + /// Windows Compatible ID (WCID) support. + /// + /// When set, the device advertises WCID descriptors for + /// automatic driver installation on Windows without + /// requiring a custom .inf file. + const WCID = 0x01; + /// LED supports dimming (PWM control). /// /// When set, the LED brightness can be adjusted. When clear, diff --git a/src/hal/rescue/mod.rs b/src/hal/rescue/mod.rs index 05f6385..1ecd2c8 100644 --- a/src/hal/rescue/mod.rs +++ b/src/hal/rescue/mod.rs @@ -418,6 +418,11 @@ pub fn read_device_details() -> Result { config.led_order = Some(val[0]); } } + PhyTag::LedNum => { + if !val.is_empty() { + config.led_num = Some(val[0]); + } + } PhyTag::EnabledUsbItf => { if !val.is_empty() { config.enabled_usb_itf = Some(val[0]); @@ -633,6 +638,7 @@ pub fn write_config(config: AppConfigInput) -> Result { /// /// # Errors /// - `PFError::Device` if the APDU fails or returns a non-success status +#[allow(dead_code)] pub fn reboot_device(to_bootsel: bool) -> Result { let (card, _, _) = connect_and_select()?; @@ -681,6 +687,7 @@ pub fn reboot_device(to_bootsel: bool) -> Result { /// # Warning /// This function is unstable and may change. Locking secure boot can permanently /// prevent firmware downgrades. Use with caution. +#[allow(dead_code)] pub fn enable_secure_boot(lock: bool) -> Result { let (card, _, _) = connect_and_select()?; diff --git a/src/hal/transport/mod.rs b/src/hal/transport/mod.rs new file mode 100644 index 0000000..224864a --- /dev/null +++ b/src/hal/transport/mod.rs @@ -0,0 +1,89 @@ +#![allow(dead_code)] +use std::fmt; + +use crate::error::PFError; +use crate::hal::fido::hid::HidTransport; +use crate::hal::types::FirmwareType; + +pub enum DeviceHandle { + Fido(HidTransport), + Rescue(pcsc::Card, FirmwareType), +} + +impl fmt::Debug for DeviceHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Fido(t) => f.debug_tuple("Fido").field(t).finish(), + Self::Rescue(_, ft) => f.debug_tuple("Rescue").field(ft).finish(), + } + } +} + +#[derive(Debug)] +pub struct DeviceIdentity { + pub vid: u16, + pub pid: u16, + pub product_name: String, + pub firmware_type: FirmwareType, +} + +impl DeviceHandle { + pub fn discover() -> Result<(Self, DeviceIdentity), PFError> { + match Self::try_fido() { + Ok(Some((handle, identity))) => { + log::info!("Device discovered via FIDO HID transport"); + return Ok((handle, identity)); + } + Ok(None) => log::info!("No FIDO HID device found"), + Err(e) => log::warn!("FIDO HID discovery error: {}", e), + } + + match Self::try_rescue() { + Ok(Some((handle, identity))) => { + log::info!("Device discovered via Rescue PC/SC transport"); + return Ok((handle, identity)); + } + Ok(None) => log::info!("No Rescue PC/SC device found"), + Err(e) => log::warn!("Rescue PC/SC discovery error: {}", e), + } + + Err(PFError::NoDevice) + } + + fn try_fido() -> Result, PFError> { + let transport = HidTransport::open()?; + let identity = DeviceIdentity { + vid: transport.vid, + pid: transport.pid, + product_name: transport.product_name.clone(), + firmware_type: FirmwareType::Unknown, + }; + Ok(Some((Self::Fido(transport), identity))) + } + + fn try_rescue() -> Result, PFError> { + let ctx = pcsc::Context::establish(pcsc::Scope::User).map_err(PFError::Pcsc)?; + let mut readers_buf = [0; 2048]; + let mut readers = ctx.list_readers(&mut readers_buf).map_err(PFError::Pcsc)?; + let reader = match readers.next() { + Some(r) => r, + None => return Ok(None), + }; + let reader_name = reader.to_string_lossy(); + let fw_type = if reader_name.contains("RS-Key") || reader_name.contains("RSK") { + FirmwareType::RSKey + } else { + FirmwareType::Unknown + }; + let card = ctx + .connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY) + .map_err(PFError::Pcsc)?; + let identity = DeviceIdentity { + vid: 0, + pid: 0, + product_name: reader_name.to_string(), + firmware_type: fw_type.clone(), + }; + Ok(Some((Self::Rescue(card, fw_type), identity))) + } +} diff --git a/src/hal/types.rs b/src/hal/types.rs index 031748e..7a26dac 100644 --- a/src/hal/types.rs +++ b/src/hal/types.rs @@ -49,6 +49,8 @@ pub struct AppConfig { pub led_order: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enabled_usb_itf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub led_num: Option, } /// Partial config update; `None` fields are left unchanged on the device. @@ -69,6 +71,7 @@ pub struct AppConfigInput { pub raw_curves_mask: Option, pub led_order: Option, pub enabled_usb_itf: Option, + pub led_num: Option, } /// Aggregated snapshot of device info, config, and security state. diff --git a/src/ui/screens/config/view_model.rs b/src/ui/screens/config/view_model.rs index d649b5b..308888f 100644 --- a/src/ui/screens/config/view_model.rs +++ b/src/ui/screens/config/view_model.rs @@ -677,6 +677,7 @@ impl ConfigViewModel { raw_curves_mask, led_order, enabled_usb_itf: final_enabled_usb_itf, + led_num: None, }; if method == DeviceMethod::Fido {