WebAuthn

A better alternative for securing our sensitive information online
 

Our online existence is built on passwords.

Often a password is all that lies between a malicious user and our bank accounts, social media accounts, and other sensitive data.

Passwords have an ever-growing list of problems associated with them, both for users and developers. Users have to worry about passwords being stolen by phishing tools, or their passwords being leaked online if websites they have accounts with are compromised. They have to worry about creating and remembering passwords without dedicated password management tools. Developers have to worry about all the complications of passing passwords through systems and safely storing them in databases.

The results are clear.

81%

of all hacking-related breaches leverage stolen or weak passwords.

One of the primary weaknesses of password-based authentication is that a password is a shared secret.

The password is the single key that proves you are in fact you. If a hacker manages to steal it, they can fully impersonate you unless you are one of the 28% of users using two-factor authentication.

There is a better way forward.

Introducing Public Key Cryptography and Web Authentication (WebAuthn)

The Web Authentication API (also known as WebAuthn) is a specification written by the W3C and FIDO, with the participation of Google, Mozilla, Microsoft, Yubico, and others. The API allows servers to register and authenticate users using public key cryptography instead of a password.

It allows servers to integrate with the strong authenticators now built into devices, like Windows Hello or Apple’s Touch ID. Instead of a password, a private-public keypair (known as a credential) is created for a website. The private key is stored securely on the user’s device; a public key and randomly generated credential ID is sent to the server for storage. The server can then use that public key to prove the user’s identity.

The public key is not secret, because it is effectively useless without the corresponding private key. The fact that the server receives no secret has far-reaching implications for the security of users and organizations. Databases are no longer as attractive to hackers, because the public keys aren’t useful to them.

WebAuthn is part of the FIDO2 framework, which is a set of technologies that enable passwordless authentication between servers, browsers, and authenticators. As of January 2019, WebAuthn is supported on Chrome, Firefox, and Edge, and Safari.

What is Public Key Cryptography?

Public key cryptography was invented in the 1970s, and was a solution to the problem of shared secrets. It is a pillar of modern internet security; for example, every time we connect to an HTTPS website, a public key transaction takes place.

Public key cryptography uses the concept of a keypair; a private key that is stored securely with the user, and a public key that can be shared with the server. These "keys" are long, random numbers that have a mathematical relationship with each other.

Web Authentication relies on three major properties:

Strong

Authentication is ideally backed by a Hardware Security Module, which can safely store private keys and perform the cryptographic operations needed for WebAuthn.

Scoped

A keypair is only useful for a specific origin, like browser cookies. A keypair registered at 'webauthn.guide' cannot be used at 'evil-webauthn.guide', mitigating the threat of phishing.

Attested

Authenticators can provide a certificate that helps servers verify that the public key did in fact come from an authenticator they trust, and not a fraudulent source.

Using the API

Registering a WebAuthn Credential

In a password-based user registration flow, a server will typically present a form to a user asking for a username and password. The password would be sent to the server for storage.

In WebAuthn, a server must provide data that binds a user to a credential (a private-public keypair); this data includes identifiers for the user and organization (also known as the "relying party"). The website would then use the Web Authentication API to prompt the user to create a new keypair. It is important to note that we need a randomly generated string from the server as a challenge to prevent replay attacks.

I want to create a new account.
Sure! Send me a public key.
All right! Creating a new keypair.
Okay, here's the public key!
Thanks! Registration complete.
navigator.credentials.create()

A server would begin creating a new credential by calling navigator.credentials.create() on the client.

const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
});

The publicKeyCredentialCreationOptions object contains a number of required and optional fields that a server specifies to create a new credential for a user.

const publicKeyCredentialCreationOptions = {
    challenge: Uint8Array.from(
        randomStringFromServer, c => c.charCodeAt(0)),
    rp: {
        name: "Duo Security",
        id: "duosecurity.com",
    },
    user: {
        id: Uint8Array.from(
            "UZSL85T9AFC", c => c.charCodeAt(0)),
        name: "[email protected]",
        displayName: "Lee",
    },
    pubKeyCredParams: [{alg: -7, type: "public-key"}],
    authenticatorSelection: {
        authenticatorAttachment: "cross-platform",
    },
    timeout: 60000,
    attestation: "direct"
};

const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
});

challenge: The challenge is a buffer of cryptographically random bytes generated on the server, and is needed to prevent "replay attacks". Read the spec.

rp: This stands for “relying party”; it can be considered as describing the organization responsible for registering and authenticating the user. The id must be a subset of the domain currently in the browser. For example, a valid id for this page is webauthn.guide. Read the spec.

user: This is information about the user currently registering. The authenticator uses the id to associate a credential with the user. It is suggested to not use personally identifying information as the id, as it may be stored in an authenticator. Read the spec.

pubKeyCredParams: This is an array of objects describing what public key types are acceptable to a server. The alg is a number described in the COSE registry; for example, -7 indicates that the server accepts Elliptic Curve public keys using a SHA-256 signature algorithm. Read the spec.

authenticatorSelection: This optional object helps relying parties make further restrictions on the type of authenticators allowed for registration. In this example we are indicating we want to register a cross-platform authenticator (like a Yubikey) instead of a platform authenticator like Windows Hello or Touch ID. Read the spec.

timeout: The time (in milliseconds) that the user has to respond to a prompt for registration before an error is returned. Read the spec.

attestation: The attestation data that is returned from the authenticator has information that could be used to track users. This option allows servers to indicate how important the attestation data is to this registration event. A value of "none" indicates that the server does not care about attestation. A value of "indirect" means that the server will allow for anonymized attestation data. direct means that the server wishes to receive the attestation data from the authenticator. Read the spec.

The credential object returned from the create() call is an object containing the public key and other attributes used to validate the registration event.

console.log(credential);

PublicKeyCredential {
    id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',
    rawId: ArrayBuffer(59),
    response: AuthenticatorAttestationResponse {
        clientDataJSON: ArrayBuffer(121),
        attestationObject: ArrayBuffer(306),
    },
    type: 'public-key'
}

id: The ID for the newly generated credential; it will be used to identify the credential when authenticating the user. The ID is provided here as a base64-encoded string. Read the spec.

rawId: The ID again, but in binary form. Read the spec.

clientDataJSON: This represents data passed from the browser to the authenticator in order to associate the new credential with the server and browser. The authenticator provides it as a UTF-8 byte array. Read the spec.

attestationObject: This object contains the credential public key, an optional attestation certificate, and other metadata used also to validate the registration event. It is binary data encoded in CBOR. Read the spec.

Parsing and Validating the Registration Data

After the PublicKeyCredential has been obtained, it is sent to the server for validation. The WebAuthn specification describes a 19-point procedure to validate the registration data; what this looks like will vary depending on the language your server software is written in.

Duo Labs has provided full example projects implementing WebAuthn written in Python and Go.

Example: Parsing the clientDataJSON

// decode the clientDataJSON into a utf-8 string
const utf8Decoder = new TextDecoder('utf-8');
const decodedClientData = utf8Decoder.decode(
    credential.response.clientDataJSON)

// parse the string as an object
const clientDataObj = JSON.parse(decodedClientData);

console.log(clientDataObj)

{
    challenge: "p5aV2uHXr0AOqUk7HQitvi-Ny1....",
    origin: "https://webauthn.guide",
    type: "webauthn.create"
}

The clientDataJSON is parsed by converting the UTF-8 byte array provided by the authenticator into a JSON-parsable string. On this server, this (and the other PublicKeyCredential data) will be verified to ensure that the registration event is valid.

challenge: This is the same challenge that was passed into the create() call. The server must validate that this returned challenge matches the one generated for this registration event.

origin: The server must validate that this "origin" string matches up with the origin of the application.

