You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/08/26 06:23:20 UTC

(camel) branch backport/25510-to-camel-4.22.x created (now 3cd04a2ca0bc)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a change to branch backport/25510-to-camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


      at 3cd04a2ca0bc CAMEL-22114: Fix PQC KeyStore tests on Java 25 (InvalidKeyException) (#25510)

This branch includes the following new commits:

     new 3cd04a2ca0bc CAMEL-22114: Fix PQC KeyStore tests on Java 25 (InvalidKeyException) (#25510)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-22114: Fix PQC KeyStore tests on Java 25 (InvalidKeyException) (#25510)

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch backport/25510-to-camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3cd04a2ca0bcf44db3316e27d9f23ceb84280cf6
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sun Aug 23 19:43:30 2026 +0200

    CAMEL-22114: Fix PQC KeyStore tests on Java 25 (InvalidKeyException) (#25510)
    
    * CAMEL-22114: Fix PQC KeyStore tests on Java 25 (InvalidKeyException)
    
    On JDK 25+, the JKS KeyStore deserialises ML-DSA keys as JDK-native
    key objects that Bouncy Castle's Signature SPI does not recognise,
    causing InvalidKeyException: unknown private key passed to ML-DSA.
    
    Re-encode keys through BC's KeyFactory after loading from a KeyStore
    (or from user-supplied KeyPair). This transparently converts JDK-native
    PQC keys into BC types and is a no-op for keys that are already BC
    instances.
    
    Fixes PQCSignatureOnlyKeyStoreTest and PQCSignatureWithKeyStoreTest
    on Java 25.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    * CAMEL-22114: Add dedicated regression test for Java 25 PQC key conversion
    
    Add @EnabledForJreRange(min = JAVA_25) test that validates ML-DSA
    sign+verify via JKS KeyStore works after the JDK-native key to BC
    key conversion fix. Skipped on Java <25 where the conversion is a no-op.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../apache/camel/component/pqc/PQCProducer.java    |  65 ++++++++
 .../pqc/PQCKeyStoreJdk25KeyConversionTest.java     | 173 +++++++++++++++++++++
 2 files changed, 238 insertions(+)

diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
index 3e7552eeaf61..be03b318ed00 100644
--- a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
+++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
@@ -21,6 +21,8 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.*;
 import java.security.cert.Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -53,9 +55,11 @@ import org.apache.camel.util.SecureRandomHelper;
 import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
 import org.bouncycastle.jcajce.spec.KEMExtractSpec;
 import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.pqc.jcajce.interfaces.LMSPrivateKey;
 import org.bouncycastle.pqc.jcajce.interfaces.XMSSMTPrivateKey;
 import org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey;
+import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -403,6 +407,15 @@ public class PQCProducer extends DefaultProducer {
             keyPair = getConfiguration().getKeyPair();
         }
 
+        // On JDK 25+, a JKS KeyStore (or user-supplied KeyPair) may contain JDK-native PQC keys
+        // (e.g. ML-DSA, ML-KEM) that Bouncy Castle's Signature / KeyGenerator SPI does not recognise,
+        // causing InvalidKeyException at initSign / initVerify time. Re-encoding through BC's KeyFactory
+        // transparently converts JDK-native keys into the BC types the rest of the component expects,
+        // and is a no-op for keys that are already BC instances.
+        if (keyPair != null) {
+            keyPair = ensureBcKeyPair(keyPair);
+        }
+
         // Initialize hybrid signature operations
         if (getConfiguration().getOperation().equals(PQCOperations.hybridSign)
                 || getConfiguration().getOperation().equals(PQCOperations.hybridVerify)) {
@@ -1199,4 +1212,56 @@ public class PQCProducer extends DefaultProducer {
         }
     }
 
+    /**
+     * Ensures both keys in the pair are Bouncy Castle key instances.
+     * <p>
+     * On JDK 25+, a JKS {@link KeyStore} may deserialise standardised PQC keys (ML-DSA, ML-KEM) into JDK-native key
+     * objects that Bouncy Castle's {@link Signature} / {@link KeyGenerator} SPI does not recognise, causing
+     * {@link InvalidKeyException} at {@code initSign} / {@code initVerify} time.
+     * <p>
+     * Re-encoding through BC's {@link KeyFactory} is a no-op for keys that are already BC instances and transparently
+     * converts JDK-native ones into the BC types the rest of the component expects.
+     */
+    private static KeyPair ensureBcKeyPair(KeyPair kp) {
+        PrivateKey priv = kp.getPrivate();
+        PublicKey pub = kp.getPublic();
+
+        boolean privIsBc = priv == null || priv.getClass().getName().startsWith("org.bouncycastle.");
+        boolean pubIsBc = pub == null || pub.getClass().getName().startsWith("org.bouncycastle.");
+        if (privIsBc && pubIsBc) {
+            return kp;
+        }
+
+        try {
+            String alg = priv != null ? priv.getAlgorithm() : pub.getAlgorithm();
+            KeyFactory kf = getBcKeyFactory(alg);
+
+            if (!privIsBc) {
+                priv = kf.generatePrivate(new PKCS8EncodedKeySpec(priv.getEncoded()));
+            }
+            if (!pubIsBc) {
+                pub = kf.generatePublic(new X509EncodedKeySpec(pub.getEncoded()));
+            }
+            return new KeyPair(pub, priv);
+        } catch (Exception e) {
+            // If conversion fails (e.g. algorithm not known to BC), return the original pair
+            // and let the caller deal with any resulting exception from the crypto operation
+            LOG.debug("Could not convert KeyPair to Bouncy Castle key types: {}", e.getMessage());
+            return kp;
+        }
+    }
+
+    /**
+     * Returns a BC {@link KeyFactory} for the given JCE algorithm name, trying the main BC provider first and falling
+     * back to the BC PQC provider.
+     */
+    private static KeyFactory getBcKeyFactory(String algorithm)
+            throws NoSuchAlgorithmException, NoSuchProviderException {
+        try {
+            return KeyFactory.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME);
+        } catch (NoSuchAlgorithmException e) {
+            return KeyFactory.getInstance(algorithm, BouncyCastlePQCProvider.PROVIDER_NAME);
+        }
+    }
+
 }
diff --git a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyStoreJdk25KeyConversionTest.java b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyStoreJdk25KeyConversionTest.java
new file mode 100644
index 000000000000..56cebe4205ce
--- /dev/null
+++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyStoreJdk25KeyConversionTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.pqc;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Produce;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Regression test for the Java 25+ JKS KeyStore key conversion fix.
+ * <p>
+ * On Java 25+, JKS KeyStore deserialises ML-DSA keys as JDK-native objects (via JEP 497) rather than Bouncy Castle
+ * objects. BC's Signature SPI does not recognise the JDK-native key types and throws {@link InvalidKeyException
+ * InvalidKeyException: unknown private key passed to ML-DSA}. The fix in {@code PQCProducer.ensureBcKeyPair()}
+ * re-encodes such keys through BC's {@link KeyFactory} transparently.
+ * <p>
+ * This test is only meaningful on Java 25+ where the JDK provides a native ML-DSA {@link KeyFactory}. On earlier JVMs,
+ * JKS always returns BC key objects and the conversion is a no-op.
+ */
+@EnabledForJreRange(min = JRE.JAVA_25)
+class PQCKeyStoreJdk25KeyConversionTest extends CamelTestSupport {
+
+    private static final String KEYSTORE_FILE = "keystore-jdk25-test.jks";
+
+    @EndpointInject("mock:sign")
+    protected MockEndpoint resultSign;
+
+    @EndpointInject("mock:verify")
+    protected MockEndpoint resultVerify;
+
+    @Produce("direct:sign")
+    protected ProducerTemplate templateSign;
+
+    PQCKeyStoreJdk25KeyConversionTest() throws NoSuchAlgorithmException {
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:sign")
+                        .to("pqc:sign?operation=sign&keyPairAlias=mykey&keyStorePassword=changeit")
+                        .to("mock:sign")
+                        .to("pqc:verify?operation=verify&keyPairAlias=mykey&keyStorePassword=changeit")
+                        .to("mock:verify");
+            }
+        };
+    }
+
+    @BeforeAll
+    static void startup() {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    @AfterAll
+    static void teardown() throws Exception {
+        Files.deleteIfExists(Path.of(KEYSTORE_FILE));
+    }
+
+    /**
+     * Verifies that ML-DSA sign + verify works via a JKS KeyStore on Java 25+, where retrieved keys are JDK-native and
+     * must be converted to BC types by PQCProducer.
+     */
+    @Test
+    void testSignAndVerifyWithJdkNativeKeysFromKeyStore() throws Exception {
+        resultSign.expectedMessageCount(1);
+        resultVerify.expectedMessageCount(1);
+        templateSign.sendBody("Hello from Java 25");
+        resultSign.assertIsSatisfied();
+        resultVerify.assertIsSatisfied();
+        assertThat(resultVerify.getExchanges().get(0).getMessage().getHeader(PQCConstants.VERIFY, Boolean.class))
+                .as("Signature verification should succeed after JDK-native key conversion")
+                .isTrue();
+    }
+
+    @BindToRegistry("Keystore")
+    public KeyStore setKeyStore()
+            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, KeyStoreException,
+            CertificateException, IOException, OperatorCreationException, UnrecoverableKeyException {
+        KeyPairGenerator kpGen = KeyPairGenerator.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+                PQCSignatureAlgorithms.MLDSA.getBcProvider());
+        kpGen.initialize(MLDSAParameterSpec.ml_dsa_65);
+        KeyPair kp = kpGen.generateKeyPair();
+
+        // Validity
+        Date startDate = new Date();
+        Date endDate = new Date(startDate.getTime() + 365L * 24 * 60 * 60 * 1000); // 1 year
+
+        // Serial Number
+        BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
+
+        X500Name dnName = new X500Name("CN=Test User");
+        // Build the certificate
+        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+                dnName,
+                serialNumber,
+                startDate,
+                endDate,
+                dnName,
+                kp.getPublic());
+
+        ContentSigner contentSigner = new JcaContentSignerBuilder(PQCSignatureAlgorithms.MLDSA.getAlgorithm())
+                .setProvider(PQCSignatureAlgorithms.MLDSA.getBcProvider())
+                .build(kp.getPrivate());
+
+        X509Certificate certificate = new JcaX509CertificateConverter()
+                .setProvider("BC")
+                .getCertificate(certBuilder.build(contentSigner));
+
+        KeyStore keyStore = KeyStore.getInstance("JKS");
+        char[] password = "changeit".toCharArray();
+        keyStore.load(null, password); // initialize new keystore
+        keyStore.setKeyEntry("mykey", kp.getPrivate(), password, new Certificate[] { certificate });
+
+        // Save keystore to file
+        try (FileOutputStream fos = new FileOutputStream(KEYSTORE_FILE)) {
+            keyStore.store(fos, password);
+        }
+        return keyStore;
+    }
+
+    @BindToRegistry("Signer")
+    public Signature getSigner() throws NoSuchAlgorithmException, NoSuchProviderException {
+        return Signature.getInstance(PQCSignatureAlgorithms.MLDSA.getAlgorithm(),
+                PQCSignatureAlgorithms.MLDSA.getBcProvider());
+    }
+}