feat: add WebAuthn PRF extension support#1435
Conversation
During registration, respond with clientExtensionResults.prf.enabled = true
when the relying party requests the PRF extension via extensions.prf.
During authentication, resolve PRF inputs from extensions.prf.eval (or
evalByCredential for the specific credential), compute deterministic PRF
outputs using HMAC-SHA-256 over a per-credential key derived from the
private key, and return them in clientExtensionResults.prf.results.
PRF computation follows the WebAuthn Level 3 spec:
prfSalt = SHA-256("WebAuthn PRF\x00" || prfInput)
hmacKey = HMAC-SHA-256(privateKeyBytes, "prf")
prfOutput = HMAC-SHA-256(hmacKey, prfSalt)
Modules changed: :common (androidMain, commonMain, androidUnitTest)
https://claude.ai/code/session_01FB6FapLddy7iUzQiXXNJcr
When the relying party includes extensions.prf.eval in the create() call, compute and return the PRF results immediately in clientExtensionResults.prf.results alongside enabled:true, saving the RP a separate authentication round-trip. https://claude.ai/code/session_01FB6FapLddy7iUzQiXXNJcr
|
I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
|
I have read the CLA Document and I hereby sign the CLA |
|
recheck |
|
Thanks! It looks very reviewable to me and on the first glance it all makes sense. I'll do a proper review later on and hopefully will merge that in! |
The This implementation is basically https://community.bitwarden.com/t/support-for-storing-prf-capable-passkeys-in-bitwarden-vault/82239/61 of the alternative Keyguard world. |
- Store a separate 32-byte random `prfSecret` per credential (the
"credRandom" equivalent) instead of deriving from the signing key.
The PRF algorithm is now: SHA-256("WebAuthn PRF\x00" || input) →
HMAC-SHA-256(prfSecret, prfSalt), matching the W3C spec exactly.
- Propagate `prfSecret` through the full data stack: BitwardenCipher,
DSecret, LoginFido2CredentialsEntity, LoginFido2CredentialsRequest,
CipherDecoder, CipherMapping, CipherCrypto, AddCredentialCipher.
- Enforce user verification before returning PRF results: if the user
was not verified, the response contains `prf: {}` instead of results.
- Always include `prf: {}` in clientExtensionResults when PRF was
requested, even when no results are available (per spec).
- Improve tests: use the new `prfSecretBytes` parameter, add an
independent reference implementation as expected value, and add a
regression test confirming the output differs from the old
private-key-derived computation.
https://claude.ai/code/session_01FB6FapLddy7iUzQiXXNJcr
|
We can't create a new property either, because we will not be able to sync it back to Bitwarden servers. This is a stalemate until Bitwarden decides to implement it themselves. |
|
Ah... I see. Oh well! |
Hi, huge Keyguard fan here. I hope this contribution finds you well. I wanted to use passkeys to derive an AES key in my website. I learned that the feature is supported in Google Password and Apple's Password manager but not (yet) for Keyguard. Normally I wouldn't allow Claude to make a PR for me, but because WebAuthn is a well-defined specification, I am more confident that it is probably implemented correctly. I did manually test it as well, using a debug build APK and Android/Firefox/Keyguard and it worked.
The PRF extension lets relying parties derive deterministic symmetric keys from a passkey — enabling phishing-resistant end-to-end encryption. See Corbado's writeup for use cases.
Changes (all in
:common):clientExtensionResults.prf.enabled = truewhenextensions.prfis requestedclientExtensionResults.prf.resultsfromextensions.prf.eval/evalByCredentialextensions.prf.evalis includedManually Tested against https://webauthn-passkeys-prf-demo.explore.corbado.com/ on Mobile Firefox 151 / Android 16: