diff --git a/NEWS b/NEWS index 07649e96b..ae4c811be 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,13 @@ +== Version 1.12.2 == + +Fixes: + +* `com.upokecenter:cbor` dependency bumped to minimum version 4.5.1 due to a + known vulnerability, see: https://github.com/advisories/GHSA-fj2w-wfgv-mwq6 +* Fixed crash in `AuthenticatorData` deserialization with `com.upokecenter:cbor` + versions later than 4.0.1 + + == Version 1.12.1 == Fixes: diff --git a/README b/README index 00f5eb037..109191c79 100644 --- a/README +++ b/README @@ -25,7 +25,7 @@ Maven: com.yubico webauthn-server-core - 1.12.1 + 1.12.2 compile ---------- @@ -33,7 +33,7 @@ Maven: Gradle: ---------- -compile 'com.yubico:webauthn-server-core:1.12.1' +compile 'com.yubico:webauthn-server-core:1.12.2' ---------- === Semantic versioning @@ -110,10 +110,7 @@ The client side involves: passing the result from `RelyingParty.startRegistration(...)` or `.startAssertion(...)` as the argument. 2. Encode the result of the successfully resolved promise and return it to the server. For this you need some way to encode `Uint8Array` values; - this guide will assume use of link:https://github.com/beatgammit/base64-js[base64-js]. - However the built-in parser methods for - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/PublicKeyCredential.html[`PublicKeyCredential`] - expect URL-safe Base64 encoding, so the below examples will include this as an additional step. + this guide will use GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library. Example code is given below. For more detailed example usage, see @@ -163,6 +160,8 @@ A registration ceremony consists of 5 main steps: 4. Validate the response using `RelyingParty.finishRegistration(...)`. 5. Update your database using the `finishRegistration` output. +This example uses GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library to do both (2) and (3) in one function call. + First, generate registration parameters and send them to the client: [source,java] @@ -196,62 +195,23 @@ Now call the WebAuthn API on the client side: [source,javascript] ---------- -function base64urlToUint8array(base64Bytes) { - const padding = '===='.substring(0, (4 - (base64Bytes.length % 4)) % 4); - return base64js.toByteArray((base64Bytes + padding).replace(/\//g, "_").replace(/\+/g, "-")); -} -function uint8arrayToBase64url(bytes) { - if (bytes instanceof Uint8Array) { - return base64js.fromByteArray(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - } else { - return uint8arrayToBase64url(new Uint8Array(bytes)); - } -} - -fetch(/* ... */) // Make the call that returns the credentialCreateJson above - .then(credentialCreateJson => ({ // Decode byte arrays from base64url - publicKey: { - ...credentialCreateJson.publicKey, - - challenge: base64urlToUint8array(credentialCreateJson.publicKey.challenge), - user: { - ...credentialCreateJson.publicKey.user, - id: base64urlToUint8array(credentialCreateJson.publicKey.user.id), - }, - excludeCredentials: credentialCreateJson.publicKey.excludeCredentials.map(credential => ({ - ...credential, - id: base64urlToUint8array(credential.id), - })), - - // Warning: Extension inputs could also contain binary data that needs encoding - extensions: credentialCreateJson.publicKey.extensions, - }, - })) - .then(credentialCreateOptions => // Call WebAuthn ceremony - navigator.credentials.create(credentialCreateOptions)) - .then(publicKeyCredential => ({ // Encode PublicKeyCredential for transport to server (example) - type: publicKeyCredential.type, - id: publicKeyCredential.id, - response: { - attestationObject: uint8arrayToBase64url(publicKeyCredential.response.attestationObject), - clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON), - - // Attempt to read transports, but recover gracefully if not supported by the browser - transports: publicKeyCredential.response.getTransports && publicKeyCredential.response.getTransports() || [], - }, - - // Warning: Client extension results could also contain binary data that needs encoding - clientExtensionResults: publicKeyCredential.getClientExtensionResults(), - })) - .then(encodedResult => - fetch(/* ... */)); // Return encoded PublicKeyCredential to server +import * as webauthnJson from "@github/webauthn-json"; + +// Make the call that returns the credentialCreateJson above +const credentialCreateOptions = await fetch(/* ... */).then(resp => resp.json()); + +// Call WebAuthn ceremony using webauthn-json wrapper +const publicKeyCredential = await webauthnJson.create(credentialCreateOptions); + +// Return encoded PublicKeyCredential to server +fetch(/* ... */, { body: JSON.stringify(publicKeyCredential) }); ---------- Validate the response on the server side: [source,java] ---------- -String publicKeyCredentialJson = /* ... */; // encodedResult from client +String publicKeyCredentialJson = /* ... */; // publicKeyCredential from client PublicKeyCredential pkc = PublicKeyCredential.parseRegistrationResponseJson(publicKeyCredentialJson); @@ -295,6 +255,8 @@ Like registration ceremonies, an authentication ceremony consists of 5 main step 4. Validate the response using `RelyingParty.finishAssertion(...)`. 5. Update your database using the `finishAssertion` output, and act upon the result (for example, grant login access). +This example uses GitHub's link:https://github.com/github/webauthn-json[webauthn-json] library to do both (2) and (3) in one function call. + First, generate authentication parameters and send them to the client: [source,java] @@ -313,58 +275,23 @@ Now call the WebAuthn API on the client side: [source,javascript] ---------- -function base64urlToUint8array(base64Bytes) { - const padding = '===='.substring(0, (4 - (base64Bytes.length % 4)) % 4); - return base64js.toByteArray((base64Bytes + padding).replace(/\//g, "_").replace(/\+/g, "-")); -} -function uint8arrayToBase64url(bytes) { - if (bytes instanceof Uint8Array) { - return base64js.fromByteArray(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - } else { - return uint8arrayToBase64url(new Uint8Array(bytes)); - } -} - -fetch(/* ... */) // Make the call that returns the credentialGetJson above - .then(credentialGetJson => ({ // Decode byte arrays from base64url - publicKey: { - ...credentialGetJson.publicKey, - allowCredentials: credentialGetJson.publicKey.allowCredentials - && credentialGetJson.publicKey.allowCredentials.map(credential => ({ - ...credential, - id: base64urlToUint8array(credential.id), - })), - - challenge: base64urlToUint8array(credentialGetJson.publicKey.challenge), - - // Warning: Extension inputs could also contain binary data that needs encoding - extensions: credentialGetJson.publicKey.extensions, - }, - })) - .then(credentialGetOptions => // Call WebAuthn ceremony - navigator.credentials.get(credentialGetOptions)) - .then(publicKeyCredential => ({ // Encode PublicKeyCredential for transport to server (example) - type: publicKeyCredential.type, - id: publicKeyCredential.id, - response: { - authenticatorData: uint8arrayToBase64url(publicKeyCredential.response.authenticatorData), - clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON), - signature: uint8arrayToBase64url(publicKeyCredential.response.signature), - userHandle: publicKeyCredential.response.userHandle && uint8arrayToBase64url(publicKeyCredential.response.userHandle), - }, - - // Warning: Client extension results could also contain binary data that needs encoding - clientExtensionResults: publicKeyCredential.getClientExtensionResults(), - })) - .then(encodedResult => - fetch(/* ... */)); // Return encoded PublicKeyCredential to server +import * as webauthnJson from "@github/webauthn-json"; + +// Make the call that returns the credentialGetJson above +const credentialGetOptions = await fetch(/* ... */).then(resp => resp.json()); + +// Call WebAuthn ceremony using webauthn-json wrapper +const publicKeyCredential = await webauthnJson.get(credentialGetOptions); + +// Return encoded PublicKeyCredential to server +fetch(/* ... */, { body: JSON.stringify(publicKeyCredential) }); ---------- Validate the response on the server side: [source,java] ---------- -String publicKeyCredentialJson = /* ... */; // encodedResult from client +String publicKeyCredentialJson = /* ... */; // publicKeyCredential from client PublicKeyCredential pkc = PublicKeyCredential.parseAssertionResponseJson(publicKeyCredentialJson); @@ -473,13 +400,13 @@ and credentials registered via the U2F API will continue to work with the WebAut To migrate to using the WebAuthn API, you need to do the following: -* Follow the link:#getting-started[Getting started] guide above to set up WebAuthn support in general. + 1. Follow the link:#getting-started[Getting started] guide above to set up WebAuthn support in general. + Note that unlike a U2F AppID, the WebAuthn link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/RelyingPartyIdentity.RelyingPartyIdentityBuilder.html#id(java.lang.String)[RP ID] consists of only the domain name of the AppID. WebAuthn does not support link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html[U2F Trusted Facet Lists]. -* Set the + 2. Set the link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.RelyingPartyBuilder.html#appId(com.yubico.webauthn.extension.appid.AppId)[`appId()`] setting on your `RelyingParty` instance. The argument to the `appid()` setting should be the same as you used for the `appId` argument to the @@ -489,36 +416,36 @@ This will enable the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sc and link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-appid-exclude-extension[`appidExclude`] extensions and configure the `RelyingParty` to accept the given AppId when verifying authenticator signatures. -* Generate a link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle[user handle] for each existing user - and store it in their account, - or decide on a method for deriving one deterministically from existing user attributes. - For example, if your user records are assigned UUIDs, you can use that UUID as the user handle. - You SHOULD NOT use a plain username or e-mail address, or hash of either, as the user handle - - for more on this, see the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-user-handle-privacy[User Handle Contents] - privacy consideration. - -* When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, - use the U2F key handle as the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#credentialId(com.yubico.webauthn.data.ByteArray)[credential ID]. - If you store key handles base64 encoded, you should decode them using - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/ByteArray.html#fromBase64(java.lang.String)[`ByteArray.fromBase64`] - or - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/ByteArray.html#fromBase64Url(java.lang.String)[`ByteArray.fromBase64Url`] - as appropriate before passing them to the `RegisteredCredential`. - -* When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, - use the - link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyEs256Raw(com.yubico.webauthn.data.ByteArray)[`publicKeyEs256Raw()`] - method instead of link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyCose(com.yubico.webauthn.data.ByteArray)[`publicKeyCose()`] - to set the credential public key. - -* Replace calls to the U2F - link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[`register`] - method with calls to `navigator.credentials.create()` as described in link:#getting-started[Getting started]. - -* Replace calls to the U2F - link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[`sign`] - method with calls to `navigator.credentials.get()` as described in link:#getting-started[Getting started]. + 3. Generate a link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle[user handle] + for each existing user and store it in their account, + or decide on a method for deriving one deterministically from existing user attributes. + For example, if your user records are assigned UUIDs, you can use that UUID as the user handle. + You SHOULD NOT use a plain username or e-mail address, or hash of either, as the user handle - + for more on this, see the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-user-handle-privacy[User Handle Contents] + privacy consideration. + + 4. When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, + use the U2F key handle as the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#credentialId(com.yubico.webauthn.data.ByteArray)[credential ID]. + If you store key handles base64 encoded, you should decode them using + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/ByteArray.html#fromBase64(java.lang.String)[`ByteArray.fromBase64`] + or + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/data/ByteArray.html#fromBase64Url(java.lang.String)[`ByteArray.fromBase64Url`] + as appropriate before passing them to the `RegisteredCredential`. + + 5. When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential, + use the + link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyEs256Raw(com.yubico.webauthn.data.ByteArray)[`publicKeyEs256Raw()`] + method instead of link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegisteredCredential.RegisteredCredentialBuilder.html#publicKeyCose(com.yubico.webauthn.data.ByteArray)[`publicKeyCose()`] + to set the credential public key. + + 6. Replace calls to the U2F + link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[`register`] + method with calls to `navigator.credentials.create()` as described in link:#getting-started[Getting started]. + + 7. Replace calls to the U2F + link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[`sign`] + method with calls to `navigator.credentials.get()` as described in link:#getting-started[Getting started]. Existing U2F credentials should now work with the WebAuthn API. diff --git a/build.gradle b/build.gradle index 3bd0c4a60..f3da8fb6e 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath 'com.cinnober.gradle:semver-git:2.5.0' - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.0.4' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.2.0' classpath 'io.github.cosmicsilence:gradle-scalafix:0.1.8' } } @@ -51,7 +51,7 @@ dependencies { api('com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:[2.11.0,3)') api('com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.11.0,3)') api('com.google.guava:guava:[24.1.1,31)') - api('com.upokecenter:cbor:[4.0.1,5)') + api('com.upokecenter:cbor:[4.5.1,5)') api('javax.ws.rs:javax.ws.rs-api:[2.1,3)') api('javax.xml.bind:jaxb-api:[2.3.0,3)') api('junit:junit:[4.12,5)') diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java index 699071670..f15c7fbdd 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorData.java @@ -25,6 +25,7 @@ package com.yubico.webauthn.data; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; @@ -81,9 +82,9 @@ public class AuthenticatorData { * * @see #flags */ - private final transient AttestedCredentialData attestedCredentialData; + @JsonIgnore private final transient AttestedCredentialData attestedCredentialData; - private final transient CBORObject extensions; + @JsonIgnore private final transient CBORObject extensions; private static final int RP_ID_HASH_INDEX = 0; private static final int RP_ID_HASH_END = RP_ID_HASH_INDEX + 32; diff --git a/webauthn-server-demo/src/main/webapp/index.html b/webauthn-server-demo/src/main/webapp/index.html index 5dd90ece8..1950e074e 100644 --- a/webauthn-server-demo/src/main/webapp/index.html +++ b/webauthn-server-demo/src/main/webapp/index.html @@ -55,9 +55,9 @@ - -