Merge pull request #32 from librekeys/set-min-pin

Fix #30
This commit is contained in:
Suyog Tandel
2026-01-23 11:41:29 +05:30
committed by GitHub
5 changed files with 130 additions and 9 deletions
+5
View File
@@ -15,6 +15,7 @@ let
webkitgtk_4_1
pcsclite
hidapi
mesa
];
packages = with pkgs; [
@@ -58,6 +59,10 @@ pkgs.mkShell {
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
export XDG_DATA_DIRS=$GSETTINGS_SCHEMAS_PATH:$XDG_DATA_DIRS
# Try to uncomment the following lines if you encounter EGL_BAD_PARAMETER errors:
# export LIBGL_ALWAYS_SOFTWARE=1
# export WEBKIT_DISABLE_COMPOSITING_MODE=1
echo "Nix development environment loaded!"
echo "Available tools: rustc, cargo, deno, node, tauri"
'';
+90
View File
@@ -372,4 +372,94 @@ 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
match self.send_cbor(CTAPHID_CBOR, &payload) {
Ok(_) => {
log::info!(
"Successfully set minimum PIN length to {}",
new_min_pin_length
);
Ok(())
}
Err(e) => {
let err_str = e.to_string();
log::error!("Failed to send setMinPINLength config: {}", err_str);
// Check for PIN policy violation (0x37) - cannot decrease min PIN length
if err_str.contains("0x37") {
return Err(PFError::Device(
"Cannot decrease minimum PIN length. The FIDO2 security policy only allows increasing the minimum PIN length, not decreasing it. A device reset is required to lower the minimum.".into()
));
}
Err(PFError::Device(format!("setMinPINLength failed: {}", e)))
}
}
}
}
+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 {}",
@@ -72,7 +72,7 @@
<AlertDialog.Cancel onclick={() => (configState.minPinDialogOpen = false)}
>Cancel</AlertDialog.Cancel
>
<AlertDialog.Action onclick={configState.handleMinPinChange}
<AlertDialog.Action onclick={() => configState.handleMinPinChange()}
>Update</AlertDialog.Action
>
</AlertDialog.Footer>
+2 -2
View File
@@ -49,14 +49,14 @@
</Button>
</div>
<div class="flex items-center justify-between p-4 border rounded-lg opacity-60">
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
<p class="font-medium">Minimum PIN Length</p>
<p class="text-sm text-muted-foreground">
Current: {device.fidoInfo?.minPinLength || 4} characters
</p>
</div>
<Button variant="outline" disabled={true}>Update Minimum Length</Button>
<Button variant="outline" disabled={!device.fidoInfo?.options?.clientPin} onclick={() => state.openMinPinDialog()}>Update Minimum Length</Button>
</div>
</Card.Content>
</Card.Root>