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

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


The following commit(s) were added to refs/heads/main by this push:
     new a80092bc631f CAMEL-25824: camel-pqc - the hybrid KEM operations stamp 
the JCE algorithm name on the secret key (#29220)
a80092bc631f is described below

commit a80092bc631fc7a35062f89a1d89570d490a5327
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 5 16:47:47 2026 +0100

    CAMEL-25824: camel-pqc - the hybrid KEM operations stamp the JCE algorithm 
name on the secret key (#29220)
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
 .../component/pqc/PQCSymmetricAlgorithms.java      |  23 ++++
 .../component/pqc/crypto/hybrid/HybridKEM.java     |   8 +-
 .../component/pqc/dataformat/PQCDataFormat.java    |  18 +--
 .../pqc/crypto/hybrid/HybridKEMKeyLabelTest.java   | 137 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 ++
 5 files changed, 180 insertions(+), 17 deletions(-)

diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCSymmetricAlgorithms.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCSymmetricAlgorithms.java
index 03bfad6b4ba2..bccd5b1e6303 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCSymmetricAlgorithms.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCSymmetricAlgorithms.java
@@ -48,4 +48,27 @@ public enum PQCSymmetricAlgorithms {
         return algorithm;
     }
 
+    /**
+     * Maps a configured symmetric algorithm name to the JCE name a {@link 
javax.crypto.Cipher} resolves.
+     * <p/>
+     * Two constants spell the two differently - {@code GOST3412_2015} is 
{@code GOST3412-2015} and {@code DESEDE} is
+     * {@code DESede} - so a key labelled with the constant name cannot be 
handed to {@code Cipher.getInstance}. A name
+     * that is not one of the constants, which includes a JCE name that has 
already been mapped, is answered unchanged
+     * rather than rejected, so this is safe to apply to a value of unknown 
provenance.
+     *
+     * @param  name the configured name, or a JCE name
+     * @return      the JCE name, or {@code name} when it is not one of the 
constants
+     */
+    public static String toJceName(String name) {
+        if (name == null) {
+            return null;
+        }
+        for (PQCSymmetricAlgorithms candidate : values()) {
+            if (candidate.name().equals(name)) {
+                return candidate.getAlgorithm();
+            }
+        }
+        return name;
+    }
+
 }
diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEM.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEM.java
index 16493c36a476..76330529fd82 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEM.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEM.java
@@ -29,6 +29,7 @@ import javax.crypto.KeyGenerator;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 
+import org.apache.camel.component.pqc.PQCSymmetricAlgorithms;
 import org.apache.camel.util.SecureRandomHelper;
 import org.bouncycastle.crypto.digests.SHA256Digest;
 import org.bouncycastle.crypto.digests.SHA384Digest;
@@ -123,7 +124,9 @@ public final class HybridKEM {
                 keyLength / 8,
                 symmetricAlgorithm);
 
-        SecretKey hybridSecretKey = new SecretKeySpec(combinedSecret, 
symmetricAlgorithm);
+        // The label only: the value handed to combineSecrets above is the 
HKDF info and must stay exactly as the
+        // caller gave it, or every encapsulation produced by an earlier 
version stops decrypting.
+        SecretKey hybridSecretKey = new SecretKeySpec(combinedSecret, 
PQCSymmetricAlgorithms.toJceName(symmetricAlgorithm));
 
         // Combine encapsulations
         byte[] hybridEncapsulation = 
combineEncapsulations(classicalEncapsulation, pqcEncapsulation);
@@ -199,7 +202,8 @@ public final class HybridKEM {
                 keyLength / 8,
                 symmetricAlgorithm);
 
-        return new SecretKeySpec(combinedSecret, symmetricAlgorithm);
+        // As in encapsulate(): the label is mapped, the HKDF info is not
+        return new SecretKeySpec(combinedSecret, 
PQCSymmetricAlgorithms.toJceName(symmetricAlgorithm));
     }
 
     /**
diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/dataformat/PQCDataFormat.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/dataformat/PQCDataFormat.java
index b36aae6941a4..e0a5a4d482a0 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/dataformat/PQCDataFormat.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/dataformat/PQCDataFormat.java
@@ -266,7 +266,7 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
         // Fail fast if the configured symmetric algorithm cannot provide 
authenticated encryption. Per-message
         // overrides via the CamelPQCSymmetricAlgorithm header are validated 
at marshal/unmarshal time.
         if (symmetricKeyAlgorithm != null) {
-            checkAeadSupported(toJceAlgorithm(symmetricKeyAlgorithm));
+            
checkAeadSupported(PQCSymmetricAlgorithms.toJceName(symmetricKeyAlgorithm));
         }
 
         if (keyEncapsulationAlgorithm != null && keyGenerator == null) {
@@ -335,21 +335,9 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
     private String getSymmetricAlgorithm(Exchange exchange) {
         String alg = exchange.getIn().getHeader(SYMMETRIC_ALGORITHM, 
String.class);
         if (alg != null) {
-            return toJceAlgorithm(alg);
-        }
-        return toJceAlgorithm(this.symmetricKeyAlgorithm);
-    }
-
-    /**
-     * Maps a {@link PQCSymmetricAlgorithms} enum name to its JCE algorithm 
name, returning the input unchanged when it
-     * is not an enum constant (e.g. it is already a JCE name).
-     */
-    private static String toJceAlgorithm(String algorithm) {
-        try {
-            return PQCSymmetricAlgorithms.valueOf(algorithm).getAlgorithm();
-        } catch (IllegalArgumentException e) {
-            return algorithm;
+            return PQCSymmetricAlgorithms.toJceName(alg);
         }
+        return PQCSymmetricAlgorithms.toJceName(this.symmetricKeyAlgorithm);
     }
 
     /**
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEMKeyLabelTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEMKeyLabelTest.java
new file mode 100644
index 000000000000..50541877c810
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEMKeyLabelTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.crypto.hybrid;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyAgreement;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+import org.apache.camel.component.pqc.PQCSymmetricAlgorithms;
+import org.bouncycastle.jcajce.spec.MLKEMParameterSpec;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * The hybrid KEM must stamp the JCE algorithm name on the secret key it 
returns, as the non-hybrid operations do, so
+ * both ends of an exchange agree on the key's identity and the key resolves 
through {@code Cipher.getInstance}.
+ * <p/>
+ * The same configured name is also the HKDF info of {@code combineSecrets}, 
which must keep its original value: a
+ * change there would derive a different key and make every encapsulation 
produced by an earlier version undecryptable.
+ */
+class HybridKEMKeyLabelTest {
+
+    @BeforeAll
+    static void setup() {
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+        if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == 
null) {
+            Security.addProvider(new BouncyCastlePQCProvider());
+        }
+    }
+
+    private static KeyPair x25519() throws Exception {
+        return KeyPairGenerator.getInstance("X25519", "BC").generateKeyPair();
+    }
+
+    private static KeyPair mlkem() throws Exception {
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM", "BC");
+        kpg.initialize(MLKEMParameterSpec.ml_kem_768, new SecureRandom());
+        return kpg.generateKeyPair();
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "AES,AES,256",
+            // the two whose configured name is not the JCE name
+            "DESEDE,DESede,192",
+            "GOST3412_2015,GOST3412-2015,256" })
+    void bothEndsStampTheJceName(String configured, String jceName, int 
keyLength) throws Exception {
+        KeyPair classical = x25519();
+        KeyPair pqc = mlkem();
+
+        HybridKEM.HybridEncapsulationResult encapsulated = 
HybridKEM.encapsulate(
+                classical.getPublic(), pqc.getPublic(),
+                KeyAgreement.getInstance("X25519", "BC"), 
KeyGenerator.getInstance("ML-KEM", "BC"),
+                configured, keyLength, "HKDF-SHA256");
+
+        SecretKey extracted = HybridKEM.extract(
+                encapsulated.encapsulation(), classical.getPrivate(), 
pqc.getPrivate(),
+                KeyAgreement.getInstance("X25519", "BC"), 
KeyGenerator.getInstance("ML-KEM", "BC"),
+                configured, keyLength, "HKDF-SHA256");
+
+        assertEquals(jceName, encapsulated.sharedSecret().getAlgorithm(),
+                "the generated key must carry the JCE name");
+        assertEquals(jceName, extracted.getAlgorithm(), "the extracted key 
must carry the JCE name");
+        assertArrayEquals(encapsulated.sharedSecret().getEncoded(), 
extracted.getEncoded(),
+                "both ends must still derive the same secret");
+
+        // the point of the JCE name: the key is directly usable
+        assertDoesNotThrow(() -> Cipher.getInstance(extracted.getAlgorithm()),
+                "the stamped name must resolve as a cipher algorithm");
+    }
+
+    @Test
+    void theDerivedSecretIsUnchangedByTheRelabelling() throws Exception {
+        KeyPair classical = x25519();
+        KeyPair pqc = mlkem();
+
+        // The HKDF info is the configured name, so deriving with the 
configured name and with its JCE name must
+        // produce different secrets. If they matched, this test could not 
tell whether the info had been remapped.
+        HybridKEM.HybridEncapsulationResult viaConfigured = 
HybridKEM.encapsulate(
+                classical.getPublic(), pqc.getPublic(),
+                KeyAgreement.getInstance("X25519", "BC"), 
KeyGenerator.getInstance("ML-KEM", "BC"),
+                "DESEDE", 192, "HKDF-SHA256");
+
+        // Extracting the same encapsulation with the JCE spelling must fail 
to reproduce the secret, which is what
+        // shows the info still carries the configured name rather than the 
mapped one.
+        SecretKey viaJceName = HybridKEM.extract(
+                viaConfigured.encapsulation(), classical.getPrivate(), 
pqc.getPrivate(),
+                KeyAgreement.getInstance("X25519", "BC"), 
KeyGenerator.getInstance("ML-KEM", "BC"),
+                "DESede", 192, "HKDF-SHA256");
+
+        assertFalse(
+                Arrays.equals(viaConfigured.sharedSecret().getEncoded(), 
viaJceName.getEncoded()),
+                "the HKDF info must still be the configured name: if remapping 
it made these equal, the wire format"
+                                                                               
                    + " would have changed and older encapsulations would no 
longer decrypt");
+    }
+
+    @Test
+    void anAlreadyJceNameIsLeftAlone() {
+        assertEquals("AES", PQCSymmetricAlgorithms.toJceName("AES"));
+        assertEquals("DESede", PQCSymmetricAlgorithms.toJceName("DESEDE"));
+        assertEquals("DESede", PQCSymmetricAlgorithms.toJceName("DESede"));
+        assertEquals("GOST3412-2015", 
PQCSymmetricAlgorithms.toJceName("GOST3412-2015"));
+        assertEquals("Unknown", PQCSymmetricAlgorithms.toJceName("Unknown"));
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index ed9b9921607e..0865882254a9 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1818,6 +1818,17 @@ be updated.
 `Symmetric Algorithm needs to be specified`, as the extract operations already 
did, instead of failing later inside
 BouncyCastle with `Missing argument`.
 
+=== camel-pqc - the hybrid KEM operations stamp the JCE algorithm name too
+
+`hybridGenerateSecretKeyEncapsulation` and 
`hybridExtractSecretKeyEncapsulation` label the `SecretKey` they produce
+with the mapped JCE algorithm name, as the non-hybrid operations do. `DESEDE` 
now yields `DESede` and
+`GOST3412_2015` now yields `GOST3412-2015`; every other algorithm is 
unaffected, since its configured name and its
+JCE name are the same.
+
+Only the label changes. The derived key itself is unchanged: the configured 
name is also the HKDF info the two
+shared secrets are combined with, and that keeps its original value, so an 
encapsulation produced by an earlier
+version still decrypts.
+
 === camel-jbang - camel ask asks before a call that changes something, reaches 
out or runs code
 
 `camel ask` now asks on the terminal before a file write (showing the diff), a 
message to an endpoint the

Reply via email to