If we look at the WebAuthn specification, then, during the "registration" ceremony, the authenticator generates a new key-pair and a unique user-id. Then the public-key and the unique user-id are sent to the relying party (server), together with some attestation statement. The relying party will store the public-key and the user-id in their database and associate them with the user's account.

Later, during the "authentication" ceremony, the relying party sends the user-id back to the client, together with a random challenge. The authenticator uses the user-id to find the correct private-key (the one that it generated during the "registration" ceremony), so that it can sign the challenge with that key. Finally, the signed challenge is sent to the relying party, where the signature will be verified with the corresponding public-key (the one that was stored during the "registration" ceremony).



This clearly requires that the authenticator has stored the private-key in its internal memory. Otherwise it wouldn't be able to look up the private-key based on the given user-id and use it to sign the challenge. At least, I cannot see that the client/authenticator gets anything back from the relying party, except for the user-id. So, actually, the authenticator must store all private-keys (and their corresponding user-id) that it ever generated, so that future logins will be possible.

Now here is what confuses me: Some sources claim that, normally, the private-key is actually stored on the relying party (in an encrypted form), not in the authenticator. And that only so-called "resident" keys (discoverable credentials) are stored locally in the authenticator. But, if that was the case, then how do "normal" (not resident) keys really work? I can see nothing in the WebAuthn API where the client or the authenticator would send an encrypted private-key to the relying party, as part of the registration. Also, I can see nothing in the WebAuthn API where the relying party would send an encrypted private-key back to the client/autenticator, as part of the authentication...

Discoverable Credentials / Resident Keys

WebAuthn enables high assurance multi-factor authentication with a passwordless login experience. One of the things that enables this is what is called Discoverable Credentials, also referred to as resident keys.

Discoverable Credential means that the private key and associated metadata is stored in persistent memory on the authenticator, instead of encrypted and stored on the relying party server.

So, where is the "normal" (not resident) WebAuthn private-key really stored? If it is stored on the relying party server, as some sources claim, where/when exactly is an encrypted private-key exchanged between the authenticator and the relying party? Which API function/object does this?


1 Answer 1


Here is a simplified model of how it can work. Each device from each different vendor might work differently, of course, but this is close to how the old Yubico U2F implementation works.

On the device there is a single 32-byte master HMAC-SHA256 secret key $k$.

  • When you register the device with a web site (navigator.credentials.create, which does authenticatorMakeAssertion), using a relying party id provided by the web site, the device:

    1. Generates a 32-byte token $t$ uniformly at random.
    2. Derives a seed $s = \operatorname{HMAC-SHA256}_k(\mathtt{0x01} \mathbin\| t \mathbin\| \mathit{rpId})$.
    3. Derives a key pair $(\mathit{sk}, \mathit{pk})$ for ECDSA over NIST P-256, deterministically from the seed $s$—e.g., interprets $s$ as a 256-bit integer and zeroes the high bit to choose a scalar $\mathit{sk}$ below the group order, and then multiplies the standard base point by $\mathit{sk}$ to derive $\mathit{pk}$.
    4. Computes a 32-byte authentication tag $a = \operatorname{HMAC-SHA256}_k(\mathtt{0x02} \mathbin\| t \mathbin\| \mathit{rpId})$.
    5. Returns $t \mathbin\| a$ as the 64-byte credential id, and $\mathit{pk}$ as the public key.
  • When you authenticate to a web site with the device (navigator.credentials.get, which does authenticatorGetAssertion), using a relying party id, credential id, and challenge provided by the web site, the device:

    1. Refuses if the credential id is not 64 bytes long.
    2. Divides the 64-byte credential id into 32-byte halves $t \mathbin\| a$.
    3. Refuses if $a \ne \operatorname{HMAC-SHA256}_k(\mathtt{0x02} \mathbin\| t \mathbin\| \mathit{rpId})$.
    4. Derives a seed $s = \operatorname{HMAC-SHA256}_k(\mathtt{0x01} \mathbin\| t \mathbin\| \mathit{rpId})$.
    5. Derives a key pair $(\mathit{sk}, \mathit{pk})$ for ECDSA over NIST P-256, deterministically from the seed $s$.
    6. Signs the challenge with the private key $\mathit{sk}$ and returns the signature.

The signing key $\mathit{sk}$ only exists by combining the master secret key $k$, stored on the device, with a token $t$ in the credential id, stored on the server. Once registration is complete, it need not be stored explicitly on the device; it is just re-derived in each authentication operation.

The trick is that deterministically generating key pairs for ECDSA over NIST P-256 from a 32-byte seed is very cheap—one fixed-base scalar multiplication on the curve. You can get a rough estimate for how much time this takes on your CPU by running openssl speed ecdhp256—not exactly the same operation (ECDH is variable-base, not fixed-base) but it's a reasonable approximation.

  • $\begingroup$ Thanks for the info! So this means that the authenticator uses the "credential id" to store an information (token) on the RP server, which will later be used, together with the master key, to derive the private-key. But, if we look at the spec, or at most descriptions of WebAuthn, that is not how the purpose of the "credential id" is described at all! So, was this approach of generating the private key "on-the-fly" added as an afterthought? Why it is not usually mentioned? Also, I think "credential id" is optional for navigator.credentials.get(). What do we do, if it its absent? $\endgroup$
    – Uwe Kohl
    Apr 4, 2023 at 8:15
  • 1
    $\begingroup$ @UweKohl The webauthn spec is...not an easy read. It spells out all the procedures and data formats in painful detail, and the procedures and formats have been generalized to support various bells and whistles like resident keys (discoverable credentials), PINs, and biometrics. Those are the afterthoughts. You might get a better picture of the main ideas from the old U2F architecture overview (from when 'credential ids' were still called 'key handles'). $\endgroup$ Apr 4, 2023 at 15:20
  • $\begingroup$ Okay. But still: Unless I'm mistaken, the "credential id" only appears within the allowCredentials array of the navigator.credentials.get() params. And that is totally optional. The server/web-app may set allowCredentials to restrict which credentials can be used to log-in, but doesn't have to. Now, if the "credential id" (which may or may not be given in allowCredentials) is necessary to compute the private-key, what does the authenticator do, if no "credential id" is given? From a server's point of view, not setting allowCredentials is perfectly allowed, I think... $\endgroup$
    – Uwe Kohl
    Apr 4, 2023 at 15:30
  • 1
    $\begingroup$ @UweKohl Omitting allowCredentials or leaving it empty means requesting resident keys / discoverable credentials. $\endgroup$ Apr 5, 2023 at 10:44
  • $\begingroup$ Why is seed simply not generated from hidden master t and rpId? Then one could have resident/discoverable/deterministic keys without having to really store them on the device and without having RP to save them either. $\endgroup$
    – Mitar
    Nov 23, 2023 at 22:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.