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 failuresThe 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 } @ defaultThat 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.MarkDimitris 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 youprovide the failure logs if there are any pending or what version you areusing 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 featuresHow well tested is this patch? And with which OpenSSL versions?The OpenSSL tests can never run because the version check is looking atthe 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 orfailures.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.javab/test/org/apache/tomcat/util/net/TestPQC.javanew 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 ormore+ * contributor license agreements. See the NOTICE file distributedwith+ * this work for additional information regarding copyrightownership.+ * The ASF licenses this file to You under the Apache License,Version 2.0+ * (the "License"); you may not use this file except in compliancewith+ * 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 orimplied.+ * See the License for the specific language governing permissionsand+ * 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(newFile(TesterSupport.CA_CERT_PEM).getAbsolutePath(),+ "X25519MLKEM768", null, null); + } + + + @Test+ public void testHostRSAwithSecP256r1MLKEM768() throws Exception {+ configureHostRSA(); + configureHostWithGroup("SecP256r1MLKEM768"); + doTestWithOpenSSLClient(newFile(TesterSupport.CA_CERT_PEM).getAbsolutePath(),+ "SecP256r1MLKEM768", null, null); + } + + @Test+ public void testHostRSAwithSecP384r1MLKEM1024() throws Exception{+ configureHostRSA(); + configureHostWithGroup("SecP384r1MLKEM1024"); + doTestWithOpenSSLClient(newFile(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() throwsException {+ File[] pqcFiles = configureHostMLDSA("ML-DSA-65"); + configureHostWithGroup("SecP256r1MLKEM768"); + doTestWithOpenSSLClient(pqcFiles[0].getAbsolutePath(),"SecP256r1MLKEM768", null, null);TesterKeystoreGenerator.generatePQCCertificate("testuser", "ML- DSA-65",+ } + + @Test + public void testClientMLDSA() throws Exception { + configureHostRSA(); + File[] clientFiles =+ null, null); + SSLHostConfig sslHostConfig =getTomcatInstance().getConnector().findSslHostConfigs()[0];+ sslHostConfig.setCertificateVerification("required"); +sslHostConfig.setCaCertificateFile(clientFiles[0].getAbsolutePath());+ doTestWithOpenSSLClient(newFile(TesterSupport.CA_CERT_PEM).getAbsolutePath(), null,+ clientFiles[0].getAbsolutePath(),clientFiles[1].getAbsolutePath());TesterKeystoreGenerator.generatePQCCertificate("testuser", "ML- DSA-65",+ } + + @Test + public void testClientMLDSAwithMLDSAServer() throws Exception { + File[] serverFiles = configureHostMLDSA("ML-DSA-65"); + File[] clientFiles =+ 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[] { newTesterSupport.TrustAllCerts() }, null);+ TesterSupport.ClientSSLSocketFactory clientSSLSocketFactory =+ newTesterSupport.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) throwsException {TesterKeystoreGenerator.generatePQCCertificate("localhost", algorithm,+ File[] pqcFiles =+ new String[] { "localhost" }, null); + + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + SSLHostConfig sslHostConfig =connector.findSslHostConfigs()[0];+ + SSLHostConfigCertificate cert = newSSLHostConfigCertificate(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 = newSSLHostConfigCertificate(sslHostConfig, Type.RSA);+ cert.setCertificateFile(newFile(TesterSupport.LOCALHOST_RSA_CERT_PEM).getAbsolutePath());+ cert.setCertificateKeyFile(newFile(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 = newSSLHostConfigCertificate(sslHostConfig, Type.EC);+ cert.setCertificateFile(newFile(TesterSupport.LOCALHOST_EC_CERT_PEM).getAbsolutePath());+ cert.setCertificateKeyFile(newFile(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[] { newTesterSupport.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, Stringgroups,+ 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 = newString(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 --gita/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.javaindex 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 PQCcertificate 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 thecertificate, or {@code null} for none+ * + * @return a two-element array: {@code [0]} is the certificatePEMfile, {@code [1]} is the+ * private key PEM file + * + * @throws Exception if certificate generation fails + */ + public static File[] generatePQCCertificate(String cn, Stringalgorithm, String[] sanNames,+CertificateExtensionsCustomizer customizer) throws Exception {+ BouncyCastleProvider bouncyCastleProvider = newBouncyCastleProvider();+ + 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 = newJcaX509v3CertificateBuilder(subject, serial, notBefore,+ notAfter, subject, keyPair.getPublic()); + + if (sanNames != null && sanNames.length > 0) { + GeneralName[] generalNames = newGeneralName[sanNames.length];+ for (int i = 0; i < sanNames.length; i++) { + generalNames[i] = newGeneralName(GeneralName.dNSName,sanNames[i]);+ } +certBuilder.addExtension(Extension.subjectAlternativeName,false, new GeneralNames(generalNames));+ } + + if (customizer != null) { + customizer.customize(keyPair, certBuilder); + } + + ContentSigner signer = newJcaContentSignerBuilder(algorithm).setProvider(bouncyCastleProvider)+ .build(keyPair.getPrivate()); + X509Certificate certificate = newJcaX509CertificateConverter().setProvider(bouncyCastleProvider)+ .getCertificate(certBuilder.build(signer)); + + File certFile = File.createTempFile("test-pqc-cert-",".pem");+ certFile.deleteOnExit(); + try (JcaPEMWriter writer = new JcaPEMWriter(newFileWriter(certFile))) {+ writer.writeObject(certificate); + } ++ File keyFile = File.createTempFile("test-pqc-key-", ".pem");+ keyFile.deleteOnExit(); + try (JcaPEMWriter writer = new JcaPEMWriter(newFileWriter(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]
