diff --git a/integration-test-groups/http/http/pom.xml b/integration-test-groups/http/http/pom.xml index 8eb2b3970709..4f2b9e2bc740 100644 --- a/integration-test-groups/http/http/pom.xml +++ b/integration-test-groups/http/http/pom.xml @@ -50,6 +50,15 @@ org.apache.camel.quarkus camel-quarkus-seda + + org.apache.camel.quarkus + camel-quarkus-support-bouncycastle + + + + org.apache.camel.quarkus + camel-quarkus-integration-tests-support-pqc-certificate-generator + @@ -78,6 +87,11 @@ camel-quarkus-integration-tests-support-certificate-generator test + + org.assertj + assertj-core + test + @@ -153,6 +167,19 @@ + + org.apache.camel.quarkus + camel-quarkus-support-bouncycastle-deployment + ${project.version} + pom + test + + + * + * + + + org.apache.camel.quarkus camel-quarkus-seda-deployment diff --git a/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpProducers.java b/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpProducers.java index 0433d8134ad1..8aa6f7472628 100644 --- a/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpProducers.java +++ b/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpProducers.java @@ -16,7 +16,13 @@ */ package org.apache.camel.quarkus.component.http.http; +import javax.net.ssl.SSLContext; + import jakarta.inject.Named; +import org.apache.camel.component.http.HttpClientConfigurer; +import org.apache.camel.quarkus.test.support.pqc.certificate.client.PqcSslClientConfigurer; +import org.apache.camel.quarkus.test.support.pqc.certificate.trustmanager.HybridPqcX509TrustManager; +import org.apache.camel.quarkus.test.support.pqc.certificate.util.BctlsSSLContextFactory; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.auth.BasicAuthCache; @@ -31,6 +37,7 @@ import static org.apache.camel.quarkus.component.http.common.AbstractHttpResource.USER_ADMIN_PASSWORD; public class HttpProducers { + @Named HttpContext basicAuthContext() { Integer port = ConfigProvider.getConfig().getValue("quarkus.http.test-ssl-port", Integer.class); @@ -50,4 +57,18 @@ HttpContext basicAuthContext() { return context; } + + @Named + public HttpClientConfigurer pqcNginxHttpClientConfigurer() { + try { + // Create SSLContext using BouncyCastle JSSE provider with hybrid certificate validator + // This enables validation of RSA+PQC composite certificates following BC Almanac recommendations + HybridPqcX509TrustManager trustManager = new HybridPqcX509TrustManager(); + SSLContext sslContext = BctlsSSLContextFactory.createSSLContext(trustManager); + return new PqcSslClientConfigurer(sslContext); + } catch (Exception e) { + throw new RuntimeException("Failed to create PQC HttpClient configurer", e); + } + } + } diff --git a/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpResource.java b/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpResource.java index 1d8b0b51b798..d7e2176f9842 100644 --- a/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpResource.java +++ b/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpResource.java @@ -197,4 +197,15 @@ public String compression() { .withHeader("Accept-Encoding", "gzip, deflate") .request(String.class); } + + @Path("/pqc/nginx/tls") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String pqcNginxTls() { + return producerTemplate + .to("direct:pqc-nginx-tls") + .withHeader(Exchange.HTTP_METHOD, "GET") + .request(String.class); + } + } diff --git a/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpRoutes.java b/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpRoutes.java index 580e886925bd..535ca204c479 100644 --- a/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpRoutes.java +++ b/integration-test-groups/http/http/src/main/java/org/apache/camel/quarkus/component/http/http/HttpRoutes.java @@ -32,5 +32,10 @@ public void configure() throws Exception { .to("seda:dlq") .end() .to("http://localhost:{{quarkus.http.test-port}}/service/common/error"); + + from("direct:pqc-nginx-tls") + .toF("https://{{pqc.nginx.host}}:{{pqc.nginx.port}}/test?httpClientConfigurer=#pqcNginxHttpClientConfigurer"); + + // Pure PQC route can not exist - TLS protocol doesn't support pure ML-DSA cipher suites YET } } diff --git a/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/HttpTest.java b/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/HttpTest.java index 41912995d1a6..d34f977f0509 100644 --- a/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/HttpTest.java +++ b/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/HttpTest.java @@ -27,6 +27,12 @@ import org.apache.camel.quarkus.component.http.common.AbstractHttpTest; import org.apache.camel.quarkus.component.http.common.HttpTestResource; import org.apache.camel.quarkus.test.support.certificate.TestCertificates; +import org.apache.camel.quarkus.test.support.pqc.certificate.CertificateFormat; +import org.apache.camel.quarkus.test.support.pqc.certificate.HybridMode; +import org.apache.camel.quarkus.test.support.pqc.certificate.PQCAlgorithm; +import org.apache.camel.quarkus.test.support.pqc.certificate.PQCCertificate; +import org.apache.camel.quarkus.test.support.pqc.certificate.PQCCertificates; +import org.apache.camel.quarkus.test.support.pqc.certificate.PrimaryAlgorithm; import org.eclipse.microprofile.config.Config; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -43,9 +49,15 @@ @TestCertificates(certificates = { @Certificate(name = HttpTestResource.KEYSTORE_NAME, formats = { Format.PKCS12 }, password = HttpTestResource.KEYSTORE_PASSWORD) }) +@PQCCertificates(certificates = { + @PQCCertificate(name = PqcNginxTestResource.CERT_NAME, hybridMode = HybridMode.CHIMERA, primaryAlgorithm = PrimaryAlgorithm.RSA_2048, pqcAlgorithm = PQCAlgorithm.MLDSA65, formats = { + CertificateFormat.PEM, CertificateFormat.PKCS12 }, password = PqcNginxTestResource.TRUSTSTORE_PASSWORD) +}) @QuarkusTest @QuarkusTestResource(HttpTestResource.class) +@QuarkusTestResource(PqcNginxTestResource.class) public class HttpTest extends AbstractHttpTest { + @Override public String component() { return "http"; @@ -137,4 +149,18 @@ static Stream proxyProviders(Config config) { arguments("*localhost*", actualPort, host, 200, expectedGroupId, true)); } + @Test + public void testPqcNginxTls() { + // Test BCTLS integration with hybrid RSA+Dilithium certificate + // Certificate contains both RSA (for TLS handshake) and Dilithium2 (alternative signature) + // Following BC Almanac Chimera-style composite certificate recommendations + // Note: Dilithium is the legacy name; ML-DSA is the NIST standardized name + RestAssured + .when() + .get("/test/client/http/pqc/nginx/tls") + .then() + .statusCode(200) + .body(is("Hybrid RSA+Dilithium(ML-DSA) certificate validated")); + } + } diff --git a/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/PqcNginxTestResource.java b/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/PqcNginxTestResource.java new file mode 100644 index 000000000000..8c7d2c5557c1 --- /dev/null +++ b/integration-test-groups/http/http/src/test/java/org/apache/camel/quarkus/component/http/http/it/PqcNginxTestResource.java @@ -0,0 +1,115 @@ +/* + * 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.camel.quarkus.component.http.http.it; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.apache.camel.quarkus.test.support.pqc.certificate.util.CertificatesUtil; +import org.eclipse.microprofile.config.ConfigProvider; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Test resource that starts a nginx container with PQC hybrid certificates. + * + * Certificates are generated via @PQCCertificates annotation on the test class. + */ +public class PqcNginxTestResource implements QuarkusTestResourceLifecycleManager { + public static final String TRUSTSTORE_PASSWORD = "changeit"; + public static final String CERT_NAME = "nginx-hybrid-pqc"; + // Use standard nginx (not OQS) to test BCTLS integration + // OQS-OpenSSL is incompatible with BouncyCastle JSSE at the protocol level + private static final String NGINX_IMAGE = ConfigProvider.getConfig().getValue("nginx.container.image", + String.class); + + private GenericContainer container; + + @Override + public Map start() { + try { + Path certFile = CertificatesUtil.getCertificatePem(CERT_NAME); + Path keyFile = CertificatesUtil.getPrimaryKeyPem(CERT_NAME); + Path truststoreFile = CertificatesUtil.getTruststore(CERT_NAME); + + if (!certFile.toFile().exists() || !keyFile.toFile().exists()) { + throw new IllegalStateException( + "PQC certificate files not found at expected locations: cert=" + certFile + ", key=" + keyFile); + } + + // Write nginx configuration + // Certificate contains both RSA (primary) and Dilithium2/ML-DSA (alternative) keys + // Use filenames from the certificate files + String certFileName = certFile.getFileName().toString(); + String keyFileName = keyFile.getFileName().toString(); + + String nginxConfig = String.format(""" + server { + listen 4433 ssl; + server_name localhost; + ssl_certificate /certs/%s; + ssl_certificate_key /certs/%s; + ssl_protocols TLSv1.3; + location /test { + return 200 "Hybrid RSA+Dilithium(ML-DSA) certificate validated"; + add_header Content-Type text/plain; + } + } + """, certFileName, keyFileName); + + // Get certificate directory from actual certificate path + Path certDirPath = certFile.getParent(); + File nginxConfigFile = certDirPath.resolve("default.conf").toFile(); + Files.writeString(nginxConfigFile.toPath(), nginxConfig); + + // Start standard nginx container + container = new GenericContainer<>(DockerImageName.parse(NGINX_IMAGE)) + .withExposedPorts(4433) + .withFileSystemBind(certDirPath.toAbsolutePath().toString(), "/certs", BindMode.READ_ONLY) + .withFileSystemBind(nginxConfigFile.getAbsolutePath(), "/etc/nginx/conf.d/default.conf", + BindMode.READ_ONLY) + .withLogConsumer(frame -> System.out.print(frame.getUtf8String())) + .waitingFor(Wait.forListeningPort()); + + container.start(); + + // Return configuration properties using paths from certificate files + Map result = new LinkedHashMap<>(); + result.put("pqc.nginx.host", container.getHost()); + result.put("pqc.nginx.port", String.valueOf(container.getMappedPort(4433))); + result.put("pqc.nginx.truststore.path", "file://" + truststoreFile.toAbsolutePath()); + result.put("pqc.nginx.truststore.password", TRUSTSTORE_PASSWORD); + + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to start PQC nginx test resource", e); + } + } + + @Override + public void stop() { + if (container != null) { + container.stop(); + } + } +} diff --git a/integration-tests-support/pom.xml b/integration-tests-support/pom.xml index 9b53b0ca3843..8d3a484c073d 100644 --- a/integration-tests-support/pom.xml +++ b/integration-tests-support/pom.xml @@ -62,6 +62,7 @@ kafka messaging mongodb + pqc-certificate-generator process-executor-support sftp splunk diff --git a/integration-tests-support/pqc-certificate-generator/pom.xml b/integration-tests-support/pqc-certificate-generator/pom.xml new file mode 100644 index 000000000000..b67d4c2298ac --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + 3.35.0-SNAPSHOT + camel-quarkus-integration-tests-support + ../pom.xml + + + camel-quarkus-integration-tests-support-pqc-certificate-generator + Camel Quarkus :: Integration Tests :: Support :: PQC Certificate Generator + Post-Quantum Cryptography certificate generation support for integration tests + + + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + org.bouncycastle + bctls-jdk18on + + + + + io.quarkus + quarkus-arc + + + + + org.junit.jupiter + junit-jupiter-api + compile + + + org.junit.platform + junit-platform-commons + compile + + + + + org.testcontainers + testcontainers + true + + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + + org.apache.camel + camel-http + + + + + org.jboss.logging + jboss-logging + + + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + jandex + + + + + + + diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/CertificateFormat.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/CertificateFormat.java new file mode 100644 index 000000000000..6bf436f842bd --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/CertificateFormat.java @@ -0,0 +1,32 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +/** + * Certificate output formats. + */ +public enum CertificateFormat { + /** + * PEM format - separate text files for certificate and keys. + */ + PEM, + + /** + * PKCS12 format - binary keystore/truststore files. + */ + PKCS12 +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridCertificateFiles.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridCertificateFiles.java new file mode 100644 index 000000000000..cd2985be62f6 --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridCertificateFiles.java @@ -0,0 +1,63 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +import java.nio.file.Path; + +/** + * Value object holding paths to generated certificate files. + */ +class HybridCertificateFiles { + private final Path certificatePem; + private final Path primaryKeyPem; + private final Path pqcKeyPem; + private final Path truststore; + private final Path keystore; + + public HybridCertificateFiles( + Path certificatePem, + Path primaryKeyPem, + Path pqcKeyPem, + Path truststore, + Path keystore) { + this.certificatePem = certificatePem; + this.primaryKeyPem = primaryKeyPem; + this.pqcKeyPem = pqcKeyPem; + this.truststore = truststore; + this.keystore = keystore; + } + + public Path getCertificatePem() { + return certificatePem; + } + + public Path getPrimaryKeyPem() { + return primaryKeyPem; + } + + public Path getPqcKeyPem() { + return pqcKeyPem; + } + + public Path getTruststore() { + return truststore; + } + + public Path getKeystore() { + return keystore; + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridCertificateGenerator.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridCertificateGenerator.java new file mode 100644 index 000000000000..89c0f4dc7dca --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridCertificateGenerator.java @@ -0,0 +1,366 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; + +import org.apache.camel.quarkus.test.support.pqc.certificate.crypto.ChimeraOids; +import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.jboss.logging.Logger; + +/** + * Generates hybrid RSA+PQC or pure PQC certificates following BouncyCastle Almanac recommendations. + * Thread-safe and reusable. + */ +class HybridCertificateGenerator { + private static final Logger LOG = Logger.getLogger(HybridCertificateGenerator.class); + private static final String BCPQC_PROVIDER = "BC"; // Using BC provider for NIST-standardized ML-DSA algorithms + private static final String BC_PROVIDER = "BC"; + + static { + // Ensure BC provider is registered + if (Security.getProvider(BC_PROVIDER) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + // BCPQC provider kept for backward compatibility but not required for ML-DSA + if (Security.getProvider("BCPQC") == null) { + Security.addProvider(new BouncyCastlePQCProvider()); + } + } + + /** + * Generates a Chimera-style hybrid certificate with RSA/ECDSA + PQC signatures. + * Following BC Almanac page 6 recommendations. + */ + public HybridCertificateFiles generateChimeraCertificate( + String name, + PrimaryAlgorithm primaryAlg, + PQCAlgorithm pqcAlg, + String cn, + List sans, + Duration validity, + Path outputDir, + String password) throws Exception { + + LOG.infof("Generating Chimera certificate: name=%s, primary=%s, pqc=%s, cn=%s", + name, primaryAlg, pqcAlg, cn); + + // Ensure output directory exists + Files.createDirectories(outputDir); + + // Generate keypairs + KeyPair primaryKeyPair = generatePrimaryKeyPair(primaryAlg); + KeyPair pqcKeyPair = generatePQCKeyPair(pqcAlg); + + // Build certificate + X509Certificate certificate = buildChimeraCertificate( + primaryKeyPair, + pqcKeyPair, + pqcAlg, + cn, + sans, + validity, + primaryAlg.getSignatureAlgorithm()); + + // Export to files + return exportCertificateFiles( + name, + certificate, + primaryKeyPair, + pqcKeyPair, + outputDir, + password, + true); // includeFormats: PEM and PKCS12 + } + + /** + * Generates a pure PQC certificate (no classical algorithm). + */ + public HybridCertificateFiles generatePurePQCCertificate( + String name, + PQCAlgorithm pqcAlg, + String cn, + List sans, + Duration validity, + Path outputDir, + String password) throws Exception { + + LOG.infof("Generating pure PQC certificate: name=%s, pqc=%s, cn=%s", name, pqcAlg, cn); + + // Ensure output directory exists + Files.createDirectories(outputDir); + + // Generate PQC keypair only + KeyPair pqcKeyPair = generatePQCKeyPair(pqcAlg); + + // Build certificate with PQC only + X509Certificate certificate = buildPurePQCCertificate( + pqcKeyPair, + pqcAlg, + cn, + sans, + validity); + + // Export to files + return exportCertificateFiles( + name, + certificate, + null, // no primary keypair + pqcKeyPair, + outputDir, + password, + true); + } + + private KeyPair generatePrimaryKeyPair(PrimaryAlgorithm alg) throws Exception { + if (alg == PrimaryAlgorithm.NONE) { + return null; + } + + LOG.debugf("Generating primary keypair: %s %d-bit", alg.getAlgorithmName(), alg.getKeySize()); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg.getAlgorithmName()); + kpg.initialize(alg.getKeySize()); + return kpg.generateKeyPair(); + } + + private KeyPair generatePQCKeyPair(PQCAlgorithm alg) throws Exception { + LOG.debugf("Generating PQC keypair: %s", alg.getAlgorithmName()); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg.getAlgorithmName(), BCPQC_PROVIDER); + return kpg.generateKeyPair(); + } + + /** + * based on example `Ex. 8: ECDSA ML-DSA X.509 Dual Key Certificate Generation` from + * https://downloads.bouncycastle.org/java/docs/PQC-Almanac.pdf + */ + private X509Certificate buildChimeraCertificate( + KeyPair primaryKeyPair, + KeyPair pqcKeyPair, + PQCAlgorithm pqcAlg, + String cn, + List sans, + Duration validity, + String primarySignatureAlg) throws Exception { + + // Certificate validity period + Instant now = Instant.now(); + Date notBefore = Date.from(now.minus(1, ChronoUnit.HOURS)); // 1 hour buffer for clock skew + Date notAfter = Date.from(now.plus(validity.toDays(), ChronoUnit.DAYS)); + + X500Name subject = new X500Name("CN=" + cn); + BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); + + // Build certificate with primary public key (RSA/ECDSA) + JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, + serialNumber, + notBefore, + notAfter, + subject, + primaryKeyPair.getPublic()); + + // Add Subject Alternative Names if provided + if (sans != null && !sans.isEmpty()) { + GeneralName[] names = sans.stream() + .map(this::parseSubjectAlternativeName) + .toArray(GeneralName[]::new); + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(names)); + } + + // Add PQC public key as alternative public key (Chimera/composite certificate) + // Following BC Almanac page 6 recommendations + SubjectPublicKeyInfo pqcPubKeyInfo = SubjectPublicKeyInfo.getInstance( + pqcKeyPair.getPublic().getEncoded()); + certBuilder.addExtension(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO, false, pqcPubKeyInfo); + + // Add alternative signature algorithm (Chimera extension) + AlgorithmIdentifier mlDsaSigAlg = new AlgorithmIdentifier(ChimeraOids.ML_DSA_65); + certBuilder.addExtension(ChimeraOids.ALT_SIGNATURE_ALGORITHM, false, mlDsaSigAlg); + + // Generate alternative PQC signature + // For Chimera-style certificates, sign the subject as a marker + // Full Chimera spec requires signing the actual TBSCertificate bytes + Signature pqcSig = Signature.getInstance(pqcAlg.getAlgorithmName(), BCPQC_PROVIDER); + pqcSig.initSign(pqcKeyPair.getPrivate()); + pqcSig.update(subject.getEncoded()); + byte[] pqcSignatureBytes = pqcSig.sign(); + + // Add alternative signature value extension + certBuilder.addExtension(ChimeraOids.ALT_SIGNATURE_VALUE, false, + new DERBitString(pqcSignatureBytes)); + + // Create primary signer (RSA/ECDSA) + ContentSigner primarySigner = new JcaContentSignerBuilder(primarySignatureAlg) + .build(primaryKeyPair.getPrivate()); + + // Build final certificate with primary signature + return new JcaX509CertificateConverter() + .setProvider(BC_PROVIDER) + .getCertificate(certBuilder.build(primarySigner)); + } + + private X509Certificate buildPurePQCCertificate( + KeyPair pqcKeyPair, + PQCAlgorithm pqcAlg, + String cn, + List sans, + Duration validity) throws Exception { + + // Certificate validity period + Instant now = Instant.now(); + Date notBefore = Date.from(now.minus(1, ChronoUnit.HOURS)); + Date notAfter = Date.from(now.plus(validity.toDays(), ChronoUnit.DAYS)); + + X500Name subject = new X500Name("CN=" + cn); + BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); + + // Build certificate with PQC public key only + JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, + serialNumber, + notBefore, + notAfter, + subject, + pqcKeyPair.getPublic()); + + // Add Subject Alternative Names if provided + if (sans != null && !sans.isEmpty()) { + GeneralName[] names = sans.stream() + .map(this::parseSubjectAlternativeName) + .toArray(GeneralName[]::new); + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(names)); + } + + // Create PQC signer + ContentSigner pqcSigner = new JcaContentSignerBuilder(pqcAlg.getAlgorithmName()) + .setProvider(BCPQC_PROVIDER) + .build(pqcKeyPair.getPrivate()); + + // Build certificate + return new JcaX509CertificateConverter() + .setProvider(BC_PROVIDER) + .getCertificate(certBuilder.build(pqcSigner)); + } + + private HybridCertificateFiles exportCertificateFiles( + String name, + X509Certificate certificate, + KeyPair primaryKeyPair, + KeyPair pqcKeyPair, + Path outputDir, + String password, + boolean includeFormats) throws Exception { + + // Export certificate to PEM + Path certPem = outputDir.resolve(name + "-cert.pem"); + try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(new FileOutputStream(certPem.toFile())))) { + pemWriter.writeObject(new PemObject("CERTIFICATE", certificate.getEncoded())); + } + + // Export primary key to PEM (if exists) + Path primaryKeyPem = null; + if (primaryKeyPair != null) { + primaryKeyPem = outputDir.resolve(name + "-key.pem"); + try (PemWriter pemWriter = new PemWriter( + new OutputStreamWriter(new FileOutputStream(primaryKeyPem.toFile())))) { + pemWriter.writeObject(new PemObject("PRIVATE KEY", primaryKeyPair.getPrivate().getEncoded())); + } + } + + // Export PQC key to PEM + Path pqcKeyPem = outputDir.resolve(name + "-pqc-key.pem"); + try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(new FileOutputStream(pqcKeyPem.toFile())))) { + pemWriter.writeObject(new PemObject("PRIVATE KEY", pqcKeyPair.getPrivate().getEncoded())); + } + + // Create PKCS12 truststore + Path truststore = outputDir.resolve(name + "-truststore.p12"); + KeyStore ts = KeyStore.getInstance("PKCS12"); + ts.load(null, null); + ts.setCertificateEntry(name, certificate); + try (FileOutputStream fos = new FileOutputStream(truststore.toFile())) { + ts.store(fos, password.toCharArray()); + } + + // Create PKCS12 keystore + // Use primary key if available (hybrid mode), otherwise use PQC key (pure PQC mode) + Path keystore = outputDir.resolve(name + "-keystore.p12"); + KeyStore ks = KeyStore.getInstance("PKCS12", BC_PROVIDER); + ks.load(null, null); + PrivateKey keyToStore = (primaryKeyPair != null) ? primaryKeyPair.getPrivate() : pqcKeyPair.getPrivate(); + ks.setKeyEntry(name, keyToStore, password.toCharArray(), + new X509Certificate[] { certificate }); + try (FileOutputStream fos = new FileOutputStream(keystore.toFile())) { + ks.store(fos, password.toCharArray()); + } + + // Log success with icons + LOG.infof("⭐ PQC certificate and keys generated successfully!"); + LOG.infof("📜 Certificate: %s", certPem); + if (primaryKeyPem != null) { + LOG.infof("🔑 Primary Key: %s", primaryKeyPem); + } + LOG.infof("🔐 PQC Key: %s", pqcKeyPem); + LOG.infof("🔐 Key Store File: %s", keystore); + LOG.infof("🔓 Trust Store File: %s", truststore); + + return new HybridCertificateFiles(certPem, primaryKeyPem, pqcKeyPem, truststore, keystore); + } + + private GeneralName parseSubjectAlternativeName(String san) { + if (san.startsWith("DNS:")) { + return new GeneralName(GeneralName.dNSName, san.substring(4)); + } else if (san.startsWith("IP:")) { + return new GeneralName(GeneralName.iPAddress, san.substring(3)); + } else { + // Default to DNS name + return new GeneralName(GeneralName.dNSName, san); + } + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridMode.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridMode.java new file mode 100644 index 000000000000..2aee3c726bdf --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/HybridMode.java @@ -0,0 +1,36 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +/** + * Hybrid certificate modes following BouncyCastle PQC Almanac recommendations. + */ +public enum HybridMode { + /** + * Chimera-style composite certificate using X.509 extensions. + * Following BC Almanac page 6 recommendations. + * Uses Extension.subjectAltPublicKeyInfo for alternative PQC public key + * and Extension.altSignatureValue for alternative PQC signature. + */ + CHIMERA, + + /** + * Pure PQC certificate with no classical algorithm. + * Uses only post-quantum cryptography for certificate signing. + */ + PQC_ONLY +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCAlgorithm.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCAlgorithm.java new file mode 100644 index 000000000000..382b88a3b3b8 --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCAlgorithm.java @@ -0,0 +1,47 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +/** + * Supported Post-Quantum Cryptography algorithms for keypair generation. + */ +public enum PQCAlgorithm { + // Signature algorithms (using NIST standardized names) + MLDSA44("ML-DSA-44"), + MLDSA65("ML-DSA-65"), + MLDSA87("ML-DSA-87"), + FALCON512("Falcon-512"), + FALCON1024("Falcon-1024"), + SPHINCSPLUS("SPHINCSPlus"), + LMS("LMS"), + XMSS("XMSS"), + + // Key encapsulation mechanisms + KYBER512("Kyber512"), + KYBER768("Kyber768"), + KYBER1024("Kyber1024"); + + private final String algorithmName; + + PQCAlgorithm(String algorithmName) { + this.algorithmName = algorithmName; + } + + public String getAlgorithmName() { + return algorithmName; + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificate.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificate.java new file mode 100644 index 000000000000..bac0c3cbac60 --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificate.java @@ -0,0 +1,107 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a single PQC certificate to generate. + * Used as a nested element within {@link PQCCertificates}. + * + *

+ * Supports two hybrid modes: + *

    + *
  • CHIMERA: Hybrid certificate with both classical (RSA/ECDSA) and PQC signatures. + * Uses X.509 extensions (Extension.subjectAltPublicKeyInfo, Extension.altSignatureValue) to embed + * alternative PQC signature alongside primary classical signature. Follows BouncyCastle Almanac page 6.
  • + *
  • PQC_ONLY: Pure PQC certificate with only quantum-resistant signature. + * Uses PQC algorithm as the primary signature algorithm.
  • + *
+ * + *

+ * Generated files (example for name="server"): + *

    + *
  • server-cert.pem - X.509 certificate in PEM format
  • + *
  • server-key.pem - Primary (RSA/ECDSA) private key in PEM format (CHIMERA mode only)
  • + *
  • server-pqc-key.pem - PQC private key in PEM format
  • + *
  • server-truststore.p12 - PKCS12 truststore containing the certificate
  • + *
  • server-keystore.p12 - PKCS12 keystore with primary key and certificate (CHIMERA mode only)
  • + *
+ */ +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface PQCCertificate { + + /** + * Certificate name prefix (used for file naming). + * Generated files will be: {name}-cert.pem, {name}-key.pem, etc. + */ + String name(); + + /** + * Hybrid mode: CHIMERA (RSA/ECDSA + PQC) or PQC_ONLY (pure PQC). + * Default: CHIMERA (recommended for transition period). + */ + HybridMode hybridMode() default HybridMode.CHIMERA; + + /** + * Primary classical algorithm (for TLS handshake compatibility). + * Used only in CHIMERA mode. Set to NONE for PQC_ONLY mode. + * Default: RSA_2048. + */ + PrimaryAlgorithm primaryAlgorithm() default PrimaryAlgorithm.RSA_2048; + + /** + * Post-Quantum Cryptography algorithm. + * Used as alternative signature in CHIMERA mode, or primary signature in PQC_ONLY mode. + * Required. + */ + PQCAlgorithm pqcAlgorithm(); + + /** + * Certificate Common Name (CN). + * Default: "localhost". + */ + String cn() default "localhost"; + + /** + * Subject Alternative Names (SANs). + * Format: "DNS:example.com" or "IP:127.0.0.1" or just "example.com" (defaults to DNS). + * Default: empty (no SANs). + */ + String[] subjectAlternativeNames() default {}; + + /** + * Certificate validity period in days. + * Default: 30 days. + */ + int validity() default 2; + + /** + * Output formats to generate. + */ + CertificateFormat[] formats(); + + /** + * Password for PKCS12 keystore/truststore. + */ + String password() default ""; +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificateGenerationExtension.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificateGenerationExtension.java new file mode 100644 index 000000000000..0cf07d24d9be --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificateGenerationExtension.java @@ -0,0 +1,173 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.jboss.logging.Logger; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; +import org.testcontainers.DockerClientFactory; + +/** + * JUnit5 extension that generates PQC certificates before tests run. + * Invoked via {@link PQCCertificates} annotation. + * + *

+ * Extension behavior: + *

    + *
  • Scans test class for {@link PQCCertificates} annotation
  • + *
  • Creates base directory if it doesn't exist
  • + *
  • For each {@link PQCCertificate}: + *
      + *
    • Checks if certificate files already exist (unless replaceIfExists=true)
    • + *
    • Handles Docker host resolution (if docker=true)
    • + *
    • Invokes {@link HybridCertificateGenerator} to create certificate
    • + *
    + *
  • + *
+ * + *

+ */ +public class PQCCertificateGenerationExtension implements BeforeAllCallback { + + private static final Logger LOG = Logger.getLogger(PQCCertificateGenerationExtension.class); + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + var maybe = AnnotationUtils.findAnnotation(extensionContext.getRequiredTestClass(), PQCCertificates.class); + if (maybe.isEmpty()) { + return; + } + var annotation = maybe.get(); + + // Resolve Docker host if needed (for external Docker hosts like Docker Desktop on Mac/Windows) + Optional dockerHost = Optional.empty(); + if (annotation.docker()) { + dockerHost = resolveDockerHost(extensionContext); + } + + // Create base directory + File baseDir = new File(annotation.baseDir()); + baseDir.mkdirs(); + Path basePath = baseDir.toPath(); + + LOG.infof("🔧 Generating PQC certificates in: %s", basePath.toAbsolutePath()); + + // Generate each certificate + HybridCertificateGenerator generator = new HybridCertificateGenerator(); + List certificateFiles = new ArrayList<>(); + + for (PQCCertificate certificate : annotation.certificates()) { + // Determine CN and SANs (with Docker host override if needed) + String cn = dockerHost.orElse(certificate.cn()); + List sans = new ArrayList<>(Arrays.asList(certificate.subjectAlternativeNames())); + + // Add Docker host as IP SAN if Docker mode enabled + dockerHost.ifPresent(host -> sans.add("IP:" + host)); + + // Check if certificate already exists (skip if not replaceIfExists) + Path certPath = basePath.resolve(certificate.name() + "-cert.pem"); + HybridCertificateFiles files = null; + + if (Files.exists(certPath) && !annotation.replaceIfExists()) { + LOG.infof("⏩ Certificate already exists, skipping generation: %s", certPath); + // Load existing certificate paths into stores/registry + Path primaryKeyPath = basePath.resolve(certificate.name() + "-key.pem"); + Path pqcKeyPath = basePath.resolve(certificate.name() + "-pqc-key.pem"); + Path truststorePath = basePath.resolve(certificate.name() + "-truststore.p12"); + Path keystorePath = basePath.resolve(certificate.name() + "-keystore.p12"); + + files = new HybridCertificateFiles( + certPath, + primaryKeyPath, + pqcKeyPath, + truststorePath, + keystorePath); + } else { + // Generate certificate based on hybrid mode + Duration validity = Duration.ofDays(certificate.validity()); + + if (certificate.hybridMode() == HybridMode.CHIMERA) { + LOG.infof("🔨 Generating Chimera certificate '%s': %s + %s (CN=%s)", + certificate.name(), + certificate.primaryAlgorithm(), + certificate.pqcAlgorithm().getAlgorithmName(), + cn); + + files = generator.generateChimeraCertificate( + certificate.name(), + certificate.primaryAlgorithm(), + certificate.pqcAlgorithm(), + cn, + sans, + validity, + basePath, + certificate.password()); + + } else if (certificate.hybridMode() == HybridMode.PQC_ONLY) { + LOG.infof("🔨 Generating pure PQC certificate '%s': %s (CN=%s)", + certificate.name(), + certificate.pqcAlgorithm().getAlgorithmName(), + cn); + + files = generator.generatePurePQCCertificate( + certificate.name(), + certificate.pqcAlgorithm(), + cn, + sans, + validity, + basePath, + certificate.password()); + + } else { + throw new IllegalArgumentException("Unsupported hybrid mode: " + certificate.hybridMode()); + } + } + // + certificateFiles.add(files); + } + + if (!certificateFiles.isEmpty()) { + LOG.infof("✅ PQC certificate generation complete. Generated %d certificate(s).", certificateFiles.size()); + } + } + + /** + * Resolves the Docker host IP address for external Docker hosts. + * Used when docker=true to override CN and SANs with the actual Docker host. + * + * @param extensionContext JUnit5 extension context + * @return Optional Docker host IP (empty if localhost/127.0.0.1) + */ + private Optional resolveDockerHost(ExtensionContext extensionContext) { + String dockerHost = DockerClientFactory.instance().dockerHostIpAddress(); + if (!dockerHost.equals("localhost") && !dockerHost.equals("127.0.0.1")) { + LOG.infof("Detected external Docker host: %s", dockerHost); + return Optional.of(dockerHost); + } + return Optional.empty(); + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificates.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificates.java new file mode 100644 index 000000000000..24972494354d --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PQCCertificates.java @@ -0,0 +1,85 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Declarative annotation for generating PQC (Post-Quantum Cryptography) certificates in integration tests. + * Generates hybrid RSA/ECDSA+PQC certificates or pure PQC certificates following BouncyCastle Almanac + * recommendations. + * + *

+ * Certificates are generated once per test class before tests run (via JUnit5 BeforeAllCallback). + * Generated files are written to the specified baseDir (default: target/certs). + * + *

+ * Example usage: + * + *

+ * @PQCCertificates(baseDir = "target/certs", certificates = {
+ *         @PQCCertificate(name = "server-hybrid", hybridMode = HybridMode.CHIMERA, primaryAlgorithm = PrimaryAlgorithm.RSA_2048, pqcAlgorithm = PQCAlgorithm.DILITHIUM2, cn = "localhost", validity = 30, formats = {
+ *                 CertificateFormat.PEM, CertificateFormat.PKCS12 })
+ * })
+ * @QuarkusTest
+ * public class MyTest {
+ *     // Certificates generated to:
+ *     // - target/certs/server-hybrid-cert.pem
+ *     // - target/certs/server-hybrid-key.pem (RSA primary key)
+ *     // - target/certs/server-hybrid-pqc-key.pem (Dilithium alternative key)
+ *     // - target/certs/server-hybrid-truststore.p12
+ *     // - target/certs/server-hybrid-keystore.p12
+ * }
+ * 
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(PQCCertificateGenerationExtension.class) +@Inherited +public @interface PQCCertificates { + + /** + * Base directory for generated certificate files. + * Defaults to "target/certs" in the module directory. + */ + String baseDir() default "target/certs"; + + /** + * Whether to replace existing certificates if they exist. + * Default: false (reuse existing certificates to speed up test runs). + */ + boolean replaceIfExists() default false; + + /** + * Whether to resolve Docker host for CN and SANs. + * If true, uses TestContainers API to detect external Docker host + * and replaces CN/SANs accordingly (useful for Docker Desktop on Mac/Windows). + * Default: false. + */ + boolean docker() default false; + + /** + * Array of PQC certificates to generate. + */ + PQCCertificate[] certificates(); +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PrimaryAlgorithm.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PrimaryAlgorithm.java new file mode 100644 index 000000000000..8d995d2ef5bc --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/PrimaryAlgorithm.java @@ -0,0 +1,51 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate; + +/** + * Classical/primary algorithms for hybrid certificates. + * Used for TLS handshake compatibility. + */ +public enum PrimaryAlgorithm { + RSA_2048("RSA", 2048, "SHA256withRSA"), + RSA_4096("RSA", 4096, "SHA256withRSA"), + ECDSA_256("EC", 256, "SHA256withECDSA"), + ECDSA_384("EC", 384, "SHA384withECDSA"), + NONE(null, 0, null); // For PQC_ONLY mode + + private final String algorithmName; + private final int keySize; + private final String signatureAlgorithm; + + PrimaryAlgorithm(String algorithmName, int keySize, String signatureAlgorithm) { + this.algorithmName = algorithmName; + this.keySize = keySize; + this.signatureAlgorithm = signatureAlgorithm; + } + + public String getAlgorithmName() { + return algorithmName; + } + + public int getKeySize() { + return keySize; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/client/PqcSslClientConfigurer.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/client/PqcSslClientConfigurer.java new file mode 100644 index 000000000000..46b71ed959a4 --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/client/PqcSslClientConfigurer.java @@ -0,0 +1,59 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate.client; + +import javax.net.ssl.SSLContext; + +import org.apache.camel.component.http.HttpClientConfigurer; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; + +/** + * HttpClient configurer that uses BouncyCastle TLS (BCTLS) for PQC support. + */ +public class PqcSslClientConfigurer implements HttpClientConfigurer { + + private final SSLContext sslContext; + + public PqcSslClientConfigurer(SSLContext sslContext) { + this.sslContext = sslContext; + } + + @Override + public void configureHttpClient(HttpClientBuilder clientBuilder) { + try { + // Create SSL socket factory with BCTLS SSLContext + // Use ALLOW_ALL_HOSTNAME_VERIFIER for testing (accepts any hostname) + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( + sslContext, + new String[] { "TLSv1.3", "TLSv1.2" }, // Supported protocols + null, // Use default cipher suites from BCTLS + (hostname, session) -> true); // Accept all hostnames for testing + + // Create connection manager with custom SSL socket factory + HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslSocketFactory) + .build(); + + clientBuilder.setConnectionManager(connectionManager); + } catch (Exception e) { + throw new RuntimeException("Failed to configure HttpClient with BCTLS", e); + } + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/crypto/ChimeraOids.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/crypto/ChimeraOids.java new file mode 100644 index 000000000000..18b1d3ad582a --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/crypto/ChimeraOids.java @@ -0,0 +1,167 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate.crypto; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; + +/** + * X.509 extension OIDs (Object Identifiers) for Chimera hybrid certificate format. + * + *

+ * OIDs are globally unique identifiers used in X.509 certificates to label specific + * data fields and extensions. These OIDs enable standard X.509 certificates to carry + * both classical (RSA) and post-quantum (ML-DSA-65) cryptographic signatures. + * + *

Chimera Hybrid Certificate Structure:

+ * + *
+ * Standard X.509 Certificate:
+ *   Subject: CN=localhost
+ *   Public Key: RSA-2048                    ← Classical (readable by all TLS stacks)
+ *   Signature: SHA256withRSA                ← Classical signature
+ *
+ *   Extensions:
+ *     [2.5.29.72] altSubjectPublicKeyInfo:  ← ML-DSA-65 public key
+ *     [2.5.29.73] altSignatureAlgorithm:    ← Identifies ML-DSA-65 (2.16.840.1.101.3.4.3.18)
+ *     [2.5.29.74] altSignatureValue:        ← ML-DSA-65 signature
+ * 
+ * + *

+ * Why this matters: Both signatures must be valid for authentication. + * This provides quantum resistance (via ML-DSA-65) while maintaining backward + * compatibility with standard TLS (via RSA). + * + *

OID Hierarchy Explained:

+ *
    + *
  • 2.5.29.x - X.509 certificate extensions (standardized by ITU-T) + *
      + *
    • 2.5.29.15 - keyUsage (common)
    • + *
    • 2.5.29.17 - subjectAltName (common)
    • + *
    • 2.5.29.72-74 - Chimera hybrid extensions (new for PQC)
    • + *
    + *
  • + *
  • 2.16.840.1.101.3.4.3.18 - ML-DSA-65 algorithm (NIST FIPS 204) + *
      + *
    • 2.16.840.1.101.3 - NIST algorithms
    • + *
    • 4.3 - Signature algorithms
    • + *
    • 18 - ML-DSA-65 (Level 3 security)
    • + *
    + *
  • + *
+ * + * @see RFC 5280 - X.509 Certificate Standard + * @see FIPS 204 - ML-DSA Standard + */ +public final class ChimeraOids { + + /** + * OID 2.5.29.72 - Alternative Subject Public Key Info extension. + * Contains the ML-DSA-65 public key in addition to the standard RSA public key. + * + *

+ * This allows a single certificate to carry two public keys: + *

    + *
  • Primary: RSA-2048 (in standard subjectPublicKeyInfo field)
  • + *
  • Alternative: ML-DSA-65 (in this extension)
  • + *
+ * + *

+ * Official References: + *

+ */ + public static final ASN1ObjectIdentifier SUBJECT_ALT_PUBLIC_KEY_INFO = new ASN1ObjectIdentifier("2.5.29.72"); + + /** + * OID 2.5.29.73 - Alternative Signature Algorithm extension. + * Identifies which algorithm was used for the alternative signature (ML-DSA-65). + * + *

+ * Contains the OID {@link #ML_DSA_65} to specify the PQC algorithm used. + * + *

+ * Official References: + *

+ */ + public static final ASN1ObjectIdentifier ALT_SIGNATURE_ALGORITHM = new ASN1ObjectIdentifier("2.5.29.73"); + + /** + * OID 2.5.29.74 - Alternative Signature Value extension. + * Contains the actual ML-DSA-65 digital signature bytes. + * + *

+ * This is the post-quantum signature that proves the certificate is authentic + * and hasn't been tampered with, computed using the ML-DSA-65 private key. + * + *

+ * Official References: + *

+ */ + public static final ASN1ObjectIdentifier ALT_SIGNATURE_VALUE = new ASN1ObjectIdentifier("2.5.29.74"); + + /** + * OID 2.16.840.1.101.3.4.3.18 - ML-DSA-65 algorithm identifier (NIST FIPS 204). + * + *

+ * ML-DSA-65 (Module-Lattice-Based Digital Signature Algorithm, parameter set 65) + * is a NIST Level 3 post-quantum signature algorithm resistant to attacks from + * quantum computers. ML-DSA was standardized from CRYSTALS-Dilithium. + * + *

+ * Security level: NIST Level 3, equivalent to AES-192 (192-bit security) + *
+ * Signature size: ~3,309 bytes (much larger than RSA-2048's ~256 bytes) + *
+ * Public key size: ~1,952 bytes + * + *

+ * NOTE: ML-DSA (standardized) and Dilithium (pre-standard) are NOT interoperable. + * This OID represents the final NIST FIPS 204 standardized version. + * + *

+ * Official References: + *

+ */ + public static final ASN1ObjectIdentifier ML_DSA_65 = new ASN1ObjectIdentifier("2.16.840.1.101.3.4.3.18"); + + private ChimeraOids() { + throw new AssertionError("Constants class cannot be instantiated"); + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/trustmanager/HybridPqcX509TrustManager.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/trustmanager/HybridPqcX509TrustManager.java new file mode 100644 index 000000000000..b646fed36e73 --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/trustmanager/HybridPqcX509TrustManager.java @@ -0,0 +1,90 @@ + +/* + * 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.camel.quarkus.test.support.pqc.certificate.trustmanager; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + +import org.apache.camel.quarkus.test.support.pqc.certificate.util.CertificateValidationException; +import org.apache.camel.quarkus.test.support.pqc.certificate.util.CertificatesUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom X509TrustManager that validates hybrid PQC certificates at the TLS layer. + * + * This TrustManager validates both RSA and ML-DSA-65 signatures during the TLS handshake, + * rejecting connections with invalid or RSA-only certificates before the application layer + * sees the request. + */ +public class HybridPqcX509TrustManager implements X509TrustManager { + + private static final Logger LOG = LoggerFactory.getLogger(HybridPqcX509TrustManager.class); + + private static final String BCPQC_PROVIDER = "BC"; + private static final String BC_PROVIDER = "BC";// Using BC provider for NIST-standardized ML-DSA algorithms + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (chain == null || chain.length == 0) { + throw new CertificateException("Client certificate chain is empty"); + } + + X509Certificate clientCert = chain[0]; + LOG.debug("Validating client certificate at TLS layer: {}", clientCert.getSubjectX500Principal()); + + try { + // Validate hybrid certificate - throws CertificateValidationException on failure + CertificatesUtil.validateHybridCertificate(clientCert); + LOG.debug("Client certificate validated successfully at TLS layer (RSA + ML-DSA-65)"); + } catch (CertificateValidationException e) { + LOG.error("Hybrid PQC certificate validation failed: {}", e.getMessage()); + throw new CertificateException("Validation failed: " + e.getMessage(), e); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (chain == null || chain.length == 0) { + throw new CertificateException("Client certificate chain is empty"); + } + + X509Certificate clientCert = chain[0]; + LOG.debug("Validating server certificate at TLS layer: {}", clientCert.getSubjectX500Principal()); + + try { + // Validate hybrid certificate - throws CertificateValidationException on failure + CertificatesUtil.validateHybridCertificate(clientCert); + LOG.debug("Server certificate validated successfully at TLS layer (RSA + ML-DSA-65)"); + } catch (CertificateValidationException e) { + LOG.error("Hybrid PQC certificate validation failed: {}", e.getMessage()); + throw new CertificateException("Validation failed: " + e.getMessage(), e); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + // Return empty array for self-signed certificates in this demo. + // + // In production with a CA hierarchy, this would return the + // list of trusted CA certificates that can issue client certificates. + return new X509Certificate[0]; + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/BctlsSSLContextFactory.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/BctlsSSLContextFactory.java new file mode 100644 index 000000000000..54c9adf7ba91 --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/BctlsSSLContextFactory.java @@ -0,0 +1,101 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate.util; + +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.Security; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; + +/** + * Factory for creating SSLContext using BouncyCastle JSSE provider. + * This enables better PQC algorithm support compared to standard Java JSSE. + */ +public class BctlsSSLContextFactory { + + private static final String BCJSSE_PROVIDER = "BCJSSE"; + + /** + * Creates an SSLContext using BouncyCastle JSSE provider with custom trust manager. + * This allows handling of PQC-signed certificates. + */ + public static SSLContext createSSLContext(TrustManager trustManager) throws Exception { + // Register BouncyCastle JSSE provider if not already registered + if (Security.getProvider(BCJSSE_PROVIDER) == null) { + Security.addProvider(new BouncyCastleJsseProvider()); + } + + // Create SSLContext using BC JSSE provider + SSLContext sslContext = SSLContext.getInstance("TLS", BCJSSE_PROVIDER); + + // Initialize with custom trust manager + sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom()); + + return sslContext; + } + + /** + * Creates an SSLContext using BouncyCastle JSSE provider with truststore. + */ + public static SSLContext createSSLContext(KeyStore trustStore, String trustStorePassword) throws Exception { + // Register BouncyCastle JSSE provider if not already registered + if (Security.getProvider(BCJSSE_PROVIDER) == null) { + Security.addProvider(new BouncyCastleJsseProvider()); + } + + // Create trust manager from truststore + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm(), BCJSSE_PROVIDER); + tmf.init(trustStore); + + // Create SSLContext using BC JSSE provider + SSLContext sslContext = SSLContext.getInstance("TLS", BCJSSE_PROVIDER); + sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); + + return sslContext; + } + + /** + * Creates a trust-all SSLContext for testing PQC connections. + * Note: This bypasses certificate validation and should only be used in test environments. + */ + public static SSLContext createTrustAllSSLContext() throws Exception { + X509TrustManager trustAllManager = new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { + // Accept all certificates - allows PQC-signed certs that JSSE can't validate + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[0]; + } + }; + + return createSSLContext(trustAllManager); + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/CertificateValidationException.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/CertificateValidationException.java new file mode 100644 index 000000000000..1663cecb0b8b --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/CertificateValidationException.java @@ -0,0 +1,33 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate.util; + +import java.security.cert.CertificateException; + +/** + * Exception thrown when hybrid PQC certificate validation fails. + */ +public class CertificateValidationException extends CertificateException { + + public CertificateValidationException(String message) { + super(message); + } + + public CertificateValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/CertificatesUtil.java b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/CertificatesUtil.java new file mode 100644 index 000000000000..7dffd441b5eb --- /dev/null +++ b/integration-tests-support/pqc-certificate-generator/src/main/java/org/apache/camel/quarkus/test/support/pqc/certificate/util/CertificatesUtil.java @@ -0,0 +1,227 @@ +/* + * 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.camel.quarkus.test.support.pqc.certificate.util; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import org.apache.camel.quarkus.test.support.pqc.certificate.crypto.ChimeraOids; +import org.bouncycastle.asn1.ASN1BitString; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for validating Chimera hybrid certificates. + * Validates both RSA and ML-DSA-65 signatures using static methods. + */ +public final class CertificatesUtil { + + private static final Logger LOG = LoggerFactory.getLogger(CertificatesUtil.class); + + public static final Path DEFAULT_CERTS_BASEDIR = Path.of("target/certs"); + + private CertificatesUtil() { + throw new AssertionError("Utility class cannot be instantiated"); + } + + public static Path getCertificatePem(String name) { + return path(name + "-cert", "pem"); + } + + public static Path getPrimaryKeyPem(String name) { + return path(name + "-key", "pem"); + } + + public static Path getPqcKeyPem(String name) { + return path(name + "-pqc-key", "pem"); + } + + public static Path getTruststore(String name) { + return path(name + "-truststore", "p12"); + } + + public static Path getKeystore(String name) { + return path(name + "-keystore", "p12"); + } + + private static Path path(String name, String extension) { + return DEFAULT_CERTS_BASEDIR.resolve(name + "." + extension); + } + + /** + * Validates a hybrid certificate by checking both RSA and ML-DSA-65 signatures. + * + * @param cert The certificate to validate + * @throws CertificateValidationException if validation fails + */ + public static void validateHybridCertificate(X509Certificate cert) throws CertificateValidationException { + LOG.debug("Validating hybrid certificate for subject: {}", cert.getSubjectX500Principal()); + + try { + // Verify RSA signature (standard X.509 verification) + if (!verifyRsaSignature(cert)) { + throw new CertificateValidationException("RSA signature validation failed"); + } + + LOG.debug("RSA signature verified"); + + // Verify alternative signature algorithm extension exists + byte[] altSigAlgExt = cert.getExtensionValue(ChimeraOids.ALT_SIGNATURE_ALGORITHM.getId()); + if (altSigAlgExt == null) { + throw new CertificateValidationException( + "PQC signature algorithm extension missing (OID 2.5.29.73)"); + } + + // Validate it's ML-DSA-65 + ASN1Primitive primitive = ASN1Primitive.fromByteArray(altSigAlgExt); + byte[] octets = ((ASN1OctetString) primitive).getOctets(); + AlgorithmIdentifier algId = AlgorithmIdentifier.getInstance(octets); + + if (!ChimeraOids.ML_DSA_65.equals(algId.getAlgorithm())) { + throw new CertificateValidationException( + "Expected ML-DSA-65 algorithm OID, found: " + algId.getAlgorithm()); + } + + LOG.debug("ML-DSA-65 algorithm OID validated"); + + // Extract and verify ML-DSA-65 signature + PublicKey mlDsaPublicKey = extractMlDsaPublicKey(cert); + if (mlDsaPublicKey == null) { + throw new CertificateValidationException( + "PQC public key extension missing (OID 2.5.29.72)"); + } + + byte[] mlDsaSignature = extractMlDsaSignature(cert); + if (mlDsaSignature == null) { + throw new CertificateValidationException( + "PQC signature extension missing (OID 2.5.29.74)"); + } + + if (!verifyMlDsaSignature(cert, mlDsaPublicKey, mlDsaSignature)) { + throw new CertificateValidationException("ML-DSA-65 signature validation failed"); + } + + LOG.debug("ML-DSA-65 signature verified - hybrid certificate valid"); + + } catch (IOException e) { + throw new CertificateValidationException("Failed to parse PQC extensions", e); + } catch (CertificateValidationException e) { + LOG.warn("Certificate validation failed: {}", e.getMessage()); + throw e; + } catch (Exception e) { + String message = "Unexpected error during certificate validation: " + e.getMessage(); + LOG.error(message, e); + throw new CertificateValidationException(message, e); + } + } + + /** + * Verifies the RSA signature using standard X.509 verification. + */ + private static boolean verifyRsaSignature(X509Certificate cert) { + try { + // Self-signed certificate - verify with its own public key + cert.verify(cert.getPublicKey()); + return true; + } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException + | NoSuchProviderException e) { + LOG.error("RSA signature verification failed", e); + return false; + } + } + + /** + * Extracts the ML-DSA-65 public key from the altSubjectPublicKeyInfo extension. + */ + private static PublicKey extractMlDsaPublicKey(X509Certificate cert) { + try { + byte[] extensionValue = cert.getExtensionValue(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO.getId()); + if (extensionValue == null) { + return null; + } + + // Extension value is wrapped in OCTET STRING + ASN1Primitive primitive = ASN1Primitive.fromByteArray(extensionValue); + byte[] octets = ((ASN1OctetString) primitive).getOctets(); + + // Parse SubjectPublicKeyInfo + SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(octets); + + // Convert to PublicKey using X509EncodedKeySpec + KeyFactory keyFactory = KeyFactory.getInstance("ML-DSA-65", "BC"); + return keyFactory.generatePublic(new X509EncodedKeySpec(spki.getEncoded())); + + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchProviderException e) { + LOG.error("Failed to extract ML-DSA-65 public key", e); + return null; + } + } + + /** + * Extracts the ML-DSA-65 signature from the altSignatureValue extension. + */ + private static byte[] extractMlDsaSignature(X509Certificate cert) { + try { + byte[] extensionValue = cert.getExtensionValue(ChimeraOids.ALT_SIGNATURE_VALUE.getId()); + if (extensionValue == null) { + return null; + } + + // Extension value is wrapped in OCTET STRING + ASN1Primitive primitive = ASN1Primitive.fromByteArray(extensionValue); + byte[] octets = ((ASN1OctetString) primitive).getOctets(); + + // Parse as BIT STRING + ASN1BitString bitString = ASN1BitString.getInstance(octets); + return bitString.getBytes(); + + } catch (IOException e) { + LOG.error("Failed to extract ML-DSA-65 signature", e); + return null; + } + } + + /** + * Verifies the ML-DSA-65 signature. + */ + private static boolean verifyMlDsaSignature(X509Certificate cert, PublicKey pqcKey, byte[] signature) { + try { + Signature mlDsaVerify = Signature.getInstance("ML-DSA-65", "BC"); + mlDsaVerify.initVerify(pqcKey); + mlDsaVerify.update(cert.getSubjectX500Principal().getEncoded()); + return mlDsaVerify.verify(signature); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) { + LOG.error("ML-DSA-65 signature verification failed", e); + return false; + } + } +} diff --git a/integration-tests/http-grouped/pom.xml b/integration-tests/http-grouped/pom.xml index 84637d075f0a..50bdb5c48529 100644 --- a/integration-tests/http-grouped/pom.xml +++ b/integration-tests/http-grouped/pom.xml @@ -87,6 +87,15 @@ org.apache.camel.quarkus camel-quarkus-integration-tests-support-http
+ + org.apache.camel.quarkus + camel-quarkus-support-bouncycastle + + + + org.apache.camel.quarkus + camel-quarkus-integration-tests-support-pqc-certificate-generator + @@ -112,9 +121,9 @@ org.apache.camel.quarkus camel-quarkus-integration-tests-support-certificate-generator - test + test - + org.apache.camel.quarkus camel-quarkus-integration-tests-support-http test @@ -308,6 +317,19 @@ + + org.apache.camel.quarkus + camel-quarkus-support-bouncycastle-deployment + ${project.version} + pom + test + + + * + * + + +
diff --git a/pom.xml b/pom.xml index f92f24f6c5b8..536a6b9c8fd0 100644 --- a/pom.xml +++ b/pom.xml @@ -273,6 +273,7 @@ mirror.gcr.io/nats:2.11.6 mirror.gcr.io/opensearchproject/opensearch:3.1.0 mirror.gcr.io/linuxserver/openssh-server:version-9.7_p1-r4 + mirror.gcr.io/nginx:alpine mirror.gcr.io/gvenzl/oracle-free:23.26.1-slim-faststart ghcr.io/pinecone-io/pinecone-local:v1.0.0.rc0 mirror.gcr.io/postgres:17.5 diff --git a/poms/bom-test/pom.xml b/poms/bom-test/pom.xml index 465fc0554071..9e46d1ec9924 100644 --- a/poms/bom-test/pom.xml +++ b/poms/bom-test/pom.xml @@ -220,6 +220,11 @@ camel-quarkus-integration-tests-support-mongodb ${camel-quarkus.version} + + org.apache.camel.quarkus + camel-quarkus-integration-tests-support-pqc-certificate-generator + ${camel-quarkus.version} + org.apache.camel.quarkus camel-quarkus-integration-wiremock-support