diff --git a/NEWS b/NEWS
index f20ac12be..f77784cb3 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,13 @@
+== Version 1.12.0 ==
+
+New features:
+
+* New method `RegisteredCredential.builder().publicKeyEs256Raw(ByteArray)`. This
+ is a mutually exclusive alternative to `.publicKeyCose(ByteArray)`, for easier
+ backwards-compatibility with U2F-formatted (Raw ANSI X9.62) public keys.
+* "Migrating from U2F" section added to project README
+
+
== Version 1.11.0 ==
Deprecated features:
diff --git a/README b/README
index 452c3b42d..a37632f04 100644
--- a/README
+++ b/README
@@ -25,7 +25,7 @@ Maven:
com.yubico
webauthn-server-core
- 1.11.0
+ 1.12.0
compile
----------
@@ -33,7 +33,7 @@ Maven:
Gradle:
----------
-compile 'com.yubico:webauthn-server-core:1.11.0'
+compile 'com.yubico:webauthn-server-core:1.12.0'
----------
=== Semantic versioning
@@ -462,6 +462,70 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration(
----------
+== Migrating from U2F
+
+This section is only relevant for applications that have user credentials registered via the
+link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html[U2F JavaScript API].
+New WebAuthn deployments can skip this section.
+
+The WebAuthn API is backwards-compatible with U2F authenticators,
+and credentials registered via the U2F API will continue to work with the WebAuthn API with the right settings.
+
+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.
++
+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
+ 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
+ link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html#high-level-javascript-api[U2F `register` and `sign` functions].
++
+This will enable the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-appid-extension[`appid`]
+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].
+
+Existing U2F credentials should now work with the WebAuthn API.
+
+Note that new credentials registered on U2F authenticators via the WebAuthn API
+are NOT backwards compatible with the U2F JavaScript API.
+
+
== Architecture
The library tries to place as few requirements on the overall application
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java
index 0c2c787fd..0ba783bf0 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java
@@ -30,6 +30,7 @@
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.AuthenticatorData;
import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserIdentity;
import lombok.Builder;
@@ -143,12 +144,91 @@ public class Step3 {
* {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required
* parameter.
*
+ *
The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable
+ * argument for this method.
+ *
+ *
Alternatively, the public key can be specified using the {@link
+ * #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (
+ * ALG_KEY_ECC_X962_RAW
as specified in FIDO
+ * Registry §3.6.2 Public Key Representation Formats). This is mostly useful for public
+ * keys registered via the U2F JavaScript API.
+ *
+ * @see #publicKeyEs256Raw(ByteArray)
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
+ * @see FIDO
+ * Registry §3.6.2 Public Key Representation Formats
*/
public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) {
return builder.publicKeyCose(publicKeyCose);
}
+
+ /**
+ * Specify the credential public key in U2F format.
+ *
+ *
An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
+ * COSEAlgorithmIdentifier#ES256 ES256} public key in ALG_KEY_ECC_X962_RAW
+ * format as specified in FIDO
+ * Registry §3.6.2 Public Key Representation Formats.
+ *
+ *
This is primarily intended for public keys registered via the U2F JavaScript API. If
+ * your application has only used the navigator.credentials.create()
API to
+ * register credentials, you should use {@link #publicKeyCose(ByteArray)} instead.
+ *
+ * @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
+ */
+ public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) {
+ return builder.publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw));
+ }
}
}
+
+ /**
+ * The credential public key encoded in COSE_Key format, as defined in Section 7 of RFC 8152. This method overwrites {@link
+ * #publicKeyEs256Raw(ByteArray)}.
+ *
+ *
The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable argument
+ * for this method.
+ *
+ *
This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature}
+ * in authentication assertions.
+ *
+ *
Alternatively, the public key can be specified using the {@link
+ * #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (
+ * ALG_KEY_ECC_X962_RAW
as specified in FIDO
+ * Registry §3.6.2 Public Key Representation Formats). This is mostly useful for public keys
+ * registered via the U2F JavaScript API.
+ *
+ * @see AttestedCredentialData#getCredentialPublicKey()
+ * @see RegistrationResult#getPublicKeyCose()
+ */
+ public RegisteredCredentialBuilder publicKeyCose(@NonNull ByteArray publicKeyCose) {
+ this.publicKeyCose = publicKeyCose;
+ return this;
+ }
+
+ /**
+ * Specify the credential public key in U2F format. This method overwrites {@link
+ * #publicKeyCose(ByteArray)}.
+ *
+ *
An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
+ * COSEAlgorithmIdentifier#ES256 ES256} public key in ALG_KEY_ECC_X962_RAW
format
+ * as specified in FIDO
+ * Registry §3.6.2 Public Key Representation Formats.
+ *
+ *
This is primarily intended for public keys registered via the U2F JavaScript API. If your
+ * application has only used the navigator.credentials.create()
API to register
+ * credentials, you should use {@link #publicKeyCose(ByteArray)} instead.
+ *
+ * @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
+ */
+ public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) {
+ return publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw));
+ }
}
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java
index 6a784759a..986061eb2 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java
@@ -40,6 +40,8 @@
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Optional;
final class WebAuthnCodecs {
@@ -63,6 +65,28 @@ static ByteArray ecPublicKeyToRaw(ECPublicKey key) {
Bytes.concat(yPadding, Arrays.copyOfRange(y, Math.max(0, y.length - 32), y.length))));
}
+ static ByteArray rawEcKeyToCose(ByteArray key) {
+ final byte[] keyBytes = key.getBytes();
+ if (!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes[0] == 0x04))) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was %d bytes starting with %02x",
+ keyBytes.length, keyBytes[0]));
+ }
+ final int start = (keyBytes.length == 64) ? 0 : 1;
+
+ final Map coseKey = new HashMap<>();
+ coseKey.put(1L, 2L); // Key type: EC
+
+ coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId());
+ coseKey.put(-1L, 1L); // Curve: P-256
+
+ coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + 32)); // x
+ coseKey.put(-3L, Arrays.copyOfRange(keyBytes, start + 32, start + 64)); // y
+
+ return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes());
+ }
+
static PublicKey importCosePublicKey(ByteArray key)
throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes());
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
index 0e3f30453..e525c3190 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
@@ -52,6 +52,7 @@ import com.yubico.webauthn.data.UserVerificationRequirement
import com.yubico.webauthn.exception.InvalidSignatureCountException
import com.yubico.webauthn.extension.appid.AppId
import com.yubico.webauthn.test.Helpers
+import com.yubico.webauthn.test.RealExamples
import com.yubico.webauthn.test.Util.toStepWithUtilities
import org.junit.runner.RunWith
import org.scalacheck.Gen
@@ -1946,6 +1947,56 @@ class RelyingPartyAssertionSpec
test(RegistrationTestData.Packed.SelfAttestationRs1)
}
}
+
+ it("a U2F-formatted public key.") {
+ val testData = RealExamples.YubiKeyNeo.asRegistrationTestData
+ val x = ByteArray.fromHex(
+ "39C94FBBDDC694A925E6F8657C66916CFE84CD0222EDFCF281B21F5CDC347923"
+ )
+ val y = ByteArray.fromHex(
+ "D6B0D2021CFE1724A6FE81E3568C4FFAE339298216A30AFC18C0B975F2E2A891"
+ )
+ val u2fPubkey = ByteArray.fromHex("04").concat(x).concat(y)
+
+ val cred1 = RegisteredCredential
+ .builder()
+ .credentialId(testData.assertion.get.response.getId)
+ .userHandle(testData.userId.getId)
+ .publicKeyEs256Raw(u2fPubkey)
+ .signatureCount(0)
+ .build()
+
+ val cred2 = RegisteredCredential
+ .builder()
+ .credentialId(testData.assertion.get.response.getId)
+ .userHandle(testData.userId.getId)
+ .publicKeyCose(u2fPubkey)
+ .signatureCount(0)
+ .publicKeyEs256Raw(u2fPubkey)
+ .build()
+
+ for { cred <- List(cred1, cred2) } {
+ val rp = RelyingParty
+ .builder()
+ .identity(testData.rpId)
+ .credentialRepository(
+ Helpers.CredentialRepository.withUser(testData.userId, cred)
+ )
+ .build()
+
+ val result = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(testData.assertion.get.request)
+ .response(testData.assertion.get.response)
+ .build()
+ )
+
+ result.isSuccess should be(true)
+ result.getUserHandle should equal(testData.userId.getId)
+ result.getCredentialId should equal(testData.response.getId)
+ }
+ }
}
describe("The default RelyingParty settings") {
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala
index 490da14ae..948109799 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala
@@ -86,13 +86,13 @@ class WebAuthnCodecsSpec
}
- describe("The rawEcdaKeyToCose method") {
+ describe("The rawEcKeyToCose method") {
it("outputs a value that can be imported by importCoseP256PublicKey") {
forAll { originalPubkey: ECPublicKey =>
val rawKey = WebAuthnCodecs.ecPublicKeyToRaw(originalPubkey)
- val coseKey = WebAuthnTestCodecs.rawEcdaKeyToCose(rawKey)
+ val coseKey = WebAuthnCodecs.rawEcKeyToCose(rawKey)
val importedPubkey: ECPublicKey = WebAuthnCodecs
.importCosePublicKey(coseKey)
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala
index 851fb9266..92a68dd67 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala
@@ -1,6 +1,7 @@
package com.yubico.webauthn
import com.upokecenter.cbor.CBORObject
+import com.yubico.webauthn.WebAuthnCodecs.rawEcKeyToCose
import com.yubico.webauthn.data.ByteArray
import com.yubico.webauthn.data.COSEAlgorithmIdentifier
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
@@ -22,39 +23,7 @@ object WebAuthnTestCodecs {
def importCosePublicKey = WebAuthnCodecs.importCosePublicKey _
def ecPublicKeyToCose(key: ECPublicKey): ByteArray =
- rawEcdaKeyToCose(ecPublicKeyToRaw(key))
-
- def rawEcdaKeyToCose(key: ByteArray): ByteArray = {
- val keyBytes = key.getBytes
- if (
- !(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes(0) == 0x04))
- ) {
- throw new IllegalArgumentException(
- s"Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was ${keyBytes.length} bytes starting with ${keyBytes(0)}"
- )
- }
- val start: Int =
- if (keyBytes.length == 64) 0
- else 1
-
- val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any]
- coseKey.put(1L, 2L) // Key type: EC
-
- coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId)
- coseKey.put(-1L, 1L) // Curve: P-256
-
- coseKey.put(
- -2L,
- java.util.Arrays.copyOfRange(keyBytes, start, start + 32),
- ) // x
-
- coseKey.put(
- -3L,
- java.util.Arrays.copyOfRange(keyBytes, start + 32, start + 64),
- ) // y
-
- new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
- }
+ rawEcKeyToCose(ecPublicKeyToRaw(key))
def publicKeyToCose(key: PublicKey): ByteArray = {
key match {
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala
index 11b36a4ab..f69873ce5 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala
@@ -110,6 +110,8 @@ object RealExamples {
attestationObject = attestation.attestationObjectBytes,
clientDataJson = attestation.clientData,
privateKey = None,
+ rpId = rp,
+ userId = user,
assertion = Some(
AssertionTestData(
request = AssertionRequest
@@ -120,6 +122,7 @@ object RealExamples {
.challenge(assertion.collectedClientData.getChallenge)
.build()
)
+ .username(user.getName)
.build(),
response = assertion.credential,
)