This is an automated email from the ASF dual-hosted git repository. hubgeter pushed a commit to branch task4-geode-10509-certificatebuilder-bouncycastle in repository https://gitbox.apache.org/repos/asf/geode.git
commit 7275e8c6fc04147062577650d87152ae9ada04c5 Author: daidai <[email protected]> AuthorDate: Fri Jun 5 15:05:00 2026 +0800 GEODE-10509: Replaced sun.security.x509.* usages with Bouncy Castle (`bcpkix-jdk18on`) public APIs --- .../gradle/plugins/DependencyConstraints.groovy | 2 + .../scripts/src/main/groovy/geode-test.gradle | 1 - geode-junit/build.gradle | 13 +- .../apache/geode/cache/ssl/CertificateBuilder.java | 157 +++++++-------------- .../ssl/CertificateBuilderExtensionsTest.java | 116 +++++++++++++++ 5 files changed, 175 insertions(+), 114 deletions(-) diff --git a/build-tools/geode-dependency-management/src/main/groovy/org/apache/geode/gradle/plugins/DependencyConstraints.groovy b/build-tools/geode-dependency-management/src/main/groovy/org/apache/geode/gradle/plugins/DependencyConstraints.groovy index d71e6717ba..e7ea889164 100644 --- a/build-tools/geode-dependency-management/src/main/groovy/org/apache/geode/gradle/plugins/DependencyConstraints.groovy +++ b/build-tools/geode-dependency-management/src/main/groovy/org/apache/geode/gradle/plugins/DependencyConstraints.groovy @@ -183,6 +183,8 @@ class DependencyConstraints { api(group: 'org.apache.shiro', name: 'shiro-core', version: get('shiro.version')) // GEODE-10583: Pin Bouncy Castle provider (pulled in via shiro-crypto-hash) to 1.84 api(group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: get('bouncycastle.version')) + // GEODE-10509: Bouncy Castle PKIX for X.509 certificate building in test utilities + api(group: 'org.bouncycastle', name: 'bcpkix-jdk18on', version: get('bouncycastle.version')) api(group: 'org.assertj', name: 'assertj-core', version: '3.22.0') api(group: 'org.awaitility', name: 'awaitility', version: '4.2.0') api(group: 'org.buildobjects', name: 'jproc', version: '2.8.0') diff --git a/build-tools/scripts/src/main/groovy/geode-test.gradle b/build-tools/scripts/src/main/groovy/geode-test.gradle index 73a56dfd2f..073ec43e79 100644 --- a/build-tools/scripts/src/main/groovy/geode-test.gradle +++ b/build-tools/scripts/src/main/groovy/geode-test.gradle @@ -249,7 +249,6 @@ gradle.taskGraph.whenReady({ graph -> "--add-opens=java.xml/jdk.xml.internal=ALL-UNNAMED", "--add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED", - "--add-exports=java.base/sun.security.x509=ALL-UNNAMED", "--add-exports=java.management/com.sun.jmx.remote.security=ALL-UNNAMED", ] diff --git a/geode-junit/build.gradle b/geode-junit/build.gradle index f2c19d86b6..02e135c5b0 100755 --- a/geode-junit/build.gradle +++ b/geode-junit/build.gradle @@ -20,18 +20,8 @@ plugins { id 'geode-publish-java' } -compileJava { - // -Xlint:-sunapi flag removed as it doesn't exist in Java 17 - // Added --add-exports for sun.security packages needed for CertificateBuilder - options.compilerArgs << '-XDenableSunApiLintControl' - options.compilerArgs << '--add-exports=java.base/sun.security.x509=ALL-UNNAMED' - options.compilerArgs << '--add-exports=java.base/sun.security.util=ALL-UNNAMED' -} - javadoc { - // Exclude classes that use internal sun.security packages to avoid javadoc errors options.addBooleanOption('Xdoclint:none', true) - exclude '**/CertificateBuilder.java' exclude '**/HybridCATestFixture.java' } @@ -68,6 +58,9 @@ dependencies { api('org.apache.commons:commons-lang3') api('org.apache.logging.log4j:log4j-api') + // GEODE-10509: X.509 certificate building for the CertificateBuilder test utility + implementation('org.bouncycastle:bcpkix-jdk18on') + api('org.awaitility:awaitility') api('org.hamcrest:hamcrest') api('io.micrometer:micrometer-core') diff --git a/geode-junit/src/main/java/org/apache/geode/cache/ssl/CertificateBuilder.java b/geode-junit/src/main/java/org/apache/geode/cache/ssl/CertificateBuilder.java index 66cec02670..6a5f17704d 100644 --- a/geode-junit/src/main/java/org/apache/geode/cache/ssl/CertificateBuilder.java +++ b/geode-junit/src/main/java/org/apache/geode/cache/ssl/CertificateBuilder.java @@ -14,8 +14,6 @@ */ package org.apache.geode.cache.ssl; -import java.io.IOException; -import java.io.UncheckedIOException; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; @@ -30,28 +28,21 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import sun.security.util.ObjectIdentifier; -import sun.security.x509.AlgorithmId; -import sun.security.x509.BasicConstraintsExtension; -import sun.security.x509.CertificateAlgorithmId; -import sun.security.x509.CertificateExtensions; -import sun.security.x509.CertificateSerialNumber; -import sun.security.x509.CertificateValidity; -import sun.security.x509.CertificateVersion; -import sun.security.x509.CertificateX509Key; -import sun.security.x509.DNSName; -import sun.security.x509.ExtendedKeyUsageExtension; -import sun.security.x509.GeneralName; -import sun.security.x509.GeneralNames; -import sun.security.x509.IPAddressName; -import sun.security.x509.KeyIdentifier; -import sun.security.x509.KeyUsageExtension; -import sun.security.x509.SubjectAlternativeNameExtension; -import sun.security.x509.SubjectKeyIdentifierExtension; -import sun.security.x509.X500Name; -import sun.security.x509.X509CertImpl; -import sun.security.x509.X509CertInfo; - +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; /** * Class which allows easily building certificates. It can also be used to build @@ -66,7 +57,7 @@ public class CertificateBuilder { private final List<InetAddress> ipAddresses; private boolean isCA; private CertificateMaterial issuer; - private final List<ObjectIdentifier> extendedKeyUsages; + private final List<ASN1ObjectIdentifier> extendedKeyUsages; public CertificateBuilder() { this(30, "SHA256withRSA"); @@ -81,27 +72,15 @@ public class CertificateBuilder { } private static GeneralName dnsGeneralName(String name) { - try { - return new GeneralName(new DNSName(name)); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } + return new GeneralName(GeneralName.dNSName, name); } private static GeneralName ipGeneralName(InetAddress hostAddress) { - try { - return new GeneralName(new IPAddressName(hostAddress.getAddress())); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } + return new GeneralName(GeneralName.iPAddress, hostAddress.getHostAddress()); } public CertificateBuilder commonName(String cn) { - try { - name = new X500Name("O=Geode, CN=" + cn); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } + name = new X500Name("O=Geode, CN=" + cn); return this; } @@ -142,12 +121,8 @@ public class CertificateBuilder { * - "1.3.6.1.5.5.7.3.3" = codeSigning */ public CertificateBuilder extendedKeyUsage(String... oids) { - try { - for (String oid : oids) { - extendedKeyUsages.add(ObjectIdentifier.of(oid)); - } - } catch (IOException ex) { - throw new UncheckedIOException(ex); + for (String oid : oids) { + extendedKeyUsages.add(new ASN1ObjectIdentifier(oid)); } return this; } @@ -166,17 +141,17 @@ public class CertificateBuilder { return extendedKeyUsage("1.3.6.1.5.5.7.3.1"); } - private GeneralNames san() throws IOException { - GeneralNames names = new GeneralNames(); - for (String name : dnsNames) { - names.add(CertificateBuilder.dnsGeneralName(name)); + private GeneralNames subjectAlternativeNames() { + List<GeneralName> names = new ArrayList<>(); + for (String dnsName : dnsNames) { + names.add(CertificateBuilder.dnsGeneralName(dnsName)); } for (InetAddress address : ipAddresses) { names.add(CertificateBuilder.ipGeneralName(address)); } - return names; + return new GeneralNames(names.toArray(new GeneralName[0])); } public CertificateMaterial generate() { @@ -202,71 +177,47 @@ public class CertificateBuilder { private X509Certificate generate(PublicKey publicKey, PrivateKey privateKey) { Date from = new Date(); Date to = new Date(from.getTime() + days * 86_400_000L); + BigInteger serialNumber = new BigInteger(64, new SecureRandom()); - CertificateValidity interval = new CertificateValidity(from, to); - BigInteger sn = new BigInteger(64, new SecureRandom()); - - X509CertInfo info = new X509CertInfo(); + X500Name issuerName; + if (issuer == null) { + // This is a self-signed certificate + issuerName = name; + } else { + issuerName = + X500Name.getInstance(issuer.getCertificate().getSubjectX500Principal().getEncoded()); + } try { - info.set(X509CertInfo.VALIDITY, interval); - info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn)); - info.set(X509CertInfo.SUBJECT, name); - info.set(X509CertInfo.KEY, new CertificateX509Key(publicKey)); - info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); - AlgorithmId algo = AlgorithmId.get("MD5withRSA"); - info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo)); - - if (issuer == null) { - // This is a self-signed certificate - info.set(X509CertInfo.ISSUER, name); - } else { - info.set(X509CertInfo.ISSUER, issuer.getCertificate().getSubjectDN()); - } - - CertificateExtensions extensions = new CertificateExtensions(); + JcaX509v3CertificateBuilder certBuilder = + new JcaX509v3CertificateBuilder(issuerName, serialNumber, from, to, name, publicKey); - byte[] keyIdBytes = new KeyIdentifier(publicKey).getIdentifier(); - SubjectKeyIdentifierExtension keyIdentifier = new SubjectKeyIdentifierExtension(keyIdBytes); - extensions.set(SubjectKeyIdentifierExtension.NAME, keyIdentifier); + JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils(); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, + extensionUtils.createSubjectKeyIdentifier(publicKey)); - GeneralNames subjectAltNames = san(); - if (!subjectAltNames.isEmpty()) { - SubjectAlternativeNameExtension altNames = - new SubjectAlternativeNameExtension(subjectAltNames); - extensions.set(SubjectAlternativeNameExtension.NAME, altNames); + GeneralNames subjectAltNames = subjectAlternativeNames(); + if (subjectAltNames.getNames().length > 0) { + certBuilder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); } if (isCA) { - KeyUsageExtension usageExtension = new KeyUsageExtension(); - usageExtension.set(KeyUsageExtension.KEY_CERTSIGN, true); - extensions.set(KeyUsageExtension.NAME, usageExtension); - - BasicConstraintsExtension basicConstraints = new BasicConstraintsExtension(true, 0); - extensions.set(BasicConstraintsExtension.NAME, basicConstraints); + certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); + certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(0)); } if (!extendedKeyUsages.isEmpty()) { - ExtendedKeyUsageExtension ekuExtension = - new ExtendedKeyUsageExtension(new java.util.Vector<>(extendedKeyUsages)); - extensions.set(ExtendedKeyUsageExtension.NAME, ekuExtension); - } - - if (!extensions.getAllExtensions().isEmpty()) { - info.set(X509CertInfo.EXTENSIONS, extensions); + KeyPurposeId[] keyPurposeIds = new KeyPurposeId[extendedKeyUsages.size()]; + for (int i = 0; i < extendedKeyUsages.size(); i++) { + keyPurposeIds[i] = KeyPurposeId.getInstance(extendedKeyUsages.get(i)); + } + certBuilder.addExtension(Extension.extendedKeyUsage, false, + new ExtendedKeyUsage(keyPurposeIds)); } - // Sign the cert to identify the algorithm that's used. - X509CertImpl cert = new X509CertImpl(info); - cert.sign(privateKey, algorithm); - - // Update the algorithm, and resign. - algo = (AlgorithmId) cert.get(X509CertImpl.SIG_ALG); - info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo); - cert = new X509CertImpl(info); - cert.sign(privateKey, algorithm); - - return cert; + ContentSigner signer = new JcaContentSignerBuilder(algorithm).build(privateKey); + X509CertificateHolder certHolder = certBuilder.build(signer); + return new JcaX509CertificateConverter().getCertificate(certHolder); } catch (Exception ex) { throw new RuntimeException("Unable to create certificate", ex); } diff --git a/geode-junit/src/test/java/org/apache/geode/cache/ssl/CertificateBuilderExtensionsTest.java b/geode-junit/src/test/java/org/apache/geode/cache/ssl/CertificateBuilderExtensionsTest.java new file mode 100644 index 0000000000..c082c31160 --- /dev/null +++ b/geode-junit/src/test/java/org/apache/geode/cache/ssl/CertificateBuilderExtensionsTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.geode.cache.ssl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.net.InetAddress; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; + +import org.junit.Test; + +/** + * Verifies that the certificates produced by {@link CertificateBuilder} carry the expected X.509 + * extensions, so that the Bouncy Castle implementation is functionally equivalent to the previous + * {@code sun.security.x509}-based one (GEODE-10509). + */ +public class CertificateBuilderExtensionsTest { + + // X509Certificate.getSubjectAlternativeNames() general-name type tags (RFC 5280) + private static final int SAN_DNS = 2; + private static final int SAN_IP = 7; + + @Test + public void subjectIsSetFromCommonName() { + X509Certificate cert = new CertificateBuilder().commonName("test-host").generate() + .getCertificate(); + + assertThat(cert.getSubjectX500Principal().getName()) + .contains("CN=test-host") + .contains("O=Geode"); + assertThat(cert.getVersion()).isEqualTo(3); + } + + @Test + public void subjectAlternativeNamesContainDnsAndIp() throws Exception { + X509Certificate cert = new CertificateBuilder() + .commonName("test-host") + .sanDnsName("example.com") + .sanIpAddress(InetAddress.getByName("127.0.0.1")) + .generate() + .getCertificate(); + + Collection<List<?>> sans = cert.getSubjectAlternativeNames(); + assertThat(sans).isNotNull(); + assertThat(sans).anySatisfy(san -> { + assertThat(san.get(0)).isEqualTo(SAN_DNS); + assertThat(san.get(1)).isEqualTo("example.com"); + }); + assertThat(sans).anySatisfy(san -> { + assertThat(san.get(0)).isEqualTo(SAN_IP); + assertThat(san.get(1)).isEqualTo("127.0.0.1"); + }); + } + + @Test + public void caCertificateHasBasicConstraintsAndKeyCertSign() { + X509Certificate ca = new CertificateBuilder().commonName("my ca").isCA().generate() + .getCertificate(); + + // getBasicConstraints() returns the path length (>= 0) for a CA, or -1 for a non-CA. + assertThat(ca.getBasicConstraints()).isGreaterThanOrEqualTo(0); + // KeyUsage bit 5 is keyCertSign. + assertThat(ca.getKeyUsage()).isNotNull(); + assertThat(ca.getKeyUsage()[5]).isTrue(); + } + + @Test + public void nonCaCertificateHasNoBasicConstraints() { + X509Certificate cert = new CertificateBuilder().commonName("leaf").generate().getCertificate(); + + assertThat(cert.getBasicConstraints()).isEqualTo(-1); + } + + @Test + public void extendedKeyUsageContainsServerAndClientAuth() throws Exception { + X509Certificate cert = new CertificateBuilder() + .commonName("svc") + .serverAuthEKU() + .clientAuthEKU() + .generate() + .getCertificate(); + + assertThat(cert.getExtendedKeyUsage()) + .contains("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2"); + } + + @Test + public void issuedCertificateIsSignedByAndChainsToTheIssuer() { + CertificateMaterial ca = new CertificateBuilder().commonName("my ca").isCA().generate(); + X509Certificate leaf = new CertificateBuilder() + .commonName("leaf") + .issuedBy(ca) + .generate() + .getCertificate(); + + assertThat(leaf.getIssuerX500Principal()) + .isEqualTo(ca.getCertificate().getSubjectX500Principal()); + // The leaf's signature must verify against the issuer's public key. + assertThatCode(() -> leaf.verify(ca.getCertificate().getPublicKey())).doesNotThrowAnyException(); + } +}
