This is an automated email from the ASF dual-hosted git repository.
jinwoo pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new 0e5edc311e GEODE-10562: Testcases for Hybrid CA TLS Configuration Test
Suite (#7988)
0e5edc311e is described below
commit 0e5edc311e945d2b40d88658c25a57e4b417cc7a
Author: Jinwoo Hwang <[email protected]>
AuthorDate: Tue Mar 10 08:34:41 2026 -0400
GEODE-10562: Testcases for Hybrid CA TLS Configuration Test Suite (#7988)
* GEODE-10562 :
Testcases — Hybrid Model (Public CA servers, Private CA clients)
* GEODE-10562 :
Testcases — Hybrid Model (Public CA servers, Private CA clients)
* Add sun.security.util exports for CertificateBuilder
- Export sun.security.util package alongside sun.security.x509
- Required for ObjectIdentifier import in CertificateBuilder.java
- Added to both compileJava and javadoc tasks for Java 17 compatibility
* javadoc
---
.../geode/cache/ssl/HybridCASSLDUnitTest.java | 344 ++++++++++++++++++
.../geode/cache/ssl/HybridCASSLNegativeTest.java | 386 +++++++++++++++++++++
geode-junit/build.gradle | 9 +-
.../apache/geode/cache/ssl/CertificateBuilder.java | 42 +++
.../geode/cache/ssl/HybridCATestFixture.java | 206 +++++++++++
5 files changed, 984 insertions(+), 3 deletions(-)
diff --git
a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/HybridCASSLDUnitTest.java
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/HybridCASSLDUnitTest.java
new file mode 100644
index 0000000000..303e829872
--- /dev/null
+++
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/HybridCASSLDUnitTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.apache.geode.distributed.ConfigurationProperties.SSL_ENABLED_COMPONENTS;
+import static
org.apache.geode.distributed.ConfigurationProperties.SSL_ENDPOINT_IDENTIFICATION_ENABLED;
+import static org.apache.geode.security.SecurableCommunicationChannels.CLUSTER;
+import static org.apache.geode.security.SecurableCommunicationChannels.SERVER;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.cache.client.ClientRegionShortcut;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.ClientVM;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.categories.ClientServerTest;
+
+/**
+ * Tests hybrid TLS configuration where:
+ * - Servers use certificates issued by a public CA (e.g., Let's Encrypt,
DigiCert)
+ * - Clients use certificates issued by a private/enterprise CA
+ *
+ * This configuration mitigates the impact of public CA changes that affect the
+ * Client Authentication Extended Key Usage (EKU). See the Apache Geode
security
+ * documentation for details.
+ *
+ * Key requirements validated:
+ * - Server certificates must include serverAuth EKU and subjectAltName
+ * - Client certificates must include clientAuth EKU
+ * - Servers trust the private CA to validate client certificates
+ * - Clients trust the public CA to validate server certificates
+ */
+@Category({ClientServerTest.class})
+public class HybridCASSLDUnitTest {
+
+ private HybridCATestFixture fixture;
+
+ @Rule
+ public ClusterStartupRule cluster = new ClusterStartupRule();
+
+ @Before
+ public void setup() {
+ fixture = new HybridCATestFixture();
+ fixture.setup();
+
+ // Ignore expected exceptions during locator/server shutdown with SSL
+ IgnoredException.addIgnoredException("Could not stop Locator");
+ IgnoredException.addIgnoredException("ForcedDisconnectException");
+ }
+
+ /**
+ * Tests basic client-server connection with hybrid TLS.
+ * Verifies that a client with a private-CA certificate can connect to
+ * a server with a public-CA certificate when trust is properly configured.
+ *
+ * Note: This test uses SSL only for client-server communication (SERVER
component),
+ * not for peer-to-peer cluster communication, which simplifies the test
setup.
+ */
+ @Test
+ public void testHybridTLSBasicConnection() throws Exception {
+ // Start locator without SSL (peer-to-peer communication doesn't require
SSL for this test)
+ MemberVM locator = cluster.startLocatorVM(0);
+
+ // Create server with public-CA certificate, SSL enabled only for SERVER
component
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(SERVER, true, true);
+ serverProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ // Create region on server
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Create client with private-CA certificate
+ CertStores clientStore = fixture.createClientStores("1");
+ Properties clientProps = clientStore.propertiesWith(SERVER, true, true);
+ clientProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ ClientVM client = cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+
+ // Verify client can perform operations
+ client.invoke(() -> {
+ Region<Object, Object> clientRegion = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create("testRegion");
+
+ clientRegion.put("key1", "value1");
+ assertThat(clientRegion.get("key1")).isEqualTo("value1");
+ });
+ }
+
+ /**
+ * Tests that client authentication is properly enforced in hybrid TLS.
+ * A client without a certificate should be rejected.
+ */
+ @Test
+ public void testHybridTLSClientAuthenticationRequired() throws Exception {
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(SERVER, true, true);
+ serverProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ MemberVM locator = cluster.startLocatorVM(0);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Create client with only truststore (no keystore with client certificate)
+ CertStores clientStore = CertStores.clientStore();
+ clientStore.trust("publicCA", fixture.getPublicCA());
+ Properties clientProps = clientStore.propertiesWith(SERVER, true, true);
+ clientProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+ IgnoredException.addIgnoredException("java.io.IOException");
+ IgnoredException.addIgnoredException("Broken pipe");
+
+ // Attempt to create client VM should fail
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ /**
+ * Tests endpoint identification (hostname verification) with hybrid TLS.
+ * Verifies that the server certificate's subjectAltName is validated.
+ */
+ @Test
+ public void testHybridTLSWithEndpointIdentification() throws Exception {
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(SERVER, true, true);
+ serverProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+ serverProps.setProperty(SSL_ENDPOINT_IDENTIFICATION_ENABLED, "true");
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ CertStores clientStore = fixture.createClientStores("1");
+ Properties clientProps = clientStore.propertiesWith(SERVER, true, true);
+ clientProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+ clientProps.setProperty(SSL_ENDPOINT_IDENTIFICATION_ENABLED, "true");
+
+ ClientVM client = cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+
+ client.invoke(() -> {
+ Region<Object, Object> clientRegion = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create("testRegion");
+
+ clientRegion.put("key1", "value1");
+ assertThat(clientRegion.get("key1")).isEqualTo("value1");
+ });
+ }
+
+ /**
+ * Tests that peer-to-peer SSL works with hybrid TLS.
+ * Multiple servers should be able to communicate using public-CA
certificates.
+ */
+ @Test
+ public void testHybridTLSPeerToPeerCommunication() throws Exception {
+ CertStores locatorStore = fixture.createLocatorStores("1");
+ Properties locatorProps = locatorStore.propertiesWith(CLUSTER, false,
true);
+
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+
+ // Start two servers
+ CertStores serverStore1 = fixture.createServerStores("1");
+ Properties serverProps1 = serverStore1.propertiesWith(CLUSTER, true, true);
+ serverProps1.setProperty(SSL_ENABLED_COMPONENTS, CLUSTER);
+ MemberVM server1 = cluster.startServerVM(1, serverProps1,
locator.getPort());
+
+ CertStores serverStore2 = fixture.createServerStores("2");
+ Properties serverProps2 = serverStore2.propertiesWith(CLUSTER, true, true);
+ serverProps2.setProperty(SSL_ENABLED_COMPONENTS, CLUSTER);
+ MemberVM server2 = cluster.startServerVM(2, serverProps2,
locator.getPort());
+
+ // Create replicated region on both servers
+ server1.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ server2.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Put data from server1
+ server1.invoke(() -> {
+ Region<String, String> region =
ClusterStartupRule.getCache().getRegion("testRegion");
+ region.put("key1", "value1");
+ });
+
+ // Verify data replicated to server2
+ server2.invoke(() -> {
+ Region<String, String> region =
ClusterStartupRule.getCache().getRegion("testRegion");
+ assertThat(region.get("key1")).isEqualTo("value1");
+ });
+ }
+
+ /**
+ * Tests that clients can connect when only SERVER component has SSL enabled.
+ * This validates component-specific SSL configuration.
+ */
+ @Test
+ public void testHybridTLSServerComponentOnly() throws Exception {
+ // Server uses hybrid TLS only for SERVER component
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(SERVER, true, true);
+ // Locator doesn't need SSL
+ serverProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ MemberVM locator = cluster.startLocatorVM(0);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Client uses hybrid TLS for SERVER component
+ CertStores clientStore = fixture.createClientStores("1");
+ Properties clientProps = clientStore.propertiesWith(SERVER, true, true);
+ clientProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ ClientVM client = cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+
+ client.invoke(() -> {
+ Region<Object, Object> clientRegion = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create("testRegion");
+
+ clientRegion.put("key1", "value1");
+ assertThat(clientRegion.get("key1")).isEqualTo("value1");
+ });
+ }
+
+ /**
+ * Tests multiple clients connecting with different private-CA certificates.
+ * Validates that each client can authenticate independently.
+ */
+ @Test
+ public void testHybridTLSMultipleClients() throws Exception {
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(SERVER, true, true);
+ serverProps.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ MemberVM locator = cluster.startLocatorVM(0);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Create first client
+ CertStores clientStore1 = fixture.createClientStores("client1");
+ Properties clientProps1 = clientStore1.propertiesWith(SERVER, true, true);
+ clientProps1.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ ClientVM client1 = cluster.startClientVM(2, clientProps1,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+
+ client1.invoke(() -> {
+ Region<Object, Object> clientRegion1 =
ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create("testRegion");
+
+ clientRegion1.put("client1-key", "client1-value");
+ });
+
+ // Create second client with different certificate
+ CertStores clientStore2 = fixture.createClientStores("client2");
+ Properties clientProps2 = clientStore2.propertiesWith(SERVER, true, true);
+ clientProps2.setProperty(SSL_ENABLED_COMPONENTS, SERVER);
+
+ ClientVM client2 = cluster.startClientVM(3, clientProps2,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+
+ client2.invoke(() -> {
+ Region<Object, Object> clientRegion2 =
ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create("testRegion");
+
+ // Verify second client can see first client's data
+ assertThat(clientRegion2.get("client1-key")).isEqualTo("client1-value");
+
+ // Verify second client can put its own data
+ clientRegion2.put("client2-key", "client2-value");
+ });
+
+ // Verify first client can see second client's data
+ client1.invoke(() -> {
+ Region<Object, Object> clientRegion1 =
+ ClusterStartupRule.getClientCache().getRegion("testRegion");
+ assertThat(clientRegion1.get("client2-key")).isEqualTo("client2-value");
+ });
+ }
+}
diff --git
a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/HybridCASSLNegativeTest.java
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/HybridCASSLNegativeTest.java
new file mode 100644
index 0000000000..30e7b8a4cc
--- /dev/null
+++
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/HybridCASSLNegativeTest.java
@@ -0,0 +1,386 @@
+/*
+ * 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.apache.geode.distributed.ConfigurationProperties.SSL_ENDPOINT_IDENTIFICATION_ENABLED;
+import static org.apache.geode.security.SecurableCommunicationChannels.ALL;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.categories.ClientServerTest;
+
+/**
+ * Negative tests for hybrid TLS configuration.
+ * Validates that improper configurations are properly rejected with
appropriate errors.
+ *
+ * These tests verify the troubleshooting scenarios documented in the security
guide:
+ * - Missing clientAuth EKU causes "certificate_unknown" alert
+ * - Wrong CA trust causes PKIX path validation failure
+ * - Missing subjectAltName causes hostname verification failure
+ */
+@Category({ClientServerTest.class})
+public class HybridCASSLNegativeTest {
+
+ private HybridCATestFixture fixture;
+
+ @Rule
+ public ClusterStartupRule cluster = new ClusterStartupRule();
+
+ @Before
+ public void setup() {
+ fixture = new HybridCATestFixture();
+ fixture.setup();
+
+ // Ignore expected exceptions during locator/server shutdown with SSL
+ IgnoredException.addIgnoredException("Could not stop Locator");
+ IgnoredException.addIgnoredException("ForcedDisconnectException");
+ }
+
+ /**
+ * Tests that a server configured to trust the wrong CA rejects client
connections.
+ * Expected error: PKIX path validation failed
+ */
+ @Test
+ public void testServerTrustsWrongCAForClient() throws Exception {
+ // Create a different CA that server will trust (but client cert is not
issued by it)
+ CertificateMaterial wrongCA = new CertificateBuilder()
+ .commonName("Wrong CA")
+ .isCA()
+ .generate();
+
+ // Server certificate from public CA
+ CertificateMaterial serverCert =
fixture.createServerCertificate("server-1");
+
+ // Server trusts wrong CA (not the private CA that issued client cert)
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCert);
+ serverStore.trust("wrongCA", wrongCA); // Should trust privateCA instead
+
+ Properties serverProps = serverStore.propertiesWith(ALL, true, true);
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Client with private-CA certificate
+ CertStores clientStore = fixture.createClientStores("1");
+ Properties clientProps = clientStore.propertiesWith(ALL, true, true);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+
IgnoredException.addIgnoredException("sun.security.validator.ValidatorException");
+ IgnoredException.addIgnoredException("PKIX path");
+ IgnoredException.addIgnoredException("java.io.IOException");
+ IgnoredException.addIgnoredException("Broken pipe");
+
+ // Client connection should fail with PKIX path validation error
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ /**
+ * Tests that a client configured to trust the wrong CA rejects server
connections.
+ * Expected error: PKIX path validation failed
+ */
+ @Test
+ public void testClientTrustsWrongCAForServer() throws Exception {
+ // Create a different CA that client will trust (but server cert is not
issued by it)
+ CertificateMaterial wrongCA = new CertificateBuilder()
+ .commonName("Wrong CA")
+ .isCA()
+ .generate();
+
+ // Server with correct configuration
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(ALL, true, true);
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Client trusts wrong CA (not the public CA that issued server cert)
+ CertificateMaterial clientCert =
fixture.createClientCertificate("client-1");
+ CertStores clientStore = CertStores.clientStore();
+ clientStore.withCertificate("client", clientCert);
+ clientStore.trust("wrongCA", wrongCA); // Should trust publicCA instead
+
+ Properties clientProps = clientStore.propertiesWith(ALL, true, true);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+
IgnoredException.addIgnoredException("sun.security.validator.ValidatorException");
+ IgnoredException.addIgnoredException("PKIX path");
+
IgnoredException.addIgnoredException("java.security.cert.CertificateException");
+
+ // Client connection should fail with PKIX path validation error
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ /**
+ * Tests that a client certificate without clientAuth EKU is rejected.
+ * Expected error: certificate_unknown (as documented in troubleshooting
guide)
+ *
+ * This validates the critical requirement that client certificates must have
+ * the clientAuth Extended Key Usage.
+ */
+ @Test
+ public void testClientCertificateMissingClientAuthEKU() throws Exception {
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(ALL, true, true);
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Client certificate WITHOUT clientAuth EKU
+ CertificateMaterial clientCert =
+ fixture.createClientCertificateWithoutClientAuthEKU("client-1");
+ CertStores clientStore = CertStores.clientStore();
+ clientStore.withCertificate("client", clientCert);
+ clientStore.trust("publicCA", fixture.getPublicCA());
+
+ Properties clientProps = clientStore.propertiesWith(ALL, true, true);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+ IgnoredException.addIgnoredException("certificate_unknown");
+ IgnoredException.addIgnoredException("java.io.IOException");
+ IgnoredException.addIgnoredException("Broken pipe");
+ IgnoredException.addIgnoredException("Connection reset");
+
+ // Connection should fail with certificate_unknown or handshake failure
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ /**
+ * Tests that a server certificate without subjectAltName fails hostname
verification.
+ * Expected error: No subject alternative names present (when endpoint
identification enabled)
+ *
+ * This validates the requirement that server certificates must include SAN
for hostname
+ * verification.
+ */
+ @Test
+ public void testServerCertificateMissingSAN() throws Exception {
+ // Server certificate without SAN
+ CertificateMaterial serverCert =
fixture.createServerCertificateWithoutSAN("server-1");
+
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCert);
+ serverStore.trust("privateCA", fixture.getPrivateCA());
+
+ Properties serverProps = serverStore.propertiesWith(ALL, true, true);
+ serverProps.setProperty(SSL_ENDPOINT_IDENTIFICATION_ENABLED, "true");
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ CertStores clientStore = fixture.createClientStores("1");
+ Properties clientProps = clientStore.propertiesWith(ALL, true, true);
+ clientProps.setProperty(SSL_ENDPOINT_IDENTIFICATION_ENABLED, "true");
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+
IgnoredException.addIgnoredException("java.security.cert.CertificateException");
+ IgnoredException.addIgnoredException("No subject alternative");
+ IgnoredException.addIgnoredException("No name matching");
+
+ // Connection should fail with hostname verification error
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ /**
+ * Tests that mutual authentication is enforced when
ssl-require-authentication=true.
+ * A server without a client certificate in its keystore should not be able
to join as a client.
+ */
+ @Test
+ public void testMutualAuthenticationEnforced() throws Exception {
+ CertStores serverStore = fixture.createServerStores("1");
+ Properties serverProps = serverStore.propertiesWith(ALL, true, true);
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ // Create a "client" with no certificate in keystore, only truststore
+ CertStores invalidClientStore = CertStores.clientStore();
+ // Only trust, no certificate
+ invalidClientStore.trust("publicCA", fixture.getPublicCA());
+
+ Properties clientProps = invalidClientStore.propertiesWith(ALL, true,
true);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+ IgnoredException.addIgnoredException("bad_certificate");
+ IgnoredException.addIgnoredException("java.io.IOException");
+ IgnoredException.addIgnoredException("Broken pipe");
+
+ // Connection should fail - server requires client authentication
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+
+ /**
+ * Tests that a server certificate without serverAuth EKU might be rejected
+ * (behavior depends on TLS implementation, but good practice to include it).
+ */
+ @Test
+ public void testServerCertificateWithoutServerAuthEKU() throws Exception {
+ // Create server cert without serverAuth EKU
+ CertificateMaterial serverCert = new CertificateBuilder()
+ .commonName("server-1")
+ .issuedBy(fixture.getPublicCA())
+ .sanDnsName("localhost")
+ // Intentionally omit serverAuthEKU()
+ .generate();
+
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCert);
+ serverStore.trust("privateCA", fixture.getPrivateCA());
+
+ Properties serverProps = serverStore.propertiesWith(ALL, true, true);
+
+ MemberVM locator = cluster.startLocatorVM(0, serverProps);
+ MemberVM server = cluster.startServerVM(1, serverProps, locator.getPort());
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create("testRegion");
+ });
+
+ CertStores clientStore = fixture.createClientStores("1");
+ Properties clientProps = clientStore.propertiesWith(ALL, true, true);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+ IgnoredException.addIgnoredException("extended key usage");
+
+ // Some TLS implementations may reject this, others may accept
+ // The test documents that including serverAuth EKU is best practice
+ try {
+ cluster.startClientVM(2, clientProps,
+ ccf -> ccf.addPoolLocator("localhost", locator.getPort()));
+ // If it succeeds, that's OK - not all implementations strictly enforce
+ } catch (Exception e) {
+ // If it fails, verify it's SSL-related
+ assertThatThrownBy(() -> {
+ throw e;
+ }).hasCauseInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+ } finally {
+ IgnoredException.removeAllExpectedExceptions();
+ }
+ }
+
+ /**
+ * Tests mixed configuration where one server has correct hybrid TLS and
another doesn't.
+ * The misconfigured server should fail to join the cluster.
+ */
+ @Test
+ public void testMisconfiguredServerCannotJoinCluster() throws Exception {
+ CertStores locatorStore = fixture.createLocatorStores("1");
+ Properties locatorProps = locatorStore.propertiesWith(ALL, true, true);
+
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+
+ // First server with correct configuration
+ CertStores serverStore1 = fixture.createServerStores("1");
+ Properties serverProps1 = serverStore1.propertiesWith(ALL, true, true);
+ MemberVM server1 = cluster.startServerVM(1, serverProps1,
locator.getPort());
+
+ // Second server with wrong CA certificates
+ CertificateMaterial wrongCA = new CertificateBuilder()
+ .commonName("Wrong CA")
+ .isCA()
+ .generate();
+
+ CertificateMaterial wrongServerCert = new CertificateBuilder()
+ .commonName("server-2")
+ .issuedBy(wrongCA)
+ .serverAuthEKU()
+ .sanDnsName("localhost")
+ .generate();
+
+ CertStores serverStore2 = CertStores.serverStore();
+ serverStore2.withCertificate("server", wrongServerCert);
+ serverStore2.trust("wrongCA", wrongCA);
+
+ Properties serverProps2 = serverStore2.propertiesWith(ALL, true, true);
+
+
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
+ IgnoredException.addIgnoredException("PKIX path");
+ IgnoredException.addIgnoredException("ForcedDisconnectException");
+ IgnoredException.addIgnoredException("java.io.IOException");
+
+ // Second server should fail to join cluster
+ assertThatThrownBy(() -> {
+ cluster.startServerVM(2, serverProps2, locator.getPort());
+ }).hasCauseInstanceOf(java.io.IOException.class);
+
+ IgnoredException.removeAllExpectedExceptions();
+ }
+}
diff --git a/geode-junit/build.gradle b/geode-junit/build.gradle
index dd1e51de36..f2c19d86b6 100755
--- a/geode-junit/build.gradle
+++ b/geode-junit/build.gradle
@@ -22,14 +22,17 @@ plugins {
compileJava {
// -Xlint:-sunapi flag removed as it doesn't exist in Java 17
- // Added --add-exports for sun.security.x509 package access needed for
CertificateBuilder
+ // 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 {
- // Add --add-exports for sun.security.x509 package access needed for
CertificateBuilder javadoc generation
- options.addStringOption('-add-exports',
'java.base/sun.security.x509=ALL-UNNAMED')
+ // Exclude classes that use internal sun.security packages to avoid javadoc
errors
+ options.addBooleanOption('Xdoclint:none', true)
+ exclude '**/CertificateBuilder.java'
+ exclude '**/HybridCATestFixture.java'
}
dependencies {
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 45995a19b8..66cec02670 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
@@ -30,6 +30,7 @@ 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;
@@ -39,6 +40,7 @@ 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;
@@ -64,6 +66,7 @@ public class CertificateBuilder {
private final List<InetAddress> ipAddresses;
private boolean isCA;
private CertificateMaterial issuer;
+ private final List<ObjectIdentifier> extendedKeyUsages;
public CertificateBuilder() {
this(30, "SHA256withRSA");
@@ -74,6 +77,7 @@ public class CertificateBuilder {
this.algorithm = algorithm;
dnsNames = new ArrayList<>();
ipAddresses = new ArrayList<>();
+ extendedKeyUsages = new ArrayList<>();
}
private static GeneralName dnsGeneralName(String name) {
@@ -130,6 +134,38 @@ public class CertificateBuilder {
return this;
}
+ /**
+ * Add Extended Key Usage purposes to the certificate.
+ * Common purposes:
+ * - "1.3.6.1.5.5.7.3.1" = serverAuth (TLS Web Server Authentication)
+ * - "1.3.6.1.5.5.7.3.2" = clientAuth (TLS Web Client Authentication)
+ * - "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);
+ }
+ return this;
+ }
+
+ /**
+ * Add TLS Web Client Authentication Extended Key Usage (for client
certificates).
+ */
+ public CertificateBuilder clientAuthEKU() {
+ return extendedKeyUsage("1.3.6.1.5.5.7.3.2");
+ }
+
+ /**
+ * Add TLS Web Server Authentication Extended Key Usage (for server
certificates).
+ */
+ public CertificateBuilder serverAuthEKU() {
+ return extendedKeyUsage("1.3.6.1.5.5.7.3.1");
+ }
+
private GeneralNames san() throws IOException {
GeneralNames names = new GeneralNames();
for (String name : dnsNames) {
@@ -210,6 +246,12 @@ public class CertificateBuilder {
extensions.set(BasicConstraintsExtension.NAME, basicConstraints);
}
+ 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);
}
diff --git
a/geode-junit/src/main/java/org/apache/geode/cache/ssl/HybridCATestFixture.java
b/geode-junit/src/main/java/org/apache/geode/cache/ssl/HybridCATestFixture.java
new file mode 100644
index 0000000000..f5e5c41e7c
--- /dev/null
+++
b/geode-junit/src/main/java/org/apache/geode/cache/ssl/HybridCATestFixture.java
@@ -0,0 +1,206 @@
+/*
+ * 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 java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Test fixture for creating hybrid TLS certificate configurations where
servers use
+ * public-CA-issued certificates and clients use private-CA-issued
certificates.
+ */
+public class HybridCATestFixture {
+ private CertificateMaterial publicCA;
+ private CertificateMaterial privateCA;
+
+ /**
+ * Initialize the fixture by creating both public and private CAs.
+ */
+ public void setup() {
+ // Create public CA (simulates certificates from Let's Encrypt, DigiCert,
etc.)
+ publicCA = new CertificateBuilder()
+ .commonName("Public CA Root")
+ .isCA()
+ .generate();
+
+ // Create private/enterprise CA (for client certificates)
+ privateCA = new CertificateBuilder()
+ .commonName("Enterprise Internal CA")
+ .isCA()
+ .generate();
+ }
+
+ /**
+ * Get the public CA certificate material.
+ */
+ public CertificateMaterial getPublicCA() {
+ return publicCA;
+ }
+
+ /**
+ * Get the private CA certificate material.
+ */
+ public CertificateMaterial getPrivateCA() {
+ return privateCA;
+ }
+
+ /**
+ * Create a server certificate issued by the public CA.
+ * The certificate includes:
+ * - serverAuth Extended Key Usage
+ * - subjectAltName with DNS names and IP addresses for hostname verification
+ *
+ * @param hostname the server's hostname/common name
+ * @return certificate material for the server
+ */
+ public CertificateMaterial createServerCertificate(String hostname) {
+ try {
+ return new CertificateBuilder()
+ .commonName(hostname)
+ .issuedBy(publicCA)
+ .serverAuthEKU()
+ .sanDnsName(hostname)
+ .sanDnsName("localhost")
+ .sanDnsName(InetAddress.getLocalHost().getHostName())
+ .sanDnsName(InetAddress.getLocalHost().getCanonicalHostName())
+ .sanIpAddress(InetAddress.getLocalHost())
+ .sanIpAddress(InetAddress.getLoopbackAddress())
+ .sanIpAddress("0.0.0.0") // for Windows compatibility
+ .generate();
+ } catch (UnknownHostException e) {
+ throw new RuntimeException("Unable to determine localhost information",
e);
+ }
+ }
+
+ /**
+ * Create a server certificate with minimal SAN (for negative testing).
+ *
+ * @param hostname the server's hostname/common name
+ * @return certificate material for the server
+ */
+ public CertificateMaterial createServerCertificateMinimalSAN(String
hostname) {
+ return new CertificateBuilder()
+ .commonName(hostname)
+ .issuedBy(publicCA)
+ .serverAuthEKU()
+ .sanDnsName(hostname)
+ .generate();
+ }
+
+ /**
+ * Create a server certificate without any SAN (for negative testing).
+ *
+ * @param hostname the server's hostname/common name
+ * @return certificate material for the server
+ */
+ public CertificateMaterial createServerCertificateWithoutSAN(String
hostname) {
+ return new CertificateBuilder()
+ .commonName(hostname)
+ .issuedBy(publicCA)
+ .serverAuthEKU()
+ .generate();
+ }
+
+ /**
+ * Create a client certificate issued by the private CA.
+ * The certificate includes clientAuth Extended Key Usage which is required
for mTLS.
+ *
+ * @param clientName the client's common name (e.g., "[email protected]")
+ * @return certificate material for the client
+ */
+ public CertificateMaterial createClientCertificate(String clientName) {
+ return new CertificateBuilder()
+ .commonName(clientName)
+ .issuedBy(privateCA)
+ .clientAuthEKU()
+ .generate();
+ }
+
+ /**
+ * Create a client certificate WITHOUT clientAuth EKU (for negative testing).
+ * This certificate will be rejected by servers requiring client
authentication.
+ *
+ * @param clientName the client's common name
+ * @return certificate material for the client (invalid for mTLS)
+ */
+ public CertificateMaterial
createClientCertificateWithoutClientAuthEKU(String clientName) {
+ return new CertificateBuilder()
+ .commonName(clientName)
+ .issuedBy(privateCA)
+ // Intentionally omit .clientAuthEKU()
+ .generate();
+ }
+
+ /**
+ * Create server CertStores configured for hybrid TLS.
+ * - Keystore contains server certificate issued by public CA
+ * - Truststore contains BOTH CAs:
+ * * Public CA: to validate other servers/locators (peer-to-peer)
+ * * Private CA: to validate client certificates
+ *
+ * @param serverId identifier for this server's certificates
+ * @return configured CertStores for server
+ */
+ public CertStores createServerStores(String serverId) {
+ CertificateMaterial serverCert = createServerCertificate("server-" +
serverId);
+
+ CertStores serverStore = CertStores.serverStore();
+ serverStore.withCertificate("server", serverCert);
+ serverStore.trust("publicCA", publicCA); // Trust public CA for
peer-to-peer
+ serverStore.trust("privateCA", privateCA); // Trust private CA to validate
clients
+
+ return serverStore;
+ }
+
+ /**
+ * Create client CertStores configured for hybrid TLS.
+ * - Keystore contains client certificate issued by private CA
+ * - Truststore contains public CA to validate server certificates
+ *
+ * @param clientId identifier for this client's certificates
+ * @return configured CertStores for client
+ */
+ public CertStores createClientStores(String clientId) {
+ CertificateMaterial clientCert = createClientCertificate("client-" +
clientId);
+
+ CertStores clientStore = CertStores.clientStore();
+ clientStore.withCertificate("client", clientCert);
+ clientStore.trust("publicCA", publicCA); // Trust public CA to validate
servers
+
+ return clientStore;
+ }
+
+ /**
+ * Create locator CertStores configured for hybrid TLS.
+ * Locators use the same configuration as servers:
+ * - Keystore contains locator certificate issued by public CA
+ * - Truststore contains BOTH CAs:
+ * * Public CA: to validate other locators/servers (peer-to-peer)
+ * * Private CA: to validate client certificates
+ *
+ * @param locatorId identifier for this locator's certificates
+ * @return configured CertStores for locator
+ */
+ public CertStores createLocatorStores(String locatorId) {
+ CertificateMaterial locatorCert = createServerCertificate("locator-" +
locatorId);
+
+ CertStores locatorStore = CertStores.locatorStore();
+ locatorStore.withCertificate("locator", locatorCert);
+ locatorStore.trust("publicCA", publicCA); // Trust public CA for
peer-to-peer
+ locatorStore.trust("privateCA", privateCA); // Trust private CA to
validate clients
+
+ return locatorStore;
+ }
+}