You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2026/01/13 11:37:41 UTC
(tomcat) 01/02: Add tests for validity of client certificate
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 <ma...@apache.org>
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: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org