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]
