This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new 0c4534124f Add tests for validity of client certificate
0c4534124f is described below
commit 0c4534124fb02cf9e73e92b261af922f377e6c2e
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 2e006b3bb3..0136491a21 100644
--- a/test/org/apache/tomcat/util/net/TestSsl.java
+++ b/test/org/apache/tomcat/util/net/TestSsl.java
@@ -259,7 +259,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);
@@ -282,7 +282,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 b51a7dbb68..6a769978f6 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();
@@ -138,7 +146,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());
}
@@ -153,10 +165,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++) {
@@ -178,18 +190,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;
@@ -198,7 +218,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]