A recent survey by the FIDO Alliance shows that passkeys have still not reached widespread adoption. The survey has found that, among users who have used passkeys, only "38% report enabling them whenever possible." This means that users who are familiar with passkeys do not think they are secure enough or convenient enough.
A passkey is used today in one of two ways: as a second factor after authentication with username and password, or by itself with user verification by a PIN or a biometric with fallback to a PIN. In this post I propose a third way of using a passkey that is more secure and more convenient than either of those two methods, and may have a better chance of achieving widespread adoption.
This third way does not require any changes to passkeys or how passkeys are synced by browsers and password managers. It does require a minor WebAuthn extension: navigator.credentials.get must return the public key in addition to the signature computed with the private key. Since the public key can be computed from the private key, there should be no difficulty in implementing this extension.
In this third way the passkey is used together with a password, providing two-factor authentication without relying on user verification with a PIN. The user supplies the password, but the fact that a passkey is being used as an additional authentication factor is entirely transparent to the user. The user experience is exactly the same as in traditional one-factor authentication with username and password.
Here is how it works.
At registration, the user provides and confirms a password to the frontend of the relying party. The frontend calls navigator.credentials.create(), which creates a key pair and returns the public key. The frontend sends the password and the public key to the backend, which computes and registers a joint hash of password and the public key by storing it in a backend database.
To authenticate the user, the backend of the relying party generates a challenge and sends it the frontend. The frontend passes the challenge to navigator.credentials.get(), which returns a signature on the challenge, as well as the public key of the passkey in accordance with the required WebAuthn extension. The frontend asks the user for the password and sends the password, the signature and the public key to the backend. The backend uses the public key to verify the signature, computes a hash of the public key and the password, verifies that the computed hash agrees with the registered joint hash, and securely deletes the password and the public key from the backend database.
This 2FA method with passkey and password where the two factors are presented together to the relying party is more secure than 2FA with the same two factors presented separately, for two reasons:
- When the factors are presented separately, the relying party registers the password by storing a salted hash of the password in the backend database along with the salt. An attacker who breaches the database can mount a dictionary attack against the password, because the salt is available to the attacker. When the factors are presented together, the password is hashed with the public key, which is not available to the attacker. Hence no dictionary attack if possible after a breach of the database.
- When the factors are presented separately, the relying party registers the public key by storing it in the clear in the backend database. Since the cryptosystems used for generating key pairs used in WebAuthn are not postquantum resistant, an attacker equipped with a quantum computer who breaches the database might be able to derive the private key from the public key. By contrast, when the factors are presented jointly, the public key is not present in the database.
Patent disclosure
The 2FA method with joint presentation of the factors described above is covered by the claims of US patent 9,887,989, which is owned by Pomcor as of this writing.