Skip to content

Commit

Permalink
Add method RegisteredCredential.builder().publicKeyEs256Raw(ByteArray)
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Nov 4, 2021
1 parent 32b26ef commit a692324
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 36 deletions.
9 changes: 9 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
== Version 1.12.0 (unreleased) ==

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.


== Version 1.11.0 ==

Deprecated features:
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,85 @@ public class Step3 {
* {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required
* parameter.
*
* <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>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 @@ -1962,7 +1962,7 @@ class RelyingPartyAssertionSpec
.builder()
.credentialId(testData.assertion.get.response.getId)
.userHandle(testData.userId.getId)
.publicKeyCose(u2fPubkey)
.publicKeyEs256Raw(u2fPubkey)
.signatureCount(0)
.build()

Expand All @@ -1972,6 +1972,7 @@ class RelyingPartyAssertionSpec
.userHandle(testData.userId.getId)
.publicKeyCose(u2fPubkey)
.signatureCount(0)
.publicKeyEs256Raw(u2fPubkey)
.build()

for { cred <- List(cred1, cred2) } {
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

0 comments on commit a692324

Please sign in to comment.