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