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