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;
+  }
+}


Reply via email to