diff --git a/NEWS b/NEWS index 979a4eafd..b5f91702b 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,10 @@ +== Version 2.5.1 (unreleased) == + +Changes: + +* Dropped dependency on COSE-Java. + + == Version 2.5.0 == `webauthn-server-core`: diff --git a/build.gradle b/build.gradle index d61a8e0bf..5bfbf93ac 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,6 @@ dependencies { constraints { api(constraintLibs.bundles.jackson) api(constraintLibs.cbor) - api(constraintLibs.cose) api(constraintLibs.guava) api(constraintLibs.httpclient5) api(constraintLibs.slf4j) diff --git a/doc/releasing.md b/doc/releasing.md index 88083b50c..3c2a46c88 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -6,13 +6,22 @@ Release candidate versions 1. Make sure release notes in `NEWS` are up to date. - 2. Run the tests one more time: + 2. Review the diff from the previous version for any changes to the public API, + and adjust the upcoming version number accordingly. + + If any implementation dependencies have been added to method signatures in + the public API, including `throws` declarations, change these dependencies + from `implementation` to `api` dependency declarations in the relevant + Gradle build script. Conversely, remove or downgrade to `implementation` any + dependencies no longer exposed in the public API. + + 3. Run the tests one more time: ``` $ ./gradlew clean check ``` - 3. Update the Java version in the [`release-verify-signatures` + 4. Update the Java version in the [`release-verify-signatures` workflow](https://github.com/Yubico/java-webauthn-server/blob/main/.github/workflows/release-verify-signatures.yml#L42). See the `openjdk version` line of output from `java -version`: @@ -34,7 +43,7 @@ Release candidate versions Commit this change, if any. - 4. Tag the head commit with an `X.Y.Z-RCN` tag: + 5. Tag the head commit with an `X.Y.Z-RCN` tag: ``` $ git tag -a -s 1.4.0-RC1 -m "Pre-release 1.4.0-RC1" @@ -42,13 +51,13 @@ Release candidate versions No tag body needed. - 5. Publish to Sonatype Nexus: + 6. Publish to Sonatype Nexus: ``` $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` - 6. Push to GitHub. + 7. Push to GitHub. If the pre-release makes significant changes to the project README, such that the README does not accurately reflect the latest non-pre-release @@ -66,7 +75,7 @@ Release candidate versions $ git push origin main 1.4.0-RC1 ``` - 7. Make GitHub release. + 8. Make GitHub release. - Use the new tag as the release tag. - Check the pre-release checkbox. @@ -76,7 +85,7 @@ Release candidate versions - Note the JDK version shown by `java -version` in step 3. For example: `openjdk version "17.0.7" 2023-04-18`. - 8. Check that the ["Reproducible binary" + 9. Check that the ["Reproducible binary" workflow](https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. @@ -86,7 +95,16 @@ Release versions 1. Make sure release notes in `NEWS` are up to date. - 2. Make a no-fast-forward merge from the last (non release candidate) release + 2. Review the diff from the previous version for any changes to the public API, + and adjust the upcoming version number accordingly. + + If any implementation dependencies have been added to method signatures in + the public API, including `throws` declarations, change these dependencies + from `implementation` to `api` dependency declarations in the relevant + Gradle build script. Conversely, remove or downgrade to `implementation` any + dependencies no longer exposed in the public API. + + 3. Make a no-fast-forward merge from the last (non release candidate) release to the commit to be released: ``` @@ -108,13 +126,13 @@ Release versions $ git branch -d release-1.4.0 ``` - 3. Remove the "(unreleased)" tag from `NEWS`. + 4. Remove the "(unreleased)" tag from `NEWS`. - 4. Update the version in the dependency snippets in the README. + 5. Update the version in the dependency snippets in the README. - 5. Update the version in JavaDoc links in the READMEs. + 6. Update the version in JavaDoc links in the READMEs. - 6. Update the Java version in the [`release-verify-signatures` + 7. Update the Java version in the [`release-verify-signatures` workflow](https://github.com/Yubico/java-webauthn-server/blob/main/.github/workflows/release-verify-signatures.yml#L42). See the `openjdk version` line of output from `java -version`: @@ -134,20 +152,20 @@ Release versions java: ["17.0.7"] ``` - 7. Amend these changes into the merge commit: + 8. Amend these changes into the merge commit: ``` $ git add NEWS README */README .github/workflows/release-verify-signatures.yml $ git commit --amend --reset-author ``` - 8. Run the tests one more time: + 9. Run the tests one more time: ``` $ ./gradlew clean check ``` - 9. Tag the merge commit with an `X.Y.Z` tag: +10. Tag the merge commit with an `X.Y.Z` tag: ``` $ git tag -a -s 1.4.0 -m "Release 1.4.0" @@ -155,19 +173,19 @@ Release versions No tag body needed since that's included in the commit. -10. Publish to Sonatype Nexus: +11. Publish to Sonatype Nexus: ``` $ ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` -11. Push to GitHub: +12. Push to GitHub: ``` $ git push origin main 1.4.0 ``` -12. Make GitHub release. +13. Make GitHub release. - Use the new tag as the release tag. - Copy the release notes from `NEWS` into the GitHub release notes; reformat @@ -176,6 +194,6 @@ Release versions - Note the JDK version shown by `java -version` in step 6. For example: `openjdk version "17.0.7" 2023-04-18`. -13. Check that the ["Reproducible binary" +14. Check that the ["Reproducible binary" workflow](https://github.com/Yubico/java-webauthn-server/actions/workflows/release-verify-signatures.yml) runs and succeeds. diff --git a/settings.gradle.kts b/settings.gradle.kts index 3cb500697..83a4d55e2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,7 +15,6 @@ dependencyResolutionManagement { versionCatalogs { create("constraintLibs") { library("cbor", "com.upokecenter:cbor:[4.5.1,5)") - library("cose", "com.augustcellars.cose:cose-java:[1.0.0,2)") library("guava", "com.google.guava:guava:[24.1.1,33)") library("httpclient5", "org.apache.httpcomponents.client5:httpclient5:[5.0.0,6)") library("slf4j", "org.slf4j:slf4j-api:[1.7.25,3)") diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts index 801446db1..3d5d07c94 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts @@ -12,9 +12,6 @@ dependencies { testImplementation("junit:junit:4.12") testImplementation("org.mockito:mockito-core:[2.27.0,3)") - // Runtime-only internal dependency of webauthn-server-core - testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)") - // Transitive dependencies from coreTestOutput testImplementation("org.scala-lang:scala-library:[2.13.1,3)") } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java index e38997392..b789838d2 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertTrue; -import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.RelyingPartyIdentity; import java.io.IOException; @@ -72,7 +71,7 @@ public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { @Test public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() - throws IOException, CoseException, InvalidKeySpecException { + throws IOException, InvalidKeySpecException { try { WebAuthnCodecs.importCosePublicKey( new AttestationObject( @@ -92,7 +91,7 @@ public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() @Test(expected = NoSuchAlgorithmException.class) public void doesNotFallBackToBouncyCastleAutomatically() - throws IOException, CoseException, InvalidKeySpecException, NoSuchAlgorithmException { + throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { for (Provider prov : Security.getProviders()) { Security.removeProvider(prov.getName()); } diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java index 78201c6a2..57cd1bcc3 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.RelyingPartyIdentity; import java.io.IOException; @@ -47,7 +46,7 @@ public void tearDown() { @Test public void importRsa() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { PublicKey key = WebAuthnCodecs.importCosePublicKey( new AttestationObject( @@ -61,7 +60,7 @@ public void importRsa() @Test public void importEcdsa() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { PublicKey key = WebAuthnCodecs.importCosePublicKey( new AttestationObject( @@ -75,7 +74,7 @@ public void importEcdsa() @Test public void importEddsa() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { PublicKey key = WebAuthnCodecs.importCosePublicKey( new AttestationObject( diff --git a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts index 1e8977835..29f2ab537 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts +++ b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts @@ -11,9 +11,6 @@ dependencies { testImplementation("junit:junit:4.12") testImplementation("org.mockito:mockito-core:[2.27.0,3)") - // Runtime-only internal dependency of webauthn-server-core - testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)") - // Transitive dependencies from coreTestOutput testImplementation("org.scala-lang:scala-library:[2.13.1,3)") } diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java index 6ce756bbc..27c43e834 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertTrue; -import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.RelyingPartyIdentity; import java.io.IOException; @@ -51,7 +50,7 @@ public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() { @Test public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey() - throws IOException, CoseException, InvalidKeySpecException { + throws IOException, InvalidKeySpecException { try { WebAuthnCodecs.importCosePublicKey( new AttestationObject( diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java index f35ce43ae..5a96dac81 100644 --- a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java +++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.RelyingPartyIdentity; import java.io.IOException; @@ -45,7 +44,7 @@ public void tearDown() { @Test public void importRsa() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { PublicKey key = WebAuthnCodecs.importCosePublicKey( new AttestationObject( @@ -59,7 +58,7 @@ public void importRsa() @Test public void importEcdsa() - throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException { + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { PublicKey key = WebAuthnCodecs.importCosePublicKey( new AttestationObject( diff --git a/webauthn-server-core/build.gradle.kts b/webauthn-server-core/build.gradle.kts index 79ed2a335..9bbd2bda8 100644 --- a/webauthn-server-core/build.gradle.kts +++ b/webauthn-server-core/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { api(platform(rootProject)) implementation(project(":yubico-util")) - implementation("com.augustcellars.cose:cose-java") implementation("com.fasterxml.jackson.core:jackson-databind") implementation("com.google.guava:guava") implementation("com.upokecenter:cbor") diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java index a962164e3..b815aa320 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AttestationStatementVerifier.java @@ -24,7 +24,6 @@ package com.yubico.webauthn; -import COSE.CoseException; import com.yubico.webauthn.data.AttestationObject; import com.yubico.webauthn.data.AttestationType; import com.yubico.webauthn.data.ByteArray; @@ -34,7 +33,7 @@ interface AttestationStatementVerifier { AttestationType getAttestationType(AttestationObject attestation) - throws IOException, CoseException, CertificateException; + throws IOException, CertificateException; boolean verifyAttestationSignature( AttestationObject attestationObject, ByteArray clientDataJsonHash); diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java index ac3f1f2f1..5806222c2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java @@ -26,7 +26,6 @@ import static com.yubico.webauthn.Crypto.isP256; -import COSE.CoseException; import com.fasterxml.jackson.databind.JsonNode; import com.yubico.internal.util.ExceptionUtil; import com.yubico.webauthn.data.AttestationObject; @@ -76,7 +75,7 @@ private static boolean validSelfSignature(X509Certificate cert) { } private static ByteArray getRawUserPublicKey(AttestationObject attestationObject) - throws IOException, CoseException { + throws IOException { final ByteArray pubkeyCose = attestationObject .getAuthenticatorData() @@ -102,7 +101,7 @@ private static ByteArray getRawUserPublicKey(AttestationObject attestationObject @Override public AttestationType getAttestationType(AttestationObject attestationObject) - throws CoseException, IOException, CertificateException { + throws IOException, CertificateException { X509Certificate attestationCertificate = getAttestationCertificate(attestationObject); if (attestationCertificate.getPublicKey() instanceof ECPublicKey @@ -153,7 +152,7 @@ && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams()))) { try { userPublicKey = getRawUserPublicKey(attestationObject); - } catch (IOException | CoseException e) { + } catch (IOException e) { RuntimeException err = new RuntimeException( String.format( diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index 88b792435..c39a87780 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -26,7 +26,6 @@ import static com.yubico.internal.util.ExceptionUtil.assertTrue; -import COSE.CoseException; import com.yubico.internal.util.OptionalUtil; import com.yubico.webauthn.data.AuthenticatorAssertionResponse; import com.yubico.webauthn.data.ByteArray; @@ -495,7 +494,7 @@ public void validate() { try { key = WebAuthnCodecs.importCosePublicKey(cose); - } catch (CoseException | IOException | InvalidKeySpecException e) { + } catch (IOException | InvalidKeySpecException e) { throw new IllegalArgumentException( String.format( "Failed to decode public key: Credential ID: %s COSE: %s", diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java index 172da13dd..4f8e20cd2 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java @@ -27,7 +27,6 @@ import static com.yubico.internal.util.ExceptionUtil.assertTrue; import static com.yubico.internal.util.ExceptionUtil.wrapAndLog; -import COSE.CoseException; import com.upokecenter.cbor.CBORObject; import com.yubico.internal.util.CertificateParser; import com.yubico.internal.util.OptionalUtil; @@ -335,7 +334,7 @@ public void validate() { .collect(Collectors.toList())); try { WebAuthnCodecs.importCosePublicKey(publicKeyCose); - } catch (CoseException | IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { throw wrapAndLog(log, "Failed to parse credential public key", e); } } @@ -421,7 +420,7 @@ public AttestationType attestationType() { return AttestationType.UNKNOWN; } } - } catch (IOException | CoseException | CertificateException e) { + } catch (IOException | CertificateException e) { throw new IllegalArgumentException("Failed to resolve attestation type.", e); } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java index ed513dbb4..0e8a97bbe 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java @@ -24,7 +24,6 @@ package com.yubico.webauthn; -import COSE.CoseException; import com.fasterxml.jackson.databind.JsonNode; import com.upokecenter.cbor.CBORObject; import com.yubico.internal.util.CertificateParser; @@ -95,7 +94,7 @@ private boolean verifySelfAttestationSignature( .getAttestedCredentialData() .get() .getCredentialPublicKey()); - } catch (IOException | CoseException | InvalidKeySpecException e) { + } catch (IOException | InvalidKeySpecException e) { throw ExceptionUtil.wrapAndLog( log, String.format( diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java index ee051e770..fc91e4205 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/TpmAttestationStatementVerifier.java @@ -24,7 +24,6 @@ package com.yubico.webauthn; -import COSE.CoseException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.upokecenter.cbor.CBORObject; @@ -178,7 +177,7 @@ public boolean verifyAttestationSignature( // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData. try { verifyPublicKeysMatch(attestationObject, pubArea); - } catch (CoseException | IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { throw new RuntimeException( "Failed to verify that public key in TPM attestation matches public key in authData.", e); } @@ -267,7 +266,7 @@ private void validateCertInfo( } private void verifyPublicKeysMatch(AttestationObject attestationObject, TpmtPublic pubArea) - throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException { + throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { final PublicKey credentialPubKey = WebAuthnCodecs.importCosePublicKey( attestationObject 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 94ba42ea1..1be854f73 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 @@ -24,8 +24,6 @@ package com.yubico.webauthn; -import COSE.CoseException; -import COSE.OneKey; import com.google.common.primitives.Bytes; import com.upokecenter.cbor.CBORObject; import com.yubico.webauthn.data.ByteArray; @@ -42,11 +40,37 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; final class WebAuthnCodecs { - private static final ByteArray ED25519_CURVE_OID = - new ByteArray(new byte[] {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70}); + private static final ByteArray EC_PUBLIC_KEY_OID = + new ByteArray( + new byte[] { + 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01 + }); // OID 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type) + private static final ByteArray P256_CURVE_OID = + new ByteArray( + new byte[] {0x2A, -122, 0x48, -50, 0x3D, 0x03, 0x01, 7}); // OID 1.2.840.10045.3.1.7 + private static final ByteArray P384_CURVE_OID = + new ByteArray(new byte[] {0x2B, -127, 0x04, 0, 34}); // OID 1.3.132.0.34 + private static final ByteArray P512_CURVE_OID = + new ByteArray(new byte[] {0x2B, -127, 0x04, 0, 35}); // OID 1.3.132.0.35 + + private static final ByteArray ED25519_ALG_ID = + new ByteArray( + new byte[] { + // SEQUENCE (5 bytes) + 0x30, + 0x05, + // OID (3 bytes) + 0x06, + 0x03, + // OID 1.3.101.112 + 0x2B, + 0x65, + 0x70 + }); static ByteArray ecPublicKeyToRaw(ECPublicKey key) { @@ -120,7 +144,7 @@ static ByteArray rawEcKeyToCose(ByteArray key) { } static PublicKey importCosePublicKey(ByteArray key) - throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException { + throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes()); final int kty = cose.get(CBORObject.FromObject(1)).AsInt32(); switch (kty) { @@ -129,7 +153,7 @@ static PublicKey importCosePublicKey(ByteArray key) // additional dependency to parse EdDSA keys via the OneKey constructor return importCoseEdDsaPublicKey(cose); case 2: - return importCoseP256PublicKey(cose); + return importCoseEcdsaPublicKey(cose); case 3: // COSE-JAVA supports RSA in v1.1.0 but not in v1.0.0 return importCoseRsaPublicKey(cose); @@ -147,8 +171,74 @@ private static PublicKey importCoseRsaPublicKey(CBORObject cose) return KeyFactory.getInstance("RSA").generatePublic(spec); } - private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException { - return (ECPublicKey) new OneKey(cose).AsPublicKey(); + private static PublicKey importCoseEcdsaPublicKey(CBORObject cose) + throws NoSuchAlgorithmException, InvalidKeySpecException { + final int crv = cose.get(CBORObject.FromObject(-1)).AsInt32Value(); + final ByteArray x = new ByteArray(cose.get(CBORObject.FromObject(-2)).GetByteString()); + final ByteArray y = new ByteArray(cose.get(CBORObject.FromObject(-3)).GetByteString()); + + final ByteArray curveOid; + switch (crv) { + case 1: + curveOid = P256_CURVE_OID; + break; + + case 2: + curveOid = P384_CURVE_OID; + break; + + case 3: + curveOid = P512_CURVE_OID; + break; + + default: + throw new IllegalArgumentException("Unknown COSE EC2 curve: " + crv); + } + + final ByteArray algId = + encodeDerSequence(encodeDerObjectId(EC_PUBLIC_KEY_OID), encodeDerObjectId(curveOid)); + + final ByteArray rawKey = + encodeDerBitStringWithZeroUnused( + new ByteArray(new byte[] {0x04}) // Raw EC public key with x and y + .concat(x) + .concat(y)); + + final ByteArray x509Key = encodeDerSequence(algId, rawKey); + + KeyFactory kFact = KeyFactory.getInstance("EC"); + return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes())); + } + + private static ByteArray encodeDerLength(final int length) { + if (length <= 127) { + return new ByteArray(new byte[] {(byte) length}); + } else if (length <= 0xffff) { + if (length <= 255) { + return new ByteArray(new byte[] {-127, (byte) length}); + } else { + return new ByteArray(new byte[] {-126, (byte) (length >> 8), (byte) (length & 0x00ff)}); + } + } else { + throw new UnsupportedOperationException("Too long: " + length); + } + } + + private static ByteArray encodeDerObjectId(final ByteArray oid) { + return new ByteArray(new byte[] {0x06, (byte) oid.size()}).concat(oid); + } + + private static ByteArray encodeDerBitStringWithZeroUnused(final ByteArray content) { + return new ByteArray(new byte[] {0x03}) + .concat(encodeDerLength(1 + content.size())) + .concat(new ByteArray(new byte[] {0})) + .concat(content); + } + + private static ByteArray encodeDerSequence(final ByteArray... items) { + final ByteArray content = + Stream.of(items).reduce(ByteArray::concat).orElseGet(() -> new ByteArray(new byte[0])); + return new ByteArray(new byte[] {0x30}).concat(encodeDerLength(content.size())).concat(content); } private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) @@ -166,10 +256,7 @@ private static PublicKey importCoseEd25519PublicKey(CBORObject cose) throws InvalidKeySpecException, NoSuchAlgorithmException { final ByteArray rawKey = new ByteArray(cose.get(CBORObject.FromObject(-2)).GetByteString()); final ByteArray x509Key = - new ByteArray(new byte[] {0x30, (byte) (ED25519_CURVE_OID.size() + 3 + rawKey.size())}) - .concat(ED25519_CURVE_OID) - .concat(new ByteArray(new byte[] {0x03, (byte) (rawKey.size() + 1), 0})) - .concat(rawKey); + encodeDerSequence(ED25519_ALG_ID, encodeDerBitStringWithZeroUnused(rawKey)); KeyFactory kFact = KeyFactory.getInstance("EdDSA"); return kFact.generatePublic(new X509EncodedKeySpec(x509Key.getBytes()));