On 13/05/2026 12:44, [email protected] wrote:
This is an automated email from the ASF dual-hosted git repository.
dsoumis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new 7ff10fab8e Add unit tests for PQC features
7ff10fab8e is described below
commit 7ff10fab8ede061fe61524ef96b463fef637429f
Author: Dimitrios Soumis <[email protected]>
AuthorDate: Wed May 13 13:44:42 2026 +0200
Add unit tests for PQC features
How well tested is this patch? And with which OpenSSL versions?
The OpenSSL tests can never run because the version check is looking at
the OpenSSLStatus rather than AprStatus.
With the above fixed, the OpenSSL tests still won't run because the
OpenSSL version isn't set until after the version check.
With that fixed, most of the OpenSSL tests result in errors or failures.
The OpenSSL-FFM tests have a similar failure rate.
Mark
---
test/org/apache/tomcat/util/net/TestPQC.java | 331
+++++++++++++++++++++
.../tomcat/util/net/TesterKeystoreGenerator.java | 65 ++++
2 files changed, 396 insertions(+)
diff --git a/test/org/apache/tomcat/util/net/TestPQC.java
b/test/org/apache/tomcat/util/net/TestPQC.java
new file mode 100644
index 0000000000..db3f53ff60
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/TestPQC.java
@@ -0,0 +1,331 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.TrustManager;
+
+import org.junit.Assert;
+import org.junit.Assume;
+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.connector.Connector;
+import org.apache.catalina.startup.TesterServlet;
+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.SSLHostConfigCertificate.Type;
+import org.apache.tomcat.util.net.openssl.OpenSSLStatus;
+
+@RunWith(Parameterized.class)
+public class TestPQC extends TomcatBaseTest {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ List<Object[]> parameterSets = new ArrayList<>();
+ parameterSets.add(new Object[] {
+ "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net
.jsse.JSSEImplementation"});
+ parameterSets.add(new Object[] {
+ "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net
.openssl.OpenSSLImplementation"});
+ parameterSets.add(new Object[] {
+ "OpenSSL-FFM", Boolean.TRUE, "
org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"});
+ return parameterSets;
+ }
+
+ @Parameter(0)
+ public String connectorName;
+
+ @Parameter(1)
+ public boolean useOpenSSL;
+
+ @Parameter(2)
+ public String sslImplementationName;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ Tomcat tomcat = getTomcatInstance();
+ Connector connector = tomcat.getConnector();
+
+ Assert.assertTrue(connector.setProperty("SSLEnabled", "true"));
+ SSLHostConfig sslHostConfig = new SSLHostConfig();
+ sslHostConfig.setProtocols(Constants.SSL_PROTO_TLSv1_3);
+ connector.addSslHostConfig(sslHostConfig);
+
+ TesterSupport.configureSSLImplementation(tomcat,
sslImplementationName, useOpenSSL);
+
+ Context ctx = getProgrammaticRootContext();
+ Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet());
+ ctx.addServletMappingDecoded("/*", "TesterServlet");
+ }
+
+ @Test
+ public void testHostMLDSA44() throws Exception {
+ File[] pqcFiles = configureHostMLDSA("ML-DSA-44");
+ doTestWithOpenSSLClient(pqcFiles[0].getAbsolutePath(), null,
null, null);
+ }
+
+
+ @Test
+ public void testHostMLDSA65() throws Exception {
+ File[] pqcFiles = configureHostMLDSA("ML-DSA-65");
+ doTestWithOpenSSLClient(pqcFiles[0].getAbsolutePath(), null,
null, null);
+ }
+
+
+ @Test
+ public void testHostMLDSA87() throws Exception {
+ File[] pqcFiles = configureHostMLDSA("ML-DSA-87");
+ doTestWithOpenSSLClient(pqcFiles[0].getAbsolutePath(), null,
null, null);
+ }
+
+ @Test
+ public void testHostRSAandMLDSA() throws Exception {
+ configureHostRSA();
+ configureHostMLDSA("ML-DSA-65");
+ doTest();
+ }
+
+ @Test
+ public void testHostECandMLDSA() throws Exception {
+ configureHostEC();
+ configureHostMLDSA("ML-DSA-65");
+ doTest();
+ }
+
+ @Test
+ public void testHostRSAwithX25519MLKEM768() throws Exception {
+ configureHostRSA();
+ configureHostWithGroup("X25519MLKEM768");
+ doTestWithOpenSSLClient(new
File(TesterSupport.CA_CERT_PEM).getAbsolutePath(),
+ "X25519MLKEM768", null, null);
+ }
+
+
+ @Test
+ public void testHostRSAwithSecP256r1MLKEM768() throws Exception {
+ configureHostRSA();
+ configureHostWithGroup("SecP256r1MLKEM768");
+ doTestWithOpenSSLClient(new
File(TesterSupport.CA_CERT_PEM).getAbsolutePath(),
+ "SecP256r1MLKEM768", null, null);
+ }
+
+ @Test
+ public void testHostRSAwithSecP384r1MLKEM1024() throws Exception {
+ configureHostRSA();
+ configureHostWithGroup("SecP384r1MLKEM1024");
+ doTestWithOpenSSLClient(new
File(TesterSupport.CA_CERT_PEM).getAbsolutePath(),
+ "SecP384r1MLKEM1024", null, null);
+ }
+
+ @Test
+ public void testHostMLDSAwithX25519MLKEM768() throws Exception {
+ File[] pqcFiles = configureHostMLDSA("ML-DSA-65");
+ configureHostWithGroup("X25519MLKEM768");
+ doTestWithOpenSSLClient(pqcFiles[0].getAbsolutePath(),
"X25519MLKEM768", null, null);
+ }
+
+ @Test
+ public void testHostMLDSAwithSecP256r1MLKEM768() throws Exception {
+ File[] pqcFiles = configureHostMLDSA("ML-DSA-65");
+ configureHostWithGroup("SecP256r1MLKEM768");
+ doTestWithOpenSSLClient(pqcFiles[0].getAbsolutePath(),
"SecP256r1MLKEM768", null, null);
+ }
+
+ @Test
+ public void testClientMLDSA() throws Exception {
+ configureHostRSA();
+ File[] clientFiles =
TesterKeystoreGenerator.generatePQCCertificate("testuser", "ML-DSA-65",
+ null, null);
+ SSLHostConfig sslHostConfig =
getTomcatInstance().getConnector().findSslHostConfigs()[0];
+ sslHostConfig.setCertificateVerification("required");
+
sslHostConfig.setCaCertificateFile(clientFiles[0].getAbsolutePath());
+ doTestWithOpenSSLClient(new
File(TesterSupport.CA_CERT_PEM).getAbsolutePath(), null,
+ clientFiles[0].getAbsolutePath(),
clientFiles[1].getAbsolutePath());
+ }
+
+ @Test
+ public void testClientMLDSAwithMLDSAServer() throws Exception {
+ File[] serverFiles = configureHostMLDSA("ML-DSA-65");
+ File[] clientFiles =
TesterKeystoreGenerator.generatePQCCertificate("testuser", "ML-DSA-65",
+ null, null);
+ SSLHostConfig sslHostConfig =
getTomcatInstance().getConnector().findSslHostConfigs()[0];
+ sslHostConfig.setCertificateVerification("required");
+
sslHostConfig.setCaCertificateFile(clientFiles[0].getAbsolutePath());
+ doTestWithOpenSSLClient(serverFiles[0].getAbsolutePath(), null,
+ clientFiles[0].getAbsolutePath(),
clientFiles[1].getAbsolutePath());
+ }
+
+ @Test(expected = SSLHandshakeException.class)
+ public void testHostMLDSAHandshakeFailure() throws Exception {
+ assumePQCSupported();
+ configureHostMLDSA("ML-DSA-65");
+
+ SSLContext sc =
SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2);
+ sc.init(null, new TrustManager[] { new
TesterSupport.TrustAllCerts() }, null);
+ TesterSupport.ClientSSLSocketFactory clientSSLSocketFactory =
+ new
TesterSupport.ClientSSLSocketFactory(sc.getSocketFactory());
+ clientSSLSocketFactory.setProtocols(new String[] {
Constants.SSL_PROTO_TLSv1_2 });
+
javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(clientSSLSocketFactory);
+
+ Tomcat tomcat = getTomcatInstance();
+ tomcat.start();
+ getUrl("https://localhost:" + getPort() + "/");
+ }
+
+
+ private void assumePQCSupported() {
+ if (!useOpenSSL) {
+ Assume.assumeTrue("JSSE does not yet support PQC", false);
+ }
+
+ Assume.assumeTrue("PQC requires OpenSSL 3.5+",
+ OpenSSLStatus.getMajorVersion() > 3 ||
+ OpenSSLStatus.getMajorVersion() == 3 &&
OpenSSLStatus.getMinorVersion() >= 5);
+ }
+
+ private File[] configureHostMLDSA(String algorithm) throws
Exception {
+ File[] pqcFiles =
TesterKeystoreGenerator.generatePQCCertificate("localhost", algorithm,
+ new String[] { "localhost" }, null);
+
+ Tomcat tomcat = getTomcatInstance();
+ Connector connector = tomcat.getConnector();
+ SSLHostConfig sslHostConfig =
connector.findSslHostConfigs()[0];
+
+ SSLHostConfigCertificate cert = new
SSLHostConfigCertificate(sslHostConfig, Type.MLDSA);
+ cert.setCertificateFile(pqcFiles[0].getAbsolutePath());
+ cert.setCertificateKeyFile(pqcFiles[1].getAbsolutePath());
+ sslHostConfig.addCertificate(cert);
+
+ return pqcFiles;
+ }
+
+ private void configureHostRSA() {
+ Tomcat tomcat = getTomcatInstance();
+ Connector connector = tomcat.getConnector();
+ SSLHostConfig sslHostConfig =
connector.findSslHostConfigs()[0];
+
+ SSLHostConfigCertificate cert = new
SSLHostConfigCertificate(sslHostConfig, Type.RSA);
+ cert.setCertificateFile(new
File(TesterSupport.LOCALHOST_RSA_CERT_PEM).getAbsolutePath());
+ cert.setCertificateKeyFile(new
File(TesterSupport.LOCALHOST_RSA_KEY_PEM).getAbsolutePath());
+ cert.setCertificateKeyPassword(TesterSupport.JKS_PASS);
+ sslHostConfig.addCertificate(cert);
+ }
+
+ private void configureHostEC() {
+ Tomcat tomcat = getTomcatInstance();
+ Connector connector = tomcat.getConnector();
+ SSLHostConfig sslHostConfig =
connector.findSslHostConfigs()[0];
+
+ SSLHostConfigCertificate cert = new
SSLHostConfigCertificate(sslHostConfig, Type.EC);
+ cert.setCertificateFile(new
File(TesterSupport.LOCALHOST_EC_CERT_PEM).getAbsolutePath());
+ cert.setCertificateKeyFile(new
File(TesterSupport.LOCALHOST_EC_KEY_PEM).getAbsolutePath());
+ sslHostConfig.addCertificate(cert);
+ }
+
+ private void configureHostWithGroup(String groupName) {
+ Tomcat tomcat = getTomcatInstance();
+ Connector connector = tomcat.getConnector();
+ SSLHostConfig sslHostConfig =
connector.findSslHostConfigs()[0];
+ sslHostConfig.setGroups(groupName);
+ }
+
+ private void doTest() throws Exception {
+ assumePQCSupported();
+ SSLContext sc =
SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3);
+ sc.init(null, new TrustManager[] { new
TesterSupport.TrustAllCerts() }, null);
+
javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ Tomcat tomcat = getTomcatInstance();
+ tomcat.start();
+ ByteChunk res = getUrl("https://localhost:" + getPort() +
"/");
+ Assert.assertEquals("OK", res.toString());
+ }
+
+ private void doTestWithOpenSSLClient(String caFile, String groups,
+ String clientCert, String clientKey) throws Exception {
+ assumePQCSupported();
+
+ Tomcat tomcat = getTomcatInstance();
+ tomcat.start();
+
+ String openSSLPath =
System.getProperty("tomcat.test.openssl.path");
+ String openSSLLibPath = null;
+ if (openSSLPath == null || openSSLPath.length() == 0) {
+ openSSLPath = "openssl";
+ } else {
+ openSSLLibPath = openSSLPath.substring(0,
openSSLPath.lastIndexOf('/'));
+ openSSLLibPath = openSSLLibPath + "/../:" + openSSLLibPath
+ "/../lib:" + openSSLLibPath + "/../lib64";
+ }
+
+ List<String> cmd = new ArrayList<>();
+ cmd.add(openSSLPath);
+ cmd.add("s_client");
+ cmd.add("-connect");
+ cmd.add("localhost:" + getPort());
+ cmd.add("-CAfile");
+ cmd.add(caFile);
+ cmd.add("-tls1_3");
+ if (groups != null) {
+ cmd.add("-groups");
+ cmd.add(groups);
+ }
+ if (clientCert != null) {
+ cmd.add("-cert");
+ cmd.add(clientCert);
+ cmd.add("-key");
+ cmd.add(clientKey);
+ }
+
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+
+ 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);
+ }
+
+ pb.redirectErrorStream(true);
+ Process p = pb.start();
+
+ p.getOutputStream().write("GET / HTTP/1.0\r\nHost:
localhost\r\n\r\n".getBytes());
+ p.getOutputStream().flush();
+
+ String output = new String(p.getInputStream().readAllBytes());
+
+ Assert.assertTrue("Process did not complete in time",
p.waitFor(10, TimeUnit.SECONDS));
+ Assert.assertTrue("TLS handshake failed:\n" + output,
output.contains("HTTP/1."));
+ Assert.assertTrue("Unexpected response body:\n" + output,
output.contains("OK"));
+ }
+}
diff --git
a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
index 9fd4affde6..00f1772fcc 100644
--- a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
+++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
@@ -19,6 +19,7 @@ package org.apache.tomcat.util.net;
import java.io.File;
import java.io.FileOutputStream;
+import java.io.FileWriter;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -33,6 +34,8 @@ import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
@@ -100,4 +103,66 @@ public final class TesterKeystoreGenerator {
return keystoreFile;
}
+
+ /**
+ * Generate temporary PEM files containing a self-signed PQC
certificate and private key.
+ *
+ * @param cn the Common Name for the certificate subject
+ * @param algorithm the PQC algorithm name, e.g. {@code
"ML-DSA-44"}, {@code "ML-DSA-65"},
+ * or {@code "ML-DSA-87"}
+ * @param sanNames DNS Subject Alternative Names to include, or
{@code null} for none
+ * @param customizer callback to add extensions to the
certificate, or {@code null} for none
+ *
+ * @return a two-element array: {@code [0]} is the certificate PEM
file, {@code [1]} is the
+ * private key PEM file
+ *
+ * @throws Exception if certificate generation fails
+ */
+ public static File[] generatePQCCertificate(String cn, String
algorithm, String[] sanNames,
+
CertificateExtensionsCustomizer customizer) throws Exception {
+ BouncyCastleProvider bouncyCastleProvider = new
BouncyCastleProvider();
+
+ KeyPairGenerator keyPairGenerator =
KeyPairGenerator.getInstance(algorithm, bouncyCastleProvider);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ X500Name subject = new X500Name("CN=" + cn);
+ BigInteger serial =
BigInteger.valueOf(System.currentTimeMillis());
+ long oneDay = 86400000L;
+ Date notBefore = new Date(System.currentTimeMillis() - oneDay);
+ Date notAfter = new Date(System.currentTimeMillis() + 365L *
oneDay);
+
+ X509v3CertificateBuilder certBuilder = new
JcaX509v3CertificateBuilder(subject, serial, notBefore,
+ notAfter, subject, keyPair.getPublic());
+
+ if (sanNames != null && sanNames.length > 0) {
+ GeneralName[] generalNames = new
GeneralName[sanNames.length];
+ for (int i = 0; i < sanNames.length; i++) {
+ generalNames[i] = new GeneralName(GeneralName.dNSName,
sanNames[i]);
+ }
+ certBuilder.addExtension(Extension.subjectAlternativeName,
false, new GeneralNames(generalNames));
+ }
+
+ if (customizer != null) {
+ customizer.customize(keyPair, certBuilder);
+ }
+
+ ContentSigner signer = new
JcaContentSignerBuilder(algorithm).setProvider(bouncyCastleProvider)
+ .build(keyPair.getPrivate());
+ X509Certificate certificate = new
JcaX509CertificateConverter().setProvider(bouncyCastleProvider)
+ .getCertificate(certBuilder.build(signer));
+
+ File certFile = File.createTempFile("test-pqc-cert-", ".pem");
+ certFile.deleteOnExit();
+ try (JcaPEMWriter writer = new JcaPEMWriter(new
FileWriter(certFile))) {
+ writer.writeObject(certificate);
+ }
+
+ File keyFile = File.createTempFile("test-pqc-key-", ".pem");
+ keyFile.deleteOnExit();
+ try (JcaPEMWriter writer = new JcaPEMWriter(new
FileWriter(keyFile))) {
+ writer.writeObject(keyPair.getPrivate());
+ }
+
+ return new File[] { certFile, keyFile };
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]