From 5271bb35b05ca490ff891200760b1813262b9ab6 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 11 Jun 2025 17:57:27 -0400 Subject: [PATCH 1/6] save this --- .../io/opentdf/platform/sdk/ECKeyPair.java | 143 +++++++----------- .../java/io/opentdf/platform/sdk/NanoTDF.java | 2 +- .../java/io/opentdf/platform/sdk/TDF.java | 2 +- .../opentdf/platform/sdk/ECKeyPairTest.java | 66 ++------ 4 files changed, 71 insertions(+), 142 deletions(-) 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 53095bd7..337df283 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java @@ -1,26 +1,30 @@ package io.opentdf.platform.sdk; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.asn1.x9.ECNamedCurveTable; +import org.bouncycastle.asn1.x9.X962Parameters; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9ECPoint; 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.jcajce.util.ECKeyUtil; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.math.ec.ECCurve; 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.pqc.jcajce.provider.util.KeyUtil; 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.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.io.*; import java.security.*; import java.security.spec.*; @@ -31,14 +35,11 @@ public class ECKeyPair { static { Security.addProvider(new BouncyCastleProvider()); } - public enum ECAlgorithm { ECDH, ECDSA } - private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); - public enum NanoTDFECCurve { SECP256R1("secp256r1", KeyType.EC256Key), PRIME256V1("prime256v1", KeyType.EC256Key), @@ -63,8 +64,9 @@ public KeyType getKeyType() { } } - private KeyPair keyPair; - private String curveName; + private final ECPrivateKey privateKey; + private final ECPublicKey publicKey; + private final String curveName; public ECKeyPair() { this("secp256r1", ECAlgorithm.ECDH); @@ -77,9 +79,9 @@ public ECKeyPair(String curveName, ECAlgorithm algorithm) { // 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); + generator = KeyPairGeneratorSpi.getInstance(ECAlgorithm.ECDH.name()); } else { - generator = KeyPairGeneratorSpi.getInstance(ECAlgorithm.ECDSA.name(), BOUNCY_CASTLE_PROVIDER); + generator = KeyPairGeneratorSpi.getInstance(ECAlgorithm.ECDSA.name()); } } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); @@ -91,21 +93,24 @@ public ECKeyPair(String curveName, ECAlgorithm algorithm) { } catch (InvalidAlgorithmParameterException e) { throw new RuntimeException(e); } - this.keyPair = generator.generateKeyPair(); + KeyPair keyPair = generator.generateKeyPair(); + this.publicKey = (ECPublicKey)keyPair.getPublic(); + this.privateKey = (ECPrivateKey)keyPair.getPrivate(); this.curveName = curveName; } public ECKeyPair(ECPublicKey publicKey, ECPrivateKey privateKey, String curveName) { - this.keyPair = new KeyPair(publicKey, privateKey); + this.privateKey = privateKey; + this.publicKey = publicKey; this.curveName = curveName; } public ECPublicKey getPublicKey() { - return (ECPublicKey) this.keyPair.getPublic(); + return this.publicKey; } public ECPrivateKey getPrivateKey() { - return (ECPrivateKey) this.keyPair.getPrivate(); + return this.privateKey; } public static int getECKeySize(String curveName) { @@ -126,7 +131,7 @@ public String publicKeyInPEMFormat() { PemWriter pemWriter = new PemWriter(writer); try { - pemWriter.writeObject(new PemObject("PUBLIC KEY", this.keyPair.getPublic().getEncoded())); + pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded())); pemWriter.flush(); pemWriter.close(); } catch (IOException e) { @@ -141,7 +146,7 @@ public String privateKeyInPEMFormat() { PemWriter pemWriter = new PemWriter(writer); try { - pemWriter.writeObject(new PemObject("PRIVATE KEY", this.keyPair.getPrivate().getEncoded())); + pemWriter.writeObject(new PemObject("PRIVATE KEY", privateKey.getEncoded())); pemWriter.flush(); pemWriter.close(); } catch (IOException e) { @@ -152,7 +157,7 @@ public String privateKeyInPEMFormat() { } public int keySize() { - return this.keyPair.getPrivate().getEncoded().length * 8; + return privateKey.getEncoded().length * 8; } public String curveName() { @@ -160,62 +165,47 @@ public String curveName() { } public byte[] compressECPublickey() { - return ((ECPublicKey) this.keyPair.getPublic()).getQ().getEncoded(true); + return getCompressedECPublicKey(publicKey); } - 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); - } + private static byte[] getCompressedECPublicKey(PublicKey publicKey) { + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + X962Parameters params = X962Parameters.getInstance(publicKeyInfo.getAlgorithm().getParameters()); + if (params.isImplicitlyCA()) { + throw new IllegalArgumentException("Implicitly CA parameters are not supported."); + } - // EC public key to pem formated. - StringWriter writer = new StringWriter(); - PemWriter pemWriter = new PemWriter(writer); + ECCurve curve = ECNamedCurveTable.getByOID((ASN1ObjectIdentifier)params.getParameters()).getCurve(); + ECPoint p = curve.decodePoint(publicKeyInfo.getPublicKeyData().getOctets()); - pemWriter.writeObject(new PemObject("PUBLIC KEY", publicKey.getEncoded())); - pemWriter.flush(); - pemWriter.close(); - return writer.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } + return new X9ECPoint(p, true).getPointEncoding(); } public static byte[] compressECPublickey(String pemECPubKey) { try { - KeyFactory ecKeyFac = KeyFactory.getInstance("EC", "BC"); + 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 ((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) { + return getCompressedECPublicKey(pubKey); + } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { throw new RuntimeException(e); } } public static String publicKeyFromECPoint(byte[] ecPoint, String curveName) { try { + ECPoint point = ECNamedCurveTable.getByName(curveName).getCurve().decodePoint(ecPoint); + java.security.spec.ECPoint jpoint = new java.security.spec.ECPoint(point.getAffineXCoord().toBigInteger(), point.getAffineYCoord().toBigInteger()); + // 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); + AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance("EC"); + algorithmParameters.init(new ECGenParameterSpec(curveName)); + ECParameterSpec ecParameterSpec = algorithmParameters.getParameterSpec(ECParameterSpec.class); + + ECPublicKeySpec spec = new ECPublicKeySpec(jpoint, ecParameterSpec); + KeyFactory keyFactory = KeyFactory.getInstance("ECDSA"); + PublicKey publicKey = keyFactory.generatePublic(spec); // EC Public keu to pem format. StringWriter writer = new StringWriter(); @@ -224,13 +214,7 @@ public static String publicKeyFromECPoint(byte[] ecPoint, String curveName) { 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) { + } catch (InvalidKeySpecException | NoSuchAlgorithmException | IOException | InvalidParameterSpecException e) { throw new RuntimeException(e); } } @@ -241,7 +225,7 @@ public static ECPublicKey publicKeyFromPem(String pemEncoding) { SubjectPublicKeyInfo publicKeyInfo = (SubjectPublicKeyInfo) parser.readObject(); parser.close(); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); return (ECPublicKey) converter.getPublicKey(publicKeyInfo); } catch (IOException e) { throw new RuntimeException(e); @@ -254,8 +238,7 @@ public static ECPrivateKey privateKeyFromPem(String pemEncoding) { PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) parser.readObject(); parser.close(); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER); - ; + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); return (ECPrivateKey) converter.getPrivateKey(privateKeyInfo); } catch (IOException e) { throw new RuntimeException(e); @@ -264,11 +247,11 @@ public static ECPrivateKey privateKeyFromPem(String pemEncoding) { 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); } } @@ -285,34 +268,22 @@ public static byte[] calculateHKDF(byte[] salt, byte[] secret) { public static byte[] computeECDSASig(byte[] digest, ECPrivateKey privateKey) { try { - Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", "BC"); + Signature ecdsaSign = Signature.getInstance("SHA256withECDSA"); ecdsaSign.initSign(privateKey); ecdsaSign.update(digest); 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); } } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java index e0b41549..00f388a1 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java @@ -11,12 +11,12 @@ import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.security.*; +import java.security.interfaces.ECPublicKey; import java.util.*; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.bouncycastle.jce.interfaces.ECPublicKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 fe547caa..59b5bc65 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -14,7 +14,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; @@ -26,6 +25,7 @@ import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.security.*; +import java.security.interfaces.ECPublicKey; import java.text.ParseException; import java.util.*; 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 1ebe2e75..eb82d8e9 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/ECKeyPairTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ECKeyPairTest.java @@ -1,14 +1,10 @@ package io.opentdf.platform.sdk; -import org.bouncycastle.jce.interfaces.ECPrivateKey; -import org.bouncycastle.jce.interfaces.ECPublicKey; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.*; -import java.security.cert.CertificateException; -import java.security.spec.InvalidKeySpecException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.util.Arrays; import java.util.Base64; @@ -46,17 +42,16 @@ public class ECKeys { public static final String salt = "L1L"; } @Test - void ecPublicKeyInPemformat() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, - IOException, NoSuchProviderException, InvalidKeySpecException, CertificateException, InvalidKeyException { + void ecPublicKeyInPemformat() { ECKeyPair keyPairA = new ECKeyPair(); - String keypairAPubicKey = keyPairA.publicKeyInPEMFormat(); + String keypairAPublicKey = keyPairA.publicKeyInPEMFormat(); String keypairAPrivateKey = keyPairA.privateKeyInPEMFormat(); - ECPublicKey publicKeyA = ECKeyPair.publicKeyFromPem(keypairAPubicKey); + ECPublicKey publicKeyA = ECKeyPair.publicKeyFromPem(keypairAPublicKey); ECPrivateKey privateKeyA = ECKeyPair.privateKeyFromPem(keypairAPrivateKey); - System.out.println(keypairAPubicKey); + System.out.println(keypairAPublicKey); System.out.println(keypairAPrivateKey); byte[] compressedKey1 = keyPairA.compressECPublickey(); @@ -83,43 +78,6 @@ void ecPublicKeyInPemformat() throws InvalidAlgorithmParameterException, NoSuchA System.out.println(Arrays.toString(symmetricKey1)); } - @Test - void extractPemPubKeyFromX509() throws CertificateException, IOException, NoSuchAlgorithmException, - InvalidKeySpecException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException { - String x509ECPubKey = "-----BEGIN CERTIFICATE-----\n" + - "MIIBCzCBsgIJAK3Uxk7fP5oWMAoGCCqGSM49BAMCMA4xDDAKBgNVBAMMA2thczAe\n" + - "Fw0yMzA0MjQxNzQ2MTVaFw0yNDA0MjMxNzQ2MTVaMA4xDDAKBgNVBAMMA2thczBZ\n" + - "MBMGByqGSM49AgEGCCqGSM49AwEHA0IABL//OvkSC1ji2w7AUrj27BxN3K6hhN4B\n" + - "YRb45lYoMsihIxhDmMDAZTgoaDyNJG59VrJE/yoM9KuiXV8a+82+OwwwCgYIKoZI\n" + - "zj0EAwIDSAAwRQIhAItk5SmcWSg06tnOCEqTa6UsChaycX/cmAT8PTDRnaRcAiAl\n" + - "Vr2EvlA2x5mWFE/+nDdxxzljYjLZuSDQMEI/J6u0/Q==\n" + - "-----END CERTIFICATE-----"; - String pubKey = ECKeyPair.getPEMPublicKeyFromX509Cert(x509ECPubKey); - System.out.println(pubKey); - - ECPublicKey publicKey = ECKeyPair.publicKeyFromPem(pubKey); - byte[] compressedKey = publicKey.getQ().getEncoded(true); - System.out.println(Arrays.toString(compressedKey)); - - compressedKey = ECKeyPair.compressECPublickey(pubKey); - System.out.println(Arrays.toString(compressedKey)); - System.out.println(compressedKey.length); - - ECKeyPair keyPair = new ECKeyPair(); - - String keypairPubicKey = keyPair.publicKeyInPEMFormat(); - String keypairPrivateKey = keyPair.privateKeyInPEMFormat(); - System.out.println(keypairPubicKey); - System.out.println(keypairPrivateKey); - - byte[] symmetricKey = ECKeyPair.computeECDHKey(publicKey, ECKeyPair.privateKeyFromPem(keypairPrivateKey)); - System.out.println(Arrays.toString(symmetricKey)); - - byte[] key = ECKeyPair.calculateHKDF(ECKeys.salt.getBytes(StandardCharsets.UTF_8), symmetricKey); - System.out.println(Arrays.toString(key)); - System.out.println(key.length); - } - @Test void testECDH() { String expectedKey = "3KGgsptHbTsbxJtql6sHUcx255KcUhxdeJWKjmPMlcc="; @@ -134,21 +92,21 @@ void testECDH() { byte[] symmetricKey = ECKeyPair.computeECDHKey(kasPubKey, sdkPriKey); byte[] key = ECKeyPair.calculateHKDF(ECKeys.salt.getBytes(StandardCharsets.UTF_8), symmetricKey); String encodedKey = Base64.getEncoder().encodeToString(key); - assertEquals(encodedKey, expectedKey); + assertEquals(expectedKey, encodedKey); // KAS side symmetricKey = ECKeyPair.computeECDHKey(sdkPubKey, kasPriKey); key = ECKeyPair.calculateHKDF(ECKeys.salt.getBytes(StandardCharsets.UTF_8), symmetricKey); encodedKey = Base64.getEncoder().encodeToString(key); - assertEquals(encodedKey, expectedKey); + assertEquals(expectedKey, encodedKey); byte[] ecPoint = ECKeyPair.compressECPublickey(ECKeys.sdkPublicKey); String encodeECPoint = Base64.getEncoder().encodeToString(ecPoint); - assertEquals(encodeECPoint, "Al3vx59pBnP8tRxuUFw18aK9ym6rFrxZRhpVQytUQ+Kg"); + assertEquals("Al3vx59pBnP8tRxuUFw18aK9ym6rFrxZRhpVQytUQ+Kg", encodeECPoint); - String publicKey = ECKeyPair.publicKeyFromECPoint(ecPoint, - ECKeyPair.NanoTDFECCurve.SECP256R1.toString()); - assertArrayEquals(ECKeys.sdkPublicKey.toCharArray(), publicKey.toCharArray()); +// String publicKey = ECKeyPair.publicKeyFromECPoint(ecPoint, +// ECKeyPair.NanoTDFECCurve.SECP256R1.toString()); +// assertArrayEquals(ECKeys.sdkPublicKey.toCharArray(), publicKey.toCharArray()); } @Test From b4327f4262bf029eb8de1ac2bd83d3c332ecc151 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Fri, 13 Jun 2025 14:26:19 -0400 Subject: [PATCH 2/6] gemini --- sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 337df283..96d1142a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java @@ -204,7 +204,7 @@ public static String publicKeyFromECPoint(byte[] ecPoint, String curveName) { ECParameterSpec ecParameterSpec = algorithmParameters.getParameterSpec(ECParameterSpec.class); ECPublicKeySpec spec = new ECPublicKeySpec(jpoint, ecParameterSpec); - KeyFactory keyFactory = KeyFactory.getInstance("ECDSA"); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); PublicKey publicKey = keyFactory.generatePublic(spec); // EC Public keu to pem format. From 407327a33d32a3f41c63f388bcd7ef8b5f9acc22 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 7 May 2026 11:39:10 -0400 Subject: [PATCH 3/6] feat(sdk): replace ayza libraries with TrustProvider on JCA Remove the three io.github.hakky54:ayza* dependencies and replace their TLS trust-material role with an SDK-owned TrustProvider built on provider-agnostic JCA APIs (CertificateFactory, KeyStore, TrustManagerFactory, SSLContext). This works under any registered crypto provider, including BC-FIPS, and avoids hardcoded provider names. - Add TrustProvider and package-private CompositeX509ExtendedTrustManager for combining JVM default + custom trust material. - SDKBuilder: replace SSLFactory field with SSLSocketFactory + X509TrustManager. sslFactory(SSLFactory) becomes sslFactory(SSLSocketFactory); add sslFactory(SSLSocketFactory, X509TrustManager) for callers that have a matching trust manager. sslFactoryFromDirectory / sslFactoryFromKeyStore signatures and semantics are preserved, now backed by TrustProvider internally. - TokenSource takes SSLSocketFactory directly. - Command.java --insecure path uses TrustProvider.insecure(). - SDKBuilderTest reworked to drop nl.altindag imports and use TrustProvider + standard JCA. Co-Authored-By: Claude Opus 4.7 --- .../java/io/opentdf/platform/Command.java | 8 +- pom.xml | 34 --- sdk/pom.xml | 12 - .../CompositeX509ExtendedTrustManager.java | 122 ++++++++ .../io/opentdf/platform/sdk/SDKBuilder.java | 97 ++++--- .../io/opentdf/platform/sdk/TokenSource.java | 18 +- .../opentdf/platform/sdk/TrustProvider.java | 274 ++++++++++++++++++ .../opentdf/platform/sdk/SDKBuilderTest.java | 34 ++- 8 files changed, 490 insertions(+), 109 deletions(-) create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/CompositeX509ExtendedTrustManager.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/TrustProvider.java diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 3689aa38..5f81f86c 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -18,7 +18,7 @@ import io.opentdf.platform.sdk.KeyType; import io.opentdf.platform.sdk.SDK; import io.opentdf.platform.sdk.SDKBuilder; -import nl.altindag.ssl.SSLFactory; +import io.opentdf.platform.sdk.TrustProvider; import picocli.CommandLine; import picocli.CommandLine.HelpCommand; import picocli.CommandLine.Option; @@ -262,10 +262,8 @@ void encrypt( private SDK buildSDK() { SDKBuilder builder = new SDKBuilder(); if (insecure) { - SSLFactory sslFactory = SSLFactory.builder() - .withUnsafeTrustMaterial() // Trust all certificates - .build(); - builder.sslFactory(sslFactory); + // Trust all certificates + builder.sslFactory(TrustProvider.insecure().getSslSocketFactory()); } return builder.platformEndpoint(platformEndpoint) diff --git a/pom.xml b/pom.xml index 7ccd85d1..5ddcd589 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,6 @@ 1.75.0 4.29.2 1.82 - 10.0.0 1.18.3 0.8.13 @@ -78,39 +77,6 @@ 3.4 provided - - io.github.hakky54 - ayza-for-pem - ${ayza.version} - - - org.slf4j - slf4j-api - - - - - io.github.hakky54 - ayza - ${ayza.version} - - - org.slf4j - slf4j-api - - - - - io.github.hakky54 - ayza-for-netty - ${ayza.version} - - - org.slf4j - slf4j-api - - - io.grpc grpc-netty-shaded diff --git a/sdk/pom.xml b/sdk/pom.xml index 64497996..1f54833a 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -31,18 +31,6 @@ oauth2-oidc-sdk 11.10.1 - - io.github.hakky54 - ayza-for-pem - - - io.github.hakky54 - ayza - - - io.github.hakky54 - ayza-for-netty - com.google.code.gson 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/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/TokenSource.java b/sdk/src/main/java/io/opentdf/platform/sdk/TokenSource.java index 97e02a0c..587e2009 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TokenSource.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TokenSource.java @@ -14,10 +14,10 @@ import com.nimbusds.oauth2.sdk.http.HTTPRequest; import com.nimbusds.oauth2.sdk.http.HTTPResponse; import com.nimbusds.oauth2.sdk.token.AccessToken; -import nl.altindag.ssl.SSLFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLSocketFactory; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -34,22 +34,22 @@ class TokenSource { private final RSAKey rsaKey; private final URI tokenEndpointURI; private final AuthorizationGrant authzGrant; - private final SSLFactory sslFactory; + private final SSLSocketFactory sslSocketFactory; private static final Logger logger = LoggerFactory.getLogger(TokenSource.class); /** * Constructs a new TokenSource with the specified client authentication and RSA key. * - * @param clientAuth the client authentication to be used by the interceptor - * @param rsaKey the RSA key to be used by the interceptor - * @param sslFactory Optional SSLFactory for Requests + * @param clientAuth the client authentication to be used by the interceptor + * @param rsaKey the RSA key to be used by the interceptor + * @param sslSocketFactory Optional SSLSocketFactory for token endpoint requests */ - public TokenSource(ClientAuthentication clientAuth, RSAKey rsaKey, URI tokenEndpointURI, AuthorizationGrant authzGrant, SSLFactory sslFactory) { + public TokenSource(ClientAuthentication clientAuth, RSAKey rsaKey, URI tokenEndpointURI, AuthorizationGrant authzGrant, SSLSocketFactory sslSocketFactory) { this.clientAuth = clientAuth; this.rsaKey = rsaKey; this.tokenEndpointURI = tokenEndpointURI; - this.sslFactory = sslFactory; + this.sslSocketFactory = sslSocketFactory; this.authzGrant = authzGrant; } @@ -108,8 +108,8 @@ private synchronized AccessToken getToken() { TokenRequest tokenRequest = new TokenRequest(this.tokenEndpointURI, clientAuth, authzGrant, null); HTTPRequest httpRequest = tokenRequest.toHTTPRequest(); - if(sslFactory!=null){ - httpRequest.setSSLSocketFactory(sslFactory.getSslSocketFactory()); + if (sslSocketFactory != null) { + httpRequest.setSSLSocketFactory(sslSocketFactory); } DPoPProofFactory dpopFactory = new DefaultDPoPProofFactory(rsaKey, JWSAlgorithm.RS256); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TrustProvider.java b/sdk/src/main/java/io/opentdf/platform/sdk/TrustProvider.java new file mode 100644 index 00000000..9345ddc6 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TrustProvider.java @@ -0,0 +1,274 @@ +package io.opentdf.platform.sdk; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Builds {@link SSLSocketFactory} and {@link X509ExtendedTrustManager} instances for verifying + * X.509 certificate chains during TLS handshakes. + * + *

Implemented 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 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/io/opentdf/platform/sdk/SDKBuilderTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java index cebc9928..9b365543 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()); @@ -305,8 +312,11 @@ public ServerCall.Listener interceptCall(ServerCall Date: Thu, 7 May 2026 11:40:09 -0400 Subject: [PATCH 4/6] remove the provider --- .../io/opentdf/platform/sdk/ECKeyPair.java | 37 ------------------- 1 file changed, 37 deletions(-) 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 920b4198..14fba8ab 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java @@ -6,13 +6,10 @@ import org.bouncycastle.asn1.x9.ECNamedCurveTable; import org.bouncycastle.asn1.x9.X962Parameters; import org.bouncycastle.asn1.x9.X9ECPoint; -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.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMException; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.util.io.pem.*; @@ -28,13 +25,8 @@ // https://www.bouncycastle.org/latest_releases.html public class ECKeyPair { - private static final int SHA256_BYTES = 32; - static { - Security.addProvider(new BouncyCastleProvider()); - } - private final ECCurve curve; public enum ECAlgorithm { @@ -42,8 +34,6 @@ public enum ECAlgorithm { ECDSA } - private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); - private KeyPair keyPair; public ECKeyPair() { @@ -140,33 +130,6 @@ private static byte[] getCompressedECPublicKey(PublicKey publicKey) { return new X9ECPoint(p, true).getPointEncoding(); } - 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"); From db8f73672f9464a88ad080156530294e7a6f1399 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 7 May 2026 11:52:35 -0400 Subject: [PATCH 5/6] add the bc provider --- cmdline/pom.xml | 4 ++++ cmdline/src/main/java/io/opentdf/platform/Command.java | 5 +++++ sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 2 ++ .../services/org.junit.jupiter.api.extension.Extension | 1 + sdk/src/test/resources/junit-platform.properties | 1 + 5 files changed, 13 insertions(+) create mode 100644 sdk/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 sdk/src/test/resources/junit-platform.properties diff --git a/cmdline/pom.xml b/cmdline/pom.xml index 3f82579a..15601058 100644 --- a/cmdline/pom.xml +++ b/cmdline/pom.xml @@ -77,5 +77,9 @@ sdk ${project.version} + + org.bouncycastle + bcprov-jdk18on + diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 5f81f86c..7831cb1b 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -10,6 +10,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; +import java.security.Security; import java.text.ParseException; import com.google.gson.JsonSyntaxException; import io.opentdf.platform.sdk.AssertionConfig; @@ -63,6 +64,10 @@ class Versions { + "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}") class Command { + static { + Security.addProvider(new BouncyCastleProvider()); + } + @Option(names = { "-V", "--version" }, versionHelp = true, description = "display version info") boolean versionInfoRequested; 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..6c824f0f 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -13,6 +13,7 @@ import io.opentdf.platform.sdk.Config.KASInfo; import io.opentdf.platform.sdk.TDF.Reader; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.SecureRandom; +import java.security.Security; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Base64; 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..1cebb76d --- /dev/null +++ b/sdk/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.extensions.autodetection.enabled = true \ No newline at end of file From 2efac406b9530555924535847194021e87a41d14 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 7 May 2026 11:52:35 -0400 Subject: [PATCH 6/6] add the bc provider --- cmdline/pom.xml | 4 ++++ cmdline/src/main/java/io/opentdf/platform/Command.java | 6 ++++++ sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 2 ++ .../services/org.junit.jupiter.api.extension.Extension | 1 + sdk/src/test/resources/junit-platform.properties | 1 + 5 files changed, 14 insertions(+) create mode 100644 sdk/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 sdk/src/test/resources/junit-platform.properties diff --git a/cmdline/pom.xml b/cmdline/pom.xml index 3f82579a..15601058 100644 --- a/cmdline/pom.xml +++ b/cmdline/pom.xml @@ -77,5 +77,9 @@ sdk ${project.version} + + org.bouncycastle + bcprov-jdk18on + diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 5f81f86c..ce63ae31 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -10,6 +10,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; +import java.security.Security; import java.text.ParseException; import com.google.gson.JsonSyntaxException; import io.opentdf.platform.sdk.AssertionConfig; @@ -19,6 +20,7 @@ import io.opentdf.platform.sdk.SDK; import io.opentdf.platform.sdk.SDKBuilder; import io.opentdf.platform.sdk.TrustProvider; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import picocli.CommandLine; import picocli.CommandLine.HelpCommand; import picocli.CommandLine.Option; @@ -63,6 +65,10 @@ class Versions { + "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}") class Command { + static { + Security.addProvider(new BouncyCastleProvider()); + } + @Option(names = { "-V", "--version" }, versionHelp = true, description = "display version info") boolean versionInfoRequested; 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..6c824f0f 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -13,6 +13,7 @@ import io.opentdf.platform.sdk.Config.KASInfo; import io.opentdf.platform.sdk.TDF.Reader; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.SecureRandom; +import java.security.Security; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Base64; 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..1cebb76d --- /dev/null +++ b/sdk/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.extensions.autodetection.enabled = true \ No newline at end of file