Skip to content

PRF extension does not work with phone authenticators over caBLE/hybrid (native CTAP prf extension unsupported; only hmac-secret is implemented) #266

@neoz

Description

@neoz

Summary

The WebAuthn PRF extension cannot produce results when used against a phone/platform authenticator over caBLE/hybrid. The library only implements the hmac-secret CTAP extension (used by FIDO2 security keys), but modern phone authenticators expose PRF via the native CTAP 2.2 prf extension instead. As a result, clientExtensionResults.prf is empty for both MakeCredential and GetAssertion over hybrid, even though the phone fully supports PRF.

Environment

  • libwebauthn 0.7.0
  • Transport: caBLE / hybrid (QR code, CableQrCodeDevice)
  • Authenticator: phone (synced passkey provider), reached over hybrid

Steps to reproduce

  1. Register a credential over caBLE with the PRF extension requested (extensions: { "prf": {} }).
  2. Authenticate over caBLE with extensions: { "prf": { "eval": { "first": "<salt>" } } }.
  3. Inspect clientExtensionResults on both responses.

Expected

clientExtensionResults.prf.enabled (registration) and clientExtensionResults.prf.results.first (assertion) are populated.

Actual

clientExtensionResults is {} on both legs. The returned authenticatorData has the ED (extension data) flag unset.

Root cause

The phone's authenticatorGetInfo advertises the native prf extension, not hmac-secret, and exposes no clientPin / pinUvAuthToken:

Ctap2GetInfoResponse {
    versions: ["FIDO_2_0", "FIDO_2_1"],
    extensions: Some(["prf"]),
    options: Some({"plat": false, "rk": true, "up": true, "uv": true}),
    pin_auth_protos: None,
    transports: Some(["internal", "hybrid"]),
    ...
}

There are two distinct CTAP mechanisms behind the WebAuthn PRF extension:

hmac-secret native prf (CTAP 2.2)
Used by FIDO2 security keys platform / phone authenticators over hybrid
Salt transport ECDH-encrypted with a per-session shared secret (needs clientPin/pinUvAuthToken) raw salts inside the prf map; the caBLE Noise tunnel already encrypts the channel, so no shared secret is required
getInfo advertises "hmac-secret" "prf"

libwebauthn maps WebAuthn PRF onto hmac-secret only:

  • Ctap2MakeCredentialsRequestExtensions.prf_input is #[serde(skip)] and is used solely to compute hmac-secret-mc — there is no native prf field on the wire.
    libwebauthn/src/proto/ctap2/model/make_credential.rs (prf_input field, ~line 197)
  • GetAssertion only ever serializes a calculated hmac-secret input.
    libwebauthn/src/proto/ctap2/model/get_assertion.rs (calculate_hmac, needs_shared_secret)
  • needs_shared_secret gates PRF on the authenticator advertising "hmac-secret":
    libwebauthn/src/proto/ctap2/model/get_assertion.rs:438

Because the phone advertises "prf" (not "hmac-secret") and has no PIN/UV auth protocol, the library establishes no shared secret and never sends/parses a native prf extension. It also still sends hmac-secret: true on MakeCredential, which the phone silently ignores.

Trace evidence (abridged)

ctap_response=Ctap2GetInfoResponse { versions: ["FIDO_2_0", "FIDO_2_1"],
    extensions: Some(["prf"]), options: Some({"plat": false, "rk": true,
    "up": true, "uv": true}), pin_auth_protos: None,
    transports: Some(["internal", "hybrid"]), ... }

WARN  No supported PIN/UV auth protocols found get_info_response.pin_auth_protos=None
DEBUG Checking if user verification is required ... needs_shared_secret=false can_establish_shared_secret=false
TRACE request=Ctap2MakeCredentialRequest { ... extensions: Some(Ctap2MakeCredentialsRequestExtensions {
    ... hmac_secret: Some(true), ... prf_input: None }), ... }

Resulting MakeCredential authenticatorData flags: 0x5d (UP, UV, BE, BS, AT; ED unset).
Resulting GetAssertion authenticatorData flags: 0x1d (UP, UV, BE, BS; ED unset).

Impact

PRF currently works only with authenticators that advertise hmac-secret (security keys over USB/NFC/BLE). It does not work with any phone/platform authenticator over hybrid, which is the common case for caBLE.

Proposed fix

Implement the native CTAP 2.2 prf extension and route to it when getInfo.extensions contains "prf":

  • MakeCredential: send a real prf extension ({} or { eval: … }); parse prf: { enabled } from the output.
  • GetAssertion: send prf: { eval, evalByCredential } carrying raw salt bytes; parse prf: { results: { first, second } }.
  • Routing: prefer native prf when advertised, fall back to hmac-secret otherwise; skip the shared-secret machinery on the prf path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No 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