Progress. Making sure the corrent OpenSSL version gets picked up for both client and server is ... interesting.

OpenSSL-FFM with openssl-master passes
OpenSSL with Native 2.0.x and openssl-master has one failure
OpenSSL with Native 2.0.14 and openssl-master has one failure

OpenSSL-FFM with openssl-3.5.6 passes
OpenSSL with Native 2.0.x and openssl-3.5.6 has three failures
OpenSSL with Native 2.0.14 and openssl-3.5.6 has three failures

The failures with Tomcat Native plus OpenSSL won't have been seen before since the OpenSSL tests weren't running. Don't know what the issue is here yet. At a guess, Tomcat Native isn't setting the groups up correctly and OpenSSL master is more tolerant of this than 3.5.6.

In theory, comparing the Tomcat Native code to the FFM equivalent should idenitfy this issue.

Mark



On 15/05/2026 15:24, Mark Thomas wrote:
On 15/05/2026 15:12, Dimitris Soumis wrote:
PQC should be enabled by default.

Agreed. And it appears to be.

dsoumis@192:~$ openssl list -signature-algorithms 2>&1 | grep -i mldsa
   { 2.16.840.1.101.3.4.3.17, id-ml-dsa-44, ML-DSA-44, MLDSA44 } @ default    { 2.16.840.1.101.3.4.3.18, id-ml-dsa-65, ML-DSA-65, MLDSA65 } @ default    { 2.16.840.1.101.3.4.3.19, id-ml-dsa-87, ML-DSA-87, MLDSA87 } @ default

That is what I see.

We should add an extra check for ML-DSA availability if that's the
issue instead of just checking the version number.
I will add this if you agree.

I don't think that is the issue given I am seeing the same results as you.

I am seeing slightly different behaviour with OpenSSL master.

It is looking more like an environmental issue. I need to keep digging to figure out what is going wrong.

Until I figure out what is going wrong, I don't think it is worth adding additional checks to the tests.

Mark



Dimitris


On Fri, May 15, 2026 at 4:55 PM Mark Thomas <[email protected]> wrote:

I'm seeing lots of failures with 3.5.5

The root cause appears to be:

15-May-2026 12:59:58.147 SEVERE [main]
org.apache.tomcat.util.net.openssl.panama.OpenSSLContext.logLastError
Error loading certificate: [error:0A0000F7:SSL routines::unknown
certificate type]

and similar variations.

Does PQC need to be explicitly enabled in the OpenSSL build?

Mark


On 15/05/2026 14:41, Dimitris Soumis wrote:
OpenSSL-FFM tests did pass for me though with Openssl 3.5.4. Could you
provide the failure logs if there are any pending or what version you are
using that triggers those failures?

On Fri, May 15, 2026 at 4:16 PM Dimitris Soumis <[email protected]>
wrote:

Apologies for the noise. Indeed, it wasn't tested properly. Thanks for
fixing it.

Dimitris

On Fri, May 15, 2026 at 3:02 PM Mark Thomas <[email protected]> wrote:

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]





---------------------------------------------------------------------
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]



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to