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
- Register a credential over caBLE with the PRF extension requested (
extensions: { "prf": {} }).
- Authenticate over caBLE with
extensions: { "prf": { "eval": { "first": "<salt>" } } }.
- 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.
Summary
The WebAuthn PRF extension cannot produce results when used against a phone/platform authenticator over caBLE/hybrid. The library only implements the
hmac-secretCTAP extension (used by FIDO2 security keys), but modern phone authenticators expose PRF via the native CTAP 2.2prfextension instead. As a result,clientExtensionResults.prfis empty for both MakeCredential and GetAssertion over hybrid, even though the phone fully supports PRF.Environment
0.7.0CableQrCodeDevice)Steps to reproduce
extensions: { "prf": {} }).extensions: { "prf": { "eval": { "first": "<salt>" } } }.clientExtensionResultson both responses.Expected
clientExtensionResults.prf.enabled(registration) andclientExtensionResults.prf.results.first(assertion) are populated.Actual
clientExtensionResultsis{}on both legs. The returnedauthenticatorDatahas theED(extension data) flag unset.Root cause
The phone's
authenticatorGetInfoadvertises the nativeprfextension, nothmac-secret, and exposes noclientPin/pinUvAuthToken:There are two distinct CTAP mechanisms behind the WebAuthn PRF extension:
hmac-secretprf(CTAP 2.2)prfmap; the caBLE Noise tunnel already encrypts the channel, so no shared secret is requiredgetInfoadvertises"hmac-secret""prf"libwebauthn maps WebAuthn PRF onto
hmac-secretonly:Ctap2MakeCredentialsRequestExtensions.prf_inputis#[serde(skip)]and is used solely to computehmac-secret-mc— there is no nativeprffield on the wire.libwebauthn/src/proto/ctap2/model/make_credential.rs(prf_inputfield, ~line 197)GetAssertiononly ever serializes a calculatedhmac-secretinput.libwebauthn/src/proto/ctap2/model/get_assertion.rs(calculate_hmac,needs_shared_secret)needs_shared_secretgates PRF on the authenticator advertising"hmac-secret":libwebauthn/src/proto/ctap2/model/get_assertion.rs:438Because 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 nativeprfextension. It also still sendshmac-secret: trueon MakeCredential, which the phone silently ignores.Trace evidence (abridged)
Resulting MakeCredential
authenticatorDataflags:0x5d(UP, UV, BE, BS, AT; ED unset).Resulting GetAssertion
authenticatorDataflags: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
prfextension and route to it whengetInfo.extensionscontains"prf":prfextension ({}or{ eval: … }); parseprf: { enabled }from the output.prf: { eval, evalByCredential }carrying raw salt bytes; parseprf: { results: { first, second } }.prfwhen advertised, fall back tohmac-secretotherwise; skip the shared-secret machinery on theprfpath.