diff --git a/pom.xml b/pom.xml
index 51f80a3..9f7822e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,8 +161,8 @@
maven-compiler-plugin
3.15.0
- 1.8
- 1.8
+ 21
+ 21
true
-XDcompilePolicy=simple
diff --git a/src/com/google/cose/AkpKey.java b/src/com/google/cose/AkpKey.java
index 14b54fd..ffd2544 100644
--- a/src/com/google/cose/AkpKey.java
+++ b/src/com/google/cose/AkpKey.java
@@ -33,43 +33,39 @@
/** Abstract class for generic AKP key */
public abstract class AkpKey extends CoseKey {
- public static final String PROVIDER = "Conscrypt";
+ public static final String CONSCRYPT_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)) {
+ if (algorithm == null || !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();
+ "Expecting an AKP signing algorithm, found "
+ + (algorithm != null ? algorithm.getJavaAlgorithmId() : getAlgorithm()));
}
+ populateKeyFromCbor();
+ }
- if (publicKeyBytes == null && privateKeyBytes == null) {
+ void populateKeyFromCbor() throws CborException, CoseException {
+ if (!labels.containsKey(Headers.KEY_PARAMETER_AKP_PUB)) {
throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE);
}
+ byte[] keyMaterial =
+ CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_AKP_PUB)).getBytes();
+ if (keyMaterial.length == 0) {
+ throw new CoseException("Could not decode public key. Expected key material.");
+ }
+ publicKeyBytes = keyMaterial;
}
void verifyAlgorithmAllowedByKey(Algorithm algorithm) throws CborException, CoseException {
@@ -89,8 +85,7 @@ public byte[] getPublicKeyBytes() {
/** Recursive builder to build out the AKP key and its subclasses. */
abstract static class Builder> extends CoseKey.Builder {
- protected byte[] publicKey;
- protected byte[] privateKey;
+ private byte[] publicKey;
@Override
void verifyKeyMaterialPresentAndComplete() throws CoseException {
@@ -108,7 +103,7 @@ void verifyKeyMaterialPresentAndComplete() throws CoseException {
}
boolean isKeyMaterialPresent() {
- return publicKey != null || privateKey != null;
+ return publicKey != null && publicKey.length != 0;
}
@Override
@@ -117,25 +112,15 @@ protected Map compile() throws CoseException {
Map cborKey = super.compile();
- if (publicKey != null) {
+ if (publicKey != null && publicKey.length != 0) {
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);
+ this.publicKey = (publicKey != null) ? Arrays.copyOf(publicKey, publicKey.length) : null;
return self();
}
}
@@ -147,6 +132,6 @@ public static boolean isAkpAlgorithm(Algorithm algorithm) {
}
public static boolean isConscryptProvider(String provider) {
- return Objects.equals(provider, PROVIDER);
+ return Objects.equals(provider, CONSCRYPT_PROVIDER);
}
}
diff --git a/src/com/google/cose/AkpSigningKey.java b/src/com/google/cose/AkpSigningKey.java
index cfae65e..62a7eb8 100644
--- a/src/com/google/cose/AkpSigningKey.java
+++ b/src/com/google/cose/AkpSigningKey.java
@@ -17,12 +17,15 @@
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.Headers;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
@@ -36,9 +39,12 @@
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
+import org.bouncycastle.crypto.params.MLDSAParameters;
+import org.bouncycastle.crypto.params.MLDSAPrivateKeyParameters;
/** Implements AKP COSE_Key spec for signing purposes. */
public final class AkpSigningKey extends AkpKey {
+ private byte[] privateKeyBytes;
public AkpSigningKey(DataItem cborKey) throws CborException, CoseException {
super(cborKey);
@@ -50,6 +56,52 @@ public AkpSigningKey(DataItem cborKey) throws CborException, CoseException {
}
}
+ @Override
+ void populateKeyFromCbor() throws CborException, CoseException {
+ privateKeyBytes = getPrivateKeyBytesFromCbor();
+ publicKeyBytes = getPublicKeyBytesFromCbor();
+ }
+
+ private byte[] getPrivateKeyBytesFromCbor() throws CborException, CoseException {
+ if (!labels.containsKey(Headers.KEY_PARAMETER_AKP_PRIV)) {
+ return null;
+ }
+ byte[] keyMaterial =
+ CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_AKP_PRIV)).getBytes();
+ if (keyMaterial.length == 0) {
+ throw new CoseException("Could not decode private key. Expected key material.");
+ }
+ return keyMaterial;
+ }
+
+ private byte[] getPublicKeyBytesFromCbor() throws CborException, CoseException {
+ if (labels.containsKey(Headers.KEY_PARAMETER_AKP_PUB)) {
+ byte[] keyMaterial =
+ CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_AKP_PUB)).getBytes();
+ if (keyMaterial.length == 0) {
+ throw new CoseException("Could not decode public key. Expected key material.");
+ }
+ return keyMaterial;
+ }
+ if (privateKeyBytes == null) {
+ throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE);
+ }
+ Algorithm algorithm = Algorithm.fromCoseAlgorithmId(getAlgorithm());
+ MLDSAParameters mldsaParameters =
+ switch (algorithm) {
+ case SIGNING_ALGORITHM_MLDSA_44 -> MLDSAParameters.ml_dsa_44;
+ case SIGNING_ALGORITHM_MLDSA_65 -> MLDSAParameters.ml_dsa_65;
+ case SIGNING_ALGORITHM_MLDSA_87 -> MLDSAParameters.ml_dsa_87;
+ default ->
+ throw new CoseException("Unsupported algorithm: " + algorithm.getJavaAlgorithmId());
+ };
+ try {
+ return new MLDSAPrivateKeyParameters(mldsaParameters, privateKeyBytes).getPublicKey();
+ } catch (RuntimeException e) {
+ throw new CoseException("Error while generating public key from private key bytes.", e);
+ }
+ }
+
public static AkpSigningKey parse(byte[] keyBytes) throws CborException, CoseException {
DataItem dataItem = CborUtils.decode(keyBytes);
return decode(dataItem);
@@ -121,6 +173,7 @@ public static AkpSigningKey generateKey(Algorithm algorithm, String provider)
/** Implements builder for AkpSigningKey. */
public static class Builder extends AkpKey.Builder {
+ private byte[] privateKey;
@Override
public Builder self() {
@@ -128,9 +181,8 @@ public Builder self() {
}
@Override
- public AkpSigningKey build() throws CborException, CoseException {
- Map cborKey = compile();
- return new AkpSigningKey(cborKey);
+ boolean isKeyMaterialPresent() {
+ return (privateKey != null && privateKey.length != 0) || super.isKeyMaterialPresent();
}
@Override
@@ -138,10 +190,26 @@ public Builder withOperations(Integer... operations) throws CoseException {
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.");
+ throw new CoseException("Signing key only supports Sign or Verify operations.");
}
return super.withOperations(operations);
}
+
+ @Override
+ public AkpSigningKey build() throws CborException, CoseException {
+ Map cborKey = compile();
+ if (privateKey != null && privateKey.length != 0) {
+ cborKey.put(
+ new NegativeInteger(Headers.KEY_PARAMETER_AKP_PRIV), new ByteString(privateKey));
+ }
+ return new AkpSigningKey(cborKey);
+ }
+
+ @CanIgnoreReturnValue
+ public Builder withPrivateKey(byte[] privateKey) {
+ this.privateKey = (privateKey != null) ? Arrays.copyOf(privateKey, privateKey.length) : null;
+ return this;
+ }
}
public static Builder builder() {
@@ -196,6 +264,9 @@ public byte[] sign(Algorithm algorithm, byte[] message, String provider)
public void verify(Algorithm algorithm, byte[] message, byte[] signature, String provider)
throws CborException, CoseException {
+ if (publicKeyBytes == null || publicKeyBytes.length == 0) {
+ throw new CoseException("Missing key material for verification.");
+ }
verifyAlgorithmMatchesKey(algorithm);
verifyAlgorithmAllowedByKey(algorithm);
verifyOperationAllowedByKey(Headers.KEY_OPERATIONS_VERIFY);
@@ -234,7 +305,7 @@ public void verify(Algorithm algorithm, byte[] message, byte[] signature, String
| InvalidKeyException
| InvalidKeySpecException
| SignatureException e) {
- throw new CoseException("Error while verifying ", e);
+ throw new CoseException("Error while verifying message.", e);
}
}
diff --git a/src/com/google/cose/utils/Algorithm.java b/src/com/google/cose/utils/Algorithm.java
index 432edf8..e4392ec 100644
--- a/src/com/google/cose/utils/Algorithm.java
+++ b/src/com/google/cose/utils/Algorithm.java
@@ -20,7 +20,6 @@
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.
@@ -73,10 +72,7 @@ public Number getCoseAlgorithmId() {
return new UnsignedInteger(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);
- }
+ public static Algorithm fromCoseAlgorithmId(int coseAlgorithmId) {
return REVERSE_LOOKUP_MAP.get(coseAlgorithmId);
}
}
diff --git a/test/com/google/cose/AkpSigningKeyTest.java b/test/com/google/cose/AkpSigningKeyTest.java
index 4cf2477..6667c0d 100644
--- a/test/com/google/cose/AkpSigningKeyTest.java
+++ b/test/com/google/cose/AkpSigningKeyTest.java
@@ -208,6 +208,30 @@ public void testParseKeyFailureMissingKeyMaterial() throws CborException, CoseEx
assertThat(exception).hasMessageThat().startsWith("Missing key material");
}
+ @Test
+ public void testEmptyPrivateKeyBytes() throws CborException, CoseException {
+ final String cborString = "A301070338302140";
+ CoseException exception =
+ assertThrows(
+ CoseException.class,
+ () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString)));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo("Could not decode private key. Expected key material.");
+ }
+
+ @Test
+ public void testEmptyPublicKeyBytes() throws CborException, CoseException {
+ final String cborString = "A301070338302040";
+ CoseException exception =
+ assertThrows(
+ CoseException.class,
+ () -> AkpSigningKey.parse(TestUtilities.hexStringToByteArray(cborString)));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo("Could not decode public key. Expected key material.");
+ }
+
@Test
public void testParseKeyFailureWrongKeyOperation() throws CborException, CoseException {
final String cborString =
@@ -327,7 +351,6 @@ public void testGenerateKeyMLDSA65() throws CborException, CoseException {
@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);
@@ -379,7 +402,7 @@ public void testSignAndVerifyWithExplicitProvider() throws CborException, CoseEx
byte[] message = TestUtilities.CONTENT_BYTES;
AkpSigningKey key = AkpSigningKey.generateKey(Algorithm.SIGNING_ALGORITHM_MLDSA_65, PROVIDER);
- String provider = AkpKey.PROVIDER;
+ String provider = AkpKey.CONSCRYPT_PROVIDER;
byte[] signature = key.sign(Algorithm.SIGNING_ALGORITHM_MLDSA_65, message, provider);
assertThat(signature).isNotNull();
@@ -393,7 +416,7 @@ public void testSignAndVerifyFailureNullProvider() throws CborException, CoseExc
AkpSigningKey key = AkpSigningKey.generateKey(algorithm, PROVIDER);
assertThrows(IllegalArgumentException.class, () -> key.sign(algorithm, message, null));
- String provider = AkpKey.PROVIDER;
+ String provider = AkpKey.CONSCRYPT_PROVIDER;
byte[] signature = key.sign(algorithm, message, provider);
assertThrows(
IllegalArgumentException.class, () -> key.verify(algorithm, message, signature, null));
diff --git a/test/com/google/cose/utils/CborUtilsTest.java b/test/com/google/cose/utils/CborUtilsTest.java
index 496a345..05590ac 100644
--- a/test/com/google/cose/utils/CborUtilsTest.java
+++ b/test/com/google/cose/utils/CborUtilsTest.java
@@ -1,3 +1,19 @@
+/*
+ * 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.utils;
import co.nstant.in.cbor.CborException;
@@ -115,7 +131,8 @@ public void testAsByteStringPositive() throws CborException {
@Test
public void testAsByteStringNegativeWrongType() {
- Assert.assertThrows(CborException.class, () -> CborUtils.asByteString(new UnicodeString("not bytes")));
+ Assert.assertThrows(
+ CborException.class, () -> CborUtils.asByteString(new UnicodeString("not bytes")));
}
@Test
@@ -141,7 +158,8 @@ public void testAsIntegerPositive() throws CborException {
@Test
public void testAsIntegerNegativeWrongType() {
- Assert.assertThrows(CborException.class, () -> CborUtils.asInteger(new UnicodeString("not a number")));
+ Assert.assertThrows(
+ CborException.class, () -> CborUtils.asInteger(new UnicodeString("not a number")));
}
@Test