NEW311099
[WebAuthn] PRF extension returns encrypted hmac-secret output for CTAP2 security keys (missing decryption step)
https://bugs.webkit.org/show_bug.cgi?id=311099
Summary [WebAuthn] PRF extension returns encrypted hmac-secret output for CTAP2 secur...
Berni
Reported 2026-03-30 05:16:18 PDT
SUMMARY When using the PRF extension with CTAP2 security keys (USB/NFC), Safari returns the AES-256-CBC encrypted hmac-secret output directly as PRF results without decrypting it. Platform authenticators (Touch ID, Face ID via ASAuthorization) are unaffected. STEPS TO REPRODUCE 1. Register a credential with PRF extension using a FIDO2 security key (e.g. YubiKey) on Safari 2. Authenticate with the same credential and salt on Safari 3. Authenticate with the same credential and salt on Chrome 4. Compare derived keys or encrypt/decrypt across browsers Test page: https://b-straub.github.io/BlazorPRF/ EXPECTED PRF output matches across browsers for the same credential + salt (deterministic, per spec). Cross-browser encryption/decryption works. ACTUAL - Safari macOS <-> Safari iPadOS: PRF outputs match (both apply the same code path, producing identical but still encrypted values) - Safari <-> Chrome: PRF outputs differ (Chrome correctly decrypts the hmac-secret response) - Cross-browser encryption/decryption fails ROOT CAUSE In DeviceResponseConverter.cpp, parseAuthenticatorDataExtensions() takes the raw hmac-secret byte string from the CTAP2 response and directly creates ArrayBuffers without decrypting: } else if (hmacIt->second.isByteString()) { auto& hmacOutput = hmacIt->second.getByteString(); // BUG: hmacOutput is still AES-256-CBC encrypted! RefPtr<ArrayBuffer> first = ArrayBuffer::tryCreate( hmacOutput.span().first(std::min<size_t>(32, hmacOutput.size()))); outputs.prf = AuthenticationExtensionsClientOutputs::PRFOutputs { }; outputs.prf->results = AuthenticationExtensionsClientOutputs::PRFValues { first, second }; } The decryption logic already exists in HmacSecretResponse::parse() (Pin.cpp), which correctly calls decryptForProtocol(). However, it is never invoked in the response processing pipeline. CtapAuthenticator (CtapAuthenticator.cpp) stores the shared key in m_hmacSecretRequest but never uses it to decrypt the hmac-secret response. SUGGESTED FIX Either: 1. readCTAPGetAssertionResponse() accepts the shared key and calls HmacSecretResponse::parse() to decrypt before constructing PRF output, or 2. CtapAuthenticator post-processes the response using its stored m_hmacSecretRequest to decrypt the extension output WHY SAFARI-TO-SAFARI APPEARS CONSISTENT Safari produces deterministic (but wrong) PRF output because the platform key agreement results in the same shared secret across sessions. Both macOS and iPadOS run the same WebKit code path, so both return identical encrypted values. This gives false confidence that cross-device PRF works, when in fact the values are not interoperable with any correctly implementing browser. ENVIRONMENT - Safari 26.4 on macOS Tahoe / iPadOS - FIDO2 security key (YubiKey) via CTAP2/HID - Platform authenticators (ASAuthorization path) are NOT affected RELATED - PR #53734 (original PRF implementation): https://github.com/WebKit/WebKit/pull/53734 - Review comment by @joostd: https://github.com/WebKit/WebKit/pull/53734#pullrequestreview-4020705594
Attachments
Note You need to log in before you can comment on or make changes to this bug.