Files
picoforge/src/device/rescue/mod.rs
T

957 lines
32 KiB
Rust

//! Rescue applet implementation for pico-fido and RS-Key firmware.
//!
//! ```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;
use crate::device::{rescue::constants::*, types::*};
use crate::error::PFError;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use pcsc::{Context, Protocols, Scope, ShareMode};
use std::io::Cursor;
/// Establishes a PC/SC connection to the first available smart card reader and selects the Rescue Applet.
///
/// Sends a SELECT APDU (`00 A4 04 04 08 A0 58 3F C1 9B 7E 4F 21`) to the device via the CCID interface.
/// The response contains device identity data (MCU type, product type, firmware version, serial number).
///
/// # Returns
/// A tuple of `(Card, SelectResponse, FirmwareType)` where:
/// - `Card` is the active PC/SC card handle for subsequent APDU exchanges
/// - `SelectResponse` is the raw FCI/identity data from the SELECT command
/// - `FirmwareType` is detected as `RSKey`, `PicoFido`, or `Unknown`
///
/// # Errors
/// - `PFError::NoDevice` if no smart card reader is found
/// - `PFError::Pcsc` if the PC/SC context cannot be established
/// - `PFError::Device` if the Rescue Applet is not found (wrong AID or device in wrong mode)
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>, FirmwareType), PFError> {
let ctx = Context::establish(Scope::User).map_err(|e| {
log::error!("Failed to establish PCSC context: {}", e);
PFError::Pcsc(e)
})?;
let mut readers_buf = [0; 2048];
let mut readers = ctx.list_readers(&mut readers_buf)?;
// Use the first reader found
let reader = readers.next().ok_or_else(|| {
log::info!("No Smart Card Reader found");
PFError::NoDevice
})?;
let reader_name = reader.to_string_lossy();
let mut fw_type = if reader_name.contains("RS-Key") || reader_name.contains("RSK") {
FirmwareType::RSKey
} else {
FirmwareType::Unknown
};
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
// Select Applet APDU: 00 A4 04 04 [Len] [AID]
let mut apdu = vec![
APDU_CLA_ISO,
APDU_INS_SELECT,
APDU_P1_SELECT_BY_DF_NAME,
APDU_P2_RETURN_FCI,
RESCUE_AID.len() as u8,
];
apdu.extend_from_slice(RESCUE_AID);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
// Check Success (0x90 0x00)
if !rx.ends_with(&[0x90, 0x00]) {
log::error!("Rescue Applet not found on the device!");
return Err(PFError::Device(
// There is no such mode as fido, i tink the rescue applet stays active and at the same time fido mode works?
// Need to study this more.
"Rescue Applet not found on device. Is it in FIDO mode?".into(),
));
}
let data = rx.to_vec();
if fw_type == FirmwareType::Unknown {
if data.len() >= 4 && data[2] >= 8 {
fw_type = FirmwareType::RSKey;
} else {
fw_type = FirmwareType::PicoFido;
}
}
log::info!("Successfully connected to Rescue Applet");
log::info!("Detected firmware type: {:?}", fw_type);
Ok((card, data, fw_type))
}
/// Reads comprehensive device details including identity, flash usage, secure boot status, and PHY configuration.
///
/// Performs three sequential APDU operations after applet selection:
/// 1. SELECT response is parsed for MCU type, firmware version, and serial number
/// 2. `READ(FlashInfo)` — reads flash usage statistics (free, used, total)
/// 3. `READ(SecureBootStatus)` — reads secure boot enable/lock state
/// 4. `READ(PhyConfig)` — reads TLV-encoded hardware configuration (VID/PID, LED, curves, etc.)
///
/// # Returns
/// A `FullDeviceStatus` struct containing device info, parsed PHY config, and secure boot state.
///
/// # Errors
/// - `PFError::Device` if the SELECT response is malformed or any READ command fails
/// - `PFError::NoDevice` if no reader is available
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
log::info!("Reading full device details");
let (card, select_resp, fw_type) = connect_and_select()?;
log::info!("Select Response: {:?}", select_resp);
// FIX: Relax the length check.
// Minimum valid response is 4 bytes data + 2 bytes SW = 6 bytes.
if select_resp.len() < 6 {
log::error!("Invalid select response length: {}", select_resp.len());
return Err(PFError::Device("Invalid select response".into()));
}
let version_major = select_resp[2];
let version_minor = select_resp[3];
// FIX: Handle missing Serial Number safely
// If the firmware sends 14 bytes, we have a serial. If it sends 6, we don't.
let serial_str = if select_resp.len() >= 14 {
hex::encode_upper(&select_resp[4..12])
} else {
log::warn!(
"Device did not return a Serial Number (Firmware mismatch?). Using placeholder."
);
"00000000".to_string()
};
log::info!("Device Version: {}.{}", version_major, version_minor);
log::info!("Device Serial: {}", serial_str);
// 2. Read Flash Info
let mut rx_buf = [0; 256];
let rx_flash = card.transmit(
&[
APDU_CLA_PROPRIETARY,
RescueInstruction::Read as u8,
ReadParam::FlashInfo as u8,
P2_UNUSED,
0x00, // Le
],
&mut rx_buf,
)?;
if !rx_flash.ends_with(&SW_SUCCESS) {
return Err(PFError::Device("Failed to read flash".into()));
}
let mut rdr = Cursor::new(&rx_flash[..rx_flash.len() - 2]);
let _free = rdr.read_u32::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().unwrap_or(0);
// NOTE: captured but currently unused variables
let _nfiles = rdr.read_u32::<BigEndian>().unwrap_or(0);
let _chip_size = rdr.read_u32::<BigEndian>().unwrap_or(0);
// --- Read Secure Boot Status ---
let rx_secure = card.transmit(
&[
APDU_CLA_PROPRIETARY,
RescueInstruction::Read as u8,
ReadParam::SecureBootStatus as u8,
P2_UNUSED,
0x00,
],
&mut rx_buf,
)?;
let (sb_enabled, sb_locked) = if rx_secure.ends_with(&[0x90, 0x00]) && rx_secure.len() >= 4 {
(rx_secure[0] != 0, rx_secure[1] != 0)
} else {
(false, false)
}; // --- Read PHY Config ---
let rx_phy = card.transmit(
&[
APDU_CLA_PROPRIETARY,
RescueInstruction::Read as u8,
ReadParam::PhyConfig as u8,
0x01,
0x00,
],
&mut rx_buf,
)?;
if !rx_phy.ends_with(&[0x90, 0x00]) {
return Err(PFError::Device("Failed to read config".into()));
}
// Parse TLV
let mut config = AppConfig::default();
let data = &rx_phy[..rx_phy.len() - 2];
let mut i = 0;
while i < data.len() {
if i + 2 > data.len() {
break;
}
let tag_byte = data[i];
let len = data[i + 1] as usize;
i += 2;
if i + len > data.len() {
break;
}
let val = &data[i..i + len];
if let Some(tag) = PhyTag::from_u8(tag_byte) {
match tag {
PhyTag::VidPid => {
if val.len() == 4 {
let vid = u16::from_be_bytes([val[0], val[1]]);
let pid = u16::from_be_bytes([val[2], val[3]]);
config.vid = format!("{:04X}", vid);
config.pid = format!("{:04X}", pid);
}
}
PhyTag::LedGpio => {
if !val.is_empty() {
config.led_gpio = val[0];
}
}
PhyTag::LedBrightness => {
if !val.is_empty() {
config.led_brightness = val[0];
}
}
PhyTag::PresenceTimeout => {
if !val.is_empty() {
config.touch_timeout = val[0];
}
}
PhyTag::UsbProduct => {
let s = std::str::from_utf8(val)
.unwrap_or("")
.trim_matches(char::from(0));
config.product_name = s.to_string();
}
PhyTag::Opts => {
if val.len() >= 2 {
let opts_val = u16::from_be_bytes([val[0], val[1]]);
let opts = RescueOptions::from_bits_truncate(opts_val);
config.led_dimmable = opts.contains(RescueOptions::LED_DIMMABLE);
config.power_cycle_on_reset =
!opts.contains(RescueOptions::DISABLE_POWER_RESET);
config.led_steady = opts.contains(RescueOptions::LED_STEADY);
}
}
PhyTag::Curves => {
if val.len() == 4 {
let curves_val = u32::from_be_bytes([val[0], val[1], val[2], val[3]]);
config.raw_curves_mask = Some(curves_val);
let curves = RescueCurves::from_bits_truncate(curves_val);
config.enable_secp256k1 = curves.contains(RescueCurves::SECP256K1);
}
}
PhyTag::LedDriver => {
if !val.is_empty() {
config.led_driver = Some(val[0]);
}
}
PhyTag::LedOrder => {
if !val.is_empty() {
config.led_order = Some(val[0]);
}
}
PhyTag::EnabledUsbItf => {
if !val.is_empty() {
config.enabled_usb_itf = Some(val[0]);
}
}
}
}
i += len;
}
log::info!(
"Successfully read device details - Serial: {}, Firmware: {}.{}",
serial_str,
version_major,
version_minor
);
Ok(FullDeviceStatus {
info: DeviceInfo {
serial: serial_str,
flash_used: Some(used / 1024),
flash_total: Some(total / 1024),
firmware_version: format!("{}.{}", version_major, version_minor),
},
config,
secure_boot: sb_enabled,
secure_lock: sb_locked,
method: DeviceMethod::Rescue,
firmware_type: fw_type,
})
}
/// Writes PHY configuration to the device via the Rescue Applet's WRITE command.
///
/// Constructs a TLV (Tag-Length-Value) blob from the provided `AppConfigInput` fields and sends
/// it as a single APDU: `80 1C 01 00 [Lc] [TLV Data]`. Supported tags include:
/// - `0x00`: VID:PID (4 bytes, big-endian)
/// - `0x04`: LED GPIO pin
/// - `0x05`: LED brightness
/// - `0x08`: Touch/presence timeout
/// - `0x06`: Options bitmask (LED_DIMMABLE, DISABLE_POWER_RESET, LED_STEADY)
/// - `0x07`: Elliptic curves bitmask (SECP256K1, etc.)
/// - `0x0C`: LED driver selection
/// - `0x09`: USB product name (null-terminated)
/// - `0x0D`: LED order (RS-Key extension)
/// - `0x0B`: Enabled USB interfaces (CCID bit is always forced on for safety)
///
/// # Returns
/// A success message string on `SW 9000`.
///
/// # Errors
/// - `PFError::Io` if VID/PID are not valid hex strings
/// - `PFError::Device` if the WRITE APDU fails or returns a non-success status
/// - `PFError::Io` if the product name exceeds 32 bytes
pub fn write_config(config: AppConfigInput) -> Result<String, PFError> {
log::info!("Writing configuration to device");
log::debug!("Config input: {:?}", config);
// 1. Construct TLV Blob
let mut tlv = Vec::new();
// VID:PID (Tag 0x00)
if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) {
let vid =
u16::from_str_radix(vid_str, 16).map_err(|_| PFError::Io("Invalid VID".into()))?;
let pid =
u16::from_str_radix(pid_str, 16).map_err(|_| PFError::Io("Invalid PID".into()))?;
tlv.push(PhyTag::VidPid as u8);
tlv.push(0x04);
tlv.write_u16::<BigEndian>(vid).unwrap();
tlv.write_u16::<BigEndian>(pid).unwrap();
}
// LED GPIO (Tag 0x04)
if let Some(val) = config.led_gpio {
tlv.push(PhyTag::LedGpio as u8);
tlv.push(0x01);
tlv.push(val);
}
// LED Brightness (Tag 0x05)
if let Some(val) = config.led_brightness {
tlv.push(PhyTag::LedBrightness as u8);
tlv.push(0x01);
tlv.push(val);
}
// Touch Timeout (Tag 0x08)
if let Some(val) = config.touch_timeout {
tlv.push(PhyTag::PresenceTimeout as u8);
tlv.push(0x01);
tlv.push(val);
}
// Options
if let (Some(dim), Some(cycle), Some(steady)) = (
config.led_dimmable,
config.power_cycle_on_reset,
config.led_steady,
) {
let mut opts = RescueOptions::empty();
if dim {
opts.insert(RescueOptions::LED_DIMMABLE);
}
if !cycle {
opts.insert(RescueOptions::DISABLE_POWER_RESET);
}
if steady {
opts.insert(RescueOptions::LED_STEADY);
}
tlv.push(PhyTag::Opts as u8);
tlv.push(0x02);
tlv.write_u16::<BigEndian>(opts.bits()).unwrap();
}
// Curves
if config.enable_secp256k1.is_some() || config.raw_curves_mask.is_some() {
let mut mask = config.raw_curves_mask.unwrap_or(0);
if let Some(enabled) = config.enable_secp256k1 {
if enabled {
mask |= RescueCurves::SECP256K1.bits();
} else {
mask &= !RescueCurves::SECP256K1.bits();
}
}
tlv.push(PhyTag::Curves as u8);
tlv.push(0x04);
tlv.write_u32::<BigEndian>(mask).unwrap();
}
// LED Driver (Tag 0x0C)
if let Some(val) = config.led_driver {
tlv.push(PhyTag::LedDriver as u8);
tlv.push(0x01);
tlv.push(val);
}
// Product Name (Tag 0x09)
if let Some(name) = config.product_name.filter(|n| !n.is_empty()) {
let name_bytes = name.as_bytes();
let len = name_bytes.len() + 1;
if len > 32 {
return Err(PFError::Io("Product name too long".into()));
}
tlv.push(PhyTag::UsbProduct as u8);
tlv.push(len as u8);
tlv.extend_from_slice(name_bytes);
tlv.push(0x00);
}
// LED Order (Tag 0x0D) — RS-Key extension, silently preserved
if let Some(val) = config.led_order {
tlv.push(PhyTag::LedOrder as u8);
tlv.push(0x01);
tlv.push(val);
}
// Enabled USB Interfaces (Tag 0x0B)
if let Some(val) = config.enabled_usb_itf {
tlv.push(PhyTag::EnabledUsbItf as u8);
tlv.push(0x01);
// SAFETY: Never write a mask without CCID, otherwise Rescue applet is unreachable.
tlv.push(val | UsbInterfaces::CCID.bits());
}
// 2. Connect and Send
if tlv.is_empty() {
log::warn!("No configuration changes to apply");
return Ok("No changes to apply".into());
}
log::debug!("TLV payload size: {} bytes", tlv.len());
let (card, _, _) = connect_and_select()?;
// APDU: 80 1C 01 00 [Lc] [Data]
let mut apdu = vec![
APDU_CLA_PROPRIETARY,
RescueInstruction::Write as u8,
WriteParam::PhyConfig as u8,
P2_UNUSED,
tlv.len() as u8, // Lc
];
apdu.extend_from_slice(&tlv);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&[0x90, 0x00]) {
log::info!("Configuration applied successfully");
Ok("Configuration Applied Successfully".into())
} else {
log::error!("Configuration write failed: {:02X?}", rx);
Err(PFError::Device(format!("Write failed: {:02X?}", rx)))
}
}
/// Reboots the device, optionally entering BOOTSEL (mass storage) mode for firmware updates.
///
/// Sends a REBOOT APDU: `80 1B [P1] 00 00` where:
/// - `P1 = 0x00` (`RebootParam::Normal`): Reboots into normal FIDO mode
/// - `P1 = 0x01` (`RebootParam::Bootsel`): Reboots into BOOTSEL/UF2 bootloader mode
///
/// # Arguments
/// * `to_bootsel` - If `true`, device enters UF2 bootloader mode for firmware flashing.
/// If `false`, device performs a normal reboot into FIDO mode.
///
/// # Returns
/// A confirmation string if the reboot command was accepted.
///
/// # Errors
/// - `PFError::Device` if the APDU fails or returns a non-success status
pub fn reboot_device(to_bootsel: bool) -> Result<String, PFError> {
let (card, _, _) = connect_and_select()?;
let param = if to_bootsel {
RebootParam::Bootsel
} else {
RebootParam::Normal
};
let apdu = [
APDU_CLA_PROPRIETARY,
RescueInstruction::Reboot as u8,
param as u8,
P2_UNUSED,
0x00,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&SW_SUCCESS) {
Ok("Reboot command sent".into())
} else {
Err(PFError::Device(format!("Reboot failed: {:02X?}", rx)))
}
}
/// Enables or disables secure boot on the device. **UNSTABLE — work in progress.**
///
/// Sends a SECURE APDU: `80 1D 00 [LockBool] 00` where:
/// - `LockBool = 0x01`: Enable and lock secure boot (irreversible on some firmware)
/// - `LockBool = 0x00`: Disable secure boot
///
/// Uses pico-fido instruction `INS_SECURE` (0x1D). RS-Key uses `INS_OTP_LOCK` (0x1B)
/// for OTP fuse locking, which is a different operation.
///
/// # Arguments
/// * `lock` - If `true`, enables secure boot with lock (may be irreversible).
///
/// # Returns
/// A confirmation string if the secure boot command was accepted.
///
/// # Errors
/// - `PFError::Device` if the APDU fails or returns a non-success status
///
/// # Warning
/// This function is unstable and may change. Locking secure boot can permanently
/// prevent firmware downgrades. Use with caution.
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
let (card, _, _) = connect_and_select()?;
// APDU: 80 1D [KeyIndex] [LockBool] 00
// KeyIndex = 0 (Default), LockBool = 1 if true
let lock_byte = if lock { 0x01 } else { 0x00 };
let apdu = [
APDU_CLA_PROPRIETARY,
RescueInstruction::Secure as u8,
0x00, // Boot Key Index (0 = Default)
lock_byte as u8,
0x00,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&[0x90, 0x00]) {
Ok("Secure Boot Enabled".into())
} else {
Err(PFError::Device(format!("Secure Boot failed: {:02X?}", rx)))
}
}
// --- Vendor/LED Applet (RS-Key) ---
/// Establishes a PC/SC connection and selects a specific vendor applet by AID.
///
/// Unlike [`connect_and_select`] which selects the Rescue Applet, this function
/// selects an arbitrary applet (e.g., LED applet `F0 00 00 00 01` or Management applet).
/// Sends a SELECT APDU: `00 A4 04 00 [Len] [AID] 00`.
///
/// # Arguments
/// * `aid` - The Application Identifier of the target applet (e.g., `VENDOR_LED_AID`, `MANAGEMENT_AID`)
///
/// # Returns
/// An active `pcsc::Card` handle ready for APDU exchange with the selected applet.
///
/// # Errors
/// - `PFError::NoDevice` if no smart card reader is found
/// - `PFError::Pcsc` if the PC/SC context cannot be established
/// - `PFError::Device` if the applet is not found (AID not recognized by firmware)
fn connect_and_select_aid(aid: &[u8]) -> Result<pcsc::Card, PFError> {
let ctx = Context::establish(Scope::User).map_err(|e| {
log::error!("Failed to establish PCSC context: {}", e);
PFError::Pcsc(e)
})?;
let mut readers_buf = [0; 2048];
let mut readers = ctx.list_readers(&mut readers_buf)?;
let reader = readers.next().ok_or_else(|| {
log::info!("No Smart Card Reader found");
PFError::NoDevice
})?;
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
let mut apdu = vec![
APDU_CLA_ISO,
APDU_INS_SELECT,
APDU_P1_SELECT_BY_DF_NAME,
0x00,
aid.len() as u8,
];
apdu.extend_from_slice(aid);
apdu.push(0x00);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if !rx.ends_with(&[0x90, 0x00]) {
return Err(PFError::Device(format!(
"Applet not found (AID {:02X?})",
aid
)));
}
Ok(card)
}
/// Reads the customized LED status configurations from the Vendor/LED applet.
///
/// Communicates with the `F0 00 00 00 01` applet to retrieve a 9-byte configuration block
/// that dictates the color and brightness for each device state (idle, processing, touch, boot),
/// as well as the global 'steady' toggle flag.
pub fn read_led_config() -> Result<LedStatusConfig, PFError> {
log::info!("Reading LED status config from Vendor/LED applet");
let card = connect_and_select_aid(VENDOR_LED_AID)?;
let apdu = [
APDU_CLA_ISO,
VendorLedInstruction::GetLed as u8,
0x00,
0x00,
0x00,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if !rx.ends_with(&SW_SUCCESS) || rx.len() < 11 {
return Err(PFError::Device("Failed to read LED config".into()));
}
let data = &rx[..rx.len() - 2];
if data.len() < 9 {
return Err(PFError::Device("LED config response too short".into()));
}
let steady = data[0] != 0;
let mut statuses = [(0u8, 0u8); 4];
for s in 0..4 {
statuses[s] = (data[1 + 2 * s], data[2 + 2 * s]);
}
log::info!("LED config: steady={}, statuses={:?}", steady, statuses);
Ok(LedStatusConfig { steady, statuses })
}
/// Applies an individual LED status update to the Vendor/LED applet.
///
/// Constructs the APDU payload combining the targeted status index, color code, and global
/// steady flag into `P2`, with the brightness value in `P1`. The update is persisted to flash
/// and applied immediately.
pub fn write_led_status(
status: u8,
color: u8,
brightness: u8,
steady: bool,
) -> Result<String, PFError> {
log::info!(
"Setting LED: status={}, color={}, brightness={}, steady={}",
status,
color,
brightness,
steady
);
let card = connect_and_select_aid(VENDOR_LED_AID)?;
let steady_bit: u8 = if steady { 0x08 } else { 0x00 };
let p2 = (color & 0x07) | steady_bit | ((status & 0x03) << 4);
let apdu = [
APDU_CLA_ISO,
VendorLedInstruction::SetLed as u8,
brightness,
p2,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&SW_SUCCESS) {
Ok("LED status updated".into())
} else {
Err(PFError::Device(format!("SET LED failed: {:02X?}", rx)))
}
}
// --- Management Applet (RS-Key) ---
/// Retrieves the device management configuration mapping from the Management applet.
///
/// Reads the active state of various USB interfaces (U2F, OATH, PIV, OpenPGP, etc.) to
/// determine which are supported by the hardware and which are currently enabled by the user.
pub fn read_management_config() -> Result<ManagementAppConfig, PFError> {
log::info!("Reading management config from Management applet");
let card = connect_and_select_aid(MANAGEMENT_AID)?;
let apdu = [
APDU_CLA_ISO,
ManagementInstruction::ReadConfig as u8,
0x00,
0x00,
0x00,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if !rx.ends_with(&SW_SUCCESS) {
return Err(PFError::Device("Failed to read management config".into()));
}
let data = &rx[..rx.len() - 2];
if data.is_empty() {
return Err(PFError::Device("Empty management config response".into()));
}
let overall_len = data[0] as usize;
let tlv_data = if data.len() > 1 + overall_len {
&data[1..1 + overall_len]
} else {
&data[1..]
};
let mut config = ManagementAppConfig::default();
let mut i = 0;
while i < tlv_data.len() {
if i + 2 > tlv_data.len() {
break;
}
let tag = tlv_data[i];
let len = tlv_data[i + 1] as usize;
i += 2;
if i + len > tlv_data.len() {
break;
}
let val = &tlv_data[i..i + len];
match tag {
MGMT_TAG_USB_SUPPORTED => {
if val.len() >= 2 {
config.usb_supported = u16::from_be_bytes([val[0], val[1]]);
}
}
MGMT_TAG_USB_ENABLED => {
if val.len() >= 2 {
config.usb_enabled = u16::from_be_bytes([val[0], val[1]]);
}
}
_ => {
log::trace!("Management TLV tag 0x{:02X} skipped", tag);
}
}
i += len;
}
log::info!(
"Management config: supported=0x{:04X}, enabled=0x{:04X}",
config.usb_supported,
config.usb_enabled
);
Ok(config)
}
/// Persists updated management endpoint configurations to the device.
///
/// Overwrites the previously enabled interfaces with a new configuration bitmask.
/// For the changes to fully apply across all composite USB endpoints, a subsequent
/// device reboot or re-plug is required.
pub fn write_management_config(enabled_mask: u16) -> Result<String, PFError> {
log::info!("Writing management config: enabled=0x{:04X}", enabled_mask);
let card = connect_and_select_aid(MANAGEMENT_AID)?;
let inner = [
MGMT_TAG_USB_ENABLED,
0x02,
(enabled_mask >> 8) as u8,
(enabled_mask & 0xFF) as u8,
];
let mut apdu = vec![
APDU_CLA_ISO,
ManagementInstruction::WriteConfig as u8,
0x00,
0x00,
(inner.len() + 1) as u8,
inner.len() as u8,
];
apdu.extend_from_slice(&inner);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&SW_SUCCESS) {
Ok("USB applications updated".into())
} else {
Err(PFError::Device(format!(
"Management write failed: {:02X?}",
rx
)))
}
}