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 1b1f6a40 http-pqc-j21: verify the server certificate instead of 
passing curl -k
1b1f6a40 is described below

commit 1b1f6a40aac84fb29bc3816edc84b79cc902cadf
Author: James Netherton <[email protected]>
AuthorDate: Fri Aug 7 07:12:39 2026 +0100

    http-pqc-j21: verify the server certificate instead of passing curl -k
    
    The manual testing command in README.adoc passed -k, which disables exactly
    the certificate and hostname verification this example exists to 
demonstrate.
    
    - README.adoc: use --cacert target/certs/server-cert.pem instead of -k. Also
      corrects the sample response body, which showed a JSON payload the route
      does not produce.
    - CertificateGenerator: add subject alternative names (DNS:localhost,
      IP:127.0.0.1) to the server certificate, and write it out as
      server-cert.pem so that curl has a CA file to trust. Without a SAN the
      certificate only verifies via OpenSSL's legacy CN fallback.
    - AbstractPqcTest: stop disabling hostname verification in the RestAssured
      and HttpClient 5 clients, so the tests fail if the SAN regresses.
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 http-pqc-j21/README.adoc                           | 16 +++++--
 .../pqc/certificates/CertificateGenerator.java     | 51 ++++++++++++++++++----
 .../java/org/acme/http/pqc/AbstractPqcTest.java    |  7 +--
 3 files changed, 58 insertions(+), 16 deletions(-)

