This is an automated email from the ASF dual-hosted git repository. JiriOndrusek pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git
commit adda06299e359d4e54a4e084c0a3e8da5588e37c 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);
