You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ja...@apache.org on 2026/04/10 17:06:00 UTC
(camel-quarkus-examples) branch camel-quarkus-main updated: Add HTTP PQC Example for Java 17
This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch camel-quarkus-main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git
The following commit(s) were added to refs/heads/camel-quarkus-main by this push:
new 81120c09 Add HTTP PQC Example for Java 17
81120c09 is described below
commit 81120c09af7b8aa6432d2fa4a878c7ec26ae2ad3
Author: JiriOndrusek <on...@gmail.com>
AuthorDate: Fri Apr 10 19:05:55 2026 +0200
Add HTTP PQC Example for Java 17
---
docs/modules/ROOT/attachments/examples.json | 5 +
http-pqc-j17/PQC-EXPLANATION.adoc | 476 +++++++++++++++++++++
http-pqc-j17/README.adoc | 189 ++++++++
http-pqc-j17/eclipse-formatter-config.xml | 276 ++++++++++++
http-pqc-j17/pom.xml | 303 +++++++++++++
.../main/java/org/acme/http/pqc/PqcCamelRoute.java | 42 ++
.../certificates/HybridCertificateGenerator.java | 247 +++++++++++
.../pqc/certificates/SecurityConfiguration.java | 54 +++
.../util/CertificateValidationException.java | 33 ++
.../pqc/certificates/util/CertificatesUtil.java | 200 +++++++++
.../java/org/acme/http/pqc/crypto/ChimeraOids.java | 167 ++++++++
.../HybridPqcTrustManagerCustomizer.java | 58 +++
.../trustmanager/HybridPqcX509TrustManager.java | 77 ++++
.../src/main/resources/application.properties | 49 +++
.../src/test/java/org/acme/http/pqc/HttpPqcIT.java | 23 +
.../test/java/org/acme/http/pqc/HttpPqcTest.java | 115 +++++
16 files changed, 2314 insertions(+)
diff --git a/docs/modules/ROOT/attachments/examples.json b/docs/modules/ROOT/attachments/examples.json
index c755be4c..c69639f1 100644
--- a/docs/modules/ROOT/attachments/examples.json
+++ b/docs/modules/ROOT/attachments/examples.json
@@ -44,6 +44,11 @@
"description": "Shows how to consume CSV files, marshal \u0026 unmarshal the data and send it onwards via FTP",
"link": "https://github.com/apache/camel-quarkus-examples/tree/main/file-bindy-ftp"
},
+ {
+ "title": "HTTP with Post-Quantum Cryptography (PQC) (Java 17)",
+ "description": "Shows how to implement quantum-resistant authentication using hybrid certificates that combine RSA with post-quantum ML-DSA-65 signatures on Java 17 using application-level validation",
+ "link": "https://github.com/apache/camel-quarkus-examples/tree/main/http-pqc-j17"
+ },
{
"title": "HTTP with vanilla JAX-RS or with Camel `platform-http` component",
"description": "Shows how to create HTTP endpoints using either the RESTEasy",
diff --git a/http-pqc-j17/PQC-EXPLANATION.adoc b/http-pqc-j17/PQC-EXPLANATION.adoc
new file mode 100644
index 00000000..f29ef7e1
--- /dev/null
+++ b/http-pqc-j17/PQC-EXPLANATION.adoc
@@ -0,0 +1,476 @@
+= Post-Quantum Cryptography: Visual Explanation
+
+== The Quantum Computer Threat
+
+=== Traditional Encryption (RSA/ECC)
+
+[source,text]
+----
+┌─────────────────────────────────────────────────────────────┐
+│ Current World (Safe) Future with Quantum Computer │
+│ │
+│ RSA-2048 encryption Quantum Computer │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Lock (RSA) │ ──────> │ Shor's Algo │ │
+│ │ Takes 1000s │ │ Breaks in │ │
+│ │ of years to │ │ MINUTES! ⚠️ │ │
+│ │ break │ └──────────────┘ │
+│ └──────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+----
+
+=== Post-Quantum Cryptography (PQC)
+
+[source,text]
+----
+┌─────────────────────────────────────────────────────────────┐
+│ PQC Solution Even with Quantum Computer │
+│ │
+│ ML-DSA (FIPS 204) Quantum Computer │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Lattice Lock │ ──────> │ Shor's Algo │ │
+│ │ Based on │ │ Doesn't work!│ │
+│ │ hard math │ │ Still takes │ │
+│ │ problems │ │ 1000s years ✓│ │
+│ └──────────────┘ └──────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+----
+
+'''
+
+== Three Approaches in Our Implementation
+
+=== 1. CURRENT HTTPS (What Most Systems Use Today)
+
+[source,text]
+----
+┌──────────────┐ ┌──────────────┐
+│ Browser │ │ Server │
+│ (Client) │ │ │
+└──────┬───────┘ └──────┬───────┘
+ │ │
+ │ 1. "Hello, I want HTTPS connection" │
+ ├──────────────────────────────────────────────>│
+ │ │
+ │ 2. "Here's my certificate" │
+ │ ┌─────────────────────────┐ │
+ │ │ Certificate: │ │
+ │ │ - Public Key: RSA-2048 │ │
+ │ │ - Signature: SHA256+RSA │ │
+ │<────│ - Subject: localhost │───────────────│
+ │ │ ⚠️ QUANTUM VULNERABLE │ │
+ │ └─────────────────────────┘ │
+ │ │
+ │ 3. Verify RSA signature ✓ │
+ │ (Works today, broken by quantum later) │
+ │ │
+ │ 4. Encrypted communication using RSA │
+ │<─────────────────────────────────────────────>│
+ │ ⚠️ Future quantum computer can break this │
+ │ │
+----
+
+**Problem:** When quantum computers arrive, they can:
+
+* Break the RSA encryption
+* Forge RSA signatures
+* Read all your "encrypted" data
+
+'''
+
+=== 2. ALTERNATIVE APPROACH: Application-Layer PQC (NOT this example)
+
+This is a **workaround approach** when you can't modify TLS behavior - demonstrate PQC **inside** the application:
+
+[source,text]
+----
+┌──────────────┐ ┌──────────────┐
+│ Browser │ │ Server │
+│ (curl) │ │ (Quarkus) │
+└──────┬───────┘ └──────┬───────┘
+ │ │
+ │ LAYER 1: HTTPS (Still uses RSA) │
+ │ ════════════════════════════════════════ │
+ │ 1. TLS Handshake with RSA certificate │
+ ├──────────────────────────────────────────────>│
+ │ ┌─────────────────────────┐ │
+ │ │ TLS Certificate: │ │
+ │ │ - RSA-2048 (classical) │ │
+ │<────│ ⚠️ Quantum vulnerable │───────────────│
+ │ └─────────────────────────┘ │
+ │ │
+ │ 2. Encrypted tunnel established │
+ │<═════════════════════════════════════════════>│
+ │ ⚠️ TLS handshake is quantum-vulnerable │
+ │ │
+ │ LAYER 2: PQC at Application Level │
+ │ ════════════════════════════════════════ │
+ │ 3. GET /pqc/sign │
+ ├──────────────────────────────────────────────>│
+ │ │
+ │ ┌──────────┴───────────┐
+ │ │ PqcSignatureService │
+ │ │ - Generate ML-DSA-65 │
+ │ │ keypair │
+ │ │ - Sign message │
+ │ │ - Verify signature │
+ │ │ ✓ Algorithm is safe │
+ │ └──────────┬───────────┘
+ │ 4. Returns PQC signature demo │
+ │<──────────────────────────────────────────────│
+----
+
+**Why we DON'T use this approach:**
+
+* ⚠️ **TLS handshake is still quantum-vulnerable** - attacker can intercept the initial connection
+* ℹ️ **Only demonstrates PQC algorithms** - doesn't actually protect the authentication
+* ✅ **Good for learning/demos** - shows how PQC algorithms work
+* ❌ **Not good for real security** - TLS layer remains vulnerable
+
+**This example uses Approach #3 instead** ⬇️
+
+'''
+
+=== 3. OUR IMPLEMENTATION: Chimera Hybrid Certificates at TLS Layer
+
+**This example implements this approach!** It validates hybrid PQC certificates during the TLS handshake:
+
+[source,text]
+----
+┌──────────────┐ ┌──────────────┐
+│ Browser │ │ Server │
+│ (Client) │ │ │
+└──────┬───────┘ └──────┬───────┘
+ │ │
+ │ 1. "Hello, I want HTTPS connection" │
+ ├──────────────────────────────────────────────>│
+ │ │
+ │ 2. "Here's my HYBRID certificate" │
+ │ ┌─────────────────────────────────────┐ │
+ │ │ Chimera Hybrid Certificate: │ │
+ │ │ ──────────────────────────────── │ │
+ │ │ PRIMARY (Classical): │ │
+ │ │ - Public Key: RSA-2048 │ │
+ │ │ - Signature: SHA256withRSA │ │
+ │ │ ✓ Works with old systems │ │
+ │ │ │ │
+ │ │ ALTERNATIVE (PQC) - Extensions: │ │
+ │<────│ - altPublicKey: ML-DSA-65 │───│
+ │ │ - altSignature: ML-DSA-65 │ │
+ │ │ ✓ QUANTUM SAFE │ │
+ │ │ │ │
+ │ │ BOTH signatures present! │ │
+ │ └─────────────────────────────────────┘ │
+ │ │
+ │ 3. Verify RSA signature ✓ │
+ │ (Works with old browsers) │
+ │ │
+ │ 4. Verify ML-DSA-65 signature ✓ │
+ │ (PQC-aware clients check this too) │
+ │ │
+ │ 5. Encrypted communication │
+ │<─────────────────────────────────────────────>│
+ │ ✓ Protected by BOTH classical & PQC │
+ │ │
+----
+
+==== How Chimera Works
+
+[source,text]
+----
+┌─────────────────────────────────────────────────────────────┐
+│ CHIMERA HYBRID CERTIFICATE │
+│ │
+│ ┌────────────────────┐ ┌────────────────────┐ │
+│ │ PRIMARY LAYER │ │ ALTERNATIVE LAYER │ │
+│ │ (Classical RSA) │ │ (PQC ML-DSA) │ │
+│ └─────────┬──────────┘ └─────────┬──────────┘ │
+│ │ │ │
+│ │ Subject: CN=localhost │ │
+│ │ Issuer: CN=PQC Hybrid CA │ │
+│ │ Serial: 1234567890 │ │
+│ │ │ │
+│ ┌─────────▼──────────┐ ┌─────────▼──────────┐ │
+│ │ RSA-2048 │ │ Extension OID │ │
+│ │ Public Key │ │ 2.5.29.72: │ │
+│ │ (Standard field) │ │ ML-DSA-65 Public │ │
+│ └────────────────────┘ │ Key │ │
+│ └────────────────────┘ │
+│ ┌────────────────────┐ ┌────────────────────┐ │
+│ │ SHA256withRSA │ │ Extension OID │ │
+│ │ Signature │ │ 2.5.29.74: │ │
+│ │ [RSA signature │ │ ML-DSA-65 │ │
+│ │ bytes] │ │ Signature bytes │ │
+│ └────────────────────┘ └────────────────────┘ │
+│ │
+│ Verification: BOTH signatures must be valid! │
+│ - Old systems: Check RSA only ✓ │
+│ - PQC systems: Check RSA ✓ AND ML-DSA ✓ │
+└─────────────────────────────────────────────────────────────┘
+----
+
+'''
+
+== Certificate Comparison
+
+=== Traditional Certificate (RSA only)
+
+[source,text]
+----
+┌─────────────────────────────────┐
+│ X.509 Certificate │
+├─────────────────────────────────┤
+│ Version: 3 │
+│ Serial: 0x123456 │
+│ Issuer: CN=My CA │
+│ Subject: CN=localhost │
+│ ┌─────────────────────────────┐ │
+│ │ Public Key: │ │
+│ │ Algorithm: RSA │ │
+│ │ Size: 2048 bits │ │
+│ │ Key: [MIIBIjANBg...] │ │
+│ └─────────────────────────────┘ │
+│ ┌─────────────────────────────┐ │
+│ │ Signature: │ │
+│ │ Algorithm: SHA256withRSA │ │
+│ │ Value: [3045022100...] │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ⚠️ QUANTUM VULNERABLE │
+└─────────────────────────────────┘
+----
+
+=== Chimera Hybrid Certificate (RSA + ML-DSA-65)
+
+[source,text]
+----
+┌─────────────────────────────────────────────────────┐
+│ X.509 Certificate (Chimera Format) │
+├─────────────────────────────────────────────────────┤
+│ Version: 3 │
+│ Serial: 0x123456 │
+│ Issuer: CN=PQC Hybrid CA │
+│ Subject: CN=localhost │
+│ ┌─────────────────────────────┐ │
+│ │ Public Key (PRIMARY): │ │
+│ │ Algorithm: RSA │ ✓ Classical │
+│ │ Size: 2048 bits │ │
+│ │ Key: [MIIBIjANBg...] │ │
+│ └─────────────────────────────┘ │
+│ ┌─────────────────────────────┐ │
+│ │ Extensions: │ │
+│ │ ┌─────────────────────────┐ │ │
+│ │ │ OID 2.5.29.72: │ │ │
+│ │ │ altSubjectPublicKeyInfo │ │ │
+│ │ │ Algorithm: ML-DSA-65 │ │ ✓ Post-Quantum │
+│ │ │ Key: [3082071E...] │ │ │
+│ │ │ Size: 1976 bytes │ │ │
+│ │ └─────────────────────────┘ │ │
+│ │ ┌─────────────────────────┐ │ │
+│ │ │ OID 2.5.29.73: │ │ │
+│ │ │ altSignatureAlgorithm │ │ │
+│ │ │ Algorithm: ML-DSA-65 │ │ │
+│ │ └─────────────────────────┘ │ │
+│ │ ┌─────────────────────────┐ │ │
+│ │ │ OID 2.5.29.74: │ │ │
+│ │ │ altSignatureValue │ │ │
+│ │ │ Signature: [D1L1...] │ │ ✓ Post-Quantum │
+│ │ │ Size: 3309 bytes │ │ │
+│ │ └─────────────────────────┘ │ │
+│ └─────────────────────────────┘ │
+│ ┌─────────────────────────────┐ │
+│ │ Signature (PRIMARY): │ │
+│ │ Algorithm: SHA256withRSA │ ✓ Classical │
+│ │ Value: [3045022100...] │ │
+│ └─────────────────────────────┘ │
+│ │
+│ ✓ HYBRID: Protected against classical AND quantum! │
+└─────────────────────────────────────────────────────┘
+----
+
+'''
+
+== Our Implementation: TLS-Layer Hybrid Certificate Validation
+
+[source,text]
+----
+┌────────────────────────────────────────────────────────────────┐
+│ HTTP PQC Example - TLS-Layer Validation │
+└────────────────────────────────────────────────────────────────┘
+
+HYBRID CERTIFICATE VALIDATION (Chimera at TLS Layer)
+────────────────────────────────────────────────────
+Endpoint: https://localhost:8443/pqc/secure
+
+┌─────────────┐ ┌─────────────────────────┐
+│ Client │─────>│ TLS Handshake │
+│ Request │ │ (with client cert) │
+└─────────────┘ └─────────┬───────────────┘
+ │
+ ┌─────────▼───────────┐
+ │ Client sends hybrid │
+ │ PQC certificate │
+ └─────────┬───────────┘
+ │
+ ┌──────────────┴──────────────┐
+ │ HybridPqcX509TrustManager │
+ └──────────────┬──────────────┘
+ │
+ ┌─────────▼─────────┐
+ │ Extract signatures│
+ │ from certificate │
+ └─────────┬─────────┘
+ │
+ ┌──────────────┴──────────────┐
+ ▼ ▼
+ ┌───────────────┐ ┌────────────────┐
+ │ Validate RSA │ │ Validate │
+ │ Signature ✓ │ │ ML-DSA-65 ✓ │
+ └───────┬───────┘ └────────┬───────┘
+ │ │
+ └──────────────┬──────────────┘
+ │
+ ┌─────────▼─────────┐
+ │ BOTH valid? │
+ │ YES → Allow │
+ │ NO → Reject TLS │
+ └─────────┬─────────┘
+ │
+ ▼
+ ┌──────────────────┐
+ │ /pqc/secure │
+ │ route executes │
+ └──────┬───────────┘
+ │
+ ▼
+ Result: ✓ Validated!
+ "✓ Hybrid PQC certificate
+ validated at TLS layer!"
+
+ (Dual protection - works
+ now + quantum-safe!)
+----
+
+'''
+
+== Why We Use Approach #3 (Chimera) Instead of Approach #2
+
+[cols="1,2,2",options="header"]
+|===
+|Aspect
+|Approach #2: App-Layer PQC
+|Approach #3: Chimera at TLS Layer ✅
+
+|**TLS Handshake**
+|⚠️ Quantum-vulnerable (RSA only)
+|✅ Validates both RSA + ML-DSA-65
+
+|**Authentication**
+|❌ No PQC authentication
+|✅ Rejects invalid certificates
+
+|**Protection Level**
+|⚠️ Demo only, TLS is vulnerable
+|✅ Real security at TLS layer
+
+|**When to use**
+|Learning, experimenting
+|Production-ready hybrid solution
+
+|**Code complexity**
+|Simple (app endpoints)
+|Moderate (custom TrustManager)
+|===
+
+**Key Difference:**
+
+* **Approach #2** is like having a regular lock on your door (RSA TLS) but demonstrating fancy new locks inside your house (PQC demos)
+* **Approach #3** is like installing a dual-lock system ON YOUR DOOR (Chimera certificates validated at TLS handshake)
+
+Approach #3 provides actual security because:
+
+. Invalid certificates are **rejected during TLS handshake** before any data is exchanged
+. Both RSA and ML-DSA-65 signatures must be valid to establish the connection
+. Works with the existing TLS infrastructure (backward compatible)
+
+'''
+
+== Why Chimera is the Best Migration Path
+
+[source,text]
+----
+┌─────────────────────────────────────────────────────────────┐
+│ MIGRATION TIMELINE │
+└─────────────────────────────────────────────────────────────┘
+
+TODAY (2026) FUTURE (Quantum Computers)
+════════════ ══════════════════════════
+
+Old Browser: PQC-Aware Browser:
+┌────────────┐ ┌────────────┐
+│ Only knows │ │ Checks RSA │
+│ RSA │ │ AND │
+└──────┬─────┘ │ ML-DSA │
+ │ └──────┬─────┘
+ │ Chimera │
+ │ Certificate │
+ ▼ ▼
+┌──────────────┐ ┌──────────────┐
+│ Verifies RSA │ │ Verifies RSA │
+│ signature ✓ │ │ signature ✓ │
+│ │ │ │
+│ Ignores │ │ ALSO checks │
+│ ML-DSA │ │ ML-DSA ✓ │
+│ (unknown │ │ │
+│ extension) │ │ Double │
+│ │ │ protected! │
+│ ✓ Works! │ │ ✓ Quantum │
+│ │ │ safe! │
+└──────────────┘ └──────────────┘
+
+ADVANTAGE: One certificate works for everyone!
+----
+
+'''
+
+== Summary: What We've Built
+
+[cols="2,3,1",options="header"]
+|===
+|Component
+|What It Does
+|Quantum Safe?
+
+|**HTTPS Transport**
+|Encrypts connection (Java 17 limitation)
+|⚠️ No (classical crypto)
+
+|**HybridCertificateGenerator**
+|Generates Chimera dual certificates (RSA + ML-DSA-65)
+|✅ Yes
+
+|**HybridPqcX509TrustManager**
+|Validates both RSA and ML-DSA-65 signatures at TLS layer
+|✅ Yes
+
+|**SecurityConfiguration**
+|Configures custom TrustManager and client authentication
+|✅ Yes
+
+|**/pqc/secure endpoint**
+|Accessible only with valid hybrid PQC certificates
+|✅ Yes
+|===
+
+**Think of it like this:**
+
+* The **transport tunnel** (HTTPS/TLS) still uses classical encryption (Java 17 limitation)
+* But the **authentication** (certificate validation) is quantum-safe with dual signatures
+* The **Chimera certificate** is like a dual-lock ID card: RSA works today, ML-DSA-65 protects against future quantum attacks
+* Only clients with valid hybrid certificates can access the secure endpoint
+
+**When Java 21+ becomes standard:**
+
+* The transport itself can be upgraded to full PQC (e.g., Kyber for key exchange)
+* Chimera certificates will work perfectly with full PQC TLS
+* Zero code changes needed - we're already PQC-ready!
diff --git a/http-pqc-j17/README.adoc b/http-pqc-j17/README.adoc
new file mode 100644
index 00000000..f448aa98
--- /dev/null
+++ b/http-pqc-j17/README.adoc
@@ -0,0 +1,189 @@
+= HTTP with Post-Quantum Cryptography (PQC): A Camel Quarkus example (Java 17)
+:cq-example-description: An example that shows how to implement quantum-resistant authentication using hybrid certificates that combine RSA with post-quantum ML-DSA-65 signatures on Java 17 using application-level validation
+
+{cq-description}
+
+This example shows how to implement quantum-resistant TLS authentication on **Java 17** by combining classical RSA with post-quantum ML-DSA-65 signatures in X.509 certificates. Both signatures must be valid for authentication to succeed.
+
+IMPORTANT: This example uses **application-level PQC validation** because Java 17 doesn't support PQC in TLS natively. For full PQC TLS support with hybrid cipher suites, use Java 21+ with BouncyCastle JSSE provider.
+
+TIP: Check the https://camel.apache.org/camel-quarkus/latest/first-steps.html[Camel Quarkus User guide] for prerequisites and other general information.
+
+== What is Post-Quantum Cryptography?
+
+Post-Quantum Cryptography (PQC) protects against "harvest now, decrypt later" attacks where adversaries capture encrypted data today to decrypt when quantum computers become available.
+
+=== Java 17 Approach
+
+Since **Java 17 doesn't support PQC algorithms natively in TLS**, this example demonstrates a workaround using:
+
+* **Chimera hybrid certificates**: Combines RSA-2048 + ML-DSA-65 (NIST FIPS 204)
+* **X.509 extensions**: Standard extension OIDs (2.5.29.72-74) for alternative signatures
+* **Application-level validation**: Custom X509TrustManager validates both RSA and ML-DSA-65 signatures during TLS handshake
+* **BouncyCastle 1.83**: Provides ML-DSA-65 post-quantum signature algorithm (JCA provider only)
+
+This approach validates PQC signatures at the application level rather than relying on Java's TLS stack. For native PQC TLS support (hybrid cipher suites like X25519Kyber768), use Java 21+ with BouncyCastle JSSE provider.
+
+NOTE: For detailed diagrams and architecture explanation, see link:PQC-EXPLANATION.adoc[PQC Visual Guide].
+
+== Certificate Generation
+
+Hybrid certificates are automatically generated on every application startup. No manual steps required.
+
+Generated files in `target/certs/`:
+
+* `server-hybrid-keystore.p12` - Server certificate with RSA + ML-DSA-65
+* `server-hybrid-truststore.p12` - Truststore for validating clients
+* `client-hybrid-keystore.p12` - Client certificate with PQC (for success test)
+* `client-hybrid-keystore-cert.pem` - Client certificate in PEM format (for curl testing)
+* `client-hybrid-keystore-key.pem` - Client private key in PEM format (for curl testing)
+* `client-rsa-only-keystore.p12` - Client certificate without PQC (for failure test)
+
+== Prerequisites
+
+* **Java 17** (this example demonstrates Java 17-specific workarounds for PQC; for Java 21+, use native PQC TLS support instead)
+* Maven 3.9.9 or later
+* GraalVM (for native mode only)
+
+== Start in Development Mode
+
+[source,shell]
+----
+$ mvn clean compile quarkus:dev
+----
+
+The above command compiles the project, starts the application and lets the Quarkus tooling watch for changes in your workspace. Any modifications in your project will automatically take effect in the running application.
+
+TIP: Please refer to the Development mode section of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_development_mode[Camel Quarkus User guide] for more details.
+
+The application starts on HTTPS port 8443 with mutual TLS (mTLS) enabled. All endpoints require a valid hybrid client certificate with RSA + ML-DSA-65.
+
+Main endpoint:
+
+* `/pqc/secure` - Requires hybrid client certificate validated at TLS layer
+
+NOTE: With `client-auth=required`, the TLS handshake rejects connections without valid hybrid PQC certificates before any application code executes.
+
+=== Manual Testing with Client Certificates
+
+PEM files are automatically generated during application startup in `target/certs/`:
+
+* `client-hybrid-keystore-cert.pem` - Hybrid certificate (RSA + ML-DSA-65)
+* `client-hybrid-keystore-key.pem` - Private key for hybrid certificate
+
+Test with hybrid certificate (should succeed):
+
+[source,shell]
+----
+$ curl --cert target/certs/client-hybrid-keystore-cert.pem \
+ --key target/certs/client-hybrid-keystore-key.pem \
+ -k \
+ https://localhost:8443/pqc/secure
+
+✓ Hybrid PQC certificate validated at TLS layer!
+Your connection is quantum-safe.
+Both RSA and ML-DSA-65 signatures were validated during TLS handshake.
+----
+
+== Package and Run the Application
+
+Once you are done with developing you may want to package and run the application.
+
+TIP: Find more details about the JVM mode and Native mode in the Package and run section of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_package_and_run_the_application[Camel Quarkus User guide]
+
+=== JVM Mode
+
+[source,shell]
+----
+$ mvn clean package
+$ java -jar target/quarkus-app/quarkus-run.jar
+...
+[io.quarkus] (main) camel-quarkus-examples-http-pqc started in 1.5s. Listening on: https://0.0.0.0:8443
+----
+
+=== Native Mode
+
+IMPORTANT: Native mode requires having GraalVM and other tools installed. Please check the Prerequisites section of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_prerequisites[Camel Quarkus User guide].
+
+To prepare a native executable using GraalVM, run the following command:
+
+[source,shell]
+----
+$ mvn clean package -Dnative
+$ ./target/*-runner
+...
+[io.quarkus] (main) camel-quarkus-examples-http-pqc started in 0.015s. Listening on: https://0.0.0.0:8443
+...
+----
+
+== Testing
+
+Run the tests to verify hybrid certificate functionality:
+
+[source,shell]
+----
+$ mvn clean test
+----
+
+The test suite validates:
+
+* Hybrid keystores are generated automatically
+* `/pqc/secure` rejects requests without client certificates
+* `/pqc/secure` accepts hybrid certificates (RSA + ML-DSA-65)
+* `/pqc/secure` rejects RSA-only certificates missing PQC extensions
+
+For native mode testing:
+
+[source,shell]
+----
+$ mvn clean verify -Dnative
+----
+
+== How It Works (Java 17 Application-Level Validation)
+
+This example works around Java 17's lack of native PQC support by implementing custom certificate validation at the application level, integrated into the TLS handshake via a custom `X509TrustManager`.
+
+=== Certificate Structure
+
+Traditional X.509 certificates are extended with three additional fields (Chimera format):
+
+* **OID 2.5.29.72** - Alternative public key (ML-DSA-65)
+* **OID 2.5.29.73** - Alternative signature algorithm identifier
+* **OID 2.5.29.74** - Alternative signature value (ML-DSA-65 signature)
+
+Both RSA and ML-DSA-65 signatures must validate for authentication to succeed.
+
+=== Validation Flow (Application-Level, Not TLS-Native)
+
+1. Client connects with TLS client certificate
+2. Java's TLS stack invokes our custom `HybridPqcX509TrustManager` during handshake
+3. `CertificatesUtil.validateHybridCertificate()` performs application-level checks:
+ - RSA signature (standard X.509 validation via Java crypto)
+ - ML-DSA-65 algorithm OID (2.5.29.73) - manual extraction
+ - ML-DSA-65 public key (2.5.29.72) - manual extraction
+ - ML-DSA-65 signature (2.5.29.74) - manual verification via BouncyCastle
+4. If all checks pass, TLS handshake continues
+5. If any check fails, TLS handshake is rejected
+
+NOTE: This validation happens at the application level, not within Java's TLS implementation. Java 17's TLS stack only handles the standard RSA certificate; our custom code validates the PQC extensions.
+
+== Important Notes
+
+* **Java 17 Only**: This example is specifically designed for Java 17, which lacks native PQC support in its TLS stack. It demonstrates application-level validation as a workaround. Do not use this approach if you can upgrade to Java 21+.
+
+* **Development Only**: Certificates are regenerated on every startup. For production, use persistent certificates from a trusted CA.
+
+* **BouncyCastle 1.83**: Uses standardized ML-DSA-65 naming per NIST FIPS 204 (effective August 2024). This example uses JCA provider only, not JSSE. Note that ML-DSA (standardized) and Dilithium (pre-standard) are NOT interoperable.
+
+== Additional Resources
+
+* link:PQC-EXPLANATION.adoc[Visual Guide] - Detailed diagrams and architecture
+* https://csrc.nist.gov/pubs/fips/204/final[NIST FIPS 204 - ML-DSA Standard]
+* https://datatracker.ietf.org/doc/html/draft-ounsworth-pq-composite-sigs[IETF Composite Signatures - Chimera format specification]
+* https://www.bouncycastle.org/[BouncyCastle - PQC provider documentation]
+* https://www.rfc-editor.org/rfc/rfc9881.html[RFC 9881 - Algorithm Identifiers for ML-DSA in X.509]
+* https://www.rfc-editor.org/rfc/rfc9882.html[RFC 9882 - Use of ML-DSA in CMS]
+
+== Feedback and Contributions
+
+For issues or contributions, please submit to the https://github.com/apache/camel-quarkus-examples[Camel Quarkus Examples] repository.
diff --git a/http-pqc-j17/eclipse-formatter-config.xml b/http-pqc-j17/eclipse-formatter-config.xml
new file mode 100644
index 00000000..2248b2b8
--- /dev/null
+++ b/http-pqc-j17/eclipse-formatter-config.xml
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<profiles version="8">
+ <profile name="Camel Java Conventions" version="8" kind="CodeFormatterProfile">
+ <setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
+ <setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_comments" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.indent_return_description" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="120"/>
+ <setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
+ <setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
+ <setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.indentation.size" value="8"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+ <setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.lineSplit" value="128"/>
+ <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+ <setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+ <setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+ <setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
+ <setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+ <setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
+ <setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="CHECKSTYLE:OFF"/>
+ <setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="CHECKSTYLE:ON"/>
+ </profile>
+</profiles>
diff --git a/http-pqc-j17/pom.xml b/http-pqc-j17/pom.xml
new file mode 100644
index 00000000..e74b8651
--- /dev/null
+++ b/http-pqc-j17/pom.xml
@@ -0,0 +1,303 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>camel-quarkus-examples-http-pqc-j17</artifactId>
+ <groupId>org.apache.camel.quarkus.examples</groupId>
+ <version>3.35.0-SNAPSHOT</version>
+
+ <name>Camel Quarkus :: Examples :: HTTP PQC</name>
+ <description>Camel Quarkus Example :: HTTP with Post-Quantum Cryptography</description>
+
+ <properties>
+ <quarkus.platform.version>3.34.1</quarkus.platform.version>
+ <camel-quarkus.platform.version>3.35.0-SNAPSHOT</camel-quarkus.platform.version>
+
+ <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
+ <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
+ <camel-quarkus.platform.group-id>org.apache.camel.quarkus</camel-quarkus.platform.group-id>
+ <camel-quarkus.platform.artifact-id>camel-quarkus-bom</camel-quarkus.platform.artifact-id>
+
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <maven.compiler.release>17</maven.compiler.release>
+
+ <formatter-maven-plugin.version>2.29.0</formatter-maven-plugin.version>
+ <impsort-maven-plugin.version>1.13.0</impsort-maven-plugin.version>
+ <license-maven-plugin.version>5.0.0</license-maven-plugin.version>
+ <maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
+ <maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
+ <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
+ <maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <!-- Import BOM -->
+ <dependency>
+ <groupId>${quarkus.platform.group-id}</groupId>
+ <artifactId>${quarkus.platform.artifact-id}</artifactId>
+ <version>${quarkus.platform.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>${camel-quarkus.platform.group-id}</groupId>
+ <artifactId>${camel-quarkus.platform.artifact-id}</artifactId>
+ <version>${camel-quarkus.platform.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-log</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-platform-http</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-support-bouncycastle</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-microprofile-health</artifactId>
+ </dependency>
+
+ <!-- Test -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.rest-assured</groupId>
+ <artifactId>rest-assured</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <pluginManagement>
+ <plugins>
+
+ <plugin>
+ <groupId>net.revelc.code.formatter</groupId>
+ <artifactId>formatter-maven-plugin</artifactId>
+ <version>${formatter-maven-plugin.version}</version>
+ <configuration>
+ <configFile>${maven.multiModuleProjectDirectory}/eclipse-formatter-config.xml</configFile>
+ <lineEnding>LF</lineEnding>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>net.revelc.code</groupId>
+ <artifactId>impsort-maven-plugin</artifactId>
+ <version>${impsort-maven-plugin.version}</version>
+ <configuration>
+ <groups>java.,javax.,org.w3c.,org.xml.,junit.</groups>
+ <removeUnused>true</removeUnused>
+ <staticAfter>true</staticAfter>
+ <staticGroups>java.,javax.,org.w3c.,org.xml.,junit.</staticGroups>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>${maven-compiler-plugin.version}</version>
+ <configuration>
+ <showDeprecation>true</showDeprecation>
+ <showWarnings>true</showWarnings>
+ <compilerArgs>
+ <arg>-Xlint:unchecked</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>${maven-surefire-plugin.version}</version>
+ <configuration>
+ <failIfNoTests>false</failIfNoTests>
+ <systemPropertyVariables>
+ <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>${quarkus.platform.group-id}</groupId>
+ <artifactId>quarkus-maven-plugin</artifactId>
+ <version>${quarkus.platform.version}</version>
+ <extensions>true</extensions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <version>${maven-surefire-plugin.version}</version>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>${maven-jar-plugin.version}</version>
+ </plugin>
+
+ <plugin>
+ <groupId>com.mycila</groupId>
+ <artifactId>license-maven-plugin</artifactId>
+ <version>${license-maven-plugin.version}</version>
+ <configuration>
+ <failIfUnknown>true</failIfUnknown>
+ <header>${maven.multiModuleProjectDirectory}/header.txt</header>
+ <excludes>
+ <exclude>**/*.adoc</exclude>
+ <exclude>**/*.txt</exclude>
+ <exclude>**/LICENSE.txt</exclude>
+ <exclude>**/LICENSE</exclude>
+ <exclude>**/NOTICE.txt</exclude>
+ <exclude>**/NOTICE</exclude>
+ <exclude>**/README</exclude>
+ <exclude>**/pom.xml.versionsBackup</exclude>
+ <exclude>**/quarkus.log*</exclude>
+ <exclude>**/*.p12</exclude>
+ </excludes>
+ <mapping>
+ <java>SLASHSTAR_STYLE</java>
+ <properties>CAMEL_PROPERTIES_STYLE</properties>
+ <kt>SLASHSTAR_STYLE</kt>
+ </mapping>
+ <headerDefinitions>
+ <headerDefinition>${maven.multiModuleProjectDirectory}/license-properties-headerdefinition.xml</headerDefinition>
+ </headerDefinitions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+
+ <plugins>
+ <plugin>
+ <groupId>${quarkus.platform.group-id}</groupId>
+ <artifactId>quarkus-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>build</id>
+ <goals>
+ <goal>build</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>net.revelc.code.formatter</groupId>
+ <artifactId>formatter-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>format</id>
+ <goals>
+ <goal>format</goal>
+ </goals>
+ <phase>process-sources</phase>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>net.revelc.code</groupId>
+ <artifactId>impsort-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>sort-imports</id>
+ <goals>
+ <goal>sort</goal>
+ </goals>
+ <phase>process-sources</phase>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>com.mycila</groupId>
+ <artifactId>license-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>license-format</id>
+ <goals>
+ <goal>format</goal>
+ </goals>
+ <phase>process-sources</phase>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>native</id>
+ <activation>
+ <property>
+ <name>native</name>
+ </property>
+ </activation>
+ <properties>
+ <quarkus.native.enabled>true</quarkus.native.enabled>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ <configuration>
+ <systemPropertyVariables>
+ <quarkus.native.enabled>${quarkus.native.enabled}</quarkus.native.enabled>
+ </systemPropertyVariables>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+</project>
diff --git a/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java
new file mode 100644
index 00000000..2d8f95ac
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java
@@ -0,0 +1,42 @@
+/*
+ * 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.acme.http.pqc;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
+
+@ApplicationScoped
+public class PqcCamelRoute extends EndpointRouteBuilder {
+
+ @Override
+ public void configure() throws Exception {
+ // Secure endpoint with TLS-layer hybrid PQC certificate validation
+ // Note: Certificate validation happens during TLS handshake via custom TrustManager
+ // Invalid certificates are rejected before this route executes
+ // With client-auth=required, only valid hybrid PQC certificates can reach this endpoint
+ from(platformHttp("/pqc/secure"))
+ .routeId("pqc-secure-route")
+ .log("Processing request with validated hybrid PQC certificate")
+ .setBody(constant(
+ "✓ Hybrid PQC certificate validated at TLS layer!\n\n" +
+ "Your connection is quantum-safe.\n" +
+ "Both RSA and ML-DSA-65 signatures were validated during TLS handshake.\n\n" +
+ "This demonstrates TLS-layer validation using a custom X509TrustManager.\n" +
+ "Invalid or RSA-only certificates are rejected during the TLS handshake.\n"))
+ .to(log("pqc-secure").showExchangePattern(false).showBodyType(false));
+ }
+}
diff --git a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java
new file mode 100644
index 00000000..e5f7d8db
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java
@@ -0,0 +1,247 @@
+/*
+ * 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.acme.http.pqc.certificates;
+
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+import org.acme.http.pqc.crypto.ChimeraOids;
+import org.bouncycastle.asn1.DERBitString;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for generating and persisting Chimera hybrid certificates.
+ * These certificates combine classical RSA with post-quantum ML-DSA-65 signatures
+ * using X.509 extensions as specified in the BouncyCastle PQC Almanac.
+ *
+ * <p>
+ * Certificates are automatically generated at application startup by
+ * {@link SecurityConfiguration} if they don't already exist.
+ */
+public class HybridCertificateGenerator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HybridCertificateGenerator.class);
+
+ // Demo keystore password - DO NOT use in production
+ private static final String KEYSTORE_PASSWORD = "changeit";
+ private static final String KEYSTORES_DIR = "target/certs";
+
+ /**
+ * Certificate data holder for keypairs and certificates.
+ */
+ public static class CertificateData {
+ public final KeyPair rsaKeyPair;
+ public final KeyPair mlDsaKeyPair;
+ public final X509Certificate certificate;
+
+ public CertificateData(KeyPair rsaKeyPair, KeyPair mlDsaKeyPair, X509Certificate certificate) {
+ this.rsaKeyPair = rsaKeyPair;
+ this.mlDsaKeyPair = mlDsaKeyPair;
+ this.certificate = certificate;
+ }
+ }
+
+ /**
+ * Generates a Chimera hybrid certificate combining RSA and ML-DSA-65.
+ *
+ * @param commonName The CN for the certificate subject
+ * @param includeAltSignature Whether to include the ML-DSA-65 alternative signature
+ * @return CertificateData containing keypairs and certificate
+ */
+ public static CertificateData generateChimeraCertificate(String commonName, boolean includeAltSignature)
+ throws Exception {
+ LOG.debug("Generating Chimera hybrid certificate for CN={}, includeAltSignature={}",
+ commonName, includeAltSignature);
+
+ // Generate RSA keypair (classical algorithm)
+ KeyPairGenerator rsaKpg = KeyPairGenerator.getInstance("RSA");
+ rsaKpg.initialize(2048, new SecureRandom());
+ KeyPair rsaKeyPair = rsaKpg.generateKeyPair();
+
+ // Generate ML-DSA-65 keypair (PQC algorithm - NIST FIPS 204)
+ KeyPairGenerator mlDsaKpg = KeyPairGenerator.getInstance("ML-DSA-65", "BC");
+ KeyPair mlDsaKeyPair = mlDsaKpg.generateKeyPair();
+
+ // Build certificate
+ X500Name issuer = new X500Name("CN=PQC Hybrid CA,O=Apache Camel Quarkus,C=US");
+ X500Name subject = new X500Name("CN=" + commonName + ",O=Apache Camel Quarkus,C=US");
+ BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
+ Date notBefore = new Date();
+ Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); // 1 year
+
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+ issuer,
+ serial,
+ notBefore,
+ notAfter,
+ subject,
+ rsaKeyPair.getPublic());
+
+ if (includeAltSignature) {
+ // Add ML-DSA-65 alternative public key (Chimera extension)
+ SubjectPublicKeyInfo mlDsaPubKeyInfo = SubjectPublicKeyInfo
+ .getInstance(mlDsaKeyPair.getPublic().getEncoded());
+ certBuilder.addExtension(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO, false, mlDsaPubKeyInfo);
+
+ // Add alternative signature algorithm (Chimera extension)
+ AlgorithmIdentifier mlDsaSigAlg = new AlgorithmIdentifier(ChimeraOids.ML_DSA_65);
+ certBuilder.addExtension(ChimeraOids.ALT_SIGNATURE_ALGORITHM, false, mlDsaSigAlg);
+
+ // Generate ML-DSA-65 alternative signature (Chimera extension)
+ Signature mlDsaSig = Signature.getInstance("ML-DSA-65", "BC");
+ mlDsaSig.initSign(mlDsaKeyPair.getPrivate());
+ mlDsaSig.update(subject.getEncoded()); // Sign subject DN as per Chimera spec
+ byte[] mlDsaSignature = mlDsaSig.sign();
+ certBuilder.addExtension(ChimeraOids.ALT_SIGNATURE_VALUE, false, new DERBitString(mlDsaSignature));
+
+ LOG.debug("ML-DSA-65 extensions added to certificate");
+ }
+
+ // Sign with RSA (primary signature)
+ ContentSigner rsaSigner = new JcaContentSignerBuilder("SHA256withRSA").build(rsaKeyPair.getPrivate());
+ X509CertificateHolder certHolder = certBuilder.build(rsaSigner);
+
+ // Convert to X509Certificate
+ X509Certificate certificate = new JcaX509CertificateConverter()
+ .setProvider("BC")
+ .getCertificate(certHolder);
+
+ LOG.debug("Chimera certificate generated successfully for CN={}", commonName);
+
+ return new CertificateData(rsaKeyPair, mlDsaKeyPair, certificate);
+ }
+
+ /**
+ * Generates a keystore with the specified certificate type.
+ *
+ * @param commonName The CN for the certificate
+ * @param includeAltSignature Whether to include ML-DSA-65 extensions
+ * @param keystorePath Path where keystore will be saved
+ * @param alias Keystore entry alias
+ * @param includeTruststore Whether to also create a truststore
+ * @param savePemFiles Whether to also save certificate and key as PEM files
+ */
+ private static void generateKeystore(
+ String commonName,
+ boolean includeAltSignature,
+ String keystorePath,
+ String alias,
+ boolean includeTruststore,
+ boolean savePemFiles) throws Exception {
+
+ CertificateData certData = generateChimeraCertificate(commonName, includeAltSignature);
+
+ KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
+ keyStore.load(null, null);
+ keyStore.setKeyEntry(alias,
+ certData.rsaKeyPair.getPrivate(),
+ KEYSTORE_PASSWORD.toCharArray(),
+ new X509Certificate[] { certData.certificate });
+
+ saveKeyStore(keyStore, keystorePath, KEYSTORE_PASSWORD);
+ LOG.info("Keystore created: {}", keystorePath);
+
+ if (includeTruststore) {
+ String trustPath = keystorePath.replace("-keystore.p12", "-truststore.p12");
+ KeyStore trustStore = KeyStore.getInstance("PKCS12", "BC");
+ trustStore.load(null, null);
+ trustStore.setCertificateEntry(alias + "-ca", certData.certificate);
+ saveKeyStore(trustStore, trustPath, KEYSTORE_PASSWORD);
+ LOG.info("Truststore created: {}", trustPath);
+ }
+
+ if (savePemFiles) {
+ // Save certificate as PEM
+ String certPemPath = keystorePath.replace(".p12", "-cert.pem");
+ try (FileWriter certWriter = new FileWriter(certPemPath);
+ JcaPEMWriter pemWriter = new JcaPEMWriter(certWriter)) {
+ pemWriter.writeObject(certData.certificate);
+ }
+ LOG.info("Certificate PEM file created: {}", certPemPath);
+
+ // Save private key as PEM
+ String keyPemPath = keystorePath.replace(".p12", "-key.pem");
+ try (FileWriter keyWriter = new FileWriter(keyPemPath);
+ JcaPEMWriter pemWriter = new JcaPEMWriter(keyWriter)) {
+ pemWriter.writeObject(certData.rsaKeyPair.getPrivate());
+ }
+ LOG.info("Private key PEM file created: {}", keyPemPath);
+ }
+ }
+
+ /**
+ * Generates server hybrid keystore with RSA + ML-DSA-65 certificate.
+ */
+ public static void generateServerKeystore() throws Exception {
+ generateKeystore("localhost", true, KEYSTORES_DIR + "/server-hybrid-keystore.p12",
+ "server", true, false);
+ }
+
+ /**
+ * Generates client hybrid keystore with RSA + ML-DSA-65 certificate.
+ * Also saves PEM files for manual curl testing.
+ */
+ public static void generateClientHybridKeystore() throws Exception {
+ generateKeystore("client-hybrid", true, KEYSTORES_DIR + "/client-hybrid-keystore.p12",
+ "client", false, true);
+ }
+
+ /**
+ * Generates client RSA-only keystore (no PQC extensions - for failure test).
+ */
+ public static void generateClientRsaOnlyKeystore() throws Exception {
+ generateKeystore("client-rsa-only", false, KEYSTORES_DIR + "/client-rsa-only-keystore.p12",
+ "client", false, false);
+ }
+
+ /**
+ * Saves a KeyStore to disk, overwriting any existing file.
+ */
+ public static void saveKeyStore(KeyStore keyStore, String path, String password) throws Exception {
+ Path dirPath = Paths.get(path).getParent();
+ if (!Files.exists(dirPath)) {
+ Files.createDirectories(dirPath);
+ LOG.info("Created directory: {}", dirPath);
+ }
+
+ try (FileOutputStream fos = new FileOutputStream(path)) {
+ keyStore.store(fos, password.toCharArray());
+ }
+ }
+}
diff --git a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java
new file mode 100644
index 00000000..44309e07
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java
@@ -0,0 +1,54 @@
+/*
+ * 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.acme.http.pqc.certificates;
+
+import java.security.Security;
+
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class SecurityConfiguration {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SecurityConfiguration.class);
+
+ void onStart(@Observes StartupEvent ev) {
+ // Register BouncyCastle as the first provider to ensure PQC algorithms are available
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ LOG.info("BouncyCastle provider registered at position 1 for PQC support");
+
+ // Generate fresh hybrid PQC keystores on every startup
+ generateKeystores();
+ }
+
+ private void generateKeystores() {
+ try {
+ LOG.info("Generating fresh hybrid PQC keystores...");
+ HybridCertificateGenerator.generateServerKeystore();
+ HybridCertificateGenerator.generateClientHybridKeystore();
+ HybridCertificateGenerator.generateClientRsaOnlyKeystore();
+ LOG.info("Hybrid PQC keystores generated successfully");
+ } catch (Exception e) {
+ LOG.error("Failed to generate hybrid PQC keystores", e);
+ throw new RuntimeException("Keystore generation failed", e);
+ }
+ }
+}
diff --git a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificateValidationException.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificateValidationException.java
new file mode 100644
index 00000000..c2015438
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/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.acme.http.pqc.certificates.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/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java
new file mode 100644
index 00000000..84c4d8c3
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java
@@ -0,0 +1,200 @@
+/*
+ * 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.acme.http.pqc.certificates.util;
+
+import java.io.IOException;
+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.acme.http.pqc.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);
+
+ private CertificatesUtil() {
+ throw new AssertionError("Utility class cannot be instantiated");
+ }
+
+ /**
+ * 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/http-pqc-j17/src/main/java/org/acme/http/pqc/crypto/ChimeraOids.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/crypto/ChimeraOids.java
new file mode 100644
index 00000000..d5fc254c
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/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.acme.http.pqc.crypto;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+
+/**
+ * X.509 extension OIDs (Object Identifiers) for Chimera hybrid certificate format.
+ *
+ * <p>
+ * 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.
+ *
+ * <h2>Chimera Hybrid Certificate Structure:</h2>
+ *
+ * <pre>
+ * 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
+ * </pre>
+ *
+ * <p>
+ * <b>Why this matters:</b> Both signatures must be valid for authentication.
+ * This provides quantum resistance (via ML-DSA-65) while maintaining backward
+ * compatibility with standard TLS (via RSA).
+ *
+ * <h2>OID Hierarchy Explained:</h2>
+ * <ul>
+ * <li><b>2.5.29.x</b> - X.509 certificate extensions (standardized by ITU-T)
+ * <ul>
+ * <li>2.5.29.15 - keyUsage (common)</li>
+ * <li>2.5.29.17 - subjectAltName (common)</li>
+ * <li>2.5.29.72-74 - Chimera hybrid extensions (new for PQC)</li>
+ * </ul>
+ * </li>
+ * <li><b>2.16.840.1.101.3.4.3.18</b> - ML-DSA-65 algorithm (NIST FIPS 204)
+ * <ul>
+ * <li>2.16.840.1.101.3 - NIST algorithms</li>
+ * <li>4.3 - Signature algorithms</li>
+ * <li>18 - ML-DSA-65 (Level 3 security)</li>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * @see <a href="https://www.rfc-editor.org/rfc/rfc5280">RFC 5280 - X.509 Certificate Standard</a>
+ * @see <a href="https://csrc.nist.gov/pubs/fips/204/final">FIPS 204 - ML-DSA Standard</a>
+ */
+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.
+ *
+ * <p>
+ * This allows a single certificate to carry two public keys:
+ * <ul>
+ * <li>Primary: RSA-2048 (in standard subjectPublicKeyInfo field)</li>
+ * <li>Alternative: ML-DSA-65 (in this extension)</li>
+ * </ul>
+ *
+ * <p>
+ * <b>Official References:</b>
+ * <ul>
+ * <li><a href="https://datatracker.ietf.org/doc/html/draft-ounsworth-pq-composite-sigs-13#section-6.2">
+ * IETF Draft: Composite Signatures - Section 6.2 (altSubjectPublicKeyInfo)</a></li>
+ * <li><a href="https://oidref.com/2.5.29.72">OID Repository: 2.5.29.72</a></li>
+ * <li><a href="https://www.rfc-editor.org/rfc/rfc5280#section-4.2">
+ * RFC 5280 - X.509 Certificate Extensions</a></li>
+ * </ul>
+ */
+ 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).
+ *
+ * <p>
+ * Contains the OID {@link #ML_DSA_65} to specify the PQC algorithm used.
+ *
+ * <p>
+ * <b>Official References:</b>
+ * <ul>
+ * <li><a href="https://datatracker.ietf.org/doc/html/draft-ounsworth-pq-composite-sigs-13#section-6.1">
+ * IETF Draft: Composite Signatures - Section 6.1 (altSignatureAlgorithm)</a></li>
+ * <li><a href="https://oidref.com/2.5.29.73">OID Repository: 2.5.29.73</a></li>
+ * </ul>
+ */
+ 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.
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * <b>Official References:</b>
+ * <ul>
+ * <li><a href="https://datatracker.ietf.org/doc/html/draft-ounsworth-pq-composite-sigs-13#section-6.3">
+ * IETF Draft: Composite Signatures - Section 6.3 (altSignatureValue)</a></li>
+ * <li><a href="https://oidref.com/2.5.29.74">OID Repository: 2.5.29.74</a></li>
+ * </ul>
+ */
+ 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).
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * <b>Security level:</b> NIST Level 3, equivalent to AES-192 (192-bit security)
+ * <br>
+ * <b>Signature size:</b> ~3,309 bytes (much larger than RSA-2048's ~256 bytes)
+ * <br>
+ * <b>Public key size:</b> ~1,952 bytes
+ *
+ * <p>
+ * <b>NOTE:</b> ML-DSA (standardized) and Dilithium (pre-standard) are NOT interoperable.
+ * This OID represents the final NIST FIPS 204 standardized version.
+ *
+ * <p>
+ * <b>Official References:</b>
+ * <ul>
+ * <li><a href="https://csrc.nist.gov/pubs/fips/204/final">
+ * NIST FIPS 204 - Module-Lattice-Based Digital Signature Standard (ML-DSA)</a></li>
+ * <li><a href="https://datatracker.ietf.org/doc/rfc9881/">
+ * RFC 9881 - Algorithm Identifiers for ML-DSA in X.509</a></li>
+ * <li><a href="https://oidref.com/2.16.840.1.101.3.4.3.18">
+ * OID Repository: 2.16.840.1.101.3.4.3.18</a></li>
+ * <li><a href=
+ * "https://github.com/bcgit/bc-java/blob/main/core/src/main/java/org/bouncycastle/asn1/nist/NISTObjectIdentifiers.java">
+ * BouncyCastle Source: NIST ML-DSA OID definitions</a></li>
+ * </ul>
+ */
+ 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/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java
new file mode 100644
index 00000000..c4c48dbf
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java
@@ -0,0 +1,58 @@
+/*
+ * 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.acme.http.pqc.trustmanager;
+
+import javax.net.ssl.X509TrustManager;
+
+import io.quarkus.vertx.http.HttpServerOptionsCustomizer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.TrustOptions;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Quarkus CDI bean that customizes the Vert.x HTTP server to use a custom TrustManager.
+ *
+ * This customizer registers {@link HybridPqcX509TrustManager} with the HTTP server,
+ * enabling TLS-layer validation of hybrid PQC certificates (RSA + ML-DSA-65) during
+ * the TLS handshake.
+ *
+ * Uses Quarkus {@link HttpServerOptionsCustomizer} interface to integrate with Vert.x.
+ */
+@ApplicationScoped
+public class HybridPqcTrustManagerCustomizer implements HttpServerOptionsCustomizer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HybridPqcTrustManagerCustomizer.class);
+
+ @Override
+ public void customizeHttpsServer(HttpServerOptions options) {
+ LOG.info("Registering custom hybrid PQC TrustManager for TLS-layer validation...");
+
+ // Create custom TrustManager
+ X509TrustManager customTrustManager = new HybridPqcX509TrustManager();
+
+ // Wrap the X509TrustManager into Vert.x TrustOptions
+ TrustOptions trustOptions = TrustOptions.wrap(customTrustManager);
+
+ // Register with Vert.x HTTP server using setTrustOptions
+ options.setTrustOptions(trustOptions);
+
+ LOG.info("Custom hybrid PQC TrustManager registered successfully");
+ LOG.info(" Client certificates will be validated at TLS layer (RSA + ML-DSA-65)");
+ }
+}
diff --git a/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java
new file mode 100644
index 00000000..7d85ec9e
--- /dev/null
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java
@@ -0,0 +1,77 @@
+/*
+ * 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.acme.http.pqc.trustmanager;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.acme.http.pqc.certificates.util.CertificateValidationException;
+import org.acme.http.pqc.certificates.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);
+
+ @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 {
+ // Not implemented - this example only validates client certificates
+ // (server-to-client authentication, not client-to-server)
+ //
+ // In mutual TLS where client validates server's hybrid certificate,
+ // this method would implement similar validation logic.
+ LOG.debug("Server certificate validation not implemented (client-auth only)");
+ }
+
+ @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/http-pqc-j17/src/main/resources/application.properties b/http-pqc-j17/src/main/resources/application.properties
new file mode 100644
index 00000000..90b2e0cd
--- /dev/null
+++ b/http-pqc-j17/src/main/resources/application.properties
@@ -0,0 +1,49 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+#
+# Quarkus
+#
+quarkus.banner.enabled = false
+quarkus.log.file.enabled = true
+
+# HTTPS Configuration with Hybrid PQC Certificates
+quarkus.http.ssl-port = 8443
+quarkus.http.insecure-requests = disabled
+
+# Server uses hybrid certificate (RSA + ML-DSA-65)
+quarkus.http.ssl.certificate.key-store-file = target/certs/server-hybrid-keystore.p12
+quarkus.http.ssl.certificate.key-store-password = changeit
+quarkus.http.ssl.certificate.key-store-file-type = PKCS12
+
+# Require client certificates (validated by custom TrustManager)
+# Certificates must be valid hybrid PQC certificates (RSA + ML-DSA-65)
+quarkus.http.ssl.client-auth = required
+
+# Truststore for client certificate validation (standard TLS layer)
+quarkus.http.ssl.certificate.trust-store-file = target/certs/server-hybrid-truststore.p12
+quarkus.http.ssl.certificate.trust-store-password = changeit
+quarkus.http.ssl.certificate.trust-store-file-type = PKCS12
+
+#
+# Quarkus - Camel
+#
+quarkus.camel.health.enabled = true
+
+#
+# Camel
+#
+camel.context.name = quarkus-camel-example-http-pqc
diff --git a/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcIT.java b/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcIT.java
new file mode 100644
index 00000000..232ace0a
--- /dev/null
+++ b/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcIT.java
@@ -0,0 +1,23 @@
+/*
+ * 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.acme.http.pqc;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+public class HttpPqcIT extends HttpPqcTest {
+}
diff --git a/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.java b/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.java
new file mode 100644
index 00000000..39387ef6
--- /dev/null
+++ b/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.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.acme.http.pqc;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.config.RestAssuredConfig;
+import io.restassured.config.SSLConfig;
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@QuarkusTest
+public class HttpPqcTest {
+
+ private static final Logger log = LoggerFactory.getLogger(HttpPqcTest.class);
+
+ @BeforeAll
+ public static void setUpClass() {
+ // Use relaxed HTTPS validation for self-signed certificates in tests
+ RestAssured.useRelaxedHTTPSValidation();
+ }
+
+ @BeforeEach
+ public void setUp() {
+ // Configure base URI and port for each test
+ // Read the port dynamically from config - works in both JVM and native tests
+ int sslPort = ConfigProvider.getConfig()
+ .getValue("quarkus.http.test-ssl-port", Integer.class);
+ RestAssured.baseURI = "https://localhost";
+ RestAssured.port = sslPort;
+ }
+
+ @Test
+ public void testPqcSecureEndpointWithoutClientCert() {
+ // With client-auth=required, TLS handshake fails before reaching the route
+ try {
+ RestAssured.given()
+ .when()
+ .get("/pqc/secure")
+ .then()
+ .statusCode(200);
+
+ fail("Expected SSL exception but connection succeeded");
+ } catch (Exception e) {
+ // Expected: SSL handshake failure (certificate_required)
+ log.info("✓ Expected SSL exception without client cert: {}", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPqcSecureEndpointWithValidClientCert() {
+ // Test /pqc/secure WITH valid hybrid client certificate
+ // TLS handshake should succeed, and custom TrustManager validates both RSA + ML-DSA-65
+ // Configure RestAssured with client certificate keystore and server truststore
+
+ RestAssured.given()
+ .config(RestAssuredConfig.config().sslConfig(
+ SSLConfig.sslConfig()
+ .keyStore("target/certs/client-hybrid-keystore.p12", "changeit")
+ .trustStore("target/certs/server-hybrid-truststore.p12", "changeit")
+ .allowAllHostnames() // Accept localhost with self-signed cert
+ ))
+ .when()
+ .get("/pqc/secure")
+ .then()
+ .statusCode(200)
+ .body(containsString("Hybrid PQC certificate validated"))
+ .body(containsString("quantum-safe"))
+ .body(containsString("TLS layer"));
+ }
+
+ @Test
+ public void testPqcSecureEndpointWithRsaOnlyCertificate() {
+ // Test /pqc/secure WITH RSA-only client certificate (no PQC extensions)
+ // TLS handshake should FAIL because custom TrustManager requires hybrid cert
+ try {
+ RestAssured.given()
+ .config(RestAssuredConfig.config().sslConfig(
+ SSLConfig.sslConfig()
+ .keyStore("target/certs/client-rsa-only-keystore.p12", "changeit")
+ .trustStore("target/certs/server-hybrid-truststore.p12", "changeit")
+ .allowAllHostnames()))
+ .when()
+ .get("/pqc/secure")
+ .then()
+ .statusCode(200); // Should NOT reach here
+
+ fail("Expected SSL exception for RSA-only certificate, but connection succeeded");
+ } catch (Exception e) {
+ // Expected: TLS handshake failure due to missing PQC extensions
+ log.info("✓ Expected SSL exception for RSA-only cert: {}", e.getMessage());
+ }
+ }
+}