Skip to content

Commit a692324

Browse files
committed
Add method RegisteredCredential.builder().publicKeyEs256Raw(ByteArray)
1 parent 32b26ef commit a692324

File tree

6 files changed

+113
-36
lines changed

6 files changed

+113
-36
lines changed

NEWS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
== Version 1.12.0 (unreleased) ==
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+
9+
110
== Version 1.11.0 ==
211

312
Deprecated features:

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

Lines changed: 74 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,85 @@ public class Step3 {
143144
* {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required
144145
* parameter.
145146
*
147+
* <p>Alternatively, the public key can be specified using the {@link
148+
* #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (<code>
149+
* ALG_KEY_ECC_X962_RAW</code> as specified in <a
150+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
151+
* Registry §3.6.2 Public Key Representation Formats</a>). This is mostly useful for public
152+
* keys registered via the U2F JavaScript API.
153+
*
154+
* @see #publicKeyEs256Raw(ByteArray)
146155
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
156+
* @see <a
157+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
158+
* Registry §3.6.2 Public Key Representation Formats</a>
147159
*/
148160
public RegisteredCredentialBuilder publicKeyCose(ByteArray publicKeyCose) {
149161
return builder.publicKeyCose(publicKeyCose);
150162
}
163+
164+
/**
165+
* Specify the credential public key in U2F format.
166+
*
167+
* <p>An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
168+
* COSEAlgorithmIdentifier#ES256 ES256} public key in <code>ALG_KEY_ECC_X962_RAW</code>
169+
* format as specified in <a
170+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
171+
* Registry §3.6.2 Public Key Representation Formats</a>.
172+
*
173+
* <p>This is primarily intended for public keys registered via the U2F JavaScript API. If
174+
* your application has only used the <code>navigator.credentials.create()</code> API to
175+
* register credentials, you should use {@link #publicKeyCose(ByteArray)} instead.
176+
*
177+
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
178+
*/
179+
public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) {
180+
return builder.publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw));
181+
}
151182
}
152183
}
184+
185+
/**
186+
* The credential public key encoded in COSE_Key format, as defined in Section 7 of <a
187+
* href="https://tools.ietf.org/html/rfc8152">RFC 8152</a>. This method overwrites {@link
188+
* #publicKeyEs256Raw(ByteArray)}.
189+
*
190+
* <p>This is used to verify the {@link AuthenticatorAssertionResponse#getSignature() signature}
191+
* in authentication assertions.
192+
*
193+
* <p>Alternatively, the public key can be specified using the {@link
194+
* #publicKeyEs256Raw(ByteArray)} method if the key is stored in the U2F format (<code>
195+
* ALG_KEY_ECC_X962_RAW</code> as specified in <a
196+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
197+
* Registry §3.6.2 Public Key Representation Formats</a>). This is mostly useful for public keys
198+
* registered via the U2F JavaScript API.
199+
*
200+
* @see AttestedCredentialData#getCredentialPublicKey()
201+
* @see RegistrationResult#getPublicKeyCose()
202+
*/
203+
public RegisteredCredentialBuilder publicKeyCose(@NonNull ByteArray publicKeyCose) {
204+
this.publicKeyCose = publicKeyCose;
205+
return this;
206+
}
207+
208+
/**
209+
* Specify the credential public key in U2F format. This method overwrites {@link
210+
* #publicKeyCose(ByteArray)}.
211+
*
212+
* <p>An alternative to {@link #publicKeyCose(ByteArray)}, this method expects an {@link
213+
* COSEAlgorithmIdentifier#ES256 ES256} public key in <code>ALG_KEY_ECC_X962_RAW</code> format
214+
* as specified in <a
215+
* href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats">FIDO
216+
* Registry §3.6.2 Public Key Representation Formats</a>.
217+
*
218+
* <p>This is primarily intended for public keys registered via the U2F JavaScript API. If your
219+
* application has only used the <code>navigator.credentials.create()</code> API to register
220+
* credentials, you should use {@link #publicKeyCose(ByteArray)} instead.
221+
*
222+
* @see RegisteredCredentialBuilder#publicKeyCose(ByteArray)
223+
*/
224+
public RegisteredCredentialBuilder publicKeyEs256Raw(ByteArray publicKeyEs256Raw) {
225+
return publicKeyCose(WebAuthnCodecs.rawEcKeyToCose(publicKeyEs256Raw));
226+
}
153227
}
154228
}

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1962,7 +1962,7 @@ class RelyingPartyAssertionSpec
19621962
.builder()
19631963
.credentialId(testData.assertion.get.response.getId)
19641964
.userHandle(testData.userId.getId)
1965-
.publicKeyCose(u2fPubkey)
1965+
.publicKeyEs256Raw(u2fPubkey)
19661966
.signatureCount(0)
19671967
.build()
19681968

@@ -1972,6 +1972,7 @@ class RelyingPartyAssertionSpec
19721972
.userHandle(testData.userId.getId)
19731973
.publicKeyCose(u2fPubkey)
19741974
.signatureCount(0)
1975+
.publicKeyEs256Raw(u2fPubkey)
19751976
.build()
19761977

19771978
for { cred <- List(cred1, cred2) } {

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 {

0 commit comments

Comments
 (0)