type: The server validates that this string is in fact "webauthn.create". If another string is provided, it indicates that the authenticator performed an incorrect operation.

Example: Parsing the attestationObject

// note: a CBOR decoder library is needed here.
const decodedAttestationObj = CBOR.decode(
    credential.response.attestationObject);

console.log(decodedAttestationObject);
{
    authData: Uint8Array(196),
    fmt: "fido-u2f",
    attStmt: {
        sig: Uint8Array(70),
        x5c: Array(1),
    },
}

authData: The authenticator data is here is a byte array that contains metadata about the registration event, as well as the public key we will use for future authentications. Read the spec.

fmt: This represents the attestation format. Authenticators can provide attestation data in a number of ways; this indicates how the server should parse and validate the attestation data. Read the spec.

attStmt: This is the attestation statement. This object will look different depending on the attestation format indicated. In this case, we are given a signature sig and attestation certificate x5c. Servers use this data to cryptographically verify the credential public key came from the authenticator. Additionally, servers can use the certificate to reject authenticators that are believed to be weak. Read the spec.

Example: Parsing the authenticator data

const {authData} = decodedAttestationObject;

// get the length of the credential ID
const dataView = new DataView(
    new ArrayBuffer(2));
const idLenBytes = authData.slice(53, 55);
idLenBytes.forEach(
    (value, index) => dataView.setUint8(
        index, value));
const credentialIdLength = dataView.getUint16();

// get the credential ID
const credentialId = authData.slice(
    55, 55 + credentialIdLength);

// get the public key object
const publicKeyBytes = authData.slice(
    55 + credentialIdLength);

// the publicKeyBytes are encoded again as CBOR
const publicKeyObject = CBOR.decode(
    publicKeyBytes.buffer);
console.log(publicKeyObject)

{
    1: 2,
    3: -7,
    -1: 1,
    -2: Uint8Array(32) ...
    -3: Uint8Array(32) ...
}

The authData is a byte array described in the spec. Parsing it will involve slicing bytes from the array and converting them into usable objects.

The publicKeyObject retrieved at the end is an object encoded in a standard called COSE, which is a concise way to describe the credential public key and the metadata needed to use it.

1: The 1 field describes the key type. The value of 2 indicates that the key type is in the Elliptic Curve format.

3: The 3 field describes the algorithm used to generate authentication signatures. The -7 value indicates this authenticator will be using ES256.

-1: The -1 field describes this key's "curve type". The value 1 indicates the that this key uses the "P-256" curve.

-2: The -2 field describes the x-coordinate of this public key.

-3: The -3 field describes the y-coordinate of this public key.

If the validation process succeeded, the server would then store the publicKeyBytes and credentialId in a database, associated with the user.

Authenticating with a WebAuthn Credential

After registration has finished, the user can now be authenticated. During authentication an assertion is created, which is proof that the user has possession of the private key. This assertion contains a signature created using the private key. The server uses the public key retrieved during registration to verify this signature.

I want to sign in.

Please sign this data so I really know it's you.

Creating a signature with the private key...

Okay, here's the signature.

Verifying the signature with the public key...

Great! This checks out. You can sign in.

navigator.credentials.get()

During authentication the user proves that they own the private key they registered with. They do so by providing an assertion, which is generated by calling navigator.credentials.get() on the client. This will retrieve the credential generated during registration with a signature included.

const credential = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions
});

The publicKeyCredentialCreationOptions object contains a number of required and optional fields that a server specifies to create a new credential for a user.

const publicKeyCredentialRequestOptions = {
    challenge: Uint8Array.from(
        randomStringFromServer, c => c.charCodeAt(0)),
    allowCredentials: [{
        id: Uint8Array.from(
            credentialId, c => c.charCodeAt(0)),
        type: 'public-key',
        transports: ['usb', 'ble', 'nfc'],
    }],
    timeout: 60000,
}

const assertion = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions
});

