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

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


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new 85f7874a6390 [backport camel-4.22.x] CAMEL-22114: Fix PQC KeyStore 
tests on Java 25 (InvalidKeyException) (#25753)
85f7874a6390 is described below

commit 85f7874a6390a3d0b3c3fd05439987bdc88b03c7
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Aug 26 11:04:51 2026 +0200

    [backport camel-4.22.x] CAMEL-22114: Fix PQC KeyStore tests on Java 25 
(InvalidKeyException) (#25753)
    
    Cherry-pick of #25510 onto camel-4.22.x. On JDK 25+, 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-encodes keys through BC's KeyFactory after loading from a
    KeyStore or user-supplied KeyPair, transparently converting
    JDK-native PQC keys into BC types. No-op on Java 17/21. Adds a
    dedicated regression test gated to Java 25+.
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../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());
+    }
+}

Reply via email to