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());
+ }
+}