diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index a06b3201..ecf0843e 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -91,7 +91,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUF_INPUT_HTTPS_USERNAME: opentdf-bot BUF_INPUT_HTTPS_PASSWORD: ${{ secrets.PERSONAL_ACCESS_TOKEN_OPENTDF }} - run: mvn --batch-mode clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=opentdf_java-sdk -P coverage + run: | + mvn --batch-mode clean verify -P 'fips,!non-fips' && \ + mvn --batch-mode verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dmaven.antrun.skip -Dsonar.projectKey=opentdf_java-sdk -P 'coverage,non-fips,!fips' platform-integration: runs-on: ubuntu-22.04 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..6ce3488c 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,9 @@ 1.75.0 4.29.2 1.82 - 10.0.0 + 2.1.2 + 2.1.11 + 2.1.23 1.18.3 0.8.13 @@ -78,39 +80,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 @@ -157,6 +126,26 @@ bcprov-jdk18on ${bouncycastle.version} + + org.bouncycastle + bctls-jdk18on + ${bouncycastle.version} + + + org.bouncycastle + bc-fips + ${bc-fips.version} + + + org.bouncycastle + bcpkix-fips + ${bcpkix-fips.version} + + + org.bouncycastle + bctls-fips + ${bctls-fips.version} + + + + @@ -31,18 +35,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 @@ -160,15 +152,7 @@ 6.0.53 provided - - - org.bouncycastle - bcpkix-jdk18on - - - org.bouncycastle - bcprov-jdk18on - + org.junit.jupiter @@ -483,11 +467,66 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + @{argLine} ${java.security.properties.test} + + - + + non-fips + + true + + + + org.bouncycastle + bcprov-jdk18on + runtime + + + org.bouncycastle + bcpkix-jdk18on + runtime + + + org.bouncycastle + bctls-jdk18on + runtime + + + + + fips + + false + + + -Djava.security.properties=${project.basedir}/src/test/java.security.fips.test + + + + org.bouncycastle + bc-fips + runtime + + + org.bouncycastle + bcpkix-fips + runtime + + + org.bouncycastle + bctls-fips + runtime + + + + fuzz 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 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