implement custom HidTransport to send set_min_pin_length command because ctap-hid-fido2 set_min_pin_length has a bug

This commit is contained in:
Fabrice Bellamy
2026-01-23 00:34:16 +01:00
parent abf93f6e55
commit ecae9ee5af
2 changed files with 110 additions and 6 deletions
+78
View File
@@ -372,4 +372,82 @@ impl HidTransport {
Ok(())
}
/// Send authenticatorConfig command to set minimum PIN length.
///
/// This bypasses the ctap-hid-fido2 library which has a bug where it sends
/// 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.
pub fn send_config_set_min_pin_length(
&self,
pin_token: &[u8],
new_min_pin_length: u8,
) -> Result<(), PFError> {
log::debug!(
"Sending setMinPINLength config command (new length: {})...",
new_min_pin_length
);
// Build subCommandParams (Key 0x02): { 0x01: newMinPINLength }
let mut sub_params_map = BTreeMap::new();
sub_params_map.insert(
Value::Integer(ConfigSubCommandParam::NewMinPinLength as i128),
Value::Integer(new_min_pin_length as i128),
);
let sub_params = Value::Map(sub_params_map);
let sub_params_bytes = to_vec(&sub_params).map_err(|e| PFError::Io(e.to_string()))?;
// Build HMAC message for signing
// Per FIDO 2.1 spec: authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams)
let mut message = vec![0xff; 32];
message.push(CtapCommand::Config as u8); // 0x0d
message.push(ConfigSubCommand::SetMinPinLength as u8); // 0x03
message.extend(&sub_params_bytes);
// Sign using provided PIN token (Protocol 1 uses HMAC-SHA256, first 16 bytes)
use ring::hmac;
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token);
let sig = hmac::sign(&hmac_key, &message);
let pin_auth = sig.as_ref()[0..16].to_vec();
// Build full authenticatorConfig map with keys in ASCENDING ORDER
// This is critical - the firmware parser rejects out-of-order keys with CTAP2_ERR_INVALID_CBOR
let mut config_map = BTreeMap::new();
config_map.insert(
Value::Integer(ConfigParam::SubCommand as i128), // 0x01
Value::Integer(ConfigSubCommand::SetMinPinLength as i128), // 0x03
);
config_map.insert(
Value::Integer(ConfigParam::SubCommandParams as i128), // 0x02
sub_params,
);
config_map.insert(
Value::Integer(ConfigParam::PinUvAuthProtocol as i128), // 0x03
Value::Integer(1), // PIN protocol version 1
);
config_map.insert(
Value::Integer(ConfigParam::PinUvAuthParam as i128), // 0x04
Value::Bytes(pin_auth),
);
let config_payload_cbor =
to_vec(&Value::Map(config_map)).map_err(|e| PFError::Io(e.to_string()))?;
// Prepend CTAP command byte
let mut payload = vec![CtapCommand::Config as u8];
payload.extend(config_payload_cbor);
// Send via HID
self.send_cbor(CTAPHID_CBOR, &payload).map_err(|e| {
log::error!("Failed to send setMinPINLength config: {}", e);
PFError::Device(format!("setMinPINLength failed: {}", e))
})?;
log::info!(
"Successfully set minimum PIN length to {}",
new_min_pin_length
);
Ok(())
}
}
+32 -6
View File
@@ -79,13 +79,39 @@ pub(crate) fn set_min_pin_length(
current_pin: String,
min_pin_length: u8,
) -> Result<String, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| format!("Failed to connect to FIDO device: {:?}", e))?;
log::info!("Starting set_min_pin_length (custom implementation)...");
device
.set_min_pin_length(min_pin_length, Some(&current_pin))
.map_err(|e| format!("Failed to set minimum PIN length: {:?}", e))?;
// 1. Obtain PIN token using the library handle
let pin_token = {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| format!("Could not connect to FIDO device: {:?}", e))?;
use ctap_hid_fido2::fidokey::pin::Permission;
// Obtain a token with AuthenticatorConfiguration permission (CTAP 2.1)
match device.get_pinuv_auth_token_with_permission(
&current_pin,
Permission::AuthenticatorConfiguration,
) {
Ok(token) => {
log::debug!("Successfully obtained PIN token with ACFG permission.");
token.key
}
Err(e) => {
log::error!("Failed to get PIN token with ACFG permission: {:?}", e);
return Err(format!("Failed to obtain PIN token: {:?}", e));
}
}
// Library handle 'device' is dropped here, closing the HID session.
};
// 2. Open custom HidTransport and send command using the token because ctap-hid-fido2 has a bug where it sends 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 requires ascending order.
let transport =
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
transport
.send_config_set_min_pin_length(&pin_token, min_pin_length)
.map_err(|e| format!("Failed to set minimum PIN length: {}", e))?;
Ok(format!(
"Minimum PIN length successfully set to {}",