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

Reply via email to