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 1a6e6f032e6..784849f73de 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 @@ -190,6 +190,7 @@ 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') diff --git a/build-tools/scripts/src/main/groovy/geode-test.gradle b/build-tools/scripts/src/main/groovy/geode-test.gradle index c3fb6733e36..f0ddb0ee92f 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 09456745fa3..966c298a031 100755 --- a/geode-junit/build.gradle +++ b/geode-junit/build.gradle @@ -20,6 +20,11 @@ plugins { id 'geode-publish-java' } +javadoc { + options.addBooleanOption('Xdoclint:none', true) + exclude '**/HybridCATestFixture.java' +} + dependencies { api(platform(project(':boms:geode-all-bom'))) @@ -53,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 d4a54b55f56..125f934ce72 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 @@ -28,9 +28,9 @@ import java.util.Date; import java.util.List; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import javax.security.auth.x500.X500Principal; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.BasicConstraints; @@ -40,6 +40,13 @@ 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.X509v3CertificateBuilder; +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; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; @@ -60,6 +67,7 @@ public class CertificateBuilder { private final List ipAddresses; private boolean isCA; private CertificateMaterial issuer; +// private final List extendedKeyUsages; private final List extendedKeyUsages; public CertificateBuilder() { @@ -125,6 +133,7 @@ public CertificateBuilder issuedBy(CertificateMaterial issuer) { */ public CertificateBuilder extendedKeyUsage(String... oids) { for (String oid : oids) { +// extendedKeyUsages.add(new ASN1ObjectIdentifier(oid)); extendedKeyUsages.add(KeyPurposeId.getInstance(new ASN1ObjectIdentifier(oid))); } return this; @@ -180,6 +189,7 @@ public CertificateMaterial generate() { 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()); BigInteger sn = new BigInteger(64, new SecureRandom()); 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 00000000000..c082c31160e --- /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> 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(); + } +}