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]

Reply via email to