Bug 311099
| Summary: | [WebAuthn] PRF extension returns encrypted hmac-secret output for CTAP2 security keys (missing decryption step) | ||
|---|---|---|---|
| Product: | WebKit | Reporter: | Berni <bsoft> |
| Component: | WebCore Misc. | Assignee: | Nobody <webkit-unassigned> |
| Status: | NEW | ||
| Severity: | Critical | CC: | nvoutsin |
| Priority: | P2 | ||
| Version: | WebKit Nightly Build | ||
| Hardware: | All | ||
| OS: | macOS 26 | ||
Berni
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 | ||
|---|---|---|
| Add attachment proposed patch, testcase, etc. |