diff --git a/http-pqc-j21/README.adoc b/http-pqc-j21/README.adoc
index ac2dba23..4b3c629c 100644
--- a/http-pqc-j21/README.adoc
+++ b/http-pqc-j21/README.adoc
@@ -34,6 +34,9 @@ Certificates are automatically generated during application 
startup in `target/c
 * `server-truststore.p12` - Truststore for validating clients
 * `client-keystore.p12` - Client certificate for testing
 * `client-truststore.p12` - Client truststore for server validation
+* `server-cert.pem` - Server certificate in PEM form, for clients that cannot 
read PKCS12 truststores (for example `curl --cacert`)
+
+The server certificate is issued for `CN=localhost` with subject alternative 
names `DNS:localhost` and `IP:127.0.0.1`, so clients can verify the server 
identity rather than having to disable verification.
 
 NOTE: Certificates use traditional RSA signatures. The PQC protection comes 
from the hybrid key exchange (X25519MLKEM768) negotiated during the TLS 
handshake, not from the certificate signatures.
 
@@ -81,9 +84,10 @@ IMPORTANT: Standard tools like `curl`, `wget`, and `openssl 
s_client` do **not**
 jdk.tls.namedGroups=X25519MLKEM768,x25519,secp256r1
 ----
 
-PKCS12 keystores are automatically generated during test execution in 
`target/certs/`:
+Client credentials are automatically generated during test execution in 
`target/certs/`:
 
 * `client-keystore.p12` - Client certificate (password: `changeit`)
+* `server-cert.pem` - Server certificate, used by curl to verify the server
 
 Start the application with fallback configuration and test with curl:
 
@@ -94,10 +98,16 @@ mvn quarkus:dev
 # In another terminal
 curl --cert target/certs/client-keystore.p12:changeit \
        --cert-type P12 \
-       -k \
+       --cacert target/certs/server-cert.pem \
        https://localhost:xxxxx/pqc/secure
 
-{"message":"Serving secure data via PQC-enabled TLS connection"}
+Secure data delivered via Post-Quantum Cryptography (PQC)!
+
+Connection details:
+- TLS version: 1.3
+- Key exchange: X25519MLKEM768 (hybrid PQC)
+- Provider: BouncyCastle JSSE
+...
 ----
 
 NOTE: With fallback enabled, curl will use classical x25519 or secp256r1 key 
exchange since it doesn't support X25519MLKEM768. The connection is secure but 
not quantum-resistant.
diff --git 
a/http-pqc-j21/src/main/java/org/acme/http/pqc/certificates/CertificateGenerator.java
 
b/http-pqc-j21/src/main/java/org/acme/http/pqc/certificates/CertificateGenerator.java
index cd532afb..17143b22 100644
--- 
a/http-pqc-j21/src/main/java/org/acme/http/pqc/certificates/CertificateGenerator.java
+++ 
b/http-pqc-j21/src/main/java/org/acme/http/pqc/certificates/CertificateGenerator.java
@@ -18,6 +18,7 @@ package org.acme.http.pqc.certificates;
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.FileWriter;
 import java.math.BigInteger;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -33,9 +34,12 @@ import java.util.Date;
 import org.bouncycastle.asn1.x500.X500Name;
 import org.bouncycastle.asn1.x509.BasicConstraints;
 import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
 import org.bouncycastle.cert.X509v3CertificateBuilder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
 import org.bouncycastle.operator.ContentSigner;
 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 import org.jboss.logging.Logger;
@@ -100,18 +104,30 @@ public class CertificateGenerator {
 
     /**
      * Generates server keystore with RSA certificate.
+     *
+     * <p>
+     * The certificate carries subject alternative names for {@code localhost} 
and {@code 127.0.0.1} so that clients
+     * can verify the server identity instead of having to disable hostname 
verification. It is also written out as a
+     * PEM file so that it can be passed to tools such as {@code curl 
--cacert}.
      */
     public static void generateServerKeystore() throws Exception {
-        serverData = generateCertificateData("CN=localhost,O=Camel 
Quarkus,C=US", true);
+        GeneralNames subjectAltNames = new GeneralNames(new GeneralName[] {
+                new GeneralName(GeneralName.dNSName, "localhost"),
+                new GeneralName(GeneralName.iPAddress, "127.0.0.1") });
+
+        serverData = generateCertificateData("CN=localhost,O=Camel 
Quarkus,C=US", true, subjectAltNames);
         saveKeyStore(Paths.get(CERT_DIR, "server-keystore.p12"), 
serverData.keyPair, serverData.certificate, "server");
         LOG.info("Server keystore created: " + CERT_DIR + 
"/server-keystore.p12");
+
+        savePem(Paths.get(CERT_DIR, "server-cert.pem"), 
serverData.certificate);
+        LOG.info("Server certificate PEM created: " + CERT_DIR + 
"/server-cert.pem");
     }
 
     /**
      * Generates client keystore with RSA certificate.
      */
     public static void generateClientKeystore() throws Exception {
-        clientData = generateCertificateData("CN=client,O=Camel Quarkus,C=US", 
false);
+        clientData = generateCertificateData("CN=client,O=Camel Quarkus,C=US", 
false, null);
         saveKeyStore(Paths.get(CERT_DIR, "client-keystore.p12"), 
clientData.keyPair, clientData.certificate, "client");
         LOG.info("Client keystore created: " + CERT_DIR + 
"/client-keystore.p12");
     }
@@ -135,13 +151,15 @@ public class CertificateGenerator {
     /**
      * Generates a certificate with RSA keypair.
      *
-     * @param  dn   The DN for the certificate subject
-     * @param  isCA Whether this is a CA certificate
-     * @return      CertificateData containing keypair and certificate
+     * @param  dn              The DN for the certificate subject
+     * @param  isCA            Whether this is a CA certificate
+     * @param  subjectAltNames The subject alternative names to add to the 
certificate, or {@code null} for none
+     * @return                 CertificateData containing keypair and 
certificate
      */
-    private static CertificateData generateCertificateData(String dn, boolean 
isCA) throws Exception {
+    private static CertificateData generateCertificateData(String dn, boolean 
isCA, GeneralNames subjectAltNames)
+            throws Exception {
         KeyPair keyPair = generateKeyPair();
-        X509Certificate certificate = generateCertificate(keyPair, dn, isCA);
+        X509Certificate certificate = generateCertificate(keyPair, dn, isCA, 
subjectAltNames);
         return new CertificateData(keyPair, certificate);
     }
 
@@ -152,7 +170,8 @@ public class CertificateGenerator {
         return keyPairGenerator.generateKeyPair();
     }
 
-    private static X509Certificate generateCertificate(KeyPair keyPair, String 
dn, boolean isCA) throws Exception {
+    private static X509Certificate generateCertificate(KeyPair keyPair, String 
dn, boolean isCA,
+            GeneralNames subjectAltNames) throws Exception {
         long now = System.currentTimeMillis();
         Date notBefore = new Date(now);
         // Valid for 3 years for development convenience
@@ -172,6 +191,10 @@ public class CertificateGenerator {
 
         certBuilder.addExtension(Extension.basicConstraints, true, new 
BasicConstraints(isCA));
 
+        if (subjectAltNames != null) {
+            certBuilder.addExtension(Extension.subjectAlternativeName, false, 
subjectAltNames);
+        }
+
         // Use default SUN provider for RSA signing - no need for BC provider
         ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA")
                 .build(keyPair.getPrivate());
@@ -198,6 +221,18 @@ public class CertificateGenerator {
         }
     }
 
+    private static void savePem(Path path, X509Certificate cert) throws 
Exception {
+        Path dirPath = path.getParent();
+        if (!Files.exists(dirPath)) {
+            Files.createDirectories(dirPath);
+            LOG.info("Created directory: " + dirPath);
+        }
+
+        try (JcaPEMWriter pemWriter = new JcaPEMWriter(new 
FileWriter(path.toFile()))) {
+            pemWriter.writeObject(cert);
+        }
+    }
+
     private static void saveTrustStore(Path path, X509Certificate cert, String 
alias) throws Exception {
         Path dirPath = path.getParent();
         if (!Files.exists(dirPath)) {
diff --git a/http-pqc-j21/src/test/java/org/acme/http/pqc/AbstractPqcTest.java 
b/http-pqc-j21/src/test/java/org/acme/http/pqc/AbstractPqcTest.java
index 30c69ce5..3ff13237 100644
--- a/http-pqc-j21/src/test/java/org/acme/http/pqc/AbstractPqcTest.java
+++ b/http-pqc-j21/src/test/java/org/acme/http/pqc/AbstractPqcTest.java
@@ -34,7 +34,6 @@ import 
org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
 import 
org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
 import org.apache.hc.client5.http.io.HttpClientConnectionManager;
-import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
 import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
 import org.apache.hc.core5.http.HttpResponse;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -76,8 +75,7 @@ abstract class AbstractPqcTest {
                 .config(RestAssuredConfig.config().sslConfig(
                         SSLConfig.sslConfig()
                                 .keyStore("target/certs/client-keystore.p12", 
"changeit")
-                                
.trustStore("target/certs/client-truststore.p12", "changeit")
-                                .allowAllHostnames()))
+                                
.trustStore("target/certs/client-truststore.p12", "changeit")))
                 .baseUri("https://localhost:"; + RestAssured.port)
                 .when()
                 .get("/pqc/secure")
@@ -92,8 +90,7 @@ abstract class AbstractPqcTest {
             SSLContext sslContext = createSslContext(securityProvider);
 
             // Create custom SSLConnectionSocketFactory that explicitly sets 
named groups
-            SSLConnectionSocketFactory sslSocketFactory = new 
SSLConnectionSocketFactory(sslContext,
-                    NoopHostnameVerifier.INSTANCE) {
+            SSLConnectionSocketFactory sslSocketFactory = new 
SSLConnectionSocketFactory(sslContext) {
                 @Override
                 protected void prepareSocket(javax.net.ssl.SSLSocket socket) 
throws java.io.IOException {
                     super.prepareSocket(socket);

Reply via email to