challenge: Like during registration, this must be cryptographically random bytes generated on the server. Read the spec.

allowCredentials: This array tells the browser which credentials the server would like the user to authenticate with. The credentialId retrieved and saved during registration is passed in here. The server can optionally indicate what transports it prefers, like USB, NFC, and Bluetooth. Read the spec.

timeout: Like during registration, this optionally indicates the time (in milliseconds) that the user has to respond to a prompt for authentication. Read the spec.

The assertion object returned from the get() call is again a PublicKeyCredential object. It is slightly different from the object we received during registration; in particular, it includes a signature member, and does not include the public key.

console.log(assertion);

PublicKeyCredential {
    id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',
    rawId: ArrayBuffer(59),
    response: AuthenticatorAssertionResponse {
        authenticatorData: ArrayBuffer(191),
        clientDataJSON: ArrayBuffer(118),
        signature: ArrayBuffer(70),
        userHandle: ArrayBuffer(10),
    },
    type: 'public-key'
}

id: The identifier for the credential that was used to generate the authentication assertion. Read the spec.

rawId: The identifier again, but in binary form. Read the spec.

authenticatorData: The authenticator data is similar to the authData received during registration, with the notable exception that the public key is not included here. It is another item used during authentication as source bytes to generate the assertion signature. Read the spec.

clientDataJSON: As during registration, the clientDataJSON is a collection of the data passed from the browser to the authenticator. It is one of the items used during authentication as the source bytes to generate the signature. Read the spec.

signature: The signature generated by the private key associated with this credential. On the server, the public key will be used to verify that this signature is valid. Read the spec.

userHandle: This field is optionally provided by the authenticator, and represents the user.id that was supplied during registration. It can be used to relate this assertion to the user on the server. It is encoded here as a UTF-8 byte array. Read the spec.

Parsing and Validating the Authentication Data

After the assertion has been obtained, it is sent to the server for validation. After the authentication data is fully validated, the signature is verified using the public key stored in the database during registration.

See these projects by Duo Labs for examples of validating the authentication data on the server, written in Python and Go.

Example: Verifying the assertion signature on the server (pseudo-code)

const storedCredential = await getCredentialFromDatabase(
    userHandle, credentialId);

const signedData = (
    authenticatorDataBytes +
    hashedClientDataJSON);

const signatureIsValid = storedCredential.publicKey.verify(
    signature, signedData);

if (signatureIsValid) {
    return "Hooray! User is authenticated! 🎉";
} else {
    return "Verification failed. 😭"
}

Verification will look different depending on the language and cryptography library used on the server. However, the general procedure remains the same.

  • The server retrieves the public key object associated with the user
  • The server uses the public key to verify the signature, which was generated using theauthenticatorData bytes and a SHA-256 hash of the clientDataJSON

Looking Ahead

While Web Authentication is an important tool, it is always important to remember that security is not a single technology; it is a way of thinking that should be incorporated into every step of how software is designed and developed. Web Authentication can be an important part of this process, by forcing 80% of hacking attacks to either adapt or die.

Suby Raman

Suby is a software engineer at Duo Security, working on the team responsible for Duo's Authentication Prompt. He has helped drive Web Authentication development at Duo.

Notably, he has contributed over 175 custom emoji to Duo's Slack workspace.

Further Reading

There's been a massive amount of progress made by both vendors and the authors of Web Authentication to bring this spec into usage in browsers and websites - here's what's new in WebAuthn and FIDO2.

While Duo is extremely bullish about the security properties of U2F, we think that the biggest change in strong authentication is coming soon. Get a look back at biometric authentication and how it's evolved over the years.

James Barclay and Nick Steele recently shared their thoughts on a passwordless future during a Twitter chat with Yubico. Get their insights on the pain points of passwords, their hopes for the future, and explore some of our resources around industry advancements paving the way, like passwordless authentication, multi-factor authentication and WebAuthn.