Skip to content

Commit

Permalink
Release 1.12.0
Browse files Browse the repository at this point in the history
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
  • Loading branch information
emlun committed Nov 22, 2021
2 parents eb2f179 + 0762b12 commit ddb1af3
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 37 deletions.
10 changes: 10 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
68 changes: 66 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ Maven:
<dependency>
<groupId>com.yubico</groupId>
<artifactId>webauthn-server-core</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
<scope>compile</scope>
</dependency>
----------

Gradle:

----------
compile 'com.yubico:webauthn-server-core:1.11.0'
compile 'com.yubico:webauthn-server-core:1.12.0'
----------

=== Semantic versioning
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,12 +144,91 @@ public class Step3 {
* {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required
* parameter.
*
* <p>The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable
* argument for this method.
*
* <p>Alternatively, the public key can be specified using the {@link
* #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (<code>
* ALG_KEY_ECC_X962_RAW</code> as specified in <a
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
* Registry §3.6.2 Public Key Representation Formats</a>). This is mostly useful for public
* keys registered via the U2F JavaScript API.
*
* @see #publicKeyEs256Raw(ByteArray)
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
* @see <a
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
* Registry §3.6.2 Public Key Representation Formats</a>
*/
public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) {
return builder.publicKeyCose(publicKeyCose);
}

/**
* Specify the credential public key in U2F format.
*
* <p>An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
* COSEAlgorithmIdentifier#ES256 ES256} public key in <code>ALG_KEY_ECC_X962_RAW</code>
* format as specified in <a
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
* Registry §3.6.2 Public Key Representation Formats</a>.
*
* <p>This is primarily intended for public keys registered via the U2F JavaScript API. If
* your application has only used the <code>navigator.credentials.create()</code> 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 <a
* href="https://tools.ietf.org/html/rfc8152">RFC 8152</a>. This method overwrites {@link
* #publicKeyEs256Raw(ByteArray)}.
*
* <p>The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable argument
* for this method.
*
* <p>This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature}
* in authentication assertions.
*
* <p>Alternatively, the public key can be specified using the {@link
* #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (<code>
* ALG_KEY_ECC_X962_RAW</code> as specified in <a
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
* Registry §3.6.2 Public Key Representation Formats</a>). 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)}.
*
* <p>An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
* COSEAlgorithmIdentifier#ES256 ES256} public key in <code>ALG_KEY_ECC_X962_RAW</code> format
* as specified in <a
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
* Registry §3.6.2 Public Key Representation Formats</a>.
*
* <p>This is primarily intended for public keys registered via the U2F JavaScript API. If your
* application has only used the <code>navigator.credentials.create()</code> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Long, Object> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ object RealExamples {
attestationObject = attestation.attestationObjectBytes,
clientDataJson = attestation.clientData,
privateKey = None,
rpId = rp,
userId = user,
assertion = Some(
AssertionTestData(
request = AssertionRequest
Expand All @@ -120,6 +122,7 @@ object RealExamples {
.challenge(assertion.collectedClientData.getChallenge)
.build()
)
.username(user.getName)
.build(),
response = assertion.credential,
)
Expand Down

0 comments on commit ddb1af3

Please sign in to comment.