This is an automated email from the ASF dual-hosted git repository.

dsoumis pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/10.1.x by this push:
     new 6a4177f729 Add unit tests for PQC features
6a4177f729 is described below

commit 6a4177f7298d3d82b91df2abaf0ae270e4db355e
Author: Dimitrios Soumis <[email protected]>
AuthorDate: Wed May 13 13:44:42 2026 +0200

    Add unit tests for PQC features
---
 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]

Reply via email to