Skip to content

fix(pin): honor getInfo PIN policy and NFC-normalize PINs #256

@AlfioEmanueleFresta

Description

@AlfioEmanueleFresta

Problem

CTAP2 PIN/UV handling ignores three authenticator policies from authenticatorGetInfo. The code is in libwebauthn/src/webauthn/pin_uv_auth_token.rs and libwebauthn/src/pin/mod.rs.

1. noMcGaPermissionsWithClientPin is never read. In user_verification_helper, the GetPinToken and GetPinUvAuthTokenUsingPinWithPermissions arms always request ctap2_request.permissions() (mc = MAKE_CREDENTIAL | GET_ASSERTION, ga = GET_ASSERTION). Per CTAP 2.1, when this option is true a clientPin token may not carry mc/ga, so those must come from built-in UV (getPinUvAuthTokenUsingUvWithPermissions). Requesting them on the PIN path makes the authenticator reject the request.

2. force_pin_change is never acted on. The field (get_info.rs, 0x0C) is parsed but only read in an example. Per CTAP 2.1, while it is true getPinToken and getPinUvAuthTokenUsingPinWithPermissions return CTAP2_ERR_PIN_POLICY_VIOLATION until the PIN is changed. The flow should detect it and drive a change-PIN flow first.

3. PINs are not NFC-normalized and min length is checked in bytes. In change_pin_internal the guard new_pin.len() < min_pin_length compares UTF-8 bytes against minPINLength, which getInfo (0x0D) defines in Unicode code points. Neither the new PIN (change_pin_internal) nor the collected PIN (obtain_pin) is normalized. CTAP 2.1 requires the PIN as UTF-8 in Normalization Form C.

Why it matters

Biometric devices that gate registration and assertion behind UV advertise noMcGaPermissionsWithClientPin. On a fallback from blocked UV, the current code still mints a clientPin mc/ga token and fails with a generic error instead of routing to UV. A device that needs a PIN change fails every ceremony without guiding the user. Without NFC, the same PIN entered as composed vs decomposed characters hashes differently and mismatches other clients. A short multi-byte PIN also passes the local guard and is rejected later by the authenticator.

Checklist

  • When noMcGaPermissionsWithClientPin is enabled, drop the mc/ga bits from the PIN request and route mc/ga to built-in UV, or return a clear error when UV is unavailable. Use the existing option_enabled(...) helper on Ctap2GetInfoResponse.
  • Before a PIN-based token, detect force_pin_change == Some(true) and drive a change-PIN flow first via the existing PinManagement/UvUpdate machinery.
  • NFC-normalize both the collected PIN (obtain_pin) and new PIN (change_pin_internal) before hashing or encrypting, using a crate like unicode-normalization.
  • Check minimum PIN length in code points (new_pin.chars().count()), keeping the 63-byte maximum on the UTF-8 length.
  • Add tests: mc/ga suppression under noMcGaPermissionsWithClientPin, a forcePINChange change flow, NFC normalization of collected and new PINs, and the code-point min-length boundary.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions