+ * Supports two hybrid modes: + *
+ * Generated files (example for name="server"): + *
+ * Extension behavior: + *
+ */
+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
+ * 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:
+ *
+ *
+ * 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.
+ *
+ *
+ * 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).
+ *
+ *
+ * This allows a single certificate to carry two public keys:
+ *
+ * Official References:
+ *
+ * Contains the OID {@link #ML_DSA_65} to specify the PQC algorithm used.
+ *
+ *
+ * Official References:
+ *
+ * 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:
+ *
+ * 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)
+ *
+ * NOTE: ML-DSA (standardized) and Dilithium (pre-standard) are NOT interoperable.
+ * This OID represents the final NIST FIPS 204 standardized version.
+ *
+ *
+ * Official References:
+ *
+ * @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.
+ *
+ * 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
+ *
+ *
+ * OID Hierarchy Explained:
+ *
+ *
+ *
+ * @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.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ 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).
+ *
+ *
+ *
+ */
+ 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.
+ *
+ *
+ *
+ */
+ 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).
+ *
+ *
+ * Signature size: ~3,309 bytes (much larger than RSA-2048's ~256 bytes)
+ *
+ * Public key size: ~1,952 bytes
+ *
+ *
+ *
+ */
+ 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 @@