diff --git a/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java b/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java index 46d0c96..0e768a6 100644 --- a/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java +++ b/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java @@ -1,11 +1,15 @@ /* See LICENSE for licensing and NOTICE for copyright. */ package org.cryptacular.adapter; +import java.security.spec.ECField; +import java.security.spec.ECFieldF2m; +import java.security.spec.ECFieldFp; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECKeyParameters; -import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; +import org.bouncycastle.math.ec.ECCurve; /** * Base class for wrapped EC keys. @@ -38,7 +42,7 @@ public ECParameterSpec getParams() final ECDomainParameters params = delegate.getParameters(); return new ECParameterSpec( - EC5Util.convertCurve(params.getCurve(), params.getSeed()), + convertCurve(params.getCurve(), params.getSeed()), new ECPoint( params.getG().normalize().getXCoord().toBigInteger(), params.getG().normalize().getYCoord().toBigInteger()), @@ -52,4 +56,29 @@ public String getAlgorithm() { return ALGORITHM; } + + private static EllipticCurve convertCurve(final ECCurve curve, final byte[] seed) + { + final ECField field; + if (curve instanceof ECCurve.Fp) { + field = new ECFieldFp(((ECCurve.Fp) curve).getQ()); + } else { + final ECCurve.F2m f2m = (ECCurve.F2m) curve; + final int m = f2m.getM(); + final int k1 = f2m.getK1(); + final int k2 = f2m.getK2(); + final int k3 = f2m.getK3(); + if (k2 == 0) { + field = new ECFieldF2m(m, new int[]{k1}); + } else { + field = new ECFieldF2m(m, new int[]{k3, k2, k1}); + } + } + return new EllipticCurve( + field, + curve.getA().toBigInteger(), + curve.getB().toBigInteger(), + seed); + } } + diff --git a/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java b/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java index 2aa5b9f..eab8b5c 100644 --- a/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java +++ b/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java @@ -10,6 +10,7 @@ import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1TaggedObject; +import org.bouncycastle.asn1.x9.ECNamedCurveTable; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.params.AsymmetricKeyParameter; import org.bouncycastle.crypto.params.DSAParameters; @@ -17,7 +18,6 @@ import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; -import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; import org.cryptacular.EncodingException; import org.cryptacular.pbe.OpenSSLAlgorithm; import org.cryptacular.pbe.OpenSSLEncryptionScheme; @@ -129,7 +129,7 @@ private ECPrivateKeyParameters parseECPrivateKey(final ASN1Sequence seq) final ASN1TaggedObject asn1Params = ASN1TaggedObject.getInstance(seq.getObjectAt(2)); final X9ECParameters params; if (asn1Params.getBaseObject() instanceof ASN1ObjectIdentifier) { - params = ECUtil.getNamedCurveByOid(ASN1ObjectIdentifier.getInstance(asn1Params.getBaseObject())); + params = ECNamedCurveTable.getByOID(ASN1ObjectIdentifier.getInstance(asn1Params.getBaseObject())); } else { params = X9ECParameters.getInstance(asn1Params.getBaseObject()); } diff --git a/src/main/java/org/cryptacular/generator/KeyPairGenerator.java b/src/main/java/org/cryptacular/generator/KeyPairGenerator.java index ebf45ae..2ec8320 100644 --- a/src/main/java/org/cryptacular/generator/KeyPairGenerator.java +++ b/src/main/java/org/cryptacular/generator/KeyPairGenerator.java @@ -3,9 +3,11 @@ import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; import org.cryptacular.CryptUtil; +import org.cryptacular.util.CertUtil; /** * Static factory that generates various types of asymmetric key pairs. @@ -33,10 +35,14 @@ public static KeyPair generateDSA(final SecureRandom random, final int bitLength if (bitLength < 1) { throw new IllegalArgumentException("Bit length must be positive"); } - final org.bouncycastle.jcajce.provider.asymmetric.dsa.KeyPairGeneratorSpi generator = - new org.bouncycastle.jcajce.provider.asymmetric.dsa.KeyPairGeneratorSpi(); - generator.initialize(bitLength, random); - return generator.generateKeyPair(); + try { + final java.security.KeyPairGenerator generator = + java.security.KeyPairGenerator.getInstance("DSA", CertUtil.bouncyCastleProvider()); + generator.initialize(bitLength, random); + return generator.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("DSA algorithm not available", e); + } } @@ -54,10 +60,14 @@ public static KeyPair generateRSA(final SecureRandom random, final int bitLength if (bitLength < 1) { throw new IllegalArgumentException("Bit length must be positive"); } - final org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi generator = - new org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi(); - generator.initialize(bitLength, random); - return generator.generateKeyPair(); + try { + final java.security.KeyPairGenerator generator = + java.security.KeyPairGenerator.getInstance("RSA", CertUtil.bouncyCastleProvider()); + generator.initialize(bitLength, random); + return generator.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("RSA algorithm not available", e); + } } @@ -75,10 +85,14 @@ public static KeyPair generateEC(final SecureRandom random, final int bitLength) if (bitLength < 1) { throw new IllegalArgumentException("Bit length must be positive"); } - final org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC generator = - new org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC(); - generator.initialize(bitLength, random); - return generator.generateKeyPair(); + try { + final java.security.KeyPairGenerator generator = + java.security.KeyPairGenerator.getInstance("EC", CertUtil.bouncyCastleProvider()); + generator.initialize(bitLength, random); + return generator.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("EC algorithm not available", e); + } } @@ -94,13 +108,15 @@ public static KeyPair generateEC(final SecureRandom random, final String namedCu { CryptUtil.assertNotNullArg(random, "Secure random cannot be null"); CryptUtil.assertNotNullArg(namedCurve, "Named curve cannot be null"); - final org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC generator = - new org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC(); try { + final java.security.KeyPairGenerator generator = + java.security.KeyPairGenerator.getInstance("EC", CertUtil.bouncyCastleProvider()); generator.initialize(new ECNamedCurveGenParameterSpec(namedCurve), random); + return generator.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("EC algorithm not available", e); } catch (InvalidAlgorithmParameterException e) { throw new IllegalArgumentException("Invalid EC curve " + namedCurve, e); } - return generator.generateKeyPair(); } } diff --git a/src/main/java/org/cryptacular/util/CertUtil.java b/src/main/java/org/cryptacular/util/CertUtil.java index 9bef6ce..b0d01a9 100644 --- a/src/main/java/org/cryptacular/util/CertUtil.java +++ b/src/main/java/org/cryptacular/util/CertUtil.java @@ -10,6 +10,8 @@ import java.nio.CharBuffer; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.Provider; +import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; @@ -37,7 +39,6 @@ import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; @@ -596,7 +597,9 @@ public static X509Certificate generateX509Certificate( final BigInteger serial = BigInteger.valueOf(now.toEpochMilli()); try { - final ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgo).build(keyPair.getPrivate()); + final Provider provider = bouncyCastleProvider(); + final ContentSigner contentSigner = + new JcaContentSignerBuilder(signatureAlgo).setProvider(provider).build(keyPair.getPrivate()); final X500Name x500Name = new X500Name(RFC4519Style.INSTANCE, dn); final X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(x500Name, @@ -607,12 +610,30 @@ public static X509Certificate generateX509Certificate( keyPair.getPublic()) .addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); return new JcaX509CertificateConverter() - .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner)); + .setProvider(provider).getCertificate(certificateBuilder.build(contentSigner)); } catch (OperatorCreationException | CertIOException | CertificateException e) { throw new RuntimeException("Certificate generation error", e); } } + public static Provider bouncyCastleProvider() + { + Provider p = Security.getProvider("BCFIPS"); + if (p != null) { + return p; + } + p = Security.getProvider("BC"); + if (p != null) { + return p; + } + try { + return (Provider) Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider") + .getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException("No BouncyCastle provider found (BC or BCFIPS)", e); + } + } + /** * Describes the behavior of string formatting of X.500 distinguished names. */ diff --git a/src/main/java/org/cryptacular/util/CsrUtil.java b/src/main/java/org/cryptacular/util/CsrUtil.java index a764be7..80cbc4a 100644 --- a/src/main/java/org/cryptacular/util/CsrUtil.java +++ b/src/main/java/org/cryptacular/util/CsrUtil.java @@ -271,7 +271,8 @@ public static PKCS10CertificationRequest generateCsr( throw new CryptoException("Error adding subject alt names to CSR", e); } } - final JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(sigAlg); + final JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(sigAlg) + .setProvider(CertUtil.bouncyCastleProvider()); try { final ContentSigner signer = csBuilder.build(keyPair.getPrivate()); return p10Builder.build(signer); diff --git a/src/main/java/org/cryptacular/util/KeyPairUtil.java b/src/main/java/org/cryptacular/util/KeyPairUtil.java index 9dcbcff..96d6493 100644 --- a/src/main/java/org/cryptacular/util/KeyPairUtil.java +++ b/src/main/java/org/cryptacular/util/KeyPairUtil.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.PrivateKey; @@ -14,6 +15,8 @@ import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.crypto.CryptoException; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.params.AsymmetricKeyParameter; @@ -24,7 +27,8 @@ import org.bouncycastle.crypto.signers.DSASigner; import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.crypto.signers.RSADigestSigner; -import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.crypto.util.PublicKeyFactory; import org.cryptacular.CryptUtil; import org.cryptacular.EncodingException; import org.cryptacular.StreamException; @@ -235,13 +239,13 @@ public static boolean isKeyPair(final ECPublicKey pubKey, final ECPrivateKey pri CryptUtil.assertNotNullArg(privKey, "Private key cannot be null"); final ECDSASigner signer = new ECDSASigner(); try { - signer.init(true, ECUtil.generatePrivateKeyParameter(privKey)); + signer.init(true, PrivateKeyFactory.createKey(PrivateKeyInfo.getInstance(privKey.getEncoded()))); final BigInteger[] sig = signer.generateSignature(SIGN_BYTES); - signer.init(false, ECUtil.generatePublicKeyParameter(pubKey)); + signer.init(false, PublicKeyFactory.createKey(SubjectPublicKeyInfo.getInstance(pubKey.getEncoded()))); return signer.verifySignature(SIGN_BYTES, sig[0], sig[1]); - } catch (Exception e) { - throw new org.cryptacular.CryptoException("Signature computation error", e); + } catch (IOException e) { + throw new org.cryptacular.CryptoException("Key encoding error", e); } } diff --git a/src/test/java/org/cryptacular/generator/KeyPairGeneratorTest.java b/src/test/java/org/cryptacular/generator/KeyPairGeneratorTest.java new file mode 100644 index 0000000..3b5dc59 --- /dev/null +++ b/src/test/java/org/cryptacular/generator/KeyPairGeneratorTest.java @@ -0,0 +1,110 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.assertj.core.api.Assertions.*; + +/** + * Unit test for {@link KeyPairGenerator} class. + * + * @author Middleware Services + */ +public class KeyPairGeneratorTest +{ + private final SecureRandom random = new SecureRandom(); + + private Provider bc; + + @BeforeMethod + public void registerProvider() + { + bc = new BouncyCastleProvider(); + Security.addProvider(bc); + } + + @AfterMethod + public void removeProvider() + { + Security.removeProvider("BC"); + } + + @DataProvider(name = "rsa-key-sizes") + public Object[][] getRsaKeySizes() + { + return new Object[][] { + new Object[] {1024}, + new Object[] {2048}, + }; + } + + @DataProvider(name = "dsa-key-sizes") + public Object[][] getDsaKeySizes() + { + return new Object[][] { + new Object[] {1024}, + }; + } + + @DataProvider(name = "ec-key-sizes") + public Object[][] getEcKeySizes() + { + return new Object[][] { + new Object[] {256}, + new Object[] {384}, + }; + } + + @DataProvider(name = "ec-named-curves") + public Object[][] getEcNamedCurves() + { + return new Object[][] { + new Object[] {"P-256"}, + new Object[] {"P-384"}, + }; + } + + @Test(dataProvider = "rsa-key-sizes") + public void testGenerateRSA(final int bitLength) + { + final RSAPublicKey pub = (RSAPublicKey) KeyPairGenerator.generateRSA(random, bitLength).getPublic(); + assertThat(pub.getModulus().bitLength()).isEqualTo(bitLength); + } + + @Test(dataProvider = "dsa-key-sizes") + public void testGenerateDSA(final int bitLength) + { + final DSAPublicKey pub = (DSAPublicKey) KeyPairGenerator.generateDSA(random, bitLength).getPublic(); + assertThat(pub.getParams().getP().bitLength()).isEqualTo(bitLength); + } + + @Test(dataProvider = "ec-key-sizes") + public void testGenerateECByBitLength(final int bitLength) + { + final ECPublicKey pub = (ECPublicKey) KeyPairGenerator.generateEC(random, bitLength).getPublic(); + assertThat(pub.getParams().getCurve().getField().getFieldSize()).isEqualTo(bitLength); + } + + @Test(dataProvider = "ec-named-curves") + public void testGenerateECByNamedCurve(final String namedCurve) + { + assertThat(KeyPairGenerator.generateEC(random, namedCurve).getPublic()).isInstanceOf(ECPublicKey.class); + } + + @Test + public void testGenerateECInvalidCurveThrows() + { + assertThatThrownBy(() -> KeyPairGenerator.generateEC(random, "not-a-curve")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid EC curve"); + } +} diff --git a/src/test/java/org/cryptacular/util/CertUtilTest.java b/src/test/java/org/cryptacular/util/CertUtilTest.java index 4cdbec0..1f7c5b8 100644 --- a/src/test/java/org/cryptacular/util/CertUtilTest.java +++ b/src/test/java/org/cryptacular/util/CertUtilTest.java @@ -5,7 +5,9 @@ import java.nio.file.Files; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.Provider; import java.security.SecureRandom; +import java.security.Security; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; @@ -15,6 +17,7 @@ import java.util.List; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.cryptacular.FailListener; import org.cryptacular.generator.KeyPairGenerator; import org.cryptacular.x509.GeneralNameType; @@ -498,6 +501,62 @@ public void testGenX509UnSupportedAlgo() } + @Test + public void testBouncyCastleProviderFallsBackToBC() + { + Security.removeProvider("BC"); + try { + final Provider provider = CertUtil.bouncyCastleProvider(); + assertThat(provider).isNotNull(); + assertThat(provider.getName()).isEqualTo("BC"); + } finally { + Security.removeProvider("BC"); + } + } + + @Test + public void testBouncyCastleProviderUsesRegisteredBC() + { + final Provider bc = new BouncyCastleProvider(); + Security.addProvider(bc); + try { + final Provider provider = CertUtil.bouncyCastleProvider(); + assertThat(provider).isSameAs(bc); + } finally { + Security.removeProvider("BC"); + } + } + + @Test + public void testGenX509WithRegisteredBCProvider() + { + final Provider bc = new BouncyCastleProvider(); + Security.addProvider(bc); + try { + final KeyPair keyPair = KeyPairGenerator.generateRSA(new SecureRandom(), 2048); + final String dn = "CN=test.example.org,DC=example,DC=org"; + final X509Certificate cert = CertUtil.generateX509Certificate( + keyPair, dn, Duration.ofDays(365), "SHA256WithRSA"); + assertThat(cert).isNotNull(); + assertThat(CertUtil.subjectCN(cert)).isEqualTo("test.example.org"); + } finally { + Security.removeProvider("BC"); + } + } + + @Test + public void testBouncyCastleProviderPrefersFips() + { + final Provider bcFipsMock = new Provider("BCFIPS", 1.0, "stub") {}; + Security.addProvider(bcFipsMock); + try { + final Provider provider = CertUtil.bouncyCastleProvider(); + assertThat(provider.getName()).isEqualTo("BCFIPS"); + } finally { + Security.removeProvider("BCFIPS"); + } + } + private OffsetDateTime truncateToSeconds(final Instant instant) { return instant.atOffset(ZoneOffset.UTC).withNano(0); diff --git a/src/test/java/org/cryptacular/util/CsrUtilTest.java b/src/test/java/org/cryptacular/util/CsrUtilTest.java index bf52d6f..5fe50cd 100644 --- a/src/test/java/org/cryptacular/util/CsrUtilTest.java +++ b/src/test/java/org/cryptacular/util/CsrUtilTest.java @@ -4,10 +4,13 @@ import java.io.IOException; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.Provider; +import java.security.Security; import java.util.Arrays; import java.util.List; import java.util.Locale; import org.bouncycastle.asn1.pkcs.CertificationRequest; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -124,4 +127,22 @@ public void testGenerateCsr(final String keyAlg) throws Exception assertThat(CsrUtil.commonNames(csr).get(0)).isEqualTo(hostname); assertThat(CsrUtil.subjectAltNames(csr)).isEqualTo(Arrays.asList(sans)); } + + @Test(dataProvider = "key-algs") + public void testGenerateCsrWithRegisteredBCProvider(final String keyAlg) throws Exception + { + final Provider bc = new BouncyCastleProvider(); + Security.addProvider(bc); + try { + final KeyPair keyPair = KeyPairGenerator.getInstance(keyAlg).generateKeyPair(); + final String hostname = keyAlg.toLowerCase(Locale.ROOT) + ".example.org"; + final String dn = "CN=" + hostname + ",DC=example,DC=org"; + final String[] sans = {"dev." + hostname, "pprd." + hostname}; + final CertificationRequest csr = CsrUtil.generateCsr(keyPair, dn, sans).toASN1Structure(); + assertThat(CsrUtil.commonNames(csr).get(0)).isEqualTo(hostname); + assertThat(CsrUtil.subjectAltNames(csr)).isEqualTo(Arrays.asList(sans)); + } finally { + Security.removeProvider("BC"); + } + } }