Skip to content

fix(passkey): send the PRF extension as binary on login and registration - #42

Open
heraldb wants to merge 1 commit into
naust-mail:mainfrom
heraldb:fix/passkey-prf-extension
Open

heraldb wants to merge 1 commit into
naust-mail:mainfrom
heraldb:fix/passkey-prf-extension

Conversation

@heraldb

@heraldb heraldb commented Oct 30, 2026

Copy link
Copy Markdown

Problem

Passkey login never works: after the user picks their passkey, the login
page drops them on the password step.

Passkey login needs the PRF extension. Its output decrypts the stored IMAP
password (passkey_login_complete in backend/src/routes/mfa.rs). But
preparePublicKeyOptions in frontend/src/app/page.tsx copies only the
challenge, rpId, allowCredentials and userVerification from the server
options. It drops extensions, so the authenticator never computes a PRF
output. extract_prf_output returns None, the server answers 401 without
logging anything, and attemptPasskey silently falls back to the password
step.

Registration (prepareCreationOptions in SecuritySettings.tsx) does pass
extensions, but the PRF salt is still the base64url string from the
server. WebAuthn requires a BufferSource there. Native browser
authenticators and hardware keys reject or ignore it. Password managers
that also accept the JSON encoding (parseCreationOptionsFromJSON), such
as Proton Pass, decode it, which is why registration seemed to work.

Change

A small prepareExtensions() helper in each file decodes the PRF salts to
ArrayBuffers:

  • login: prf.evalByCredential[credId].first
  • registration: prf.eval.first

Because those password managers already decoded the string salt the same
way, passkeys registered before this fix keep working; no re-enrollment is
needed.

Testing

Deployed on a self-hosted instance (0.2.4, Firefox on Linux, Proton Pass):

  • Before: picking the passkey always led to the password step.
  • After: passkey login goes straight to the mailbox, both with a passkey
    registered before the fix and with a newly registered one.

🤖 Generated with Claude Code

Passkey login never sent the PRF extension: preparePublicKeyOptions
dropped `extensions`, so the authenticator returned no PRF output, the
server rejected the assertion with 401 and the login page silently fell
back to the password step.

Registration did pass the extension, but with the base64url salt still
as a string. WebAuthn requires a BufferSource; browsers' native
authenticators reject or ignore it, so registration only worked with
password managers that accept the JSON encoding.

Decode the PRF salts to ArrayBuffers in both places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant