Score:1

How do non-"resident" keys work in WebAuthn?

ch flag

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).

https://www.w3.org/TR/webauthn-2/images/webauthn-registration-flow-01.svg

https://www.w3.org/TR/webauthn-2/images/webauthn-authentication-flow-01.svg

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?

Score:2
cn flag

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.

Uwe Kohl avatar
ch flag
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?
Taylor R Campbell avatar
cn flag
@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](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-overview-v1.2-ps-20170411.html) (from when 'credential ids' were still called 'key handles').
Uwe Kohl avatar
ch flag
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...
Taylor R Campbell avatar
cn flag
@UweKohl Omitting allowCredentials or leaving it empty means requesting resident keys / discoverable credentials.
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.