diff --git a/src/com/google/cose/CoseKey.java b/src/com/google/cose/CoseKey.java index a5a3e3c..53aa96a 100644 --- a/src/com/google/cose/CoseKey.java +++ b/src/com/google/cose/CoseKey.java @@ -180,6 +180,18 @@ protected Map compile() throws CoseException { return cborKey; } + public T copyFrom(CoseKey key) { + keyType = key.keyType; + keyId = key.keyId; + algorithm = (key.algorithm == null) ? null : Algorithm.fromCoseAlgorithmId(key.algorithm); + operations.clear(); + if (key.operations != null) { + operations.addAll(key.operations); + } + baseIv = key.baseIv; + return self(); + } + public T withKeyType(int keyType) { this.keyType = keyType; return self(); diff --git a/src/com/google/cose/Ec2Key.java b/src/com/google/cose/Ec2Key.java index 2d45a1c..6027a53 100644 --- a/src/com/google/cose/Ec2Key.java +++ b/src/com/google/cose/Ec2Key.java @@ -12,7 +12,14 @@ import com.google.cose.utils.CoseUtils; import com.google.cose.utils.Headers; import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECPoint; /** * Abstract class for generic Ec2 key @@ -20,7 +27,8 @@ public abstract class Ec2Key extends CoseKey { private static final int SIGN_POSITIVE = 1; - private ECPublicKey publicKey; + private int curve; + protected KeyPair keyPair; Ec2Key(DataItem cborKey) throws CborException, CoseException { super(cborKey); @@ -33,10 +41,29 @@ void populateKeyFromCbor() throws CborException, CoseException { } // Get curve information - int curve = CborUtils.asInteger(labels.get(Headers.KEY_PARAMETER_CURVE)); + curve = CborUtils.asInteger(labels.get(Headers.KEY_PARAMETER_CURVE)); + + // Get private key. + final ECPrivateKey privateKey; + if (labels.containsKey(Headers.KEY_PARAMETER_D)) { + byte[] key = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_D)).getBytes(); + if (key.length == 0) { + throw new CoseException("Cannot decode private key. Missing coordinate information."); + } + privateKey = CoseUtils.getEc2PrivateKeyFromInteger(curve, new BigInteger(SIGN_POSITIVE, key)); + } else { + privateKey = null; + } if (!labels.containsKey(Headers.KEY_PARAMETER_X)) { - throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE); + if (privateKey == null) { + throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE); + } else { + keyPair = new KeyPair( + CoseUtils.getEc2PublicKeyFromPrivateKey(curve, privateKey), + privateKey); + return; + } } final ByteString xCor = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_X)); @@ -46,15 +73,22 @@ void populateKeyFromCbor() throws CborException, CoseException { throw new IllegalStateException("X coordinate provided but Y coordinate is missing."); } final ByteString yCor = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_Y)); - publicKey = (ECPublicKey) CoseUtils.getEc2PublicKeyFromCoordinates( + final PublicKey publicKey = CoseUtils.getEc2PublicKeyFromCoordinates( curve, new BigInteger(SIGN_POSITIVE, xCor.getBytes()), new BigInteger(SIGN_POSITIVE, yCor.getBytes()) ); + keyPair = new KeyPair(publicKey, privateKey); } public ECPublicKey getPublicKey() { - return publicKey; + return (ECPublicKey) this.keyPair.getPublic(); + } + + public abstract Ec2Key getPublic() throws CborException, CoseException; + + public int getCurve() { + return curve; } void verifyAlgorithmAllowedByKey(Algorithm algorithm) throws CborException, CoseException { @@ -79,9 +113,32 @@ void verifyAlgorithmAllowedByKey(Algorithm algorithm) throws CborException, Cose } } + // Big endian: Do not reuse for little endian encodings + private static byte[] arrayFromBigNum(BigInteger num, int keySize) + throws IllegalArgumentException { + // Roundup arithmetic from bits to bytes. + byte[] keyBytes = new byte[(keySize + 7) / 8]; + byte[] keyBytes2 = num.toByteArray(); + if (keyBytes.length == keyBytes2.length) { + return keyBytes2; + } + if (keyBytes2.length > keyBytes.length) { + // There should be no more than one padding(0) byte, invalid key otherwise. + if (keyBytes2.length - keyBytes.length > 1 && keyBytes2[0] != 0) { + throw new IllegalArgumentException(); + } + System.arraycopy(keyBytes2, keyBytes2.length - keyBytes.length, keyBytes, 0, keyBytes.length); + } else { + System.arraycopy( + keyBytes2, 0, keyBytes, keyBytes.length - keyBytes2.length, keyBytes2.length); + } + return keyBytes; + } + /** Recursive builder to build out the Ec2 key and its subclasses. */ abstract static class Builder> extends CoseKey.Builder { private Integer curve = null; + private byte[] dParameter; private byte[] xCor; private byte[] yCor; @@ -96,7 +153,7 @@ void verifyKeyMaterialPresentAndComplete() throws CoseException { } boolean isKeyMaterialPresent() { - return xCor != null && yCor != null; + return dParameter != null || (xCor != null && yCor != null); } boolean isPublicKeyMaterialIncomplete() { @@ -113,6 +170,9 @@ protected Map compile() throws CoseException { Map cborKey = super.compile(); cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_CURVE), new UnsignedInteger(curve)); + if (dParameter != null) { + cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_D), new ByteString(dParameter)); + } if (xCor != null) { cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_X), new ByteString(xCor)); } @@ -122,6 +182,30 @@ protected Map compile() throws CoseException { return cborKey; } + public T copyFrom(Ec2Key key) { + curve = key.curve; + int keySize; + switch (curve) { + case Headers.CURVE_EC2_P256: + keySize = 256; + break; + case Headers.CURVE_EC2_P384: + keySize = 384; + break; + case Headers.CURVE_EC2_P521: + keySize = 521; + break; + default: + throw new AssertionError(); + } + ECPublicKey pubKey = (ECPublicKey) key.keyPair.getPublic(); + ECPrivateKey privKey = (ECPrivateKey) key.keyPair.getPrivate(); + xCor = arrayFromBigNum(pubKey.getW().getAffineX(), keySize); + yCor = arrayFromBigNum(pubKey.getW().getAffineY(), keySize); + dParameter = (privKey == null) ? null : privKey.getS().toByteArray(); + return super.copyFrom(key); + } + public T withCurve(int curve) throws CoseException { if ((curve < 0) || (curve > Headers.CURVE_EC2_P521)) { throw new CoseException(CoseException.UNSUPPORTED_CURVE_EXCEPTION_MESSAGE); @@ -139,5 +223,83 @@ public T withYCoordinate(byte[] yCor) { this.yCor = yCor; return self(); } + + public T withGeneratedKeyPair(int curve) throws CoseException { + KeyPair keyPair; + int keySize; + String curveName; + + switch (curve) { + case Headers.CURVE_EC2_P256: + curveName = "secp256r1"; + keySize = 256; + break; + + case Headers.CURVE_EC2_P384: + curveName = "secp384r1"; + keySize = 384; + break; + + case Headers.CURVE_EC2_P521: + curveName = "secp521r1"; + keySize = 521; + break; + + default: + throw new CoseException("Unsupported curve: " + curve); + } + try { + ECGenParameterSpec paramSpec = new ECGenParameterSpec(curveName); + KeyPairGenerator gen = KeyPairGenerator.getInstance("EC"); + gen.initialize(paramSpec); + keyPair = gen.genKeyPair(); + ECPoint pubPoint = ((ECPublicKey) keyPair.getPublic()).getW(); + + return withPrivateKeyRepresentation() + .withPkcs8EncodedBytes(keyPair.getPrivate().getEncoded()) + .withXCoordinate(arrayFromBigNum(pubPoint.getAffineX(), keySize)) + .withYCoordinate(arrayFromBigNum(pubPoint.getAffineY(), keySize)) + .withCurve(curve); + } catch (GeneralSecurityException e) { + throw new CoseException("Failed to generate key pari for curve: " + curve, e); + } + } + + public PrivateKeyRepresentationBuilder withPrivateKeyRepresentation() { + return new PrivateKeyRepresentationBuilder(this); + } + + /** + * Helper class to get the raw bytes out of the encoded private keys. + */ + public static class PrivateKeyRepresentationBuilder> { + Builder builder; + + PrivateKeyRepresentationBuilder(Builder builder) { + this.builder = builder; + } + + public T withPrivateKey(ECPrivateKey privateKey) { + builder.dParameter = privateKey.getS().toByteArray(); + return builder.self(); + } + + public T withPkcs8EncodedBytes(byte[] keyBytes) throws CoseException { + ECPrivateKey key = CoseUtils.getEc2PrivateKeyFromEncodedKeyBytes(keyBytes); + builder.dParameter = key.getS().toByteArray(); + return builder.self(); + } + + /** + * This function expects the BigInteger byte array of the private key. This is typically the + * multiplier in the EC2 private key which can generate EC2 public key from generator point. + * @param rawBytes byte array representation of BigInteger + * @return {@link Builder} + */ + public T withDParameter(byte[] rawBytes) { + builder.dParameter = rawBytes; + return builder.self(); + } + } } } diff --git a/src/com/google/cose/Ec2KeyAgreementKey.java b/src/com/google/cose/Ec2KeyAgreementKey.java index db877dd..7b8cc3d 100644 --- a/src/com/google/cose/Ec2KeyAgreementKey.java +++ b/src/com/google/cose/Ec2KeyAgreementKey.java @@ -19,6 +19,7 @@ import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.DataItem; import com.google.cose.exceptions.CoseException; +import com.google.cose.utils.Algorithm; import com.google.cose.utils.CborUtils; import com.google.cose.utils.Headers; @@ -42,6 +43,30 @@ public static Ec2KeyAgreementKey decode(DataItem cborKey) throws CborException, return new Ec2KeyAgreementKey(cborKey); } + @Override + public Ec2KeyAgreementKey getPublic() throws CborException, CoseException { + if (keyPair.getPrivate() == null) { + return this; + } else { + return builder().copyFrom(this).withPrivateKeyRepresentation().withDParameter(null).build(); + } + } + + /** Generates a COSE formatted Ec2 key agreement key given a specific algorithm and curve. */ + public static Ec2KeyAgreementKey generateKey(Algorithm algorithm, int curve) + throws CborException, CoseException { + switch (algorithm) { + case ECDH_ES_HKDF_256: + return builder() + .withGeneratedKeyPair(curve) + .withAlgorithm(algorithm) + .build(); + + default: + throw new CoseException("Unsupported algorithm: " + algorithm.getJavaAlgorithmId()); + } + } + public static class Builder extends Ec2Key.Builder { @Override public Builder self() { diff --git a/src/com/google/cose/Ec2SigningKey.java b/src/com/google/cose/Ec2SigningKey.java index e0f55ae..18d7f6a 100644 --- a/src/com/google/cose/Ec2SigningKey.java +++ b/src/com/google/cose/Ec2SigningKey.java @@ -17,36 +17,20 @@ 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 java.math.BigInteger; -import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECPoint; /** Implements EC2 COSE_Key spec for signing purposes. */ public final class Ec2SigningKey extends Ec2Key { - private static final int SIGN_POSITIVE = 1; - - private KeyPair keyPair; - public Ec2SigningKey(DataItem cborKey) throws CborException, CoseException { super(cborKey); @@ -57,53 +41,6 @@ public Ec2SigningKey(DataItem cborKey) throws CborException, CoseException { } } - @Override - void populateKeyFromCbor() throws CborException, CoseException { - if (getKeyType() != Headers.KEY_TYPE_EC2) { - throw new CoseException("Expecting EC2 key (type 2), found type " + getKeyType()); - } - - // Get curve information - int curve = CborUtils.asInteger(labels.get(Headers.KEY_PARAMETER_CURVE)); - - // Get private key. - final ECPrivateKey privateKey; - if (labels.containsKey(Headers.KEY_PARAMETER_D)) { - byte[] key = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_D)).getBytes(); - if (key.length == 0) { - throw new CoseException("Cannot decode private key. Missing coordinate information."); - } - privateKey = CoseUtils.getEc2PrivateKeyFromInteger(curve, new BigInteger(SIGN_POSITIVE, key)); - } else { - privateKey = null; - } - - if (!labels.containsKey(Headers.KEY_PARAMETER_X)) { - if (privateKey == null) { - throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE); - } else { - keyPair = new KeyPair( - CoseUtils.getEc2PublicKeyFromPrivateKey(curve, privateKey), - privateKey); - return; - } - } - - final ByteString xCor = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_X)); - // Get the public key for EC2 key. - // We should not have a case where x is provided but y is not. - if (!labels.containsKey(Headers.KEY_PARAMETER_Y)) { - throw new IllegalStateException("X coordinate provided but Y coordinate is missing."); - } - final ByteString yCor = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_Y)); - final PublicKey publicKey = CoseUtils.getEc2PublicKeyFromCoordinates( - curve, - new BigInteger(SIGN_POSITIVE, xCor.getBytes()), - new BigInteger(SIGN_POSITIVE, yCor.getBytes()) - ); - keyPair = new KeyPair(publicKey, privateKey); - } - public static Ec2SigningKey parse(byte[] keyBytes) throws CborException, CoseException { DataItem dataItem = CborUtils.decode(keyBytes); return decode(dataItem); @@ -114,30 +51,12 @@ public static Ec2SigningKey decode(DataItem cborKey) throws CborException, CoseE } @Override - public ECPublicKey getPublicKey() { - return (ECPublicKey) this.keyPair.getPublic(); - } - - // Big endian: Do not reuse for little endian encodings - private static byte[] arrayFromBigNum(BigInteger num, int keySize) - throws IllegalArgumentException { - // Roundup arithmetic from bits to bytes. - byte[] keyBytes = new byte[(keySize + 7) / 8]; - byte[] keyBytes2 = num.toByteArray(); - if (keyBytes.length == keyBytes2.length) { - return keyBytes2; - } - if (keyBytes2.length > keyBytes.length) { - // There should be no more than one padding(0) byte, invalid key otherwise. - if (keyBytes2.length - keyBytes.length > 1 && keyBytes2[0] != 0) { - throw new IllegalArgumentException(); - } - System.arraycopy(keyBytes2, keyBytes2.length - keyBytes.length, keyBytes, 0, keyBytes.length); + public Ec2SigningKey getPublic() throws CborException, CoseException { + if (keyPair.getPrivate() == null) { + return this; } else { - System.arraycopy( - keyBytes2, 0, keyBytes, keyBytes.length - keyBytes2.length, keyBytes2.length); + return builder().copyFrom(this).withPrivateKeyRepresentation().withDParameter(null).build(); } - return keyBytes; } /** @@ -145,84 +64,41 @@ private static byte[] arrayFromBigNum(BigInteger num, int keySize) * chosen based on section 6.2.1 of RFC 5656 */ public static Ec2SigningKey generateKey(Algorithm algorithm) throws CborException, CoseException { - KeyPair keyPair; - int keySize; - int header; - String curveName; + int curve; switch (algorithm) { case SIGNING_ALGORITHM_ECDSA_SHA_256: - curveName = "secp256r1"; - keySize = 256; - header = Headers.CURVE_EC2_P256; + curve = Headers.CURVE_EC2_P256; break; case SIGNING_ALGORITHM_ECDSA_SHA_384: - curveName = "secp384r1"; - keySize = 384; - header = Headers.CURVE_EC2_P384; + curve = Headers.CURVE_EC2_P384; break; case SIGNING_ALGORITHM_ECDSA_SHA_512: - curveName = "secp521r1"; - keySize = 521; - header = Headers.CURVE_EC2_P521; + curve = Headers.CURVE_EC2_P521; break; default: throw new CoseException("Unsupported algorithm curve: " + algorithm.getJavaAlgorithmId()); } - try { - ECGenParameterSpec paramSpec = new ECGenParameterSpec(curveName); - KeyPairGenerator gen = KeyPairGenerator.getInstance("EC"); - gen.initialize(paramSpec); - keyPair = gen.genKeyPair(); - - ECPoint pubPoint = ((ECPublicKey) keyPair.getPublic()).getW(); - byte[] x = arrayFromBigNum(pubPoint.getAffineX(), keySize); - byte[] y = arrayFromBigNum(pubPoint.getAffineY(), keySize); - byte[] privEncodedKey = keyPair.getPrivate().getEncoded(); - - return Ec2SigningKey.builder() - .withPrivateKeyRepresentation() - .withPkcs8EncodedBytes(privEncodedKey) - .withXCoordinate(x) - .withYCoordinate(y) - .withCurve(header) - .withAlgorithm(algorithm) - .build(); - } catch (NoSuchAlgorithmException e) { - throw new CoseException("No provider for algorithm: " + algorithm.getJavaAlgorithmId(), e); - } catch (InvalidAlgorithmParameterException e) { - throw new CoseException("The curve is not supported: " + algorithm.getJavaAlgorithmId(), e); - } catch (IllegalArgumentException e) { - throw new CoseException( - "Invalid Coordinates generated for: " + algorithm.getJavaAlgorithmId(), e); - } + return Ec2SigningKey.builder() + .withGeneratedKeyPair(curve) + .withAlgorithm(algorithm) + .build(); } /** Implements builder for Ec2SigningKey. */ public static class Builder extends Ec2Key.Builder { - private byte[] dParameter; - @Override public Builder self() { return this; } - @Override - boolean isKeyMaterialPresent() { - return dParameter != null || super.isKeyMaterialPresent(); - } - @Override public Ec2SigningKey build() throws CborException, CoseException { - Map cborKey = compile(); - if (dParameter != null) { - cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_D), new ByteString(dParameter)); - } - return new Ec2SigningKey(cborKey); + return new Ec2SigningKey(compile()); } @Override @@ -235,43 +111,6 @@ public Builder withOperations(Integer...operations) throws CoseException { } return super.withOperations(operations); } - - public PrivateKeyRepresentationBuilder withPrivateKeyRepresentation() { - return new PrivateKeyRepresentationBuilder(this); - } - - /** - * Helper class to get the raw bytes out of the encoded private keys. - */ - public static class PrivateKeyRepresentationBuilder { - Builder builder; - - PrivateKeyRepresentationBuilder(Builder builder) { - this.builder = builder; - } - - public Builder withPrivateKey(ECPrivateKey privateKey) { - builder.dParameter = privateKey.getS().toByteArray(); - return builder; - } - - public Builder withPkcs8EncodedBytes(byte[] keyBytes) throws CoseException { - ECPrivateKey key = CoseUtils.getEc2PrivateKeyFromEncodedKeyBytes(keyBytes); - builder.dParameter = key.getS().toByteArray(); - return builder; - } - - /** - * This function expects the BigInteger byte array of the private key. This is typically the - * multiplier in the EC2 private key which can generate EC2 public key from generator point. - * @param rawBytes byte array representation of BigInteger - * @return {@link Builder} - */ - public Builder withDParameter(byte[] rawBytes) { - builder.dParameter = rawBytes; - return builder; - } - } } public static Builder builder() { diff --git a/src/com/google/cose/OkpKey.java b/src/com/google/cose/OkpKey.java index 58552f1..3b65115 100644 --- a/src/com/google/cose/OkpKey.java +++ b/src/com/google/cose/OkpKey.java @@ -9,13 +9,16 @@ import com.google.cose.exceptions.CoseException; import com.google.cose.utils.CborUtils; import com.google.cose.utils.Headers; +import java.security.PublicKey; import java.util.Arrays; /** * Abstract class for generic Ec2 key */ public abstract class OkpKey extends CoseKey { - private byte[] publicKeyBytes; + protected byte[] privateKeyBytes; + protected byte[] publicKeyBytes; + protected int curve; OkpKey(DataItem cborKey) throws CborException, CoseException { super(cborKey); @@ -26,28 +29,59 @@ public abstract class OkpKey extends CoseKey { } void populateKeyFromCbor() throws CborException, CoseException { + privateKeyBytes = getPrivateKeyBytesFromCbor(); + publicKeyBytes = getPublicKeyBytesFromCbor(); + curve = CborUtils.asInteger(labels.get(Headers.KEY_PARAMETER_CURVE)); + } + + private byte[] getPrivateKeyBytesFromCbor() throws CborException, CoseException { + if (labels.containsKey(Headers.KEY_PARAMETER_D)) { + byte[] keyMaterial = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_D)).getBytes(); + if (keyMaterial.length == 0) { + throw new CoseException("Could not decode private key. Expected key material."); + } + return keyMaterial; + } + return null; + } + + private byte[] getPublicKeyBytesFromCbor() throws CborException, CoseException { if (labels.containsKey(Headers.KEY_PARAMETER_X)) { byte[] keyMaterial = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_X)).getBytes(); if (keyMaterial.length == 0) { throw new CoseException("Could not decode public key. Expected key material."); } - publicKeyBytes = keyMaterial; - } else { + return keyMaterial; + } + if (privateKeyBytes == null) { throw new CoseException(CoseException.MISSING_KEY_MATERIAL_EXCEPTION_MESSAGE); } + return publicFromPrivate(privateKeyBytes); } + + protected abstract byte[] publicFromPrivate(byte[] privateKey) throws CoseException; + + public abstract PublicKey getPublicKey() throws CoseException; + + public abstract OkpKey getPublic() throws CborException, CoseException; + public byte[] getPublicKeyBytes() { return Arrays.copyOf(publicKeyBytes, publicKeyBytes.length); } + public int getCurve() { + return curve; + } + /** Recursive builder to build out the Ec2 key and its subclasses. */ abstract static class Builder> extends CoseKey.Builder { private Integer curve = null; + private byte[] dParameter; private byte[] xCor; boolean isKeyMaterialPresent() { - return xCor != null && xCor.length != 0; + return (dParameter != null && dParameter.length != 0) || (xCor != null && xCor.length != 0); } @Override @@ -67,12 +101,22 @@ protected Map compile() throws CoseException { Map cborKey = super.compile(); cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_CURVE), new UnsignedInteger(curve)); + if (dParameter != null) { + cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_D), new ByteString(dParameter)); + } if (xCor != null && xCor.length != 0) { cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_X), new ByteString(xCor)); } return cborKey; } + public T copyFrom(OkpKey key) { + curve = key.curve; + dParameter = key.privateKeyBytes; + xCor = key.publicKeyBytes; + return super.copyFrom(key); + } + public T withCurve(int curve) throws CoseException { if ((curve != Headers.CURVE_OKP_X25519) && (curve != Headers.CURVE_OKP_ED25519)) { throw new CoseException(CoseException.UNSUPPORTED_CURVE_EXCEPTION_MESSAGE); @@ -81,6 +125,11 @@ public T withCurve(int curve) throws CoseException { return self(); } + public T withDParameter(byte[] dParam) { + this.dParameter = dParam; + return self(); + } + public T withXCoordinate(byte[] xCor) { this.xCor = xCor; return self(); diff --git a/src/com/google/cose/OkpKeyAgreementKey.java b/src/com/google/cose/OkpKeyAgreementKey.java index ee0675c..58018b9 100644 --- a/src/com/google/cose/OkpKeyAgreementKey.java +++ b/src/com/google/cose/OkpKeyAgreementKey.java @@ -19,18 +19,33 @@ import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.DataItem; 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.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.XECPrivateKey; +import java.security.interfaces.XECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.NamedParameterSpec; +import java.security.spec.XECPublicKeySpec; +import org.bouncycastle.math.ec.rfc7748.X25519; /** * Implements OKP COSE_Key spec for key wrapping purposes. * Currently, only supports X25519 curve. */ public final class OkpKeyAgreementKey extends OkpKey { + private static final int SIGN_POSITIVE = 1; + public OkpKeyAgreementKey(DataItem cborKey) throws CborException, CoseException { super(cborKey); - int curve = CborUtils.asInteger(labels.get(Headers.KEY_PARAMETER_CURVE)); if (curve != Headers.CURVE_OKP_X25519) { throw new CoseException(CoseException.UNSUPPORTED_CURVE_EXCEPTION_MESSAGE); } @@ -49,6 +64,48 @@ public static OkpKeyAgreementKey decode(DataItem cborKey) throws CborException, return new OkpKeyAgreementKey(cborKey); } + @Override + protected byte[] publicFromPrivate(byte[] privateKey) throws CoseException { + byte[] r = new byte[32]; + X25519.generatePublicKey(privateKeyBytes, 0, r, 0); + return r; + } + + @Override + public PublicKey getPublicKey() throws CoseException { + try { + BigInteger u = new BigInteger(SIGN_POSITIVE, publicKeyBytes); + XECPublicKeySpec spec = new XECPublicKeySpec(NamedParameterSpec.X25519, u); + return KeyFactory.getInstance("X25519").generatePublic(spec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new CoseException("Failed to generate X25519 public key", e); + } + } + + @Override + public OkpKeyAgreementKey getPublic() throws CborException, CoseException { + if (privateKeyBytes == null) { + return this; + } else { + return builder().copyFrom(this).withDParameter(null).build(); + } + } + + /** Generates a COSE formatted OKP key agreement key from scratch. */ + public static OkpKeyAgreementKey generateKey(Algorithm algorithm, int curve) + throws CborException, CoseException { + switch (algorithm) { + case ECDH_ES_HKDF_256: + return builder() + .withGeneratedKeyPair(curve) + .withAlgorithm(algorithm) + .build(); + + default: + throw new CoseException("Unsupported algorithm: " + algorithm.getJavaAlgorithmId()); + } + } + public static class Builder extends OkpKey.Builder { @Override public Builder self() { @@ -71,6 +128,19 @@ public Builder withOperations(Integer...operations) throws CoseException { } return super.withOperations(operations); } + + public Builder withGeneratedKeyPair(int curve) throws CoseException { + if (curve != Headers.CURVE_OKP_X25519) { + throw new CoseException("Unsupported curve: " + curve); + } + try { + KeyPair keyPair = KeyPairGenerator.getInstance("X25519").generateKeyPair(); + return withDParameter(((XECPrivateKey) keyPair.getPrivate()).getScalar().get()) + .withXCoordinate(((XECPublicKey) keyPair.getPublic()).getU().toByteArray()); + } catch (GeneralSecurityException e) { + throw new CoseException("Failed to generate key pair", e); + } + } } public static Builder builder() { diff --git a/src/com/google/cose/OkpSigningKey.java b/src/com/google/cose/OkpSigningKey.java index f5ada59..c3c7eb1 100644 --- a/src/com/google/cose/OkpSigningKey.java +++ b/src/com/google/cose/OkpSigningKey.java @@ -17,10 +17,7 @@ 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; @@ -29,6 +26,11 @@ import com.google.crypto.tink.subtle.Ed25519Sign.KeyPair; import com.google.crypto.tink.subtle.Ed25519Verify; import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; /** @@ -36,19 +38,13 @@ * Currently only supports Ed25519 curve. */ public final class OkpSigningKey extends OkpKey { - private byte[] privateKeyBytes; - private byte[] publicKeyBytes; - public OkpSigningKey(DataItem cborKey) throws CborException, CoseException { super(cborKey); - int curve = CborUtils.asInteger(labels.get(Headers.KEY_PARAMETER_CURVE)); if (curve != Headers.CURVE_OKP_ED25519) { throw new CoseException(CoseException.UNSUPPORTED_CURVE_EXCEPTION_MESSAGE); } - populateKeyFromCbor(); - if ((operations != null) && !operations.contains(Headers.KEY_OPERATIONS_VERIFY) && !operations.contains(Headers.KEY_OPERATIONS_SIGN)) { @@ -56,34 +52,17 @@ public OkpSigningKey(DataItem cborKey) throws CborException, CoseException { } } - @Override - void populateKeyFromCbor() throws CborException, CoseException { - privateKeyBytes = getPrivateKeyBytesFromCbor(); - publicKeyBytes = getPublicKeyBytesFromCbor(); + public static OkpSigningKey parse(byte[] keyBytes) throws CborException, CoseException { + DataItem dataItem = CborUtils.decode(keyBytes); + return decode(dataItem); } - private byte[] getPrivateKeyBytesFromCbor() throws CborException, CoseException { - if (labels.containsKey(Headers.KEY_PARAMETER_D)) { - byte[] keyMaterial = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_D)).getBytes(); - if (keyMaterial.length == 0) { - throw new CoseException("Could not decode private key. Expected key material."); - } - return keyMaterial; - } - return null; + public static OkpSigningKey decode(DataItem cborKey) throws CborException, CoseException { + return new OkpSigningKey(cborKey); } - private byte[] getPublicKeyBytesFromCbor() throws CborException, CoseException { - if (labels.containsKey(Headers.KEY_PARAMETER_X)) { - byte[] keyMaterial = CborUtils.asByteString(labels.get(Headers.KEY_PARAMETER_X)).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); - } + @Override + protected byte[] publicFromPrivate(byte[] privateKey) throws CoseException { try { return KeyPair.newKeyPairFromSeed(privateKeyBytes).getPublicKey(); } catch (GeneralSecurityException e) { @@ -91,51 +70,53 @@ private byte[] getPublicKeyBytesFromCbor() throws CborException, CoseException { } } - public static OkpSigningKey parse(byte[] keyBytes) throws CborException, CoseException { - DataItem dataItem = CborUtils.decode(keyBytes); - return decode(dataItem); - } - - public static OkpSigningKey decode(DataItem cborKey) throws CborException, CoseException { - return new OkpSigningKey(cborKey); + @Override + public PublicKey getPublicKey() throws CoseException { + try { + // Ed25519 support was added to Java 15 with the EdECPublicKeySpec but, in order to support + // older versions, generate the key with an x509EncodedKeySpec with an encoded key as + // defined in rfc8410. Before Java 15, a security provider that can handle Ed25519 keys needs + // to be installed. + byte[] subjectPublicKeyInfo = + new byte[] { + 0x30, 0x2a, // SEQUENCE + 0x30, 0x05, // SEQUENCE + 0x06, 0x03, 0x2b, 0x65, 0x70, // OBJECT IDENTIFIER { 1 3 101 112 } + 0x03, 0x21, 0x00, // BIT STRING + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + System.arraycopy(publicKeyBytes, 0, subjectPublicKeyInfo, 12, 32); + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(subjectPublicKeyInfo); + return KeyFactory.getInstance("Ed25519").generatePublic(x509EncodedKeySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new CoseException("Failed to generate Ed25519 public key", e); + } } @Override - public byte[] getPublicKeyBytes() { - return Arrays.copyOf(publicKeyBytes, publicKeyBytes.length); + public OkpSigningKey getPublic() throws CborException, CoseException { + if (privateKeyBytes == null ) { + return this; + } else { + return builder().copyFrom(this).withDParameter(null).build(); + } } /** Generates a COSE formatted OKP signing key from scratch */ public static OkpSigningKey generateKey() throws CborException, CoseException { - KeyPair keyPair; - try { - keyPair = KeyPair.newKeyPair(); - } catch (GeneralSecurityException e) { - throw new CoseException("Error while generating key pair: ", e); - } - byte[] privateKey = keyPair.getPrivateKey(); - byte[] publicKey = keyPair.getPublicKey(); - - return OkpSigningKey.builder() - .withXCoordinate(publicKey) - .withDParameter(privateKey) + return builder() + .withGeneratedKeyPair(Headers.CURVE_OKP_ED25519) .withAlgorithm(Algorithm.SIGNING_ALGORITHM_EDDSA) .build(); } public static class Builder extends OkpKey.Builder { - private byte[] dParameter; - @Override public Builder self() { return this; } - @Override - boolean isKeyMaterialPresent() { - return (dParameter != null && dParameter.length != 0) || super.isKeyMaterialPresent(); - } - @Override public Builder withOperations(Integer...operations) throws CoseException { for (int operation : operations) { @@ -150,17 +131,21 @@ public Builder withOperations(Integer...operations) throws CoseException { @Override public OkpSigningKey build() throws CborException, CoseException { withCurve(Headers.CURVE_OKP_ED25519); - - Map cborKey = compile(); - if (dParameter != null) { - cborKey.put(new NegativeInteger(Headers.KEY_PARAMETER_D), new ByteString(dParameter)); - } - return new OkpSigningKey(cborKey); + return new OkpSigningKey(compile()); } - public Builder withDParameter(byte[] dParam) { - this.dParameter = dParam; - return this; + public Builder withGeneratedKeyPair(int curve) throws CoseException { + if (curve != Headers.CURVE_OKP_ED25519) { + throw new CoseException("Unsupported curve: " + curve); + } + KeyPair keyPair; + try { + keyPair = KeyPair.newKeyPair(); + } catch (GeneralSecurityException e) { + throw new CoseException("Error while generating key pair: ", e); + } + return withDParameter(keyPair.getPrivateKey()) + .withXCoordinate(keyPair.getPublicKey()); } } diff --git a/test/com/google/cose/Ec2KeyAgreementKeyTest.java b/test/com/google/cose/Ec2KeyAgreementKeyTest.java index d9b1d97..df750b9 100644 --- a/test/com/google/cose/Ec2KeyAgreementKeyTest.java +++ b/test/com/google/cose/Ec2KeyAgreementKeyTest.java @@ -24,6 +24,7 @@ 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.Headers; import org.junit.Assert; import org.junit.Test; @@ -31,7 +32,7 @@ import org.junit.runners.JUnit4; /** - * Test class for testing {@link Ec2SigningKey}. Key values used in test cases are referenced from + * Test class for testing {@link Ec2KeyAgreement}. Key values used in test cases are referenced from * https://datatracker.ietf.org/doc/html/rfc8152#appendix-C */ @RunWith(JUnit4.class) @@ -59,7 +60,7 @@ public void testRoundTrip() throws CborException, CoseException { final Ec2KeyAgreementKey key = new Ec2KeyAgreementKey(map); byte[] a = key.serialize(); - final Ec2SigningKey newKey = Ec2SigningKey.parse(a); + final Ec2KeyAgreementKey newKey = Ec2KeyAgreementKey.parse(a); Assert.assertEquals(Headers.KEY_TYPE_EC2, newKey.getKeyType()); Assert.assertArrayEquals(keyId, newKey.getKeyId()); @@ -100,6 +101,31 @@ public void testBuilder() throws CborException, CoseException { Assert.assertEquals(cborString, TestUtilities.bytesToHexString(key.serialize())); } + @Test + public void testBuilderPkcs8EncodedPrivateKey() throws CborException, CoseException { + String cborString = "A30102200123582100DE7B7261710775352BF3C0669FA54229D9B2998EE9265645A3AF9F2" + + "FEFC93968"; + byte[] d = TestUtilities.hexStringToByteArray("3041020100301306072A8648CE3D020106082A8648CE3D0" + + "30107042730250201010420DE7B7261710775352BF3C0669FA54229D9B2998EE9265645A3AF9F2FEFC93968"); + Ec2KeyAgreementKey keyAgreementKey = Ec2KeyAgreementKey.builder() + .withCurve(Headers.CURVE_EC2_P256) + .withPrivateKeyRepresentation().withPkcs8EncodedBytes(d) + .build(); + Assert.assertEquals(cborString, TestUtilities.bytesToHexString(keyAgreementKey.serialize())); + Assert.assertNotNull(keyAgreementKey.getPublicKey()); + } + + @Test + public void testBuilderDParamPrivateKey() throws CborException, CoseException { + String cborString = "A3010220012358205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA444B6243" + + "43167FE"; + Ec2KeyAgreementKey keyAgreementKey = Ec2KeyAgreementKey.builder() + .withCurve(Headers.CURVE_EC2_P256) + .withPrivateKeyRepresentation().withDParameter(D_BYTES) + .build(); + Assert.assertEquals(cborString, TestUtilities.bytesToHexString(keyAgreementKey.serialize())); + } + @Test public void testBuilderFailureWrongOperation() throws CoseException { Ec2KeyAgreementKey.Builder builder = Ec2KeyAgreementKey.builder() @@ -112,6 +138,22 @@ public void testBuilderFailureWrongOperation() throws CoseException { () -> builder.withOperations(Headers.KEY_OPERATIONS_DECRYPT, Headers.KEY_OPERATIONS_SIGN)); } + @Test + public void testGetPublic() throws CborException, CoseException { + Ec2KeyAgreementKey.Builder builder = Ec2KeyAgreementKey.builder() + .withCurve(Headers.CURVE_EC2_P256) + .withXCoordinate(X_BYTES) + .withYCoordinate(Y_BYTES); + Ec2KeyAgreementKey publicKey = builder.build(); + Ec2KeyAgreementKey keyPairPublic = builder + .withPrivateKeyRepresentation().withDParameter(D_BYTES) + .build() + .getPublic(); + + Assert.assertSame(publicKey.getPublic(), publicKey); + Assert.assertArrayEquals(keyPairPublic.serialize(), publicKey.serialize()); + } + @Test public void testOkpKeyParsingInEc2KeyAgreementKey() throws CborException { String cborString = "A401012006215820D75A980182B10AB7D54BFED3C964073A0EE172F3DAA62325AF021A68F" @@ -131,10 +173,27 @@ public void testEc2KeyParsingWithIncorrectCurve() throws CborException { } @Test - public void testKeyParsingWithNullDParameterBytes() throws CborException, CoseException { - String cborString = "A5010220012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA444B6243" - + "43167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC672340"; - // Even if D parameter is null, we don't care since it will not be used for key agreement. - Ec2KeyAgreementKey.parse(TestUtilities.hexStringToByteArray(cborString)); + public void testP256Ec2GeneratedKey() throws CborException, CoseException { + Ec2KeyAgreementKey p256Key = + Ec2KeyAgreementKey.generateKey(Algorithm.ECDH_ES_HKDF_256, Headers.CURVE_EC2_P256); + } + + @Test + public void testP384Ec2GeneratedKey() throws CborException, CoseException { + Ec2KeyAgreementKey p384Key = + Ec2KeyAgreementKey.generateKey(Algorithm.ECDH_ES_HKDF_256, Headers.CURVE_EC2_P384); + } + + @Test + public void testP521Ec2GeneratedKey() throws CborException, CoseException { + Ec2KeyAgreementKey p521Key = + Ec2KeyAgreementKey.generateKey(Algorithm.ECDH_ES_HKDF_256, Headers.CURVE_EC2_P521); + } + + @Test + public void testEc2GeneratedKey_invalidInput() throws CborException { + assertThrows( + CoseException.class, + () -> Ec2KeyAgreementKey.generateKey(Algorithm.MAC_ALGORITHM_HMAC_SHA_256_256)); } } diff --git a/test/com/google/cose/Ec2SigningKeyTest.java b/test/com/google/cose/Ec2SigningKeyTest.java index 52a36eb..619448e 100644 --- a/test/com/google/cose/Ec2SigningKeyTest.java +++ b/test/com/google/cose/Ec2SigningKeyTest.java @@ -192,6 +192,22 @@ public void testBuilderFailureWrongOperation() throws CoseException { () -> builder.withOperations(Headers.KEY_OPERATIONS_DECRYPT, Headers.KEY_OPERATIONS_SIGN)); } + @Test + public void testGetPublic() throws CborException, CoseException { + Ec2KeyAgreementKey.Builder builder = Ec2KeyAgreementKey.builder() + .withCurve(Headers.CURVE_EC2_P256) + .withXCoordinate(X_BYTES) + .withYCoordinate(Y_BYTES); + Ec2KeyAgreementKey publicKey = builder.build(); + Ec2KeyAgreementKey keyPairPublic = builder + .withPrivateKeyRepresentation().withDParameter(D_BYTES) + .build() + .getPublic(); + + Assert.assertSame(publicKey.getPublic(), publicKey); + Assert.assertArrayEquals(keyPairPublic.serialize(), publicKey.serialize()); + } + @Test public void testOkpKeyParsingInEc2SigningKey() throws CborException { String cborString = "A401012006215820D75A980182B10AB7D54BFED3C964073A0EE172F3DAA62325AF021A68F" diff --git a/test/com/google/cose/OkpKeyAgreementKeyTest.java b/test/com/google/cose/OkpKeyAgreementKeyTest.java new file mode 100644 index 0000000..65edaed --- /dev/null +++ b/test/com/google/cose/OkpKeyAgreementKeyTest.java @@ -0,0 +1,200 @@ +/* + * Copyright 2023 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 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.Headers; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test class for testing {@link OkpKeyAgreementKey}. */ +@RunWith(JUnit4.class) +public class OkpKeyAgreementKeyTest { + static final String X_VAL = "8520F0098930A754748B7DDCB43EF75A0DBF3A0D26381AF4EBA4A98EAA9B4E6A"; + private static final byte[] X_BYTES = TestUtilities.hexStringToByteArray(X_VAL); + static final String D_VAL = "77076D0A7318A57D3C16C17251B26645DF4C2F87EBC0992AB177FBA51DB92C2A"; + private static final byte[] D_BYTES = TestUtilities.hexStringToByteArray(D_VAL); + + @Test + public void testRoundTrip() throws CborException, CoseException { + final byte[] keyId = "29".getBytes(StandardCharsets.UTF_8); + final Map map = new Map(); + map.put(new UnsignedInteger(Headers.KEY_PARAMETER_KEY_TYPE), + new UnsignedInteger(Headers.KEY_TYPE_OKP)); + map.put(new UnsignedInteger(Headers.KEY_PARAMETER_KEY_ID), new ByteString(keyId)); + map.put(new NegativeInteger(Headers.KEY_PARAMETER_CURVE), + new UnsignedInteger(Headers.CURVE_OKP_X25519)); + map.put(new NegativeInteger(Headers.KEY_PARAMETER_X), new ByteString(X_BYTES)); + map.put(new NegativeInteger(Headers.KEY_PARAMETER_D), new ByteString(D_BYTES)); + + final OkpKeyAgreementKey keyWithConstructor = new OkpKeyAgreementKey(map); + OkpKeyAgreementKey keyWithBuilder = OkpKeyAgreementKey.builder() + .withDParameter(D_BYTES) + .withXCoordinate(X_BYTES) + .withKeyId(keyId) + .build(); + Assert.assertArrayEquals(keyWithConstructor.serialize(), keyWithBuilder.serialize()); + + final OkpKeyAgreementKey rebuiltKey = OkpKeyAgreementKey.parse(keyWithConstructor.serialize()); + + Assert.assertEquals(Headers.KEY_TYPE_OKP, rebuiltKey.getKeyType()); + Assert.assertEquals(new UnsignedInteger(Headers.CURVE_OKP_X25519), + rebuiltKey.labels.get(Headers.KEY_PARAMETER_CURVE)); + Assert.assertEquals(new ByteString(X_BYTES), rebuiltKey.labels.get(Headers.KEY_PARAMETER_X)); + Assert.assertEquals(new ByteString(D_BYTES), rebuiltKey.labels.get(Headers.KEY_PARAMETER_D)); + + Assert.assertArrayEquals(keyId, rebuiltKey.getKeyId()); + Assert.assertArrayEquals(keyWithConstructor.serialize(), rebuiltKey.serialize()); + } + + @Test + public void testConversion() throws CborException, CoseException { + final String cborString = "A4010120042158208520F0098930A754748B7DDCB43EF75A0DBF3A0D26381AF4EBA" + + "4A98EAA9B4E6A23582077076D0A7318A57D3C16C17251B26645DF4C2F87EBC0992AB177FBA51DB92C2A"; + final OkpKeyAgreementKey sKey = OkpKeyAgreementKey.parse(TestUtilities.hexStringToByteArray(cborString)); + Assert.assertEquals(Headers.KEY_TYPE_OKP, sKey.getKeyType()); + Assert.assertEquals(new UnsignedInteger(Headers.CURVE_OKP_X25519), + sKey.getLabels().get(Headers.KEY_PARAMETER_CURVE)); + Assert.assertEquals(new ByteString(X_BYTES), sKey.getLabels().get(Headers.KEY_PARAMETER_X)); + Assert.assertEquals(new ByteString(D_BYTES), sKey.getLabels().get(Headers.KEY_PARAMETER_D)); + } + + @Test + public void testBuilder() throws CborException, CoseException { + final String cborString = "A3010120042158208520F0098930A754748B7DDCB43EF75A0DBF3A0D26381AF4EBA" + + "4A98EAA9B4E6A"; + OkpKeyAgreementKey key = OkpKeyAgreementKey.builder() + .withXCoordinate(X_BYTES) + .build(); + Assert.assertEquals(cborString, TestUtilities.bytesToHexString(key.serialize())); + } + + @Test + public void testBuilderPrivateKey() throws CborException, CoseException { + final String cborString = "A4010120042158208520F0098930A754748B7DDCB43EF75A0DBF3A0D26381AF4EBA" + + "4A98EAA9B4E6A23582077076D0A7318A57D3C16C17251B26645DF4C2F87EBC0992AB177FBA51DB92C2A"; + OkpKeyAgreementKey key = OkpKeyAgreementKey.builder() + .withXCoordinate(X_BYTES) + .withDParameter(D_BYTES) + .build(); + Assert.assertEquals(cborString, TestUtilities.bytesToHexString(key.serialize())); + } + + @Test + public void testEmptyBuilderFailure() throws CborException { + OkpKeyAgreementKey.Builder builder = OkpKeyAgreementKey.builder(); + assertThrows( + "Expected failure when building on empty builder.", + CoseException.class, + () -> builder.build()); + } + + @Test + public void testBuilderPassOnMissingX() throws CborException, CoseException { + OkpKeyAgreementKey keyAgreementKey = OkpKeyAgreementKey.builder() + .withDParameter(D_BYTES) + .build(); + Assert.assertArrayEquals(keyAgreementKey.getPublicKeyBytes(), X_BYTES); + Assert.assertNotNull(keyAgreementKey.getPublicKey()); + } + + @Test + public void testBuilderPassOnMissingD() throws CborException, CoseException { + OkpKeyAgreementKey.builder() + .withXCoordinate(X_BYTES) + .build(); + } + + @Test + public void testBuilderFailureOnWrongOperation() throws CborException, CoseException { + OkpKeyAgreementKey.Builder builder = OkpKeyAgreementKey.builder() + .withXCoordinate(X_BYTES) + .withDParameter(D_BYTES); + assertThrows( + "Expected builder to fail with wrong operations.", + CoseException.class, + () -> builder.withOperations(Headers.KEY_OPERATIONS_DECRYPT, Headers.KEY_OPERATIONS_SIGN)); + builder.build(); + } + + @Test + public void testGetPublic() throws CborException, CoseException { + OkpKeyAgreementKey.Builder builder = OkpKeyAgreementKey.builder() + .withXCoordinate(X_BYTES); + OkpKeyAgreementKey publicKey = builder.build(); + OkpKeyAgreementKey keyPairPublic = builder + .withDParameter(D_BYTES) + .build() + .getPublic(); + + Assert.assertSame(publicKey.getPublic(), publicKey); + Assert.assertArrayEquals(keyPairPublic.serialize(), publicKey.serialize()); + } + + @Test + public void testEc2KeyParsingInOkpKeyAgreementKey() throws CborException { + String cborString = "A4010220012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44" + + "4B624343167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC67"; + assertThrows( + CoseException.class, + () -> OkpKeyAgreementKey.parse(TestUtilities.hexStringToByteArray(cborString))); + } + + @Test + public void testOkpKeyParsingWithIncorrectCurve() throws CborException { + String cborString = "A4010220012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44" + + "4B624343167FE225820B16E8CF858DDC7690407BA61D4C338237A8CFCF3DE6AA672FC60A557AA32FC67"; + assertThrows( + CoseException.class, + () -> OkpKeyAgreementKey.parse(TestUtilities.hexStringToByteArray(cborString))); + } + + @Test + public void testEmptyPrivateKeyBytes() throws CborException { + String cborString = "A401012004215820D75A980182B10AB7D54BFED3C964073A0EE172F3DAA62325AF021A68F" + + "707511A2340"; + assertThrows( + CoseException.class, + () -> OkpKeyAgreementKey.parse(TestUtilities.hexStringToByteArray(cborString))); + } + + @Test + public void testEmptyPublicKeyBytes() throws CborException { + String cborString = "A40101200421402358209D61B19DEFFD5A60BA844AF492EC2CC44449C5697B326919703BA" + + "C031CAE7F60"; + assertThrows( + CoseException.class, + () -> OkpKeyAgreementKey.parse(TestUtilities.hexStringToByteArray(cborString))); + } + + @Test + public void testOkpGeneratedKey_verificationWithSignature() throws CborException, CoseException { + OkpKeyAgreementKey okpKey = + OkpKeyAgreementKey.generateKey(Algorithm.ECDH_ES_HKDF_256, Headers.CURVE_OKP_X25519); + } +} diff --git a/test/com/google/cose/OkpSigningKeyTest.java b/test/com/google/cose/OkpSigningKeyTest.java index c0e88ee..55232ff 100644 --- a/test/com/google/cose/OkpSigningKeyTest.java +++ b/test/com/google/cose/OkpSigningKeyTest.java @@ -119,9 +119,11 @@ public void testEmptyBuilderFailure() throws CborException { @Test public void testBuilderPassOnMissingX() throws CborException, CoseException { - OkpSigningKey.builder() + OkpSigningKey signingKey = OkpSigningKey.builder() .withDParameter(D_BYTES) .build(); + Assert.assertArrayEquals(signingKey.getPublicKeyBytes(), X_BYTES); + Assert.assertNotNull(signingKey.getPublicKey()); } @Test @@ -143,6 +145,20 @@ public void testBuilderFailureOnWrongOperation() throws CborException, CoseExcep builder.build(); } + @Test + public void testGetPublic() throws CborException, CoseException { + OkpSigningKey.Builder builder = OkpSigningKey.builder() + .withXCoordinate(X_BYTES); + OkpSigningKey publicKey = builder.build(); + OkpSigningKey keyPairPublic = builder + .withDParameter(D_BYTES) + .build() + .getPublic(); + + Assert.assertSame(publicKey.getPublic(), publicKey); + Assert.assertArrayEquals(keyPairPublic.serialize(), publicKey.serialize()); + } + @Test public void testEc2KeyParsingInOkpSigningKey() throws CborException { String cborString = "A4010220012158205A88D182BCE5F42EFA59943F33359D2E8A968FF289D93E5FA44"