diff --git a/pom.xml b/pom.xml
index 7ccd85d1..4c1fc5cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,6 +157,11 @@
bcprov-jdk18on
${bouncycastle.version}
+
+ org.bouncycastle
+ bctls-jdk18on
+ ${bouncycastle.version}
+
diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java b/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java
index 36110853..18191de3 100644
--- a/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java
+++ b/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java
@@ -1,39 +1,31 @@
package io.opentdf.platform.sdk;
-import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
-import org.bouncycastle.cert.X509CertificateHolder;
-import org.bouncycastle.crypto.digests.SHA256Digest;
-import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
-import org.bouncycastle.crypto.params.HKDFParameters;
-import org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi;
-import org.bouncycastle.jce.ECNamedCurveTable;
-import org.bouncycastle.jce.interfaces.ECPrivateKey;
-import org.bouncycastle.jce.interfaces.ECPublicKey;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
-import org.bouncycastle.math.ec.ECPoint;
-import org.bouncycastle.openssl.PEMException;
-import org.bouncycastle.openssl.PEMParser;
-import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
-import org.bouncycastle.util.io.pem.*;
-import org.bouncycastle.util.io.pem.PemReader;
-import org.bouncycastle.jce.spec.ECPublicKeySpec;
-
import javax.crypto.KeyAgreement;
-import java.io.*;
-import java.security.*;
-import java.security.spec.*;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+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.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
import java.util.Objects;
-// https://www.bouncycastle.org/latest_releases.html
public class ECKeyPair {
private static final int SHA256_BYTES = 32;
-
- static {
- Security.addProvider(new BouncyCastleProvider());
- }
+ private static final String EC_ALGORITHM = "EC";
private final ECCurve curve;
@@ -42,37 +34,28 @@ public enum ECAlgorithm {
ECDSA
}
- private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
-
private KeyPair keyPair;
public ECKeyPair() {
- this(ECCurve.SECP256R1, ECAlgorithm.ECDH);
+ this(ECCurve.SECP256R1);
}
- public ECKeyPair(ECCurve curve, ECAlgorithm algorithm) {
+ public ECKeyPair(ECCurve curve) {
this.curve = Objects.requireNonNull(curve);
- KeyPairGenerator generator;
-
try {
- // Should this just use the algorithm vs use ECDH only for ECDH and ECDSA for
- // everything else.
- if (algorithm == ECAlgorithm.ECDH) {
- generator = KeyPairGeneratorSpi.getInstance(ECAlgorithm.ECDH.name(), BOUNCY_CASTLE_PROVIDER);
- } else {
- generator = KeyPairGeneratorSpi.getInstance(ECAlgorithm.ECDSA.name(), BOUNCY_CASTLE_PROVIDER);
- }
- } catch (NoSuchAlgorithmException e) {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance(EC_ALGORITHM);
+ generator.initialize(new ECGenParameterSpec(this.curve.getCurveName()));
+ this.keyPair = generator.generateKeyPair();
+ } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
+ }
- ECGenParameterSpec spec = new ECGenParameterSpec(this.curve.getCurveName());
- try {
- generator.initialize(spec);
- } catch (InvalidAlgorithmParameterException e) {
- throw new RuntimeException(e);
- }
- this.keyPair = generator.generateKeyPair();
+ static ECPublicKey publicKeyFromPem(String pem) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ String pemData = pem.replaceAll("-----(BEGIN|END) [A-Z ]+-----", "").replaceAll("\\s", "");
+ byte[] der = Base64.getDecoder().decode(pemData);
+ KeyFactory keyFactory = KeyFactory.getInstance("EC");
+ return (ECPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(der));
}
public ECPublicKey getPublicKey() {
@@ -88,33 +71,11 @@ ECCurve getCurve() {
}
public String publicKeyInPEMFormat() {
- StringWriter writer = new StringWriter();
- PemWriter pemWriter = new PemWriter(writer);
-
- try {
- pemWriter.writeObject(new PemObject("PUBLIC KEY", this.keyPair.getPublic().getEncoded()));
- pemWriter.flush();
- pemWriter.close();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- return writer.toString();
+ return toPem("PUBLIC KEY", this.keyPair.getPublic().getEncoded());
}
public String privateKeyInPEMFormat() {
- StringWriter writer = new StringWriter();
- PemWriter pemWriter = new PemWriter(writer);
-
- try {
- pemWriter.writeObject(new PemObject("PRIVATE KEY", this.keyPair.getPrivate().getEncoded()));
- pemWriter.flush();
- pemWriter.close();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- return writer.toString();
+ return toPem("PRIVATE KEY", this.keyPair.getPrivate().getEncoded());
}
public int keySize() {
@@ -122,163 +83,98 @@ public int keySize() {
}
public byte[] compressECPublickey() {
- return ((ECPublicKey) this.keyPair.getPublic()).getQ().getEncoded(true);
- }
-
- public static String getPEMPublicKeyFromX509Cert(String pemInX509Format) {
- try {
- PEMParser parser = new PEMParser(new StringReader(pemInX509Format));
- X509CertificateHolder x509CertificateHolder = (X509CertificateHolder) parser.readObject();
- parser.close();
- SubjectPublicKeyInfo publicKeyInfo = x509CertificateHolder.getSubjectPublicKeyInfo();
- JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
- ECPublicKey publicKey = null;
- try {
- publicKey = (ECPublicKey) converter.getPublicKey(publicKeyInfo);
- } catch (PEMException e) {
- throw new RuntimeException(e);
- }
-
- // EC public key to pem formated.
- StringWriter writer = new StringWriter();
- PemWriter pemWriter = new PemWriter(writer);
-
- pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded()));
- pemWriter.flush();
- pemWriter.close();
- return writer.toString();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static byte[] compressECPublickey(String pemECPubKey) {
- try {
- KeyFactory ecKeyFac = KeyFactory.getInstance("EC", "BC");
- PemReader pemReader = new PemReader(new StringReader(pemECPubKey));
- PemObject pemObject = pemReader.readPemObject();
- PublicKey pubKey = ecKeyFac.generatePublic(new X509EncodedKeySpec(pemObject.getContent()));
- return ((ECPublicKey) pubKey).getQ().getEncoded(true);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- } catch (IOException e) {
- throw new RuntimeException(e);
- } catch (InvalidKeySpecException e) {
- throw new RuntimeException(e);
- } catch (NoSuchProviderException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static String publicKeyFromECPoint(byte[] ecPoint, String curveName) {
- try {
- // Create EC Public key
- ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec(curveName);
- ECPoint point = ecSpec.getCurve().decodePoint(ecPoint);
- ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(point, ecSpec);
- KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", "BC");
- PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
-
- // EC Public keu to pem format.
- StringWriter writer = new StringWriter();
- PemWriter pemWriter = new PemWriter(writer);
- pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded()));
- pemWriter.flush();
- pemWriter.close();
- return writer.toString();
- } catch (InvalidKeySpecException e) {
- throw new RuntimeException(e);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- } catch (NoSuchProviderException e) {
- throw new RuntimeException(e);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static ECPublicKey publicKeyFromPem(String pemEncoding) {
- try {
- PEMParser parser = new PEMParser(new StringReader(pemEncoding));
- SubjectPublicKeyInfo publicKeyInfo = (SubjectPublicKeyInfo) parser.readObject();
- parser.close();
-
- JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
- return (ECPublicKey) converter.getPublicKey(publicKeyInfo);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static ECPrivateKey privateKeyFromPem(String pemEncoding) {
- try {
- PEMParser parser = new PEMParser(new StringReader(pemEncoding));
- PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) parser.readObject();
- parser.close();
-
- JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
- return (ECPrivateKey) converter.getPrivateKey(privateKeyInfo);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ return encodeCompressedPoint((ECPublicKey) this.keyPair.getPublic());
}
public static byte[] computeECDHKey(ECPublicKey publicKey, ECPrivateKey privateKey) {
try {
- KeyAgreement aKeyAgree = KeyAgreement.getInstance("ECDH", "BC");
+ KeyAgreement aKeyAgree = KeyAgreement.getInstance("ECDH");
aKeyAgree.init(privateKey);
aKeyAgree.doPhase(publicKey, true);
return aKeyAgree.generateSecret();
- } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException e) {
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
+ private static final String HMAC_SHA_256 = "HmacSHA256";
/**
* Returns a HKDF key derived from the provided salt and secret
* that is 32 bytes (256 bits) long.
*/
public static byte[] calculateHKDF(byte[] salt, byte[] secret) {
- byte[] key = new byte[SHA256_BYTES];
- HKDFParameters params = new HKDFParameters(secret, salt, null);
-
- HKDFBytesGenerator hkdf = new HKDFBytesGenerator(SHA256Digest.newInstance());
- hkdf.init(params);
- hkdf.generateBytes(key, 0, key.length);
- return key;
+ try {
+ // RFC 5869: if salt is absent, substitute a zero-filled buffer of Hash output size.
+ byte[] effectiveSalt = (salt == null || salt.length == 0) ? new byte[SHA256_BYTES] : salt;
+ Mac hmac = Mac.getInstance(HMAC_SHA_256);
+ hmac.init(new SecretKeySpec(effectiveSalt, HMAC_SHA_256));
+ byte[] prk = hmac.doFinal(secret);
+
+ // HKDF-Expand with empty info and L = 32 (a single HMAC block).
+ hmac.init(new SecretKeySpec(prk, HMAC_SHA_256));
+ hmac.update((byte) 0x01);
+ return hmac.doFinal();
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new RuntimeException(e);
+ }
}
- public static byte[] computeECDSASig(byte[] digest, ECPrivateKey privateKey) {
+ public static byte[] computeECDSASig(byte[] data, ECPrivateKey privateKey) {
try {
- Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", "BC");
+ Signature ecdsaSign = Signature.getInstance("SHA256withECDSA");
ecdsaSign.initSign(privateKey);
- ecdsaSign.update(digest);
+ ecdsaSign.update(data);
return ecdsaSign.sign();
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- } catch (NoSuchProviderException e) {
- throw new RuntimeException(e);
- } catch (InvalidKeyException e) {
- throw new RuntimeException(e);
- } catch (SignatureException e) {
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
}
}
public static Boolean verifyECDSAig(byte[] digest, byte[] signature, ECPublicKey publicKey) {
try {
- Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", "BC");
+ Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA");
ecdsaVerify.initVerify(publicKey);
ecdsaVerify.update(digest);
return ecdsaVerify.verify(signature);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- } catch (NoSuchProviderException e) {
- throw new RuntimeException(e);
- } catch (InvalidKeyException e) {
- throw new RuntimeException(e);
- } catch (SignatureException e) {
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
}
}
-}
\ No newline at end of file
+
+ private static String toPem(String type, byte[] der) {
+ String b64 = Base64.getEncoder().encodeToString(der);
+ StringBuilder sb = new StringBuilder();
+ sb.append("-----BEGIN ").append(type).append("-----\n");
+ for (int i = 0; i < b64.length(); i += 64) {
+ sb.append(b64, i, Math.min(i + 64, b64.length())).append('\n');
+ }
+ sb.append("-----END ").append(type).append("-----\n");
+ return sb.toString();
+ }
+
+ private static byte[] encodeCompressedPoint(ECPublicKey publicKey) {
+ ECPoint w = publicKey.getW();
+ ECParameterSpec params = publicKey.getParams();
+ int size = (params.getCurve().getField().getFieldSize() + 7) / 8;
+ byte[] x = toFixedLength(w.getAffineX(), size);
+ byte[] result = new byte[size + 1];
+ result[0] = (byte) (w.getAffineY().testBit(0) ? 0x03 : 0x02);
+ System.arraycopy(x, 0, result, 1, size);
+ return result;
+ }
+
+ private static byte[] toFixedLength(BigInteger value, int length) {
+ byte[] bytes = value.toByteArray();
+ if (bytes.length == length) {
+ return bytes;
+ }
+ byte[] result = new byte[length];
+ if (bytes.length > length) {
+ // BigInteger.toByteArray() may prepend a zero sign byte; strip it.
+ System.arraycopy(bytes, bytes.length - length, result, 0, length);
+ } else {
+ System.arraycopy(bytes, 0, result, length - bytes.length, bytes.length);
+ }
+ return result;
+ }
+}
diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java
index 91ada8b0..dec68a33 100644
--- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java
+++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java
@@ -17,13 +17,15 @@
import io.opentdf.platform.kas.PublicKeyResponse;
import io.opentdf.platform.kas.RewrapRequest;
import io.opentdf.platform.kas.RewrapResponse;
-import io.opentdf.platform.sdk.Config.KASInfo;
import io.opentdf.platform.sdk.SDK.KasBadRequestException;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
@@ -121,7 +123,7 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessi
ECKeyPair ecKeyPair = null;
if (sessionKeyType.isEc()) {
var curve = sessionKeyType.getECCurve();
- ecKeyPair = new ECKeyPair(curve, ECKeyPair.ECAlgorithm.ECDH);
+ ecKeyPair = new ECKeyPair(curve);
clientPublicKey = ecKeyPair.publicKeyInPEMFormat();
} else {
// Initialize the RSA key pair only once and reuse it for future unwrap operations
@@ -176,7 +178,12 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessi
}
var kasEphemeralPublicKey = response.getSessionPublicKey();
- var publicKey = ECKeyPair.publicKeyFromPem(kasEphemeralPublicKey);
+ ECPublicKey publicKey;
+ try {
+ publicKey = ECKeyPair.publicKeyFromPem(kasEphemeralPublicKey);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new SDKException("error decoding KAS session public key", e);
+ }
byte[] symKey = ECKeyPair.computeECDHKey(publicKey, ecKeyPair.getPrivateKey());
var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey);
diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
index 3ee4ba22..c0e28a6f 100644
--- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
+++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
@@ -11,7 +11,6 @@
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
-import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,6 +21,8 @@
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.security.*;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.InvalidKeySpecException;
import java.text.ParseException;
import java.util.*;
@@ -243,9 +244,14 @@ private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KA
private ECKeyWrappedKeyInfo createECWrappedKey(Config.KASInfo kasInfo,
byte[] symKey, KeyType keyType) {
var curveName = keyType.getECCurve();
- var keyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH);
+ var keyPair = new ECKeyPair(curveName);
- ECPublicKey kasPubKey = ECKeyPair.publicKeyFromPem(kasInfo.PublicKey);
+ ECPublicKey kasPubKey;
+ try {
+ kasPubKey = ECKeyPair.publicKeyFromPem(kasInfo.PublicKey);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new SDKException("error decoding KAS EC public key", e);
+ }
byte[] symmetricKey = ECKeyPair.computeECDHKey(kasPubKey, keyPair.getPrivateKey());
var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symmetricKey);
diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/CryptoProviderSetupExtension.java b/sdk/src/test/java/io/opentdf/platform/sdk/CryptoProviderSetupExtension.java
new file mode 100644
index 00000000..79b9c489
--- /dev/null
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/CryptoProviderSetupExtension.java
@@ -0,0 +1,22 @@
+package io.opentdf.platform.sdk;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.security.Provider;
+import java.security.Security;
+import java.util.Arrays;
+
+public class CryptoProviderSetupExtension implements BeforeAllCallback {
+ private BouncyCastleProvider securityProvider;
+
+ @Override
+ public synchronized void beforeAll(ExtensionContext extensionContext) {
+ if (this.securityProvider == null) {
+ Security.insertProviderAt(this.securityProvider = new BouncyCastleProvider(), 1);
+ Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
+ }
+ }
+}
diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ECKeyPairTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ECKeyPairTest.java
index d454b6c6..185965c1 100644
--- a/sdk/src/test/java/io/opentdf/platform/sdk/ECKeyPairTest.java
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/ECKeyPairTest.java
@@ -1,7 +1,7 @@
package io.opentdf.platform.sdk;
-import org.bouncycastle.jce.interfaces.ECPrivateKey;
-import org.bouncycastle.jce.interfaces.ECPublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
import org.junit.jupiter.api.Test;
import java.io.IOException;
@@ -54,17 +54,17 @@ void ecPublicKeyInPemformat() {
String keypairAPubicKey = keyPairA.publicKeyInPEMFormat();
String keypairAPrivateKey = keyPairA.privateKeyInPEMFormat();
- ECPublicKey publicKeyA = ECKeyPair.publicKeyFromPem(keypairAPubicKey);
- ECPrivateKey privateKeyA = ECKeyPair.privateKeyFromPem(keypairAPrivateKey);
+ ECPublicKey publicKeyA = PemTestUtils.publicKeyFromPem(keypairAPubicKey);
+ ECPrivateKey privateKeyA = PemTestUtils.privateKeyFromPem(keypairAPrivateKey);
System.out.println(keypairAPubicKey);
System.out.println(keypairAPrivateKey);
byte[] compressedKey1 = keyPairA.compressECPublickey();
- byte[] compressedKey2 = ECKeyPair.compressECPublickey(keyPairA.publicKeyInPEMFormat());
+ byte[] compressedKey2 = PemTestUtils.compressECPublickey(keyPairA.publicKeyInPEMFormat());
assertArrayEquals(compressedKey1, compressedKey2);
- String publicKey = ECKeyPair.publicKeyFromECPoint(compressedKey1, SECP256R1.getCurveName());
+ String publicKey = PemTestUtils.publicKeyFromECPoint(compressedKey1, SECP256R1.getCurveName());
assertEquals(keyPairA.publicKeyInPEMFormat(), publicKey);
ECKeyPair keyPairB = new ECKeyPair();
@@ -74,8 +74,8 @@ void ecPublicKeyInPemformat() {
System.out.println(keypairBPubicKey);
System.out.println(keypairBPrivateKey);
- ECPublicKey publicKeyB = ECKeyPair.publicKeyFromPem(keypairBPubicKey);
- ECPrivateKey privateKeyB = ECKeyPair.privateKeyFromPem(keypairBPrivateKey);
+ ECPublicKey publicKeyB = PemTestUtils.publicKeyFromPem(keypairBPubicKey);
+ ECPrivateKey privateKeyB = PemTestUtils.privateKeyFromPem(keypairBPrivateKey);
byte[] symmetricKey1 = ECKeyPair.computeECDHKey(publicKeyA, privateKeyB);
byte[] symmetricKey2 = ECKeyPair.computeECDHKey(publicKeyB, privateKeyA);
@@ -94,14 +94,14 @@ void extractPemPubKeyFromX509() throws CertificateException, IOException, NoSuch
"zj0EAwIDSAAwRQIhAItk5SmcWSg06tnOCEqTa6UsChaycX/cmAT8PTDRnaRcAiAl\n" +
"Vr2EvlA2x5mWFE/+nDdxxzljYjLZuSDQMEI/J6u0/Q==\n" +
"-----END CERTIFICATE-----";
- String pubKey = ECKeyPair.getPEMPublicKeyFromX509Cert(x509ECPubKey);
+ String pubKey = PemTestUtils.getPEMPublicKeyFromX509Cert(x509ECPubKey);
System.out.println(pubKey);
- ECPublicKey publicKey = ECKeyPair.publicKeyFromPem(pubKey);
- byte[] compressedKey = publicKey.getQ().getEncoded(true);
+ ECPublicKey publicKey = PemTestUtils.publicKeyFromPem(pubKey);
+ byte[] compressedKey = PemTestUtils.compressECPublickey(pubKey);
System.out.println(Arrays.toString(compressedKey));
- compressedKey = ECKeyPair.compressECPublickey(pubKey);
+ compressedKey = PemTestUtils.compressECPublickey(pubKey);
System.out.println(Arrays.toString(compressedKey));
System.out.println(compressedKey.length);
@@ -112,7 +112,7 @@ void extractPemPubKeyFromX509() throws CertificateException, IOException, NoSuch
System.out.println(keypairPubicKey);
System.out.println(keypairPrivateKey);
- byte[] symmetricKey = ECKeyPair.computeECDHKey(publicKey, ECKeyPair.privateKeyFromPem(keypairPrivateKey));
+ byte[] symmetricKey = ECKeyPair.computeECDHKey(publicKey, PemTestUtils.privateKeyFromPem(keypairPrivateKey));
System.out.println(Arrays.toString(symmetricKey));
byte[] key = ECKeyPair.calculateHKDF(ECKeys.salt.getBytes(StandardCharsets.UTF_8), symmetricKey);
@@ -124,8 +124,8 @@ void extractPemPubKeyFromX509() throws CertificateException, IOException, NoSuch
@Test
void createSymmetricKeysWithOtherCurves() {
- ECKeyPair pubPair = new ECKeyPair(ECCurve.SECP384R1, ECKeyPair.ECAlgorithm.ECDH);
- ECKeyPair keyPair = new ECKeyPair(ECCurve.SECP384R1, ECKeyPair.ECAlgorithm.ECDH);
+ ECKeyPair pubPair = new ECKeyPair(ECCurve.SECP384R1);
+ ECKeyPair keyPair = new ECKeyPair(ECCurve.SECP384R1);
byte[] sharedSecret = ECKeyPair.computeECDHKey(pubPair.getPublicKey(), keyPair.getPrivateKey());
byte[] encryptionKey = ECKeyPair.calculateHKDF(ECKeys.salt.getBytes(StandardCharsets.UTF_8), sharedSecret);
@@ -138,11 +138,11 @@ void testECDH() {
String expectedKey = "3KGgsptHbTsbxJtql6sHUcx255KcUhxdeJWKjmPMlcc=";
// SDK side
- ECPublicKey kasPubKey = ECKeyPair.publicKeyFromPem(ECKeys.kasPublicKey);
- ECPrivateKey kasPriKey = ECKeyPair.privateKeyFromPem(ECKeys.kasPrivateKey);
+ ECPublicKey kasPubKey = PemTestUtils.publicKeyFromPem(ECKeys.kasPublicKey);
+ ECPrivateKey kasPriKey = PemTestUtils.privateKeyFromPem(ECKeys.kasPrivateKey);
- ECPublicKey sdkPubKey = ECKeyPair.publicKeyFromPem(ECKeys.sdkPublicKey);
- ECPrivateKey sdkPriKey = ECKeyPair.privateKeyFromPem(ECKeys.sdkPrivateKey);
+ ECPublicKey sdkPubKey = PemTestUtils.publicKeyFromPem(ECKeys.sdkPublicKey);
+ ECPrivateKey sdkPriKey = PemTestUtils.privateKeyFromPem(ECKeys.sdkPrivateKey);
byte[] symmetricKey = ECKeyPair.computeECDHKey(kasPubKey, sdkPriKey);
byte[] key = ECKeyPair.calculateHKDF(ECKeys.salt.getBytes(StandardCharsets.UTF_8), symmetricKey);
@@ -155,11 +155,11 @@ void testECDH() {
encodedKey = Base64.getEncoder().encodeToString(key);
assertEquals(encodedKey, expectedKey);
- byte[] ecPoint = ECKeyPair.compressECPublickey(ECKeys.sdkPublicKey);
+ byte[] ecPoint = PemTestUtils.compressECPublickey(ECKeys.sdkPublicKey);
String encodeECPoint = Base64.getEncoder().encodeToString(ecPoint);
assertEquals(encodeECPoint, "Al3vx59pBnP8tRxuUFw18aK9ym6rFrxZRhpVQytUQ+Kg");
- String publicKey = ECKeyPair.publicKeyFromECPoint(ecPoint,
+ String publicKey = PemTestUtils.publicKeyFromECPoint(ecPoint,
SECP256R1.name());
assertArrayEquals(ECKeys.sdkPublicKey.toCharArray(), publicKey.toCharArray());
}
@@ -169,7 +169,10 @@ void testECDSA() {
String plainText = "Virtru!";
for (var curve: ECCurve.values()) {
- ECKeyPair keyPair = new ECKeyPair(curve, ECKeyPair.ECAlgorithm.ECDSA);
+ if (!curve.isSupported()) {
+ continue;
+ }
+ ECKeyPair keyPair = new ECKeyPair(curve);
byte[] signature = ECKeyPair.computeECDSASig(plainText.getBytes(), keyPair.getPrivateKey());
boolean verify = ECKeyPair.verifyECDSAig(plainText.getBytes(), signature, keyPair.getPublicKey());
assertEquals(verify, true);
diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/PemTestUtils.java b/sdk/src/test/java/io/opentdf/platform/sdk/PemTestUtils.java
new file mode 100644
index 00000000..4f615b90
--- /dev/null
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/PemTestUtils.java
@@ -0,0 +1,132 @@
+package io.opentdf.platform.sdk;
+
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
+import org.bouncycastle.jce.spec.ECPublicKeySpec;
+import org.bouncycastle.math.ec.ECPoint;
+import org.bouncycastle.openssl.PEMException;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
+import org.bouncycastle.util.io.pem.PemWriter;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Objects;
+
+/**
+ * BouncyCastle-flavored PEM/X.509 helpers used only by tests. Kept here so the SDK
+ * production classpath does not have a BouncyCastle dependency. The implementations
+ * use BouncyCastle internally but expose standard JCA key interfaces, so callers can
+ * pass results directly to JCA-based APIs.
+ */
+final class PemTestUtils {
+ private static final JcaPEMKeyConverter converter;
+
+ static {
+ var provider = Objects.requireNonNull(
+ Security.getProvider("BC"),
+ "BC provider must be registered");
+ converter = new JcaPEMKeyConverter().setProvider(provider);
+ }
+
+ private PemTestUtils() {
+ }
+
+ static ECPublicKey publicKeyFromPem(String pemEncoding) {
+ try {
+ PEMParser parser = new PEMParser(new StringReader(pemEncoding));
+ SubjectPublicKeyInfo publicKeyInfo = (SubjectPublicKeyInfo) parser.readObject();
+ parser.close();
+
+ return (ECPublicKey) converter.getPublicKey(publicKeyInfo);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static ECPrivateKey privateKeyFromPem(String pemEncoding) {
+ try {
+ PEMParser parser = new PEMParser(new StringReader(pemEncoding));
+ PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) parser.readObject();
+ parser.close();
+
+ return (ECPrivateKey) converter.getPrivateKey(privateKeyInfo);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static String getPEMPublicKeyFromX509Cert(String pemInX509Format) {
+ try {
+ ECPublicKey publicKey = getEcPublicKey(pemInX509Format);
+
+ StringWriter writer = new StringWriter();
+ PemWriter pemWriter = new PemWriter(writer);
+ pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded()));
+ pemWriter.flush();
+ pemWriter.close();
+ return writer.toString();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ECPublicKey getEcPublicKey(String pemInX509Format) throws IOException {
+ PEMParser parser = new PEMParser(new StringReader(pemInX509Format));
+ X509CertificateHolder x509CertificateHolder = (X509CertificateHolder) parser.readObject();
+ parser.close();
+ SubjectPublicKeyInfo publicKeyInfo = x509CertificateHolder.getSubjectPublicKeyInfo();
+ ECPublicKey publicKey;
+ try {
+ publicKey = (ECPublicKey) converter.getPublicKey(publicKeyInfo);
+ } catch (PEMException e) {
+ throw new RuntimeException(e);
+ }
+ return publicKey;
+ }
+
+ static byte[] compressECPublickey(String pemECPubKey) {
+ try {
+ KeyFactory ecKeyFac = KeyFactory.getInstance("EC");
+ PemReader pemReader = new PemReader(new StringReader(pemECPubKey));
+ PemObject pemObject = pemReader.readPemObject();
+ PublicKey pubKey = ecKeyFac.generatePublic(new X509EncodedKeySpec(pemObject.getContent()));
+ return ((org.bouncycastle.jce.interfaces.ECPublicKey) pubKey).getQ().getEncoded(true);
+ } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static String publicKeyFromECPoint(byte[] ecPoint, String curveName) {
+ try {
+ ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec(curveName);
+ ECPoint point = ecSpec.getCurve().decodePoint(ecPoint);
+ ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(point, ecSpec);
+ KeyFactory keyFactory = KeyFactory.getInstance("ECDSA");
+ PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
+
+ StringWriter writer = new StringWriter();
+ PemWriter pemWriter = new PemWriter(writer);
+ pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded()));
+ pemWriter.flush();
+ pemWriter.close();
+ return writer.toString();
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java
index cebc9928..49f5affc 100644
--- a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java
@@ -161,11 +161,13 @@ void sdkServicesSetup(boolean useSSLPlatform, boolean useSSLIDP) throws Exceptio
HeldCertificate rootCertificate = new HeldCertificate.Builder()
.certificateAuthority(0)
+ .rsa2048()
.build();
String localhost = InetAddress.getByName("localhost").getCanonicalHostName();
HeldCertificate serverCertificate = new HeldCertificate.Builder()
.addSubjectAlternativeName(localhost)
- .commonName("CN=localhost")
+ .rsa2048()
+ .commonName("localhost")
.signedBy(rootCertificate)
.build();
diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
index 0813852b..085f068f 100644
--- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
@@ -10,7 +10,6 @@
import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient;
import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest;
import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse;
-import io.opentdf.platform.sdk.Config.KASInfo;
import io.opentdf.platform.sdk.TDF.Reader;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.junit.jupiter.api.BeforeAll;
@@ -89,9 +88,9 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessio
if (sessionKeyType.isEc()) {
var kasPrivateKey = CryptoUtils
.getPrivateKeyPEM(keypairs.get(index).getPrivate());
- var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey);
+ var privateKey = PemTestUtils.privateKeyFromPem(kasPrivateKey);
var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey;
- var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey);
+ var publicKey = PemTestUtils.publicKeyFromPem(clientEphemeralPublicKey);
byte[] symKey = ECKeyPair.computeECDHKey(publicKey, privateKey);
var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey);
diff --git a/sdk/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/sdk/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 00000000..b4c1aab4
--- /dev/null
+++ b/sdk/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+io.opentdf.platform.sdk.CryptoProviderSetupExtension
\ No newline at end of file
diff --git a/sdk/src/test/resources/junit-platform.properties b/sdk/src/test/resources/junit-platform.properties
new file mode 100644
index 00000000..25ce5c98
--- /dev/null
+++ b/sdk/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.extensions.autodetection.enabled = true