Skip to content

Commit ddb1af3

Browse files
committed
Release 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
2 parents eb2f179 + 0762b12 commit ddb1af3

File tree

8 files changed

+238
-37
lines changed

8 files changed

+238
-37
lines changed

NEWS

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
== Version 1.12.0 ==
2+
3+
New features:
4+
5+
* New method `RegisteredCredential.builder().publicKeyEs256Raw(ByteArray)`. This
6+
is a mutually exclusive alternative to `.publicKeyCose(ByteArray)`, for easier
7+
backwards-compatibility with U2F-formatted (Raw ANSI X9.62) public keys.
8+
* "Migrating from U2F" section added to project README
9+
10+
111
== Version 1.11.0 ==
212

313
Deprecated features:

README

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ Maven:
2525
<dependency>
2626
<groupId>com.yubico</groupId>
2727
<artifactId>webauthn-server-core</artifactId>
28-
<version>1.11.0</version>
28+
<version>1.12.0</version>
2929
<scope>compile</scope>
3030
</dependency>
3131
----------
3232

3333
Gradle:
3434

3535
----------
36-
compile 'com.yubico:webauthn-server-core:1.11.0'
36+
compile 'com.yubico:webauthn-server-core:1.12.0'
3737
----------
3838

3939
=== Semantic versioning
@@ -462,6 +462,70 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration(
462462
----------
463463

464464

465+
== Migrating from U2F
466+
467+
This section is only relevant for applications that have user credentials registered via the
468+
link:https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html[U2F JavaScript API].
469+
New WebAuthn deployments can skip this section.
470+
471+
The WebAuthn API is backwards-compatible with U2F authenticators,
472+
and credentials registered via the U2F API will continue to work with the WebAuthn API with the right settings.
473+
474+
To migrate to using the WebAuthn API, you need to do the following:
475+
476+
* Follow the link:#getting-started[Getting started] guide above to set up WebAuthn support in general.
477+
+
478+
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]
479+
consists of only the domain name of the AppID.
480+
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].
481+
482+
* Set the
483+
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()`]
484+
setting on your `RelyingParty` instance.
485+
The argument to the `appid()` setting should be the same as you used for the `appId` argument to the
486+
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].
487+
+
488+
This will enable the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-appid-extension[`appid`]
489+
and link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-appid-exclude-extension[`appidExclude`]
490+
extensions and configure the `RelyingParty` to accept the given AppId when verifying authenticator signatures.
491+
492+
* Generate a link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle[user handle] for each existing user
493+
and store it in their account,
494+
or decide on a method for deriving one deterministically from existing user attributes.
495+
For example, if your user records are assigned UUIDs, you can use that UUID as the user handle.
496+
You SHOULD NOT use a plain username or e-mail address, or hash of either, as the user handle -
497+
for more on this, see the link:https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-user-handle-privacy[User Handle Contents]
498+
privacy consideration.
499+
500+
* When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential,
501+
use the U2F key handle as the
502+
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].
503+
If you store key handles base64 encoded, you should decode them using
504+
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`]
505+
or
506+
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`]
507+
as appropriate before passing them to the `RegisteredCredential`.
508+
509+
* When your `CredentialRepository` creates a `RegisteredCredential` for a U2F credential,
510+
use the
511+
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()`]
512+
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()`]
513+
to set the credential public key.
514+
515+
* Replace calls to the U2F
516+
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`]
517+
method with calls to `navigator.credentials.create()` as described in link:#getting-started[Getting started].
518+
519+
* Replace calls to the U2F
520+
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`]
521+
method with calls to `navigator.credentials.get()` as described in link:#getting-started[Getting started].
522+
523+
Existing U2F credentials should now work with the WebAuthn API.
524+
525+
Note that new credentials registered on U2F authenticators via the WebAuthn API
526+
are NOT backwards compatible with the U2F JavaScript API.
527+
528+
465529
== Architecture
466530

467531
The library tries to place as few requirements on the overall application

webauthn-server-core/src/main/java/com/yubico/webauthn/RegisteredCredential.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
3131
import com.yubico.webauthn.data.AuthenticatorData;
3232
import com.yubico.webauthn.data.ByteArray;
33+
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
3334
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
3435
import com.yubico.webauthn.data.UserIdentity;
3536
import lombok.Builder;
@@ -143,12 +144,91 @@ public class Step3 {
143144
* {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required
144145
* parameter.
145146
*
147+
* <p>The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable
148+
* argument for this method.
149+
*
150+
* <p>Alternatively, the public key can be specified using the {@link
151+
* #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (<code>
152+
* ALG_KEY_ECC_X962_RAW</code> as specified in <a
153+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
154+
* Registry §3.6.2 Public Key Representation Formats</a>). This is mostly useful for public
155+
* keys registered via the U2F JavaScript API.
156+
*
157+
* @see #publicKeyEs256Raw(ByteArray)
146158
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
159+
* @see <a
160+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
161+
* Registry §3.6.2 Public Key Representation Formats</a>
147162
*/
148163
public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) {
149164
return builder.publicKeyCose(publicKeyCose);
150165
}
166+
167+
/**
168+
* Specify the credential public key in U2F format.
169+
*
170+
* <p>An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
171+
* COSEAlgorithmIdentifier#ES256 ES256} public key in <code>ALG_KEY_ECC_X962_RAW</code>
172+
* format as specified in <a
173+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
174+
* Registry §3.6.2 Public Key Representation Formats</a>.
175+
*
176+
* <p>This is primarily intended for public keys registered via the U2F JavaScript API. If
177+
* your application has only used the <code>navigator.credentials.create()</code> API to
178+
* register credentials, you should use {@link #publicKeyCose(ByteArray)} instead.
179+
*
180+
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
181+
*/
182+
public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) {
183+
return builder.publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw));
184+
}
151185
}
152186
}
187+
188+
/**
189+
* The credential public key encoded in COSE_Key format, as defined in Section 7 of <a
190+
* href="https://tools.ietf.org/html/rfc8152">RFC 8152</a>. This method overwrites {@link
191+
* #publicKeyEs256Raw(ByteArray)}.
192+
*
193+
* <p>The return value of {@link RegistrationResult#getPublicKeyCose()} is a suitable argument
194+
* for this method.
195+
*
196+
* <p>This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature}
197+
* in authentication assertions.
198+
*
199+
* <p>Alternatively, the public key can be specified using the {@link
200+
* #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (<code>
201+
* ALG_KEY_ECC_X962_RAW</code> as specified in <a
202+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
203+
* Registry §3.6.2 Public Key Representation Formats</a>). This is mostly useful for public keys
204+
* registered via the U2F JavaScript API.
205+
*
206+
* @see AttestedCredentialData#getCredentialPublicKey()
207+
* @see RegistrationResult#getPublicKeyCose()
208+
*/
209+
public RegisteredCredentialBuilder publicKeyCose(@NonNull ByteArray publicKeyCose) {
210+
this.publicKeyCose = publicKeyCose;
211+
return this;
212+
}
213+
214+
/**
215+
* Specify the credential public key in U2F format. This method overwrites {@link
216+
* #publicKeyCose(ByteArray)}.
217+
*
218+
* <p>An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
219+
* COSEAlgorithmIdentifier#ES256 ES256} public key in <code>ALG_KEY_ECC_X962_RAW</code> format
220+
* as specified in <a
221+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
222+
* Registry §3.6.2 Public Key Representation Formats</a>.
223+
*
224+
* <p>This is primarily intended for public keys registered via the U2F JavaScript API. If your
225+
* application has only used the <code>navigator.credentials.create()</code> API to register
226+
* credentials, you should use {@link #publicKeyCose(ByteArray)} instead.
227+
*
228+
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
229+
*/
230+
public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) {
231+
return publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw));
232+
}
153233
}
154234
}

webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import java.security.spec.RSAPublicKeySpec;
4141
import java.security.spec.X509EncodedKeySpec;
4242
import java.util.Arrays;
43+
import java.util.HashMap;
44+
import java.util.Map;
4345
import java.util.Optional;
4446

4547
final class WebAuthnCodecs {
@@ -63,6 +65,28 @@ static ByteArray ecPublicKeyToRaw(ECPublicKey key) {
6365
Bytes.concat(yPadding, Arrays.copyOfRange(y, Math.max(0, y.length - 32), y.length))));
6466
}
6567

68+
static ByteArray rawEcKeyToCose(ByteArray key) {
69+
final byte[] keyBytes = key.getBytes();
70+
if (!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes[0] == 0x04))) {
71+
throw new IllegalArgumentException(
72+
String.format(
73+
"Raw key must be 64 bytes long or be 65 bytes long and start with 0x04, was %d bytes starting with %02x",
74+
keyBytes.length, keyBytes[0]));
75+
}
76+
final int start = (keyBytes.length == 64) ? 0 : 1;
77+
78+
final Map<Long, Object> coseKey = new HashMap<>();
79+
coseKey.put(1L, 2L); // Key type: EC
80+
81+
coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId());
82+
coseKey.put(-1L, 1L); // Curve: P-256
83+
84+
coseKey.put(-2L, Arrays.copyOfRange(keyBytes, start, start + 32)); // x
85+
coseKey.put(-3L, Arrays.copyOfRange(keyBytes, start + 32, start + 64)); // y
86+
87+
return new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes());
88+
}
89+
6690
static PublicKey importCosePublicKey(ByteArray key)
6791
throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
6892
CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes());

webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import com.yubico.webauthn.data.UserVerificationRequirement
5252
import com.yubico.webauthn.exception.InvalidSignatureCountException
5353
import com.yubico.webauthn.extension.appid.AppId
5454
import com.yubico.webauthn.test.Helpers
55+
import com.yubico.webauthn.test.RealExamples
5556
import com.yubico.webauthn.test.Util.toStepWithUtilities
5657
import org.junit.runner.RunWith
5758
import org.scalacheck.Gen
@@ -1946,6 +1947,56 @@ class RelyingPartyAssertionSpec
19461947
test(RegistrationTestData.Packed.SelfAttestationRs1)
19471948
}
19481949
}
1950+
1951+
it("a U2F-formatted public key.") {
1952+
val testData = RealExamples.YubiKeyNeo.asRegistrationTestData
1953+
val x = ByteArray.fromHex(
1954+
"39C94FBBDDC694A925E6F8657C66916CFE84CD0222EDFCF281B21F5CDC347923"
1955+
)
1956+
val y = ByteArray.fromHex(
1957+
"D6B0D2021CFE1724A6FE81E3568C4FFAE339298216A30AFC18C0B975F2E2A891"
1958+
)
1959+
val u2fPubkey = ByteArray.fromHex("04").concat(x).concat(y)
1960+
1961+
val cred1 = RegisteredCredential
1962+
.builder()
1963+
.credentialId(testData.assertion.get.response.getId)
1964+
.userHandle(testData.userId.getId)
1965+
.publicKeyEs256Raw(u2fPubkey)
1966+
.signatureCount(0)
1967+
.build()
1968+
1969+
val cred2 = RegisteredCredential
1970+
.builder()
1971+
.credentialId(testData.assertion.get.response.getId)
1972+
.userHandle(testData.userId.getId)
1973+
.publicKeyCose(u2fPubkey)
1974+
.signatureCount(0)
1975+
.publicKeyEs256Raw(u2fPubkey)
1976+
.build()
1977+
1978+
for { cred <- List(cred1, cred2) } {
1979+
val rp = RelyingParty
1980+
.builder()
1981+
.identity(testData.rpId)
1982+
.credentialRepository(
1983+
Helpers.CredentialRepository.withUser(testData.userId, cred)
1984+
)
1985+
.build()
1986+
1987+
val result = rp.finishAssertion(
1988+
FinishAssertionOptions
1989+
.builder()
1990+
.request(testData.assertion.get.request)
1991+
.response(testData.assertion.get.response)
1992+
.build()
1993+
)
1994+
1995+
result.isSuccess should be(true)
1996+
result.getUserHandle should equal(testData.userId.getId)
1997+
result.getCredentialId should equal(testData.response.getId)
1998+
}
1999+
}
19492000
}
19502001

19512002
describe("The default RelyingParty settings") {

webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnCodecsSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,13 @@ class WebAuthnCodecsSpec
8686

8787
}
8888

89-
describe("The rawEcdaKeyToCose method") {
89+
describe("The rawEcKeyToCose method") {
9090

9191
it("outputs a value that can be imported by importCoseP256PublicKey") {
9292
forAll { originalPubkey: ECPublicKey =>
9393
val rawKey = WebAuthnCodecs.ecPublicKeyToRaw(originalPubkey)
9494

95-
val coseKey = WebAuthnTestCodecs.rawEcdaKeyToCose(rawKey)
95+
val coseKey = WebAuthnCodecs.rawEcKeyToCose(rawKey)
9696

9797
val importedPubkey: ECPublicKey = WebAuthnCodecs
9898
.importCosePublicKey(coseKey)

webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.yubico.webauthn
22

33
import com.upokecenter.cbor.CBORObject
4+
import com.yubico.webauthn.WebAuthnCodecs.rawEcKeyToCose
45
import com.yubico.webauthn.data.ByteArray
56
import com.yubico.webauthn.data.COSEAlgorithmIdentifier
67
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
@@ -22,39 +23,7 @@ object WebAuthnTestCodecs {
2223
def importCosePublicKey = WebAuthnCodecs.importCosePublicKey _
2324

2425
def ecPublicKeyToCose(key: ECPublicKey): ByteArray =
25-
rawEcdaKeyToCose(ecPublicKeyToRaw(key))
26-
27-
def rawEcdaKeyToCose(key: ByteArray): ByteArray = {
28-
val keyBytes = key.getBytes
29-
if (
30-
!(keyBytes.length == 64 || (keyBytes.length == 65 && keyBytes(0) == 0x04))
31-
) {
32-
throw new IllegalArgumentException(
33-
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)}"
34-
)
35-
}
36-
val start: Int =
37-
if (keyBytes.length == 64) 0
38-
else 1
39-
40-
val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any]
41-
coseKey.put(1L, 2L) // Key type: EC
42-
43-
coseKey.put(3L, COSEAlgorithmIdentifier.ES256.getId)
44-
coseKey.put(-1L, 1L) // Curve: P-256
45-
46-
coseKey.put(
47-
-2L,
48-
java.util.Arrays.copyOfRange(keyBytes, start, start + 32),
49-
) // x
50-
51-
coseKey.put(
52-
-3L,
53-
java.util.Arrays.copyOfRange(keyBytes, start + 32, start + 64),
54-
) // y
55-
56-
new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
57-
}
26+
rawEcKeyToCose(ecPublicKeyToRaw(key))
5827

5928
def publicKeyToCose(key: PublicKey): ByteArray = {
6029
key match {

webauthn-server-core/src/test/scala/com/yubico/webauthn/test/RealExamples.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ object RealExamples {
110110
attestationObject = attestation.attestationObjectBytes,
111111
clientDataJson = attestation.clientData,
112112
privateKey = None,
113+
rpId = rp,
114+
userId = user,
113115
assertion = Some(
114116
AssertionTestData(
115117
request = AssertionRequest
@@ -120,6 +122,7 @@ object RealExamples {
120122
.challenge(assertion.collectedClientData.getChallenge)
121123
.build()
122124
)
125+
.username(user.getName)
123126
.build(),
124127
response = assertion.credential,
125128
)

0 commit comments

Comments
 (0)