Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>21</source>
<target>21</target>
<fork>true</fork>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
Expand Down
53 changes: 19 additions & 34 deletions src/com/google/cose/AkpKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -89,8 +85,7 @@ public byte[] getPublicKeyBytes() {

/** Recursive builder to build out the AKP key and its subclasses. */
abstract static class Builder<T extends Builder<T>> extends CoseKey.Builder<T> {
protected byte[] publicKey;
protected byte[] privateKey;
private byte[] publicKey;

@Override
void verifyKeyMaterialPresentAndComplete() throws CoseException {
Expand All @@ -108,7 +103,7 @@ void verifyKeyMaterialPresentAndComplete() throws CoseException {
}

boolean isKeyMaterialPresent() {
return publicKey != null || privateKey != null;
return publicKey != null && publicKey.length != 0;
}

@Override
Expand All @@ -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();
}
}
Expand All @@ -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);
}
}
81 changes: 76 additions & 5 deletions src/com/google/cose/AkpSigningKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -121,27 +173,43 @@ public static AkpSigningKey generateKey(Algorithm algorithm, String provider)

/** Implements builder for AkpSigningKey. */
public static class Builder extends AkpKey.Builder<Builder> {
private byte[] privateKey;

@Override
public Builder self() {
return this;
}

@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
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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}

Expand Down
6 changes: 1 addition & 5 deletions src/com/google/cose/utils/Algorithm.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
29 changes: 26 additions & 3 deletions test/com/google/cose/AkpSigningKeyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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));
Expand Down
22 changes: 20 additions & 2 deletions test/com/google/cose/utils/CborUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading