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