This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 5ba6246fda4c1b72fa495430d6014328acf976c9
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Dec 4 17:19:03 2025 +0000

    Add tests for validity of client certificate
    
    Includes exposing the OCSP test responder provided by OpenSSL
---
 test/org/apache/tomcat/util/net/TestSsl.java       |   4 +-
 test/org/apache/tomcat/util/net/TesterSupport.java |  38 ++++-
 .../tomcat/util/net/ocsp/TestOcspEnabled.java      | 187 +++++++++++++++++++++
 .../tomcat/util/net/ocsp/TesterOcspResponder.java  | 123 ++++++++++++++
 .../tomcat/util/net/ocsp/ocsp-responder.lock       |   0
 5 files changed, 341 insertions(+), 11 deletions(-)

diff --git a/test/org/apache/tomcat/util/net/TestSsl.java 
b/test/org/apache/tomcat/util/net/TestSsl.java
index 6ae2dde1d1..ef87ed26e4 100644
--- a/test/org/apache/tomcat/util/net/TestSsl.java
+++ b/test/org/apache/tomcat/util/net/TestSsl.java
@@ -258,7 +258,7 @@ public class TestSsl extends TomcatBaseTest {
         Context ctxt  = tomcat.addWebapp(null, "/examples", 
appDir.getAbsolutePath());
         ctxt.addApplicationListener(WsContextListener.class.getName());
 
-        TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS,
+        TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, 
false,
                 TesterSupport.JKS_PASS, null, TesterSupport.JKS_KEY_PASS, 
null);
 
         TesterSupport.configureSSLImplementation(tomcat, 
sslImplementationName, useOpenSSL);
@@ -281,7 +281,7 @@ public class TestSsl extends TomcatBaseTest {
         Context ctxt  = tomcat.addWebapp(null, "/examples", 
appDir.getAbsolutePath());
         ctxt.addApplicationListener(WsContextListener.class.getName());
 
-        TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS,
+        TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, 
false,
                       null, TesterSupport.JKS_PASS_FILE, null, 
TesterSupport.JKS_KEY_PASS_FILE);
 
         TesterSupport.configureSSLImplementation(tomcat, 
sslImplementationName, useOpenSSL);
diff --git a/test/org/apache/tomcat/util/net/TesterSupport.java 
b/test/org/apache/tomcat/util/net/TesterSupport.java
index 9ea402da1a..675b17cffe 100644
--- a/test/org/apache/tomcat/util/net/TesterSupport.java
+++ b/test/org/apache/tomcat/util/net/TesterSupport.java
@@ -90,6 +90,8 @@ public final class TesterSupport {
     public static final String DB_INDEX = SSL_DIR + "index.db";
     public static final String OCSP_RESPONDER_RSA_CERT = SSL_DIR + 
"ocsp-responder-rsa-cert.pem";
     public static final String OCSP_RESPONDER_RSA_KEY = SSL_DIR + 
"ocsp-responder-rsa-key.pem";
+    public static final String LOCALHOST_CRL_RSA_JKS = SSL_DIR + 
"localhost-crl-rsa.jks";
+    public static final String CLIENT_CRL_JKS = SSL_DIR + "user2-crl.jks";
     public static final boolean TLSV13_AVAILABLE;
 
     public static final String ROLE = "testrole";
@@ -113,10 +115,16 @@ public final class TesterSupport {
     }
 
     public static void initSsl(Tomcat tomcat) {
-        initSsl(tomcat, LOCALHOST_RSA_JKS, null, null, null, null);
+        // TLS material for tests uses default password
+        initSsl(tomcat, LOCALHOST_RSA_JKS, false);
     }
 
-    protected static void initSsl(Tomcat tomcat, String keystore,
+    public static void initSsl(Tomcat tomcat, String keystore, boolean 
opensslTrust) {
+        // TLS material for tests uses default password
+        initSsl(tomcat, keystore, opensslTrust, null, null, null, null);
+    }
+
+    protected static void initSsl(Tomcat tomcat, String keystore, boolean 
opensslTrust,
             String keystorePass, String keystorePassFile, String keyPass, 
String keyPassFile) {
 
         Connector connector = tomcat.getConnector();
@@ -140,7 +148,11 @@ public final class TesterSupport {
             }
             sslHostConfig.setSslProtocol("tls");
             certificate.setCertificateKeystoreFile(new 
File(keystore).getAbsolutePath());
-            sslHostConfig.setTruststoreFile(new 
File(CA_JKS).getAbsolutePath());
+            if (opensslTrust) {
+                sslHostConfig.setCaCertificateFile(new 
File(CA_CERT_PEM).getAbsolutePath());
+            } else {
+                sslHostConfig.setTruststoreFile(new 
File(CA_JKS).getAbsolutePath());
+            }
             if (keystorePassFile != null) {
                 certificate.setCertificateKeystorePasswordFile(new 
File(keystorePassFile).getAbsolutePath());
             }
@@ -184,10 +196,10 @@ public final class TesterSupport {
         }
     }
 
-    protected static KeyManager[] getUser1KeyManagers() throws Exception {
+    protected static KeyManager[] getUserKeyManagers(String keyStore) throws 
Exception {
         KeyManagerFactory kmf = KeyManagerFactory.getInstance(
                 KeyManagerFactory.getDefaultAlgorithm());
-        kmf.init(getKeyStore(CLIENT_JKS), JKS_PASS.toCharArray());
+        kmf.init(getKeyStore(keyStore), JKS_PASS.toCharArray());
         KeyManager[] managers = kmf.getKeyManagers();
         KeyManager manager;
         for (int i=0; i < managers.length; i++) {
@@ -209,18 +221,26 @@ public final class TesterSupport {
     }
 
     public static ClientSSLSocketFactory configureClientSsl() {
-        return configureClientSsl(false);
+        return configureClientSsl(false, null, CLIENT_JKS);
+    }
+
+    public static ClientSSLSocketFactory configureClientSsl(String keyStore) {
+        return configureClientSsl(false, null, keyStore);
     }
 
     public static ClientSSLSocketFactory configureClientSsl(String[] ciphers) {
-        return configureClientSsl(false, ciphers);
+        return configureClientSsl(false, ciphers, CLIENT_JKS);
     }
 
     public static ClientSSLSocketFactory configureClientSsl(boolean 
forceTls12) {
-        return configureClientSsl(forceTls12, null);
+        return configureClientSsl(forceTls12, null, CLIENT_JKS);
     }
 
     public static ClientSSLSocketFactory configureClientSsl(boolean 
forceTls12, String[] ciphers) {
+        return configureClientSsl(forceTls12, ciphers, CLIENT_JKS);
+    }
+
+    public static ClientSSLSocketFactory configureClientSsl(boolean 
forceTls12, String[] ciphers, String keyStore) {
         ClientSSLSocketFactory clientSSLSocketFactory = null;
         try {
             SSLContext sc;
@@ -229,7 +249,7 @@ public final class TesterSupport {
             } else {
                 sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2);
             }
-            sc.init(getUser1KeyManagers(), getTrustManagers(), null);
+            sc.init(getUserKeyManagers(keyStore), getTrustManagers(), null);
             clientSSLSocketFactory = new 
ClientSSLSocketFactory(sc.getSocketFactory());
             if (ciphers != null) {
                 clientSSLSocketFactory.setCipher(ciphers);
diff --git a/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java 
b/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java
new file mode 100644
index 0000000000..31b77e1709
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java
@@ -0,0 +1,187 @@
+/*
+ * 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.tomcat.util.net.ocsp;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.net.SSLHostConfig;
+import org.apache.tomcat.util.net.TesterSupport;
+import org.apache.tomcat.util.net.TesterSupport.SimpleServlet;
+
+
+@RunWith(Parameterized.class)
+public class TestOcspEnabled extends TomcatBaseTest {
+
+    private static TesterOcspResponder ocspResponder;
+    private static final File lockFile = new 
File("test/org/apache/tomcat/util/net/ocsp/ocsp-responder.lock");
+    private static FileLock lock = null;
+
+    @BeforeClass
+    public static void obtainOcspResponderLock() throws IOException {
+        @SuppressWarnings("resource")
+        FileOutputStream fos = new FileOutputStream(lockFile);
+        lock = fos.getChannel().lock();
+    }
+
+    @AfterClass
+    public static void releaseOcspResponderLock() throws IOException {
+        // Should not be null be in case obtaining the lock fails, avoid a 
second error.
+        if (lock != null) {
+            lock.release();
+        }
+    }
+
+
+    @Parameterized.Parameters(name = "{0} with OpenSSL trust {2}")
+    public static Collection<Object[]> parameters() {
+        List<Object[]> parameterSets = new ArrayList<>();
+        parameterSets.add(new Object[] { "JSSE", Boolean.FALSE, Boolean.FALSE,
+                "org.apache.tomcat.util.net.jsse.JSSEImplementation"});
+        parameterSets.add(new Object[] { "OpenSSL", Boolean.TRUE, Boolean.TRUE,
+                "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" });
+        parameterSets.add(new Object[] { "OpenSSL", Boolean.TRUE, 
Boolean.FALSE,
+                "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" });
+        parameterSets.add(new Object[] { "OpenSSL-FFM", Boolean.TRUE, 
Boolean.TRUE,
+                
"org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" });
+        parameterSets.add(new Object[] { "OpenSSL-FFM", Boolean.TRUE, 
Boolean.FALSE,
+                
"org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" });
+
+        return parameterSets;
+    }
+
+    @Parameter(0)
+    public String connectorName;
+
+    @Parameter(1)
+    public boolean useOpenSSL;
+
+    @Parameter(2)
+    public boolean useOpenSSLTrust;
+
+    @Parameter(3)
+    public String sslImplementationName;
+
+
+    @BeforeClass
+    public static void startOcspResponder() throws IOException {
+        ocspResponder = new TesterOcspResponder();
+        ocspResponder.start();
+    }
+
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        Tomcat tomcat = getTomcatInstance();
+        TesterSupport.configureSSLImplementation(tomcat, 
sslImplementationName, useOpenSSL);
+    }
+
+
+    @AfterClass
+    public static void stopOcspResponder() {
+        ocspResponder.stop();
+        ocspResponder = null;
+    }
+
+
+    @Test
+    public void testValidClientValidServerVerifyNone() throws Exception {
+        doTest(true, true, false, false, HttpServletResponse.SC_OK);
+    }
+
+    @Test
+    public void testValidClientRevokedServerVerifyNone() throws Exception {
+        doTest(true, false, false, false, HttpServletResponse.SC_OK);
+    }
+
+    @Test
+    public void testRevokedClientValidServerVerifyNone() throws Exception {
+        doTest(false, true, false, false, HttpServletResponse.SC_OK);
+    }
+
+    @Test
+    public void testRevokedClientRevokedServerVerifyNone() throws Exception {
+        doTest(false, false, false, false, HttpServletResponse.SC_OK);
+    }
+
+
+    @Test
+    public void testValidClientValidServerVerifyClient() throws Exception {
+        doTest(true, true, true, false, HttpServletResponse.SC_OK);
+    }
+
+    @Test(expected = SSLHandshakeException.class)
+    public void testRevokedClientValidServerVerifyClient() throws Exception {
+        doTest(false, true, true, false, HttpServletResponse.SC_OK);
+    }
+
+
+    private void doTest(boolean clientCertValid, boolean serverCertValid, 
boolean verifyClientCert,
+            boolean verifyServerCert, int expectedStatusCode) throws Exception 
{
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        Context ctx = tomcat.addContext("", null);
+
+        Tomcat.addServlet(ctx, "simple", new SimpleServlet());
+        ctx.addServletMappingDecoded("/simple", "simple");
+
+        if (serverCertValid) {
+            TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_RSA_JKS, 
useOpenSSLTrust);
+        } else {
+            TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_CRL_RSA_JKS, 
useOpenSSLTrust);
+        }
+        SSLHostConfig sslHostConfig = 
tomcat.getConnector().findSslHostConfigs()[0];
+        sslHostConfig.setCertificateVerification("required");
+        sslHostConfig.setOcspEnabled(verifyClientCert);
+
+        if (clientCertValid) {
+            TesterSupport.configureClientSsl(TesterSupport.CLIENT_JKS);
+        } else {
+            TesterSupport.configureClientSsl(TesterSupport.CLIENT_CRL_JKS);
+        }
+        // TODO enable client-side OCSP checks
+
+        tomcat.start();
+
+        int rc = getUrl("https://localhost:"; + getPort() + "/simple", new 
ByteChunk(), false);
+
+        Assert.assertEquals(expectedStatusCode, rc);
+    }
+}
diff --git a/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java 
b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java
new file mode 100644
index 0000000000..eb7fa10189
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java
@@ -0,0 +1,123 @@
+/*
+ * 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.tomcat.util.net.ocsp;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Assert;
+
+import org.apache.tomcat.util.net.TesterSupport;
+
+/*
+ * The OpenSSL ocsp tool is great. But is does generate a lot of output. That 
needs to be swallowed else the
+ * process will freeze with the output buffers (stdout and stderr) are full.
+ *
+ * There is a command line option to redirect stdout (which could be 
redirected to /dev/null) but there is no option to
+ * redirect stderr. Therefore, this class uses a couple of dedicated threads 
to read stdout and stderr. By default, the
+ * output is ignored but it can be dumped to Java's stdout/stderr if required 
for debugging purposes.
+ */
+public class TesterOcspResponder {
+
+    private static List<String> ocspArgs = Arrays.asList("ocsp", "-port", 
"8888", "-text", "-index",
+            TesterSupport.DB_INDEX, "-CA", TesterSupport.CA_CERT_PEM, "-rkey", 
TesterSupport.OCSP_RESPONDER_RSA_KEY,
+            "-rsigner", TesterSupport.OCSP_RESPONDER_RSA_CERT, "-nmin", "60");
+
+    private Process p;
+
+    public void start() throws IOException {
+        if (p != null) {
+            throw new IllegalStateException("Already started");
+        }
+
+        String openSSLPath = System.getProperty("tomcat.test.openssl.path");
+        String openSSLLibPath = null;
+        if (openSSLPath == null || openSSLPath.length() == 0) {
+            openSSLPath = "openssl";
+        } else {
+            // Explicit OpenSSL path may also need explicit lib path
+            // (e.g. Gump needs this)
+            openSSLLibPath = openSSLPath.substring(0, 
openSSLPath.lastIndexOf('/'));
+            openSSLLibPath = openSSLLibPath + "/../:" + openSSLLibPath + 
"/../lib:" + openSSLLibPath + "/../lib64";
+        }
+        List<String> cmd = new ArrayList<>();
+        cmd.add(openSSLPath);
+        cmd.addAll(ocspArgs);
+
+        ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0]));
+
+        if (openSSLLibPath != null) {
+            Map<String,String> env = pb.environment();
+            String libraryPath = env.get("LD_LIBRARY_PATH");
+            if (libraryPath == null) {
+                libraryPath = openSSLLibPath;
+            } else {
+                libraryPath = libraryPath + ":" + openSSLLibPath;
+            }
+            env.put("LD_LIBRARY_PATH", libraryPath);
+        }
+
+        p = pb.start();
+
+        redirect(p.inputReader(), System.out, true);
+        redirect(p.errorReader(), System.err, true);
+
+        Assert.assertTrue(p.isAlive());
+    }
+
+    public void stop() {
+        if (p == null) {
+            throw new IllegalStateException("Not started");
+        }
+        p.destroy();
+
+        try {
+            if (!p.waitFor(30, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("Failed to stop");
+            }
+        } catch (InterruptedException e) {
+            throw new IllegalStateException("Interrupted while waiting to 
stop", e);
+        }
+    }
+
+
+    private void redirect(final Reader r, final PrintStream os, final boolean 
swallow) {
+        /*
+         * InputStream will close when process ends. Thread will exit once 
stream closes.
+         */
+        new Thread( () -> {
+            char[] cbuf = new char[1024];
+            try {
+                int read;
+                while ((read = r.read(cbuf)) > 0) {
+                    if (!swallow) {
+                        os.print(new String(cbuf, 0, read));
+                    }
+                }
+            } catch (IOException ignore) {
+                // Ignore
+            }
+
+        }).start();
+    }
+}
diff --git a/test/org/apache/tomcat/util/net/ocsp/ocsp-responder.lock 
b/test/org/apache/tomcat/util/net/ocsp/ocsp-responder.lock
new file mode 100644
index 0000000000..e69de29bb2


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to