Ethan T. McGee's Blog

Passkey Registration Ceremony

This is the second post in a multi-part series covering passkeys (or WebAuthn). Each post builds on previous posts in the series, so catch up if needed before continuing.

Table of Contents

Note: will be updated as more posts are completed

The registration ceremony is the process by which a credential (public / private key) pair is requested from the authenticator. This post will focus on the creation process from the perspective of a browser as that is the most common use case, but the underlying C API is quite similar to the browser, albeit more low level.

Note: All specifications linked throughout this document are up to date at time of writing, remember to check for updated specifications before implementing.

Before we begin, a few terms must be defined:

The below diagram provides a general flow of the passkey registration ceremony. The grey box along the Relying Party line indicates the need for a singular value to be persisted into a session and for how long it must be persisted.

Registration Ceremony

The ceremony has a few distinct phases: the trigger, generation of the creation options, generation of the credential, and validation of the credential. The creation options must be safely stored by the Relying Party from the start of the process to the end. However stored, the options must be free from tampering to provide the security guarantees that WebAuthn promises. Storage is usually done via a backend system like Redis where the options expire after a short timeout, usually a few minutes at most.

For our purposes, we will use the standard use case for WebAuthn. The user will be visiting a website, the Relying Party will be the website itself, and the authenticator will be a hardware authenticator. The trigger is easy enough, the user either visits a registration page, or clicks a button on the site that initiates registration. Some basic information might be collected (like name, email, etc.) but this is not strictly required. Provided the site can generate a unique id for the user, it is certainly possible to collect no data at all.

The generation of the credential options comes next. Here the Relying Party has numerous options that can be supplied to the authenticator in the form of a JSON payload. We discuss what each field does after the example payload.

{
  "attestation": "none (default) | enterprise | direct | indirect",
  "attestationFormats": ["packed | tpm | android-key | android-safetynet | fido-u2f | apple | none | ..."],
  "authenticatorSelection": {
    "authenticatorAttachment": "cross-platform | platform",
    "requiresResidentKey": "boolean",
    "residentKey": "discouraged (default) | preferred | required",
    "userVerification": "discouraged | preferred (default) | required"
  },
  "challenge": "string",
  "excludeCredentials": [{
    "id": "string",
    "type": "public-key",
    "transports": ["ble | hybrid | internal | nfc | usb"]
  }],
  "extensions": {},
  "hints": ["security-key | hybrid | client-device"],
  "pubKeyCredParams": [{
    "alg": "number",
    "type": "public-key"
  }],
  "rp": {
    "id": "string",
    "name": "string"
  },
  "timeout": "number",
  "user": {
    "displayName": "string",
    "id": "string",
    "name": "string"
  }
}

Once the options are generated, they are provided to the authenticator via the standard browser API navigator.credentials.create({options}). The browser must validate that the site in question is authorized to use the rp.id before it can ask the authenticator to generate a credential.

That validation process includes two checks. For websites, the rp.id must be a valid domain. The current website must either be a subdomain of the rp.id (for example, rp.id is example.com and current website is sub.example.com) or rp.id must expose a /.well-known/webauthn endpoint listing the current website as an officially supported origin. This is the step that gives WebAuthn its famed phishing resistance.

Should validation succeed, the credential is returned to the relying party. The relying party must now confirm the credential conforms to the options provided, and if it does not, validation errors must be returned to the user alerting them that their authenticator does not meet the requirements of the site or that registration failed for some other reason. An example credential is provided, then a discussion of the fields and validations needed on each follows.

{
  "authenticatorAttachment": "cross-platform | platform",
  "id": "string",
  "rawId": "string",
  "response": {
    "clientDataJSON": "string",
    "attestationObject": "string",
    "transports": ["ble | hybrid | internal | nfc | usb"],
    "authenticatorData": "string",
    "publicKeyAlgorithm": "number"
  },
  "type": "public-key",
  "clientExtensionResults": {}
}
{
  "type": "webauthn.create",
  "challenge": "8F2o-uHH0b4OXQh4p4FJgw",
  "origin": "https://example.com",
  "crossOrigin": false
}

Once the registration succeeds, fails, or times out, the Relying Party should destroy the creation options, and that's it! The registration ceremony is complete. The authentication ceremony is next.

Want to try out the registration ceremony yourself? Give the developer tools from Yubico a try.