From c1514fcc269e10b1c22cd7d3c7d4e542454e2c49 Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Thu, 30 Jul 2026 20:38:32 +0000 Subject: [PATCH 1/9] Add null checks to CborUtils and test cases. This change adds a new test file CborUtilsTest for testing the utility functions. It also adds null checks in the helper functions to avoid NullPointerException. --- src/com/google/cose/utils/CborUtils.java | 27 ++++ test/com/google/cose/utils/CborUtilsTest.java | 153 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 test/com/google/cose/utils/CborUtilsTest.java diff --git a/src/com/google/cose/utils/CborUtils.java b/src/com/google/cose/utils/CborUtils.java index 6f17461..bdee45c 100644 --- a/src/com/google/cose/utils/CborUtils.java +++ b/src/com/google/cose/utils/CborUtils.java @@ -46,6 +46,9 @@ public class CborUtils { * @return DataItem cbor object */ public static DataItem decode(final byte[] data) throws CborException { + if (data == null) { + throw new CborException("data cannot be null"); + } final ByteArrayInputStream bais = new ByteArrayInputStream(data); final CborDecoder decoder = new CborDecoder(bais); decoder.setMaxPreallocationSize(data.length); @@ -68,6 +71,9 @@ public static DataItem decode(final byte[] data) throws CborException { * @return encoded bytes */ public static byte[] encode(final DataItem dataItem) throws CborException { + if (dataItem == null) { + throw new CborException("dataItem cannot be null"); + } final ByteArrayOutputStream baos = new ByteArrayOutputStream(); CborEncoder encoder = new CborEncoder(baos); encoder.encode(dataItem); @@ -80,6 +86,9 @@ public static byte[] encode(final DataItem dataItem) throws CborException { * @return Map object */ public static Map asMap(final DataItem dataItem) throws CborException { + if (dataItem == null) { + throw new CborException("dataItem cannot be null"); + } if (dataItem.getMajorType() != MajorType.MAP) { throw new CborException( String.format("Expected a map, got %s", dataItem.getMajorType().name())); @@ -93,6 +102,9 @@ public static Map asMap(final DataItem dataItem) throws CborException { * @return Array object */ public static Array asArray(final DataItem dataItem) throws CborException { + if (dataItem == null) { + throw new CborException("dataItem cannot be null"); + } if (dataItem.getMajorType() != MajorType.ARRAY) { throw new CborException( String.format("Expected an array, got %s", dataItem.getMajorType().name())); @@ -102,6 +114,9 @@ public static Array asArray(final DataItem dataItem) throws CborException { public static Array asArray(final DataItem dataItem, final int length, final String semanticName) throws CborException { + if (semanticName == null) { + throw new CborException("semanticName cannot be null"); + } Array item = asArray(dataItem); if (item.getDataItems().size() != length) { throw new CborException(String.format("Expected %s to be of size %d, recieved %d", @@ -125,6 +140,9 @@ public static List getDataItems(final DataItem dataItem) throws CborEx * @return ByteString object */ public static ByteString asByteString(final DataItem dataItem) throws CborException { + if (dataItem == null) { + throw new CborException("dataItem cannot be null"); + } if (dataItem.getMajorType() != MajorType.BYTE_STRING) { throw new CborException( String.format("Expected a byte string, got %s", dataItem.getMajorType().name())); @@ -147,6 +165,9 @@ public static byte[] getBytes(final DataItem dataItem) throws CborException { * @return UnicodeString object */ public static UnicodeString asUnicodeString(final DataItem dataItem) throws CborException { + if (dataItem == null) { + throw new CborException("dataItem cannot be null"); + } if (dataItem.getMajorType() != MajorType.UNICODE_STRING) { throw new CborException( String.format("Expected a unicode string, got %s", dataItem.getMajorType().name())); @@ -170,6 +191,9 @@ public static String getString(final DataItem dataItem) throws CborException { * @throws CborException if dataItem is neither UnsignedInteger not NegativeInteger */ public static int asInteger(final DataItem dataItem) throws CborException { + if (dataItem == null) { + throw new CborException("dataItem cannot be null"); + } if (dataItem.getMajorType() == MajorType.UNSIGNED_INTEGER) { return ((UnsignedInteger) dataItem).getValue().intValue(); } @@ -186,6 +210,9 @@ public static int asInteger(final DataItem dataItem) throws CborException { * @return true if the item represents NULL */ public static boolean isNull(final DataItem item) { + if (item == null) { + return false; + } return (item.getMajorType() == MajorType.SPECIAL) && ((Special) item).getSpecialType() == SpecialType.SIMPLE_VALUE && ((SimpleValue) item).getSimpleValueType() == SimpleValueType.NULL; diff --git a/test/com/google/cose/utils/CborUtilsTest.java b/test/com/google/cose/utils/CborUtilsTest.java new file mode 100644 index 0000000..496a345 --- /dev/null +++ b/test/com/google/cose/utils/CborUtilsTest.java @@ -0,0 +1,153 @@ +package com.google.cose.utils; + +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.NegativeInteger; +import co.nstant.in.cbor.model.SimpleValue; +import co.nstant.in.cbor.model.UnicodeString; +import co.nstant.in.cbor.model.UnsignedInteger; +import org.junit.Assert; +import org.junit.Test; + +public class CborUtilsTest { + @Test + public void testDecodeNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.decode(null)); + } + + @Test + public void testEncodeNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.encode(null)); + } + + @Test + public void testAsMapNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asMap(null)); + } + + @Test + public void testAsArrayNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asArray(null)); + } + + @Test + public void testAsArrayThreeArgsNullSemanticNameThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asArray(new Array(), 0, null)); + } + + @Test + public void testAsArrayThreeArgsNullDataItemThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asArray(null, 0, "name")); + } + + @Test + public void testAsByteStringNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asByteString(null)); + } + + @Test + public void testAsUnicodeStringNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asUnicodeString(null)); + } + + @Test + public void testAsIntegerNullThrows() { + Assert.assertThrows(CborException.class, () -> CborUtils.asInteger(null)); + } + + @Test + public void testIsNullReturnsFalseForNull() { + Assert.assertFalse(CborUtils.isNull(null)); + } + + @Test + public void testEncodeDecode() throws CborException { + UnicodeString item = new UnicodeString("test"); + byte[] encoded = CborUtils.encode(item); + DataItem decoded = CborUtils.decode(encoded); + Assert.assertEquals(item, decoded); + } + + @Test + public void testAsMapPositive() throws CborException { + Map map = new Map(); + Assert.assertEquals(map, CborUtils.asMap(map)); + } + + @Test + public void testAsMapNegativeWrongType() { + Assert.assertThrows(CborException.class, () -> CborUtils.asMap(new Array())); + } + + @Test + public void testAsArrayPositive() throws CborException { + Array array = new Array(); + Assert.assertEquals(array, CborUtils.asArray(array)); + } + + @Test + public void testAsArrayNegativeWrongType() { + Assert.assertThrows(CborException.class, () -> CborUtils.asArray(new Map())); + } + + @Test + public void testAsArrayThreeArgsPositive() throws CborException { + Array array = new Array(); + array.add(new UnicodeString("item")); + Assert.assertEquals(array, CborUtils.asArray(array, 1, "test-array")); + } + + @Test + public void testAsArrayThreeArgsWrongSizeThrows() { + Array array = new Array(); + Assert.assertThrows(CborException.class, () -> CborUtils.asArray(array, 1, "test-array")); + } + + @Test + public void testAsByteStringPositive() throws CborException { + ByteString bs = new ByteString(new byte[]{1, 2, 3}); + Assert.assertEquals(bs, CborUtils.asByteString(bs)); + Assert.assertArrayEquals(new byte[]{1, 2, 3}, CborUtils.getBytes(bs)); + } + + @Test + public void testAsByteStringNegativeWrongType() { + Assert.assertThrows(CborException.class, () -> CborUtils.asByteString(new UnicodeString("not bytes"))); + } + + @Test + public void testAsUnicodeStringPositive() throws CborException { + UnicodeString us = new UnicodeString("hello"); + Assert.assertEquals(us, CborUtils.asUnicodeString(us)); + Assert.assertEquals("hello", CborUtils.getString(us)); + } + + @Test + public void testAsUnicodeStringNegativeWrongType() { + Assert.assertThrows(CborException.class, () -> CborUtils.asUnicodeString(new ByteString(new byte[]{1}))); + } + + @Test + public void testAsIntegerPositive() throws CborException { + UnsignedInteger ui = new UnsignedInteger(123); + Assert.assertEquals(123, CborUtils.asInteger(ui)); + + NegativeInteger ni = new NegativeInteger(-123); + Assert.assertEquals(-123, CborUtils.asInteger(ni)); + } + + @Test + public void testAsIntegerNegativeWrongType() { + Assert.assertThrows(CborException.class, () -> CborUtils.asInteger(new UnicodeString("not a number"))); + } + + @Test + public void testIsNullPositive() { + Assert.assertTrue(CborUtils.isNull(SimpleValue.NULL)); + Assert.assertFalse(CborUtils.isNull(SimpleValue.TRUE)); + Assert.assertFalse(CborUtils.isNull(new UnicodeString("not null"))); + } +} From fddec63735cffee2e9831bfaa59a1730f1a4c463 Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Thu, 17 Sep 2026 15:31:26 -0700 Subject: [PATCH 2/9] Add support for ML-DSA. Currently only Conscrypt support for ML-DSA has been added. JCE and Bouncy Castle support would be added once they start supporting raw private key format. --- pom.xml | 48 ++- src/com/google/cose/AkpKey.java | 152 ++++++++ src/com/google/cose/AkpSigningKey.java | 252 ++++++++++++ src/com/google/cose/CoseKey.java | 19 +- src/com/google/cose/utils/Algorithm.java | 9 +- src/com/google/cose/utils/Headers.java | 4 + test/com/google/cose/AkpSigningKeyTest.java | 405 ++++++++++++++++++++ 7 files changed, 874 insertions(+), 15 deletions(-) create mode 100644 src/com/google/cose/AkpKey.java create mode 100644 src/com/google/cose/AkpSigningKey.java create mode 100644 test/com/google/cose/AkpSigningKeyTest.java diff --git a/pom.xml b/pom.xml index 140c50b..b4f0209 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.google.cose cose - 20230908 + 20260917 co.nstant.in @@ -34,6 +34,17 @@ bcprov-jdk18on 1.84 + + com.google.truth + truth + 1.4.5 + test + + + org.conscrypt + conscrypt-openjdk-uber + 2.6.0 + jar COSE for Java @@ -99,10 +110,10 @@ - 3.0.5 + 3.6.3 - 1.8.0 + 16.0.0 @@ -117,14 +128,9 @@ org.apache.maven.plugins maven-compiler-plugin - - 8 - 8 - true + 16 + 16 + true -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED @@ -158,9 +164,22 @@ 1.8 1.8 + true -XDcompilePolicy=simple -Xplugin:ErrorProne + --should-stop=ifError=FLOW + -Xlint:-options + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED @@ -227,11 +246,14 @@ ${test.include} false - 0 + 1 + + ${project.build.directory} + alphabetical - -Xmx1536M -Duser.language=hi -Duser.country=IN ${test.add.opens} + -Xmx1536M --enable-native-access=ALL-UNNAMED -Duser.language=hi -Duser.country=IN ${test.add.opens} diff --git a/src/com/google/cose/AkpKey.java b/src/com/google/cose/AkpKey.java new file mode 100644 index 0000000..14b54fd --- /dev/null +++ b/src/com/google/cose/AkpKey.java @@ -0,0 +1,152 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cose; + +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.NegativeInteger; +import com.google.cose.exceptions.CoseException; +import com.google.cose.utils.Algorithm; +import com.google.cose.utils.CborUtils; +import com.google.cose.utils.CoseUtils; +import com.google.cose.utils.Headers; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.Arrays; +import java.util.Objects; + +/** Abstract class for generic AKP key */ +public abstract class AkpKey extends CoseKey { + + public static final String PROVIDER = "Conscrypt"; + + protected byte[] publicKeyBytes; + protected byte[] privateKeyBytes; + + AkpKey(DataItem cborKey) throws CborException, CoseException { + super(cborKey); + populateKeyFromCbor(); + } + + void populateKeyFromCbor() throws CborException, CoseException { + if (getKeyType() != Headers.KEY_TYPE_AKP) { + throw new CoseException("Expecting KEY_TYPE_AKP (type 7), found type " + getKeyType()); + } + + if (getAlgorithm() == null) { + throw new CoseException("Algorithm is required for AKP keys."); + } + + Algorithm algorithm = Algorithm.fromCoseAlgorithmId(getAlgorithm()); + + if (!isAkpAlgorithm(algorithm)) { + throw new CoseException( + "Expecting an AKP signing algorithm, found " + algorithm.getJavaAlgorithmId()); + } + + if (labels.containsKey(Headers.KEY_PARAMETER_AKP_PUB)) { + publicKeyBytes = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_AKP_PUB)).getBytes(); + } + if (labels.containsKey(Headers.KEY_PARAMETER_AKP_PRIV)) { + privateKeyBytes = + CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_AKP_PRIV)).getBytes(); + } + + if (publicKeyBytes == null && privateKeyBytes == null) { + throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE); + } + } + + void verifyAlgorithmAllowedByKey(Algorithm algorithm) throws CborException, CoseException { + Map keyMap = CborUtils.asMap(encode()); + DataItem algo = CoseUtils.getValueFromMap(keyMap, Headers.KEY_PARAMETER_ALGORITHM); + if (algo == null) { + throw new CoseException("Algorithm is required for AKP keys."); + } + if (!algo.equals(algorithm.getCoseAlgorithmId())) { + throw new CoseException("Algorithm not compatible with AKP key."); + } + } + + public byte[] getPublicKeyBytes() { + return Arrays.copyOf(publicKeyBytes, publicKeyBytes.length); + } + + /** Recursive builder to build out the AKP key and its subclasses. */ + abstract static class Builder> extends CoseKey.Builder { + protected byte[] publicKey; + protected byte[] privateKey; + + @Override + void verifyKeyMaterialPresentAndComplete() throws CoseException { + if (!isKeyMaterialPresent()) { + throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE); + } + if (algorithm == null) { + throw new CoseException("Algorithm is required for AKP keys."); + } + + if (!isAkpAlgorithm(algorithm)) { + throw new CoseException( + "Expecting an AKP signing algorithm, found " + algorithm.getJavaAlgorithmId()); + } + } + + boolean isKeyMaterialPresent() { + return publicKey != null || privateKey != null; + } + + @Override + protected Map compile() throws CoseException { + withKeyType(Headers.KEY_TYPE_AKP); + + Map cborKey = super.compile(); + + if (publicKey != null) { + cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_AKP_PUB), new ByteString(publicKey)); + } + if (privateKey != null) { + cborKey.put( + new NegativeInteger(Headers.KEY_PARAMETER_AKP_PRIV), new ByteString(privateKey)); + } + return cborKey; + } + + @CanIgnoreReturnValue + public T withPublicKey(byte[] publicKey) { + this.publicKey = Arrays.copyOf(publicKey, publicKey.length); + return self(); + } + + @CanIgnoreReturnValue + public T withPrivateKey(byte[] privateKey) { + this.privateKey = Arrays.copyOf(privateKey, privateKey.length); + return self(); + } + } + + public static boolean isAkpAlgorithm(Algorithm algorithm) { + return algorithm == Algorithm.SIGNING_ALGORITHM_MLDSA_44 + || algorithm == Algorithm.SIGNING_ALGORITHM_MLDSA_65 + || algorithm == Algorithm.SIGNING_ALGORITHM_MLDSA_87; + } + + public static boolean isConscryptProvider(String provider) { + return Objects.equals(provider, PROVIDER); + } +} diff --git a/src/com/google/cose/AkpSigningKey.java b/src/com/google/cose/AkpSigningKey.java new file mode 100644 index 0000000..e3520ed --- /dev/null +++ b/src/com/google/cose/AkpSigningKey.java @@ -0,0 +1,252 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cose; + +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import com.google.cose.exceptions.CoseException; +import com.google.cose.utils.Algorithm; +import com.google.cose.utils.CborUtils; +import com.google.cose.utils.Headers; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; + +/** Implements AKP COSE_Key spec for signing purposes. */ +public final class AkpSigningKey extends AkpKey { + + public AkpSigningKey(DataItem cborKey) throws CborException, CoseException { + super(cborKey); + + if (operations != null + && !operations.contains(Headers.KEY_OPERATIONS_VERIFY) + && !operations.contains(Headers.KEY_OPERATIONS_SIGN)) { + throw new CoseException("Signing key requires either sign or verify operation."); + } + } + + public static AkpSigningKey parse(byte[] keyBytes) throws CborException, CoseException { + DataItem dataItem = CborUtils.decode(keyBytes); + return decode(dataItem); + } + + public static AkpSigningKey decode(DataItem cborKey) throws CborException, CoseException { + return new AkpSigningKey(cborKey); + } + + /** + * Generates a COSE formatted AKP signing key given a specific algorithm. The selected key size is + * chosen based on table 2 of FIPS-204, document link: https://doi.org/10.6028/NIST.FIPS.204. + * + *

JCE supports MLDSA but it uses the extended private key representation. For now, we will + * only support Conscrypt provider so that we can use the 32-byte seed representation for the + * private key as described in the draft RFC for AKP keys. + */ + public static AkpSigningKey generateKey(Algorithm algorithm, String provider) + throws CborException, CoseException { + KeyPair keyPair; + + // Support for Conscrypt only till we port to openjdk 24+ + if (!isConscryptProvider(provider)) { + throw new IllegalArgumentException("Only Conscrypt provider is supported."); + } + + if (!isAkpAlgorithm(algorithm)) { + throw new CoseException("Unsupported algorithm: " + algorithm.getJavaAlgorithmId()); + } + + try { + KeyPairGenerator keyPairGenerator; + KeyFactory keyFactory; + switch (algorithm) { + case SIGNING_ALGORITHM_MLDSA_44 -> { + keyPairGenerator = KeyPairGenerator.getInstance("ML-DSA-44", provider); + keyFactory = KeyFactory.getInstance("ML-DSA-44", provider); + } + case SIGNING_ALGORITHM_MLDSA_65 -> { + keyPairGenerator = KeyPairGenerator.getInstance("ML-DSA-65", provider); + keyFactory = KeyFactory.getInstance("ML-DSA-65", provider); + } + case SIGNING_ALGORITHM_MLDSA_87 -> { + keyPairGenerator = KeyPairGenerator.getInstance("ML-DSA-87", provider); + keyFactory = KeyFactory.getInstance("ML-DSA-87", provider); + } + default -> + throw new CoseException("Unsupported algorithm: " + algorithm.getJavaAlgorithmId()); + } + keyPair = keyPairGenerator.generateKeyPair(); + + byte[] seed = keyFactory.getKeySpec(keyPair.getPrivate(), RawKeySpec.class).getEncoded(); + byte[] pubBytes = keyFactory.getKeySpec(keyPair.getPublic(), RawKeySpec.class).getEncoded(); + + return AkpSigningKey.builder() + .withPrivateKey(seed) + .withPublicKey(pubBytes) + .withAlgorithm(algorithm) + .build(); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new CoseException("No provider for algorithm: " + algorithm.getJavaAlgorithmId(), e); + } catch (InvalidKeySpecException e) { + throw new CoseException("Error while extracting key material.", e); + } catch (IllegalArgumentException e) { + throw new CoseException( + "Error while generating key for: " + algorithm.getJavaAlgorithmId(), e); + } + } + + /** Implements builder for AkpSigningKey. */ + public static class Builder extends AkpKey.Builder { + + @Override + public Builder self() { + return this; + } + + @Override + public AkpSigningKey build() throws CborException, CoseException { + Map cborKey = compile(); + return new AkpSigningKey(cborKey); + } + + @Override + public Builder withOperations(Integer... operations) throws CoseException { + for (int operation : operations) { + if (operation != Headers.KEY_OPERATIONS_SIGN + && operation != Headers.KEY_OPERATIONS_VERIFY) { + throw new CoseException("Signing key only supports Sign or Verify operations."); + } + } + return super.withOperations(operations); + } + } + + public static Builder builder() { + return new Builder(); + } + + public byte[] sign(Algorithm algorithm, byte[] message, String provider) + throws CborException, CoseException { + if (privateKeyBytes == null || privateKeyBytes.length == 0) { + throw new CoseException("Missing key material for signing."); + } + verifyAlgorithmMatchesKey(algorithm); + verifyAlgorithmAllowedByKey(algorithm); + verifyOperationAllowedByKey(Headers.KEY_OPERATIONS_SIGN); + if (!isConscryptProvider(provider)) { + throw new IllegalArgumentException("Only Conscrypt provider is supported."); + } + + try { + RawKeySpec privateKeySpec = new RawKeySpec(privateKeyBytes); + KeyFactory keyFactory; + Signature signature; + + switch (algorithm) { + case SIGNING_ALGORITHM_MLDSA_44 -> { + keyFactory = KeyFactory.getInstance("ML-DSA-44", provider); + signature = Signature.getInstance("ML-DSA-44", provider); + } + case SIGNING_ALGORITHM_MLDSA_65 -> { + keyFactory = KeyFactory.getInstance("ML-DSA-65", provider); + signature = Signature.getInstance("ML-DSA-65", provider); + } + case SIGNING_ALGORITHM_MLDSA_87 -> { + keyFactory = KeyFactory.getInstance("ML-DSA-87", provider); + signature = Signature.getInstance("ML-DSA-87", provider); + } + default -> throw new CoseException("Unknown algorithm."); + } + PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + + signature.initSign(privateKey); + signature.update(message); + return signature.sign(); + } catch (NoSuchAlgorithmException + | SignatureException + | InvalidKeySpecException + | InvalidKeyException + | NoSuchProviderException e) { + throw new CoseException("Error while signing message.", e); + } + } + + public void verify(Algorithm algorithm, byte[] message, byte[] signature, String provider) + throws CborException, CoseException { + verifyAlgorithmMatchesKey(algorithm); + verifyAlgorithmAllowedByKey(algorithm); + verifyOperationAllowedByKey(Headers.KEY_OPERATIONS_VERIFY); + if (!isConscryptProvider(provider)) { + throw new IllegalArgumentException("Only Conscrypt provider is supported."); + } + + try { + RawKeySpec publicKeySpec = new RawKeySpec(publicKeyBytes); + KeyFactory keyFactory; + Signature signer; + + switch (algorithm) { + case SIGNING_ALGORITHM_MLDSA_44 -> { + keyFactory = KeyFactory.getInstance("ML-DSA-44", provider); + signer = Signature.getInstance("ML-DSA-44", provider); + } + case SIGNING_ALGORITHM_MLDSA_65 -> { + keyFactory = KeyFactory.getInstance("ML-DSA-65", provider); + signer = Signature.getInstance("ML-DSA-65", provider); + } + case SIGNING_ALGORITHM_MLDSA_87 -> { + keyFactory = KeyFactory.getInstance("ML-DSA-87", provider); + signer = Signature.getInstance("ML-DSA-87", provider); + } + default -> throw new CoseException("Unknown algorithm."); + } + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + signer.initVerify(publicKey); + signer.update(message); + if (!signer.verify(signature)) { + throw new CoseException("Failed verification."); + } + } catch (NoSuchAlgorithmException + | NoSuchProviderException + | InvalidKeyException + | InvalidKeySpecException + | SignatureException e) { + throw new CoseException("Error while verifying ", e); + } + } + + /** Representation of the raw keys for interoperability with Conscrypt. */ + public static final class RawKeySpec extends EncodedKeySpec { + public RawKeySpec(byte[] encoded) { + super(encoded); + } + + @Override + public String getFormat() { + return "raw"; + } + } +} diff --git a/src/com/google/cose/CoseKey.java b/src/com/google/cose/CoseKey.java index a5a3e3c..ac2dce1 100644 --- a/src/com/google/cose/CoseKey.java +++ b/src/com/google/cose/CoseKey.java @@ -144,10 +144,11 @@ public static CoseKey generateKey(Algorithm algorithm) throws CborException, Cos abstract static class Builder> { private int keyType; private byte[] keyId; - private Algorithm algorithm; private final Set operations = new LinkedHashSet<>(); private byte[] baseIv; + protected Algorithm algorithm; + abstract T self(); abstract CoseKey build() throws CborException, CoseException; abstract void verifyKeyMaterialPresentAndComplete() throws CoseException; @@ -205,4 +206,20 @@ public T withBaseIv(byte[] baseIv) { return self(); } } + + @Override + public boolean equals(Object key) { + if (this == key) { + return true; + } + if (key == null || !(key instanceof CoseKey otherKey)) { + return false; + } + return cborKey.equals(otherKey.cborKey); + } + + @Override + public int hashCode() { + return cborKey.hashCode(); + } } diff --git a/src/com/google/cose/utils/Algorithm.java b/src/com/google/cose/utils/Algorithm.java index 712ffc4..432edf8 100644 --- a/src/com/google/cose/utils/Algorithm.java +++ b/src/com/google/cose/utils/Algorithm.java @@ -20,6 +20,7 @@ import co.nstant.in.cbor.model.Number; import co.nstant.in.cbor.model.UnsignedInteger; import com.google.common.collect.ImmutableMap; +import com.google.cose.exceptions.CoseException; /** * Algorithms to be used by cose library. @@ -30,6 +31,9 @@ public enum Algorithm { SIGNING_ALGORITHM_ECDSA_SHA_384(-35, "SHA384withECDSA"), SIGNING_ALGORITHM_ECDSA_SHA_512(-36, "SHA512withECDSA"), SIGNING_ALGORITHM_EDDSA(-8, "NonewithEdDSA"), + SIGNING_ALGORITHM_MLDSA_44(-48, "ML-DSA-44"), + SIGNING_ALGORITHM_MLDSA_65(-49, "ML-DSA-65"), + SIGNING_ALGORITHM_MLDSA_87(-50, "ML-DSA-87"), MAC_ALGORITHM_HMAC_SHA_256_256(5, "HmacSHA256"), MAC_ALGORITHM_HMAC_SHA_384_384(6, "HmacSHA384"), MAC_ALGORITHM_HMAC_SHA_512_512(7, "HmacSHA512"), @@ -69,7 +73,10 @@ public Number getCoseAlgorithmId() { return new UnsignedInteger(coseAlgorithmId); } - public static Algorithm fromCoseAlgorithmId(int coseAlgorithmId) { + public static Algorithm fromCoseAlgorithmId(int coseAlgorithmId) throws CoseException { + if (!REVERSE_LOOKUP_MAP.containsKey(coseAlgorithmId)) { + throw new CoseException("Expecting a valid COSE algorithm, found " + coseAlgorithmId); + } return REVERSE_LOOKUP_MAP.get(coseAlgorithmId); } } diff --git a/src/com/google/cose/utils/Headers.java b/src/com/google/cose/utils/Headers.java index f67d190..7a0e6be 100644 --- a/src/com/google/cose/utils/Headers.java +++ b/src/com/google/cose/utils/Headers.java @@ -29,6 +29,7 @@ public class Headers { public static final int KEY_TYPE_RESERVED = 0; public static final int KEY_TYPE_OKP = 1; public static final int KEY_TYPE_EC2 = 2; + public static final int KEY_TYPE_AKP = 7; public static final int KEY_TYPE_SYMMETRIC = 4; public static final int KEY_OPERATIONS_SIGN = 1; @@ -57,6 +58,9 @@ public class Headers { public static final int KEY_PARAMETER_K = -1; + public static final int KEY_PARAMETER_AKP_PUB = -1; + public static final int KEY_PARAMETER_AKP_PRIV = -2; + public static final int CURVE_EC2_P256 = 1; public static final int CURVE_EC2_P384 = 2; public static final int CURVE_EC2_P521 = 3; diff --git a/test/com/google/cose/AkpSigningKeyTest.java b/test/com/google/cose/AkpSigningKeyTest.java new file mode 100644 index 0000000..e62b5a9 --- /dev/null +++ b/test/com/google/cose/AkpSigningKeyTest.java @@ -0,0 +1,405 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cose; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; + +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.NegativeInteger; +import co.nstant.in.cbor.model.UnsignedInteger; +import com.google.cose.exceptions.CoseException; +import com.google.cose.utils.Algorithm; +import com.google.cose.utils.CborUtils; +import com.google.cose.utils.Headers; + +import java.lang.reflect.Method; +import java.security.Provider; +import java.security.Security; +import java.security.spec.NamedParameterSpec; + +import org.conscrypt.Conscrypt; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test class for testing {@link AkpSigningKey}. */ +@RunWith(JUnit4.class) +public class AkpSigningKeyTest { + private static final String COSE_ENCODED_MLDSA87_KEY = + "A50107025820D9BC439F97BD6D4093E68F0F3FCF09C9A97ADF888ED7308DD565247A166CB4FA03383120590A20E" + + "45FFC8CC73DB885DC662E62A18CD8E3803297117FA5658814A985B5FF1DB7B468CFC82BB929F1D86B77ED" + + "14F5AE16A65368772CE51912410105E0456975AE91FDB643B512F124D5E60BD68B8C7E31FE01C7B0DC65A" + + "E470501CC565A6E1DFCFCFD12565433C4AFEDD511821E2E9610C45275E2836DEE35CED69D7EFA672FD1E4" + + "318BEF5EB6E897E8B451AA202DED042B2AAEF77A7BE3F699146DA229A8BDB3FFA496445967E75217BFBC9" + + "048F9956443D8731F833EB30DE10DAC96FFFE7CF65EA0445C3E31E8601E133BE6A100764FE3196E267726" + + "441F31751FBF9A6F5880644F4E7275E57DE2B0F105E4DB055D50DD1C9C934FDDF535B8DE28B0C74C0449F" + + "222CD2ED0BB8FBC775CCEE8C940665B40F712F4F7E00750E9E1E4CD9CFF25D1945C3E9BCA53CCD4F12EEE" + + "7581856EBD68F26845956E3E7BEB761F0FE75BDD31BFE2FA018113397B387BD59D62A68B8AF7FA245AB93" + + "2E69F778E2CEEFD21304FBB8099EA13D8EA57C1813197A2F75AE251075B51DAD38F853669E9D5F98A3655" + + "098941993A1594860FBA71FE530EE5C29F58F2978AF688CCB75A5838A359C112E98E25A8583AC8DAC1F86" + + "1FD58E2AFBA5DE5A52E020904F5B42BC0874E35BEFCF3E6119684768F36E008F04712177CEBE627607381" + + "E56EAAEE161C1729B8DE51DBDE474D48CC68249EA27162B87993E60C84ED6CC6423CB3676D9EB50B2CAB5" + + "A3A049EF131381D623FA6FBCBC9DB1E7CC025EA0418B9DAD2CC6CCD4E95FA2CEC24FEECA70318A751716B" + + "7213F63EDBF65A63338357F838F94EC071822C24851248885107B3D1C4E924678C7614EA1AF038104619F" + + "2AE372940BECFA69E29CBB5FF6C3E20A47BE4A4F74BAC34C133C00A6A706ACCC6FFD3D8E4FBD69A99704E" + + "1283C850D8C58D1E5753CD9587B83C4C346CB9A58137213EC10834C66ADFE2BB5C501A8EF2ECADD1B677A" + + "3DF1A6DEB86EBF0722C4F5030E20F9018DD5B6FC53EEA24FD92B7B5B4025FEAE996D3E48FD4C650D82DBA" + + "D7EAF936639698512F26253D2EF6847C8518E8565CC9A5495C6FFF57CDE7323882C54A7DB470AB2DAF8FF" + + "D2BF794FA7C692D9E7FBD532EECC1D7880E2CA0B3216128BE28B4A9F1D151FAC97808B0BD98B7B43A612A" + + "9AC865812BFEAC6F47460277840B52A3B087F916CA7CEDC0F768EA2BD19EA21155F84B4A04C4000AD2AE0" + + "587154D560BC0A477A4F9329A8984DD31EB1F2A05E3D918701D630CFCA9AF61EF088D2C5581ACB463E439" + + "902E5D425719E956B8D6DF7305B28E0FF27D3AD0DE2085D292499B19A3390D4396FB3BAC9A8D8CBEAD2A7" + + "A4290FC9AC6FCA045F98A614A45A39CBE24360F84D14F8E472712ACEB74DBF45B53D49A0E4737E476FFC4" + + "D5B2F7CD247AA186D3B764AD9E9CFEEE456A73C291D8DE3912414AC43911C372173AD7B472AF35C6853CE" + + "D2FE7B5FE0A89565AB33BAA6F65CDD928319D7065E040E7A5E84F9AA903F7648094BAD07136B16927B8EC" + + "6DBC2BEF0CC2856DE1E795923E1412C49F24DEEB6C21F6C8A9765C9C7986E0DA4B4C67D8E0D0C8D466824" + + "FB923D8573148990CD2EF133C78CEECAB72ED9DD285C5A3766852D54534207FFD34027F6C76EDE8FD1A32" + + "D72C30048BBAA797D5DF6FDE27D087DE5721AD7B7FA3E8D3F70D6BFC3AB2E252335368BBFA15ACB5CB37D" + + "4694E8B23CEBE25DE9C925A221A183B904D3F85DF9929A919C54D6F87457373A0D6ECC1403E4CBBE62099" + + "9435E80696634CD1A8E4747E9825BFA336E5BBAD14F73640F1B9FEBE800DBAEFE1630C61FAE635B074C56" + + "4EAA9DB189C9E7302873FC64E6D497BC5C29080987A07A21D4AF210703A4FA07F2FD816F12FD1E29B4C0F" + + "44AFE9BD4A1EAA8A7AE6F02A5B4258F52CAF6127F62632A67CF4E8310BE56A7C28C86B2E277600C3E92C8" + + "D23D42586244C571E90568DF202F2F6D81F860A565F9EB91A3C78372E2A8B1BE61C5418CF49BF2D6C8955" + + "D4A482A9919B7660B3F9A4404FFC454EA073E1E4B2689AB2CCA4E46BD7004A6C491FA26EE7A57D60F35ED" + + "B2B821E6266442C8F335D452D524C772E0353724C23C7DD15B7AA155E91442022140C5FCB0153147EDCF3" + + "E8952F6F0399A3C88066A72756C9409915DE63F64FA797841C57C796C6FC550EF745DFE9F179457F94755" + + "AE5A2506A764F327E550BE3DC14DD41F3B04B147D454938C63A8D69B2EA4C5710EC0B36E3A6C72571FA5D" + + "59DDE036C42033DF35AF056966FF0CD1204008971AA6BA9FB97B685AB9FFA2A9D1778104CD2C3B326DE1F" + + "CBC242E94D0311C3275B12850ED30CEEAD3A2EE6D060508411D4396F5421D8B6D067CF7CB5E826785FBE1" + + "19E05E21BD879B64F57CB0CD1972C2815F20ABE7CE6AB34D0F471AF44BAAD179E90644122F5F33288E689" + + "DDDDC5CE833E9755DF1E73C65C5A201C4EDE2FFA6B19274927719D2D38FDB7A65AA43708B7FA9A94AA7D3" + + "210253D78D3B181E1020D0000BD0A1DC05D447F9F58EBEB84C65B36C8AFCB83727A1508994E826957A663" + + "B0B9B8A003325AB6D6D6462EE4E106019C0DFFE10323B7BDE7D82A38F85FD08786E860BA66C161B64B070" + + "8C363DE5C6AF62D8DB3C243D1E1B712CB1D59E942B9B6B4295A5A500B182CBD5FD1BC6CE9376D91B47A22" + + "84F1FBE0AD1C048CC2CFBB4AFA3A9EB9697503B69FECA990EBA7E9441AF9CA44CB3AC6B5ED66E591C201F" + + "E30EFA8A7C471DC613D6254C263A8E132104BEC47F1AACB3B2FCD4051B69B5E3FCB1C147A65C2F90C4B51" + + "88BAFC521CAB03C12A309DA50B5A7517727ED41228ED123FE1B152F6A6319CD623BF34AD7B8E064AB9932" + + "60BCBD405F5B7FFF9B2FA40BA5ED5630242539E5D96823E89DC818A13D16675EE3079D976F694F5ACC976" + + "0AE789E9B3391B289E0E22A7EF17CC6A4577157B6D95C09BAA4FD532E3EE0A290810ED35E56BB19D9B61F" + + "B98A97C617425B06093D98A5CF0EE2DD127F0EEA600B9A0C67FBE761DB9B77E5D5BBA9701DA1B883E521A" + + "0CFE88451F57BD36085B67E56F061F84A2E6A152A71BCE6E522DAAB6A0A33CE22E537FA9793D28B617E6C" + + "0A4176A83AA3BE578AFAC0F2F5547C5516D218984755B7445C7143AFA4E551FCE0071BDB873B34E6B9E2B" + + "9E79ED0C69D288ED6421F237E860A0C6492EBBDD2A44C2C4F368DBE99941B1E8561D859D3859F496CEE3D" + + "741F252973F8FCC539C409E35CC80A5ED6DF23CC3A65601313F5D681FD9540C5291A9E30A72E38C96413C" + + "47C61FF84FDE78D011B01B4154D1B920AF003F7ABB1E1999DEA6A766CF9FD2702B3CE0EE57AF931B62124" + + "B0861B163A3B91AA4BEA28076C3432DF3B29B6C4E1BA588DEF420071FC157DE90EB2722ECC9AB00DF3C66" + + "9383A61A91BB67BD287CE349B4745EE7A479DBCEEF166B9ACC412EB579FCD6437307EDDA253D606B7BE75" + + "99C38092BC52A8598480EDAB8B82B1D21C565D2137CEAE0B6642619B16133D91205D6355029E9CDFEB9A2" + + "8B373D95916B6B707D4C712C09CF36DAF1A511B2BEDB1AA70EE58D46A0666BB287784B0A3840C589A7A04" + + "D5D6F2216BE90AA4A512D5632F5C9BFE7B8B13382F999B95D367C7C46B968074CE315197A5FF3545C7B77" + + "A804ADE56A95B5C24CDECE5937B5C0366D93AD03DA9BC5DB1B551DFB91E9B343D2B57B763439686D4A321" + + "58200000000000000000000000000000000000000000000000000000000000000000"; + + private static final byte[] pubBytes = new byte[1952]; // ML-DSA-65 public key length + private static final byte[] privBytes = new byte[32]; // ML-DSA-65 seed length + private static final String PROVIDER; + + static { + for (int i = 0; i < pubBytes.length; i++) { + pubBytes[i] = (byte) (i % 256); + } + for (int i = 0; i < privBytes.length; i++) { + privBytes[i] = (byte) ((31 - i) % 256); + } + + Provider conscrypt = Conscrypt.newProvider(); + if (Security.getProvider(conscrypt.getName()) == null) { + Security.addProvider(conscrypt); + } + PROVIDER = conscrypt.getName(); + } + + @Test + public void testRoundTrip() throws CborException, CoseException { + final byte[] keyId = TestUtilities.KEYID_BYTES; + final Map map = new Map(); + map.put( + new UnsignedInteger(Headers.KEY_PARAMETER_KEY_TYPE), + new UnsignedInteger(Headers.KEY_TYPE_AKP)); + map.put(new UnsignedInteger(Headers.KEY_PARAMETER_KEY_ID), new ByteString(keyId)); + map.put( + new UnsignedInteger(Headers.KEY_PARAMETER_ALGORITHM), + Algorithm.SIGNING_ALGORITHM_MLDSA_65.getCoseAlgorithmId()); + map.put(new NegativeInteger(Headers.KEY_PARAMETER_AKP_PUB), new ByteString(pubBytes)); + map.put(new NegativeInteger(Headers.KEY_PARAMETER_AKP_PRIV), new ByteString(privBytes)); + + final AkpSigningKey key = new AkpSigningKey(map); + byte[] serialized = key.serialize(); + + final AkpSigningKey parsedKey = AkpSigningKey.parse(serialized); + + assertThat(parsedKey).isNotNull(); + + assertThat(parsedKey.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertArrayEquals(keyId, parsedKey.getKeyId()); + assertThat(parsedKey).isEqualTo(key); + } + + @Test + public void testParseKeyFailureWrongKeyType() throws CborException, CoseException { + final String cborString = + "A4010220012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44" + + "4B624343167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC67"; + CoseException exception = + assertThrows( + CoseException.class, + () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString))); + assertThat(exception).hasMessageThat().startsWith("Expecting KEY_TYPE_AKP"); + } + + @Test + public void testParseKeyFailureMissingAlgorithm() throws CborException, CoseException { + final String cborString = + "A4010720012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44" + + "4B624343167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC67"; + CoseException exception = + assertThrows( + CoseException.class, + () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString))); + assertThat(exception).hasMessageThat().isEqualTo("Algorithm is required for AKP keys."); + } + + @Test + public void testParseKeyFailureOkpKeyAsAkpKey() throws CborException, CoseException { + // OKP Keys have a different public key structure. + final String cborString = + "A5010703382F20012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44" + + "4B624343167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC67"; + CborException exception = + assertThrows( + CborException.class, + () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString))); + assertThat(exception).hasMessageThat().startsWith("Expected a byte string"); + } + + @Test + public void testParseKeyFailureUnsupportedAlgorithm() throws CborException, CoseException { + final String cborString = + "A50107032720012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44" + + "4B624343167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC67"; + CoseException exception = + assertThrows( + CoseException.class, + () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString))); + assertThat(exception).hasMessageThat().startsWith("Expecting an AKP signing"); + } + + @Test + public void testParseKeyFailureMissingKeyMaterial() throws CborException, CoseException { + final String cborString = "A20107033830"; + CoseException exception = + assertThrows( + CoseException.class, + () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString))); + assertThat(exception).hasMessageThat().startsWith("Missing key material"); + } + + @Test + public void testParseKeyFailureWrongKeyOperation() throws CborException, CoseException { + final String cborString = + "A401070338302158200000000000000000000000000000000000000000000000000000000000000000" + + "04820304"; + CoseException exception = + assertThrows( + CoseException.class, + () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString))); + assertThat(exception) + .hasMessageThat() + .startsWith("Signing key requires either sign or verify operation."); + } + + @Test + public void testParseKeySuccess() throws CborException, CoseException { + AkpSigningKey key = + AkpSigningKey.parse(TestUtilities.hexStringToByteArray(COSE_ENCODED_MLDSA87_KEY)); + assertThat(key).isNotNull(); + assertThat(key.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertThat(key.getAlgorithm()) + .isEqualTo(CborUtils.asInteger(Algorithm.SIGNING_ALGORITHM_MLDSA_87.getCoseAlgorithmId())); + assertThat(key.getPublicKeyBytes()).isNotNull(); + assertThat(TestUtilities.bytesToHexString(key.serialize())).isEqualTo(COSE_ENCODED_MLDSA87_KEY); + } + + @Test + public void testBuilder() throws CborException, CoseException { + AkpSigningKey signingKey = + AkpSigningKey.builder() + .withAlgorithm(Algorithm.SIGNING_ALGORITHM_MLDSA_65) + .withPublicKey(pubBytes) + .withPrivateKey(privBytes) + .build(); + + assertThat(signingKey.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertArrayEquals(pubBytes, signingKey.getPublicKeyBytes()); + Map map = CborUtils.asMap(signingKey.encode()); + assertThat(map.get(new UnsignedInteger(Headers.KEY_PARAMETER_ALGORITHM))) + .isEqualTo(Algorithm.SIGNING_ALGORITHM_MLDSA_65.getCoseAlgorithmId()); + assertThat(map.get(new NegativeInteger(Headers.KEY_PARAMETER_AKP_PUB))) + .isEqualTo(new ByteString(pubBytes)); + assertThat(map.get(new NegativeInteger(Headers.KEY_PARAMETER_AKP_PRIV))) + .isEqualTo(new ByteString(privBytes)); + } + + @Test + public void testBuilderFailureMissingAlgorithm() { + AkpSigningKey.Builder builder = + AkpSigningKey.builder().withPublicKey(pubBytes).withPrivateKey(privBytes); + assertThrows(CoseException.class, builder::build); + } + + @Test + public void testBuilderFailureMissingKeyMaterial() { + AkpSigningKey.Builder builder = + AkpSigningKey.builder().withAlgorithm(Algorithm.SIGNING_ALGORITHM_MLDSA_65); + CoseException exception = assertThrows(CoseException.class, builder::build); + assertThat(exception).hasMessageThat().startsWith("Missing key material"); + } + + @Test + public void testBuilderFailureWrongOperation() { + AkpSigningKey.Builder builder = + AkpSigningKey.builder() + .withAlgorithm(Algorithm.SIGNING_ALGORITHM_MLDSA_65) + .withPublicKey(pubBytes); + assertThrows(CoseException.class, () -> builder.withOperations(Headers.KEY_OPERATIONS_ENCRYPT)); + } + + @Test + public void testBuilderSuccessOnlyPrivateKey() throws CborException, CoseException { + AkpSigningKey signingKey = + AkpSigningKey.builder() + .withAlgorithm(Algorithm.SIGNING_ALGORITHM_MLDSA_65) + .withPrivateKey(privBytes) + .build(); + assertThat(signingKey.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertThat(signingKey.getAlgorithm()) + .isEqualTo(CborUtils.asInteger(Algorithm.SIGNING_ALGORITHM_MLDSA_65.getCoseAlgorithmId())); + } + + @Test + public void testBuilderFailureWrongKeyOperation() { + AkpSigningKey.Builder builder = + AkpSigningKey.builder() + .withAlgorithm(Algorithm.SIGNING_ALGORITHM_MLDSA_65) + .withPrivateKey(privBytes); + assertThrows(CoseException.class, () -> builder.withOperations(Headers.KEY_OPERATIONS_ENCRYPT)); + } + + @Test + public void testBuilderSuccess() throws CborException, CoseException { + AkpSigningKey signingKey = + AkpSigningKey.builder() + .withAlgorithm(Algorithm.SIGNING_ALGORITHM_MLDSA_65) + .withPublicKey(pubBytes) + .withOperations(Headers.KEY_OPERATIONS_SIGN, Headers.KEY_OPERATIONS_VERIFY) + .build(); + assertThat(signingKey.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertThat(signingKey.getAlgorithm()) + .isEqualTo(CborUtils.asInteger(Algorithm.SIGNING_ALGORITHM_MLDSA_65.getCoseAlgorithmId())); + signingKey.verifyOperationAllowedByKey(Headers.KEY_OPERATIONS_SIGN); + signingKey.verifyOperationAllowedByKey(Headers.KEY_OPERATIONS_VERIFY); + } + + @Test + public void testGenerateKeyMLDSA65() throws CborException, CoseException { + AkpSigningKey key = AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_65, PROVIDER); + assertThat(key).isNotNull(); + assertThat(key.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertThat(key.getAlgorithm()) + .isEqualTo(CborUtils.asInteger(Algorithm.SIGNING_ALGORITHM_MLDSA_65.getCoseAlgorithmId())); + assertThat(key.getPublicKeyBytes()).isNotNull(); + assertThat(key.getPublicKeyBytes()).hasLength(1952); + } + + @Test + public void testGenerateKeyMLDSA87() throws CborException, CoseException { + System.out.println("provider: " + PROVIDER); + AkpSigningKey key = AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_87, PROVIDER); + assertThat(key).isNotNull(); + assertThat(key.getKeyType()).isEqualTo(Headers.KEY_TYPE_AKP); + assertThat(key.getAlgorithm()) + .isEqualTo(CborUtils.asInteger(Algorithm.SIGNING_ALGORITHM_MLDSA_87.getCoseAlgorithmId())); + } + + @Test + public void testGenerateKeyFailureUnsupportedAlgorithm() { + assertThrows( + CoseException.class, + () -> AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_EDDSA, PROVIDER)); + } + + @Test + public void testSignAndVerifyMLDSA65() throws CborException, CoseException { + AkpSigningKey signingKey = + AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_65, PROVIDER); + byte[] message = TestUtilities.CONTENT_BYTES; + byte[] signature = signingKey.sign(Algorithm.SIGNING_ALGORITHM_MLDSA_65, message, PROVIDER); + + signingKey.verify(Algorithm.SIGNING_ALGORITHM_MLDSA_65, message, signature, PROVIDER); + } + + @Test + public void testSignAndVerifyMLDSA87() throws CborException, CoseException { + AkpSigningKey signingKey = + AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_87, PROVIDER); + byte[] message = TestUtilities.CONTENT_BYTES; + byte[] signature = signingKey.sign(Algorithm.SIGNING_ALGORITHM_MLDSA_87, message, PROVIDER); + + signingKey.verify(Algorithm.SIGNING_ALGORITHM_MLDSA_87, message, signature, PROVIDER); + } + + @Test + public void testSignAndVerifyFailureWrongAlgorithm() throws CborException, CoseException { + AkpSigningKey signingKey = + AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_65, PROVIDER); + byte[] message = TestUtilities.CONTENT_BYTES; + byte[] signature = signingKey.sign(Algorithm.SIGNING_ALGORITHM_MLDSA_65, message, PROVIDER); + assertThrows( + CoseException.class, + () -> + signingKey.verify(Algorithm.SIGNING_ALGORITHM_MLDSA_87, message, signature, PROVIDER)); + } + + @Test + public void testSignAndVerifyWithExplicitProvider() throws CborException, CoseException { + byte[] message = TestUtilities.CONTENT_BYTES; + AkpSigningKey key = AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_65, PROVIDER); + + String provider = AkpKey.PROVIDER; + byte[] signature = key.sign(Algorithm.SIGNING_ALGORITHM_MLDSA_65, message, provider); + assertThat(signature).isNotNull(); + + key.verify(Algorithm.SIGNING_ALGORITHM_MLDSA_65, message, signature, provider); + } + + @Test + public void testSignAndVerifyFailureNullProvider() throws CborException, CoseException { + byte[] message = TestUtilities.CONTENT_BYTES; + Algorithm algorithm = Algorithm.SIGNING_ALGORITHM_MLDSA_65; + AkpSigningKey key = AkpSigningKey.generateKey(algorithm, PROVIDER); + assertThrows(IllegalArgumentException.class, () -> key.sign(algorithm, message, null)); + + String provider = AkpKey.PROVIDER; + byte[] signature = key.sign(algorithm, message, provider); + assertThrows( + IllegalArgumentException.class, () -> key.verify(algorithm, message, signature, null)); + } +} From b4024acd835ffdf0496ad540cb43dca2c1f79e5b Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Thu, 17 Sep 2026 16:10:57 -0700 Subject: [PATCH 3/9] Update workflow to use Java 21. --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 9a75391..35a3684 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -21,10 +21,10 @@ jobs: egress-policy: audit disable-telemetry: true - uses: actions/checkout@v4 # v3 - - name: set up JDK 11 + - name: set up JDK 21 uses: actions/setup-java@v4 # v3 with: - java-version: '11' + java-version: '21' distribution: 'temurin' cache: maven - name: Build From d7f9c6adedbbaa9e9e99aaf699afc793c55f6352 Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Fri, 18 Sep 2026 13:13:32 -0700 Subject: [PATCH 4/9] Fix zizmor/unpinned-uses issue --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 35a3684..c251577 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -20,9 +20,9 @@ jobs: # try to nail them down, but it would turn into a game of whack-a-mole. egress-policy: audit disable-telemetry: true - - uses: actions/checkout@v4 # v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # satisfies zizmor/unpinned-uses - name: set up JDK 21 - uses: actions/setup-java@v4 # v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # satisfies zizmor/unpinned-uses with: java-version: '21' distribution: 'temurin' From d5f2568c425266e1d35dda62c1b4d3b2a84581fd Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Fri, 18 Sep 2026 13:22:33 -0700 Subject: [PATCH 5/9] update: Moving to JDK 21 --- pom.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b4f0209..b52d427 100644 --- a/pom.xml +++ b/pom.xml @@ -113,7 +113,7 @@ 3.6.3 - 16.0.0 + 21.0.0 @@ -128,8 +128,7 @@ org.apache.maven.plugins maven-compiler-plugin - 16 - 16 + 21 true -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED From 84d8f0fb85ff1fb4e300a5ff67a121d2496328cb Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Fri, 18 Sep 2026 13:31:14 -0700 Subject: [PATCH 6/9] fix: Fix the setup-java action workflow --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c251577..b4315d6 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -22,7 +22,7 @@ jobs: disable-telemetry: true - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # satisfies zizmor/unpinned-uses - name: set up JDK 21 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # satisfies zizmor/unpinned-uses + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # satisfies zizmor/unpinned-uses with: java-version: '21' distribution: 'temurin' From ba0fe296133498799f6db1d1a1359cff08e1fd8b Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Fri, 18 Sep 2026 13:38:07 -0700 Subject: [PATCH 7/9] fix: Fix zizmor findings --- .github/workflows/unit-test.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b4315d6..4820ad5 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -7,6 +7,9 @@ on: pull_request: branches: [ main ] +# Deny all default permissions globally +permissions: {} + jobs: build: @@ -14,15 +17,17 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@248ae51c2e8cc9622ecf50685c8bf7150c6e8813 # v1.4.3 + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: # Using audit because some endpoints are not reliably fixed. We could # try to nail them down, but it would turn into a game of whack-a-mole. egress-policy: audit disable-telemetry: true - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # satisfies zizmor/unpinned-uses + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: set up JDK 21 - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # satisfies zizmor/unpinned-uses + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: java-version: '21' distribution: 'temurin' From c7fe3b56418e3cd5554093b818a72bd8d4e1cf3c Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Fri, 18 Sep 2026 14:09:43 -0700 Subject: [PATCH 8/9] fix: Remove obsolete package imports --- test/com/google/cose/AkpSigningKeyTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/com/google/cose/AkpSigningKeyTest.java b/test/com/google/cose/AkpSigningKeyTest.java index e62b5a9..4cf2477 100644 --- a/test/com/google/cose/AkpSigningKeyTest.java +++ b/test/com/google/cose/AkpSigningKeyTest.java @@ -29,12 +29,8 @@ import com.google.cose.utils.Algorithm; import com.google.cose.utils.CborUtils; import com.google.cose.utils.Headers; - -import java.lang.reflect.Method; import java.security.Provider; import java.security.Security; -import java.security.spec.NamedParameterSpec; - import org.conscrypt.Conscrypt; import org.junit.Test; import org.junit.runner.RunWith; From e38056001b43369bb38f0cb443de662fcf9b2bac Mon Sep 17 00:00:00 2001 From: Vikram Gaur Date: Fri, 18 Sep 2026 14:36:37 -0700 Subject: [PATCH 9/9] Use stream operation instead of for loop. --- src/com/google/cose/AkpSigningKey.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/google/cose/AkpSigningKey.java b/src/com/google/cose/AkpSigningKey.java index e3520ed..cfae65e 100644 --- a/src/com/google/cose/AkpSigningKey.java +++ b/src/com/google/cose/AkpSigningKey.java @@ -35,6 +35,7 @@ import java.security.SignatureException; import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; /** Implements AKP COSE_Key spec for signing purposes. */ public final class AkpSigningKey extends AkpKey { @@ -134,11 +135,10 @@ public AkpSigningKey build() throws CborException, CoseException { @Override public Builder withOperations(Integer... operations) throws CoseException { - for (int operation : operations) { - if (operation != Headers.KEY_OPERATIONS_SIGN - && operation != Headers.KEY_OPERATIONS_VERIFY) { + if (!Arrays.stream(operations) + .allMatch( + op -> op == Headers.KEY_OPERATIONS_SIGN || op == Headers.KEY_OPERATIONS_VERIFY)) { throw new CoseException("Signing key only supports Sign or Verify operations."); - } } return super.withOperations(operations); }