diff --git a/NEWS b/NEWS index f20ac12be..1a38fc2b5 100644 --- a/NEWS +++ b/NEWS @@ -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: 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..ef1cfb89f 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,85 @@ public class Step3 { * {@link RegisteredCredentialBuilder#publicKeyCose(ByteArray) publicKeyCose} is a required * parameter. * + *

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)}. + * + *

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 cacceeb5d..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 @@ -1962,7 +1962,7 @@ class RelyingPartyAssertionSpec .builder() .credentialId(testData.assertion.get.response.getId) .userHandle(testData.userId.getId) - .publicKeyCose(u2fPubkey) + .publicKeyEs256Raw(u2fPubkey) .signatureCount(0) .build() @@ -1972,6 +1972,7 @@ class RelyingPartyAssertionSpec .userHandle(testData.userId.getId) .publicKeyCose(u2fPubkey) .signatureCount(0) + .publicKeyEs256Raw(u2fPubkey) .build() for { cred <- List(cred1, cred2) } { 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 {