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