diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java
index 71445f69..aa91d62b 100644
--- a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java
+++ b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java
@@ -3,6 +3,7 @@
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
@@ -20,10 +21,27 @@
public class AesGcm {
public static final int GCM_NONCE_LENGTH = 12; // in bytes
public static final int GCM_TAG_LENGTH = 16; // in bytes
+ public static final int GCM_KEY_SIZE_BITS = 256;
+ private static final String KEY_ALGORITHM = "AES";
private static final String CIPHER_TRANSFORM = "AES/GCM/NoPadding";
private final SecretKey key;
+ /**
+ * Generate a fresh 256-bit AES key using the JCA {@link KeyGenerator}.
+ *
+ * @return the encoded key bytes
+ */
+ public static byte[] generateKey() {
+ try {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
+ keyGenerator.init(GCM_KEY_SIZE_BITS);
+ return keyGenerator.generateKey().getEncoded();
+ } catch (NoSuchAlgorithmException e) {
+ throw new SDKException("error generating AES key", e);
+ }
+ }
+
/**
* Return symmetric key
diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/CompositeX509ExtendedTrustManager.java b/sdk/src/main/java/io/opentdf/platform/sdk/CompositeX509ExtendedTrustManager.java
new file mode 100644
index 00000000..14a117f5
--- /dev/null
+++ b/sdk/src/main/java/io/opentdf/platform/sdk/CompositeX509ExtendedTrustManager.java
@@ -0,0 +1,122 @@
+package io.opentdf.platform.sdk;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedTrustManager;
+import java.net.Socket;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+final class CompositeX509ExtendedTrustManager extends X509ExtendedTrustManager {
+
+ private final List delegates;
+ private final X509Certificate[] acceptedIssuers;
+
+ CompositeX509ExtendedTrustManager(List delegates) {
+ if (delegates == null || delegates.isEmpty()) {
+ throw new IllegalArgumentException("at least one trust manager is required");
+ }
+ this.delegates = Collections.unmodifiableList(new ArrayList<>(delegates));
+ Set issuers = new LinkedHashSet<>();
+ for (X509ExtendedTrustManager tm : this.delegates) {
+ X509Certificate[] tmIssuers = tm.getAcceptedIssuers();
+ if (tmIssuers != null) {
+ Collections.addAll(issuers, tmIssuers);
+ }
+ }
+ this.acceptedIssuers = issuers.toArray(new X509Certificate[0]);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ CertificateException last = null;
+ for (X509ExtendedTrustManager tm : delegates) {
+ try {
+ tm.checkClientTrusted(chain, authType);
+ return;
+ } catch (CertificateException e) {
+ last = e;
+ }
+ }
+ throw last;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
+ CertificateException last = null;
+ for (X509ExtendedTrustManager tm : delegates) {
+ try {
+ tm.checkClientTrusted(chain, authType, socket);
+ return;
+ } catch (CertificateException e) {
+ last = e;
+ }
+ }
+ throw last;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
+ CertificateException last = null;
+ for (X509ExtendedTrustManager tm : delegates) {
+ try {
+ tm.checkClientTrusted(chain, authType, engine);
+ return;
+ } catch (CertificateException e) {
+ last = e;
+ }
+ }
+ throw last;
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ CertificateException last = null;
+ for (X509ExtendedTrustManager tm : delegates) {
+ try {
+ tm.checkServerTrusted(chain, authType);
+ return;
+ } catch (CertificateException e) {
+ last = e;
+ }
+ }
+ throw last;
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
+ CertificateException last = null;
+ for (X509ExtendedTrustManager tm : delegates) {
+ try {
+ tm.checkServerTrusted(chain, authType, socket);
+ return;
+ } catch (CertificateException e) {
+ last = e;
+ }
+ }
+ throw last;
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
+ CertificateException last = null;
+ for (X509ExtendedTrustManager tm : delegates) {
+ try {
+ tm.checkServerTrusted(chain, authType, engine);
+ return;
+ } catch (CertificateException e) {
+ last = e;
+ }
+ }
+ throw last;
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return acceptedIssuers.clone();
+ }
+}
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/SDKBuilder.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java
index bb350b66..9b2f75e8 100644
--- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java
+++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java
@@ -34,22 +34,17 @@
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse;
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClient;
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface;
-import nl.altindag.ssl.SSLFactory;
-import nl.altindag.ssl.pem.util.PemUtils;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
+import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509ExtendedTrustManager;
-import java.io.File;
-import java.io.FileInputStream;
+import javax.net.ssl.X509TrustManager;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.file.Path;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -63,7 +58,8 @@ public class SDKBuilder {
private String platformEndpoint = null;
private ClientAuthentication clientAuth = null;
private Boolean usePlainText;
- private SSLFactory sslFactory;
+ private SSLSocketFactory sslSocketFactory;
+ private X509TrustManager trustManager;
private AuthorizationGrant authzGrant;
private ProtocolType protocolType = ProtocolType.CONNECT;
private SrtSigner srtSigner;
@@ -80,42 +76,61 @@ public static SDKBuilder newBuilder() {
return builder;
}
- public SDKBuilder sslFactory(SSLFactory sslFactory) {
- this.sslFactory = sslFactory;
+ /**
+ * Configure the SDK to use the supplied {@link SSLSocketFactory} for outbound TLS connections.
+ * Callers using this overload bring their own pre-built socket factory; cert-chain trust
+ * material is whatever the supplied factory was built with. For full PKIX validation under a
+ * matching {@link X509TrustManager}, use {@link #sslFactoryFromDirectory(String)} or
+ * {@link #sslFactoryFromKeyStore(String, String)} which build both via {@link TrustProvider}.
+ */
+ public SDKBuilder sslFactory(SSLSocketFactory sslSocketFactory) {
+ this.sslSocketFactory = sslSocketFactory;
+ this.trustManager = null;
+ return this;
+ }
+
+ /**
+ * Configure the SDK to use the supplied {@link SSLSocketFactory} together with a matching
+ * {@link X509TrustManager}. The trust manager is used by OkHttp for certificate pinning and
+ * cleartext-fallback decisions; supply this overload when the caller has a trust manager that
+ * matches the socket factory's trust material (e.g. both built from a {@link TrustProvider}).
+ */
+ public SDKBuilder sslFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) {
+ this.sslSocketFactory = sslSocketFactory;
+ this.trustManager = trustManager;
return this;
}
/**
* Add SSL Context with trusted certs from certDirPath
- *
+ *
* @param certsDirPath Path to a directory containing .pem or .crt trusted certs
*/
public SDKBuilder sslFactoryFromDirectory(String certsDirPath) throws Exception {
- File certsDir = new File(certsDirPath);
- File[] certFiles = certsDir.listFiles((dir, name) -> name.endsWith(".pem") || name.endsWith(".crt"));
- logger.info("Loading certificates from: " + certsDir.getAbsolutePath());
- List certStreams = new ArrayList<>(certFiles.length);
- for (File certFile : certFiles) {
- certStreams.add(new FileInputStream(certFile));
- }
- X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial(certStreams.toArray(new InputStream[0]));
- this.sslFactory = SSLFactory.builder().withDefaultTrustMaterial().withSystemTrustMaterial()
- .withTrustMaterial(trustManager).build();
+ logger.info("Loading certificates from: {}", certsDirPath);
+ TrustProvider provider = TrustProvider.fromDirectory(certsDirPath);
+ this.sslSocketFactory = provider.getSslSocketFactory();
+ this.trustManager = provider.getTrustManager();
return this;
}
/**
* Add SSL Context with default system trust material + certs contained in a
* Java keystore
- *
+ *
* @param keystorePath Path to keystore
* @param keystorePassword Password to keystore
*/
public SDKBuilder sslFactoryFromKeyStore(String keystorePath, String keystorePassword) {
- this.sslFactory = SSLFactory.builder().withDefaultTrustMaterial().withSystemTrustMaterial()
- .withTrustMaterial(Path.of(keystorePath),
- keystorePassword == null ? "".toCharArray() : keystorePassword.toCharArray())
- .build();
+ try {
+ TrustProvider provider = TrustProvider.fromKeyStore(
+ Path.of(keystorePath),
+ keystorePassword == null ? "".toCharArray() : keystorePassword.toCharArray());
+ this.sslSocketFactory = provider.getSslSocketFactory();
+ this.trustManager = provider.getTrustManager();
+ } catch (IOException | java.security.GeneralSecurityException e) {
+ throw new SDKException("failed to load keystore from " + keystorePath, e);
+ }
return this;
}
@@ -223,8 +238,8 @@ private Interceptor getAuthInterceptor(RSAKey rsaKey) {
OIDCProviderMetadata providerMetadata;
try {
providerMetadata = OIDCProviderMetadata.resolve(issuer, httpRequest -> {
- if (sslFactory != null) {
- httpRequest.setSSLSocketFactory(sslFactory.getSslSocketFactory());
+ if (sslSocketFactory != null) {
+ httpRequest.setSSLSocketFactory(sslSocketFactory);
}
});
} catch (IOException | GeneralException e) {
@@ -234,7 +249,7 @@ private Interceptor getAuthInterceptor(RSAKey rsaKey) {
if (this.authzGrant == null) {
this.authzGrant = new ClientCredentialsGrant();
}
- var ts = new TokenSource(clientAuth, rsaKey, providerMetadata.getTokenEndpointURI(), this.authzGrant, sslFactory);
+ var ts = new TokenSource(clientAuth, rsaKey, providerMetadata.getTokenEndpointURI(), this.authzGrant, sslSocketFactory);
return new AuthInterceptor(ts);
}
@@ -344,7 +359,7 @@ public SDK.KAS kas() {
return new ServicesAndInternals(
authInterceptor,
- sslFactory == null ? null : sslFactory.getTrustManager().orElse(null),
+ trustManager,
services,
client,
srtSignerToUse);
@@ -378,6 +393,7 @@ private ProtocolClient getProtocolClient(String endpoint, OkHttpClient httpClien
return new ProtocolClient(new ConnectOkHttpClient(httpClient), protocolClientConfig);
}
+ @SuppressWarnings("deprecation")
private OkHttpClient getHttpClient() {
// using a single http client is apparently the best practice, subject to everyone wanting to
// have the same protocols
@@ -387,17 +403,24 @@ private OkHttpClient getHttpClient() {
// expect HTTP/2, and Connect protocol can communicate with gRPC servers over HTTP/2
httpClient.protocols(List.of(Protocol.H2_PRIOR_KNOWLEDGE));
}
- if (sslFactory != null) {
- var trustManager = sslFactory.getTrustManager();
- if (trustManager.isEmpty()) {
- throw new SDKException("SSL factory must have a trust manager");
+ if (sslSocketFactory != null) {
+ if (trustManager != null) {
+ httpClient.sslSocketFactory(sslSocketFactory, trustManager);
+ } else {
+ // Caller supplied an SSLSocketFactory without a matching trust manager (e.g. via
+ // sslFactory(SSLSocketFactory)). Falls back to OkHttp's reflection-based platform
+ // default trust manager — only the SSLSocketFactory governs the actual handshake.
+ httpClient.sslSocketFactory(sslSocketFactory);
}
- httpClient.sslSocketFactory(sslFactory.getSslSocketFactory(), trustManager.get());
}
return httpClient.build();
}
- SSLFactory getSslFactory() {
- return this.sslFactory;
+ SSLSocketFactory getSslFactory() {
+ return this.sslSocketFactory;
+ }
+
+ X509TrustManager getTrustManager() {
+ return this.trustManager;
}
}
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..b30460eb 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.*;
@@ -97,8 +98,6 @@ private static byte[] tdfECKeySaltCompute() {
private static final String kTDFAsZip = "zip";
private static final String kTDFZipReference = "reference";
- private static final SecureRandom sRandom = new SecureRandom();
-
private static final Gson gson = new GsonBuilder().create();
static class EncryptedMetadata {
@@ -161,8 +160,7 @@ private void prepareManifest(Config.TDFConfig tdfConfig, MapImplemented entirely on top of provider-agnostic JCA APIs ({@link CertificateFactory},
+ * {@link KeyStore}, {@link TrustManagerFactory}, {@link SSLContext}). The actual cryptographic
+ * work is fulfilled by whichever {@link java.security.Provider} is registered with the JVM,
+ * including FIPS-mode providers.
+ */
+public final class TrustProvider {
+
+ private final SSLSocketFactory sslSocketFactory;
+ private final X509ExtendedTrustManager trustManager;
+ private final SSLContext sslContext;
+
+ private TrustProvider(SSLContext sslContext, X509ExtendedTrustManager trustManager) {
+ this.sslContext = sslContext;
+ this.trustManager = trustManager;
+ this.sslSocketFactory = sslContext.getSocketFactory();
+ }
+
+ public X509ExtendedTrustManager getTrustManager() {
+ return trustManager;
+ }
+
+ public SSLContext getSslContext() {
+ return sslContext;
+ }
+
+ public SSLSocketFactory getSslSocketFactory() {
+ return sslSocketFactory;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builds a {@link TrustProvider} that trusts JVM default cacerts plus every {@code .pem} or
+ * {@code .crt} certificate found in the supplied directory.
+ */
+ public static TrustProvider fromDirectory(String certsDirPath) throws IOException, GeneralSecurityException {
+ File certsDir = new File(certsDirPath);
+ File[] certFiles = certsDir.listFiles((dir, name) -> name.endsWith(".pem") || name.endsWith(".crt"));
+ if (certFiles == null) {
+ throw new IOException("not a directory or unreadable: " + certsDirPath);
+ }
+ Builder builder = builder().withDefaultTrustMaterial();
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ for (File certFile : certFiles) {
+ try (InputStream in = new FileInputStream(certFile)) {
+ Collection extends Certificate> certs = cf.generateCertificates(in);
+ List x509s = new ArrayList<>(certs.size());
+ for (Certificate c : certs) {
+ if (c instanceof X509Certificate) {
+ x509s.add((X509Certificate) c);
+ }
+ }
+ builder.withTrustMaterial(x509s.toArray(new X509Certificate[0]));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Builds a {@link TrustProvider} that trusts JVM default cacerts plus the trusted-certificate
+ * entries in the supplied keystore.
+ */
+ public static TrustProvider fromKeyStore(Path keystorePath, char[] password) throws IOException, GeneralSecurityException {
+ if (!Files.isRegularFile(keystorePath)) {
+ throw new IOException("keystore not found: " + keystorePath);
+ }
+ KeyStore ks;
+ try (InputStream in = Files.newInputStream(keystorePath)) {
+ ks = loadKeyStore(in, password);
+ }
+ return builder().withDefaultTrustMaterial().withTrustMaterial(ks).build();
+ }
+
+ /**
+ * Builds a {@link TrustProvider} that accepts every server certificate. Intended only for
+ * tests and {@code --insecure} CLI flows.
+ */
+ public static TrustProvider insecure() {
+ try {
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ X509ExtendedTrustManager trustAll = new InsecureTrustManager();
+ ctx.init(new KeyManager[0], new TrustManager[]{trustAll}, new SecureRandom());
+ return new TrustProvider(ctx, trustAll);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException("failed to build insecure TrustProvider", e);
+ }
+ }
+
+ private static KeyStore loadKeyStore(InputStream in, char[] password)
+ throws IOException, GeneralSecurityException {
+ // Try JKS first since it remains the JVM default; fall back to PKCS12 which is portable
+ // across both bcprov-jdk18on and bc-fips. We do not pin a provider; whichever provider is
+ // registered fulfills the request.
+ byte[] bytes = readAll(in);
+ KeyStoreException last = null;
+ for (String type : new String[]{KeyStore.getDefaultType(), "JKS", "PKCS12"}) {
+ try {
+ KeyStore ks = KeyStore.getInstance(type);
+ ks.load(new java.io.ByteArrayInputStream(bytes), password);
+ return ks;
+ } catch (KeyStoreException e) {
+ last = e;
+ } catch (IOException | NoSuchAlgorithmException | CertificateException e) {
+ // wrong format or wrong password — try next type
+ last = new KeyStoreException(e);
+ }
+ }
+ throw last != null ? last : new KeyStoreException("could not load keystore");
+ }
+
+ private static byte[] readAll(InputStream in) throws IOException {
+ java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
+ byte[] buf = new byte[8192];
+ int n;
+ while ((n = in.read(buf)) >= 0) {
+ out.write(buf, 0, n);
+ }
+ return out.toByteArray();
+ }
+
+ private static X509ExtendedTrustManager extractTrustManager(KeyStore trustStore)
+ throws GeneralSecurityException {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+ for (TrustManager tm : tmf.getTrustManagers()) {
+ if (tm instanceof X509ExtendedTrustManager) {
+ return (X509ExtendedTrustManager) tm;
+ }
+ }
+ throw new NoSuchAlgorithmException("no X509ExtendedTrustManager available from provider");
+ }
+
+ public static final class Builder {
+ private boolean includeDefault;
+ private final List keyStores = new ArrayList<>();
+ private final List certificates = new ArrayList<>();
+
+ private Builder() {
+ }
+
+ /**
+ * Include the JVM default cacerts (i.e. those returned by initialising a
+ * {@link TrustManagerFactory} with a {@code null} keystore).
+ */
+ public Builder withDefaultTrustMaterial() {
+ this.includeDefault = true;
+ return this;
+ }
+
+ public Builder withTrustMaterial(X509Certificate... certs) {
+ if (certs != null) {
+ Collections.addAll(this.certificates, certs);
+ }
+ return this;
+ }
+
+ public Builder withTrustMaterial(Collection certs) {
+ if (certs != null) {
+ this.certificates.addAll(certs);
+ }
+ return this;
+ }
+
+ public Builder withTrustMaterial(KeyStore keyStore) {
+ if (keyStore != null) {
+ this.keyStores.add(keyStore);
+ }
+ return this;
+ }
+
+ public Builder withTrustMaterial(Path keystorePath, char[] password) throws IOException, GeneralSecurityException {
+ try (InputStream in = Files.newInputStream(keystorePath)) {
+ this.keyStores.add(loadKeyStore(in, password));
+ }
+ return this;
+ }
+
+ public TrustProvider build() {
+ try {
+ List trustManagers = new ArrayList<>();
+
+ if (includeDefault) {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init((KeyStore) null);
+ for (TrustManager tm : tmf.getTrustManagers()) {
+ if (tm instanceof X509ExtendedTrustManager) {
+ trustManagers.add((X509ExtendedTrustManager) tm);
+ }
+ }
+ }
+
+ for (KeyStore ks : keyStores) {
+ trustManagers.add(extractTrustManager(ks));
+ }
+
+ if (!certificates.isEmpty()) {
+ KeyStore custom = newEmptyKeyStore();
+ int i = 0;
+ for (X509Certificate cert : certificates) {
+ custom.setCertificateEntry("trust-anchor-" + (i++), cert);
+ }
+ trustManagers.add(extractTrustManager(custom));
+ }
+
+ if (trustManagers.isEmpty()) {
+ throw new IllegalStateException("TrustProvider requires at least one source of trust material");
+ }
+
+ X509ExtendedTrustManager combined = trustManagers.size() == 1
+ ? trustManagers.get(0)
+ : new CompositeX509ExtendedTrustManager(trustManagers);
+
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(new KeyManager[0], new TrustManager[]{combined}, new SecureRandom());
+ return new TrustProvider(ctx, combined);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new IllegalStateException("failed to build TrustProvider", e);
+ }
+ }
+
+ private static KeyStore newEmptyKeyStore() throws GeneralSecurityException, IOException {
+ // PKCS12 is supported by both stock JDK and BC (FIPS and non-FIPS).
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(null, null);
+ return ks;
+ }
+ }
+
+ private static final class InsecureTrustManager extends X509ExtendedTrustManager {
+ private static final X509Certificate[] EMPTY = new X509Certificate[0];
+
+ @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { }
+ @Override public void checkClientTrusted(X509Certificate[] chain, String authType, java.net.Socket socket) { }
+ @Override public void checkClientTrusted(X509Certificate[] chain, String authType, javax.net.ssl.SSLEngine engine) { }
+ @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { }
+ @Override public void checkServerTrusted(X509Certificate[] chain, String authType, java.net.Socket socket) { }
+ @Override public void checkServerTrusted(X509Certificate[] chain, String authType, javax.net.ssl.SSLEngine engine) { }
+ @Override public X509Certificate[] getAcceptedIssuers() { return EMPTY; }
+ }
+}
diff --git a/sdk/src/test/java.security.fips.test b/sdk/src/test/java.security.fips.test
new file mode 100644
index 00000000..ebf73ab6
--- /dev/null
+++ b/sdk/src/test/java.security.fips.test
@@ -0,0 +1,22 @@
+# the default for these is usually SunX509 but BC doesn't
+# support them. tell it to use PKIX instead which is supported by BC
+ssl.KeyManagerFactory.algorithm=PKIX
+ssl.TrustManagerFactory.algorithm=PKIX
+
+securerandom.strongAlgorithms=NativePRNGBlocking:SUN
+
+security.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
+security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS
+# the SUN provider is required so that we can get the NativePRNGBlocking algorithm
+security.provider.3=SUN
+security.provider.4=
+security.provider.5=
+security.provider.6=
+security.provider.7=
+security.provider.8=
+security.provider.9=
+security.provider.10=
+security.provider.11=
+security.provider.12=
+security.provider.13=
+security.provider.14=
\ No newline at end of file
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..0b7bb62c
--- /dev/null
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/PemTestUtils.java
@@ -0,0 +1,158 @@
+package io.opentdf.platform.sdk;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.EllipticCurve;
+import java.security.GeneralSecurityException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+/**
+ * PEM / X.509 helpers used only by tests. Uses standard java.security APIs so
+ * the SDK does not require BouncyCastle on the test classpath either.
+ */
+final class PemTestUtils {
+ private PemTestUtils() {
+ }
+
+ static ECPublicKey publicKeyFromPem(String pemEncoding) {
+ try {
+ KeyFactory kf = KeyFactory.getInstance("EC");
+ return (ECPublicKey) kf.generatePublic(new X509EncodedKeySpec(decodePem(pemEncoding)));
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static ECPrivateKey privateKeyFromPem(String pemEncoding) {
+ try {
+ KeyFactory kf = KeyFactory.getInstance("EC");
+ return (ECPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodePem(pemEncoding)));
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static String getPEMPublicKeyFromX509Cert(String pemInX509Format) {
+ try {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate cert = (X509Certificate) cf.generateCertificate(
+ new ByteArrayInputStream(pemInX509Format.getBytes(StandardCharsets.UTF_8)));
+ return encodePem("PUBLIC KEY", cert.getPublicKey().getEncoded());
+ } catch (CertificateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static byte[] compressECPublickey(String pemECPubKey) {
+ ECPublicKey publicKey = publicKeyFromPem(pemECPubKey);
+ ECParameterSpec params = publicKey.getParams();
+ int fieldSize = (params.getCurve().getField().getFieldSize() + 7) / 8;
+ ECPoint w = publicKey.getW();
+ byte[] x = toFixedLength(w.getAffineX(), fieldSize);
+ byte[] out = new byte[1 + fieldSize];
+ out[0] = (byte) (w.getAffineY().testBit(0) ? 0x03 : 0x02);
+ System.arraycopy(x, 0, out, 1, fieldSize);
+ return out;
+ }
+
+ static String publicKeyFromECPoint(byte[] ecPoint, String curveName) {
+ try {
+ AlgorithmParameters ap = AlgorithmParameters.getInstance("EC");
+ ap.init(new ECGenParameterSpec(curveName));
+ ECParameterSpec params = ap.getParameterSpec(ECParameterSpec.class);
+ ECPoint w = decodePoint(ecPoint, params.getCurve());
+ KeyFactory kf = KeyFactory.getInstance("EC");
+ PublicKey publicKey = kf.generatePublic(new ECPublicKeySpec(w, params));
+ return encodePem("PUBLIC KEY", publicKey.getEncoded());
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ECPoint decodePoint(byte[] data, EllipticCurve curve) {
+ int fieldSize = (curve.getField().getFieldSize() + 7) / 8;
+ if (data.length == 0) {
+ throw new IllegalArgumentException("empty EC point");
+ }
+ int prefix = data[0] & 0xff;
+ if (prefix == 0x04 && data.length == 1 + 2 * fieldSize) {
+ BigInteger x = new BigInteger(1, java.util.Arrays.copyOfRange(data, 1, 1 + fieldSize));
+ BigInteger y = new BigInteger(1, java.util.Arrays.copyOfRange(data, 1 + fieldSize, data.length));
+ return new ECPoint(x, y);
+ }
+ if ((prefix == 0x02 || prefix == 0x03) && data.length == 1 + fieldSize) {
+ BigInteger x = new BigInteger(1, java.util.Arrays.copyOfRange(data, 1, 1 + fieldSize));
+ BigInteger y = getY(curve, x, prefix);
+ return new ECPoint(x, y);
+ }
+ throw new IllegalArgumentException("unsupported EC point encoding: prefix=0x"
+ + Integer.toHexString(prefix) + ", length=" + data.length);
+ }
+
+ private static @NonNull BigInteger getY(EllipticCurve curve, BigInteger x, int prefix) {
+ BigInteger p = ((ECFieldFp) curve.getField()).getP();
+ // y^2 = x^3 + a*x + b (mod p); valid for all NIST P-curves we support.
+ BigInteger rhs = x.modPow(BigInteger.valueOf(3), p)
+ .add(curve.getA().multiply(x))
+ .add(curve.getB())
+ .mod(p);
+ // NIST P-curves all have p ≡ 3 (mod 4), so √rhs = rhs^((p+1)/4) mod p.
+ if (!p.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3))) {
+ throw new IllegalArgumentException("point decompression only supports curves with p mod 4 == 3");
+ }
+ BigInteger y = rhs.modPow(p.add(BigInteger.ONE).shiftRight(2), p);
+ boolean wantOdd = (prefix == 0x03);
+ if (y.testBit(0) != wantOdd) {
+ y = p.subtract(y);
+ }
+ return y;
+ }
+
+ 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) {
+ System.arraycopy(bytes, bytes.length - length, result, 0, length);
+ } else {
+ System.arraycopy(bytes, 0, result, length - bytes.length, bytes.length);
+ }
+ return result;
+ }
+
+ private static byte[] decodePem(String pem) {
+ String body = pem.replaceAll("-----(BEGIN|END) [A-Z ]+-----", "").replaceAll("\\s", "");
+ return Base64.getDecoder().decode(body);
+ }
+
+ private static String encodePem(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();
+ }
+}
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..4d6c750d 100644
--- a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java
+++ b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java
@@ -20,9 +20,6 @@
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationRequest;
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse;
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceGrpc;
-import nl.altindag.ssl.SSLFactory;
-import nl.altindag.ssl.pem.util.PemUtils;
-import nl.altindag.ssl.util.KeyStoreUtils;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.tls.HandshakeCertificates;
@@ -30,6 +27,8 @@
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
@@ -40,6 +39,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Base64;
@@ -70,24 +70,31 @@ void testDirCertsSSLContext() throws Exception {
IOUtils.write(EXAMPLE_COM_PEM, fos);
fos.close();
SDKBuilder builder = SDKBuilder.newBuilder().sslFactoryFromDirectory(certDirPath.toAbsolutePath().toString());
- SSLFactory sslFactory = builder.getSslFactory();
- assertNotNull(sslFactory);
- X509Certificate[] acceptedIssuers = sslFactory.getTrustManager().get().getAcceptedIssuers();
+ SSLSocketFactory sslSocketFactory = builder.getSslFactory();
+ assertNotNull(sslSocketFactory);
+ X509TrustManager trustManager = builder.getTrustManager();
+ assertNotNull(trustManager);
+ X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers();
assertEquals(1, Arrays.stream(acceptedIssuers).filter(x -> x.getIssuerX500Principal().getName()
.equals("CN=example.com")).count());
}
@Test
void testKeystoreSSLContext() throws Exception {
- KeyStore keystore = KeyStoreUtils.createKeyStore();
- keystore.setCertificateEntry("example.com", PemUtils.parseCertificate(EXAMPLE_COM_PEM).get(0));
+ KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keystore.load(null, null);
+ X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
+ .generateCertificate(new ByteArrayInputStream(EXAMPLE_COM_PEM.getBytes(StandardCharsets.UTF_8)));
+ keystore.setCertificateEntry("example.com", cert);
Path keyStorePath = Files.createTempFile("ca", "jks");
keystore.store(new FileOutputStream(keyStorePath.toAbsolutePath().toString()), "foo".toCharArray());
SDKBuilder builder = SDKBuilder.newBuilder().sslFactoryFromKeyStore(keyStorePath.toAbsolutePath().toString(),
"foo");
- SSLFactory sslFactory = builder.getSslFactory();
- assertNotNull(sslFactory);
- X509Certificate[] acceptedIssuers = sslFactory.getTrustManager().get().getAcceptedIssuers();
+ SSLSocketFactory sslSocketFactory = builder.getSslFactory();
+ assertNotNull(sslSocketFactory);
+ X509TrustManager trustManager = builder.getTrustManager();
+ assertNotNull(trustManager);
+ X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers();
assertEquals(1, Arrays.stream(acceptedIssuers).filter(x -> x.getIssuerX500Principal().getName()
.equals("CN=example.com")).count());
@@ -161,11 +168,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();
@@ -305,10 +314,12 @@ public ServerCall.Listener interceptCall(ServerCall