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 cc291b31f43a [backport camel-4.22.x] CAMEL-25603: camel-pqc - do not 
share a KeyGenerator or KeyAgreement across concurrent KEM operations (#27905)
cc291b31f43a is described below

commit cc291b31f43a6c7b0027069eeb663166db4f2cc1
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 15 20:39:04 2026 +0200

    [backport camel-4.22.x] CAMEL-25603: camel-pqc - do not share a 
KeyGenerator or KeyAgreement across concurrent KEM operations (#27905)
    
    Backport of #27768 to camel-4.22.x.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/docs/pqc-component.adoc   |   2 +
 .../apache/camel/catalog/docs/pqc-dataformat.adoc  |  18 +--
 .../camel-pqc/src/main/docs/pqc-component.adoc     |   2 +
 .../camel-pqc/src/main/docs/pqc-dataformat.adoc    |  18 +--
 .../apache/camel/component/pqc/PQCProducer.java    | 102 +++++++++----
 .../component/pqc/crypto/hybrid/HybridKEM.java     |  44 ++++--
 .../component/pqc/dataformat/PQCDataFormat.java    |  42 ++---
 .../camel/component/pqc/InterleavingProbe.java     |  83 ++++++++++
 .../PQCKemConcurrentExtractionIsolationTest.java   | 123 +++++++++++++++
 .../pqc/PQCKemSharedKeyGeneratorIsolationTest.java | 151 ++++++++++++++++++
 .../camel/component/pqc/ProbedKeyAgreement.java    |  90 +++++++++++
 .../camel/component/pqc/ProbedKeyGenerator.java    |  75 +++++++++
 .../crypto/hybrid/HybridKEMConcurrencyTest.java    | 152 ++++++++++++++++++
 .../dataformat/PQCDataFormatConcurrencyTest.java   | 170 +++++++++++++++++++++
 14 files changed, 983 insertions(+), 89 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-component.adoc
index a6c46b43e257..cba1521c17f0 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-component.adoc
@@ -425,6 +425,8 @@ The component expects to find a KeyGenerator and a KeyPair 
in to the Camel Regis
 
 In case the KeyPair and the KeyGenerator Objects are not in the registry, it 
will provide two instances of the Objects with default implementation.
 
+NOTE: A KEM `KeyGenerator`, like the classical `KeyAgreement` of the hybrid 
KEM operations, keeps state from its `init` call until the call that returns 
the result, and is not thread-safe. When the producer creates the instance 
itself, it uses a new one for every exchange. A configured instance (a registry 
bean, which is autowired into every `pqc` endpoint, or the default material) is 
shared, so each operation holds its lock from `init` to the result and 
concurrent exchanges that use it  [...]
+
 This will be true for standardized algorithms and for experimental ones.
 
 TIP: To combine a classical key agreement (X25519, ECDH) with a PQC KEM 
(ML-KEM) for defense-in-depth, use the hybrid KEM operations described in 
xref:others:pqc-hybrid.adoc[Hybrid Cryptography].
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-dataformat.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-dataformat.adoc
index 7d7c4093a700..3e88d3e86267 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-dataformat.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pqc-dataformat.adoc
@@ -140,7 +140,7 @@ from("file:encrypted")
 |keyGenerator
 |KeyGenerator
 |null
-|Custom KeyGenerator for KEM operations
+|Custom KeyGenerator for KEM operations, shared by every operation of the data 
format. If not set, a new one is created for every operation
 |===
 
 == Registering KeyPair
@@ -408,21 +408,9 @@ The DataFormat approach is significantly simpler and 
handles all KEM operations
 
 == Performance Considerations
 
-**1. Caching KeyGenerator**
+**1. KeyGenerator instances**
 
-Configure a KeyGenerator once to avoid repeated initialization:
-
-._Java-only: caching a KeyGenerator to avoid repeated initialization_
-[source,java]
-----
-@BindToRegistry("kemKeyGenerator")
-public KeyGenerator createKeyGenerator() throws Exception {
-    return KeyGenerator.getInstance("ML-KEM", "BCPQC");
-}
-
-PQCDataFormat pqcFormat = new PQCDataFormat();
-pqcFormat.setKeyGenerator(kemKeyGenerator);
-----
+A KEM `KeyGenerator` keeps the spec passed to `init()` until `generateKey()` 
reads it, and is not thread-safe. Unless one is configured, the data format 
creates a new `KeyGenerator` for every marshal and unmarshal, so concurrent 
exchanges never share one. A `KeyGenerator` set with `setKeyGenerator(...)` is 
shared by every operation of the data format instance, and the operations then 
run one at a time on it, so only configure one when you need a specific 
instance.
 
 **2. Large Payloads**
 
diff --git a/components/camel-pqc/src/main/docs/pqc-component.adoc 
b/components/camel-pqc/src/main/docs/pqc-component.adoc
index a6c46b43e257..cba1521c17f0 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -425,6 +425,8 @@ The component expects to find a KeyGenerator and a KeyPair 
in to the Camel Regis
 
 In case the KeyPair and the KeyGenerator Objects are not in the registry, it 
will provide two instances of the Objects with default implementation.
 
+NOTE: A KEM `KeyGenerator`, like the classical `KeyAgreement` of the hybrid 
KEM operations, keeps state from its `init` call until the call that returns 
the result, and is not thread-safe. When the producer creates the instance 
itself, it uses a new one for every exchange. A configured instance (a registry 
bean, which is autowired into every `pqc` endpoint, or the default material) is 
shared, so each operation holds its lock from `init` to the result and 
concurrent exchanges that use it  [...]
+
 This will be true for standardized algorithms and for experimental ones.
 
 TIP: To combine a classical key agreement (X25519, ECDH) with a PQC KEM 
(ML-KEM) for defense-in-depth, use the hybrid KEM operations described in 
xref:others:pqc-hybrid.adoc[Hybrid Cryptography].
diff --git a/components/camel-pqc/src/main/docs/pqc-dataformat.adoc 
b/components/camel-pqc/src/main/docs/pqc-dataformat.adoc
index 7d7c4093a700..3e88d3e86267 100644
--- a/components/camel-pqc/src/main/docs/pqc-dataformat.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-dataformat.adoc
@@ -140,7 +140,7 @@ from("file:encrypted")
 |keyGenerator
 |KeyGenerator
 |null
-|Custom KeyGenerator for KEM operations
+|Custom KeyGenerator for KEM operations, shared by every operation of the data 
format. If not set, a new one is created for every operation
 |===
 
 == Registering KeyPair
@@ -408,21 +408,9 @@ The DataFormat approach is significantly simpler and 
handles all KEM operations
 
 == Performance Considerations
 
-**1. Caching KeyGenerator**
+**1. KeyGenerator instances**
 
-Configure a KeyGenerator once to avoid repeated initialization:
-
-._Java-only: caching a KeyGenerator to avoid repeated initialization_
-[source,java]
-----
-@BindToRegistry("kemKeyGenerator")
-public KeyGenerator createKeyGenerator() throws Exception {
-    return KeyGenerator.getInstance("ML-KEM", "BCPQC");
-}
-
-PQCDataFormat pqcFormat = new PQCDataFormat();
-pqcFormat.setKeyGenerator(kemKeyGenerator);
-----
+A KEM `KeyGenerator` keeps the spec passed to `init()` until `generateKey()` 
reads it, and is not thread-safe. Unless one is configured, the data format 
creates a new `KeyGenerator` for every marshal and unmarshal, so concurrent 
exchanges never share one. A `KeyGenerator` set with `setKeyGenerator(...)` is 
shared by every operation of the data format instance, and the operations then 
run one at a time on it, so only configure one when you need a specific 
instance.
 
 **2. Large Payloads**
 
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 030d20133df3..ecadab55d4b7 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
@@ -125,6 +125,11 @@ public class PQCProducer extends DefaultProducer {
     private String signerAlgorithm;
     private String signerProvider;
     private String classicalSignerAlgorithm;
+    // The same for the KEM KeyGenerator and the classical KeyAgreement of the 
hybrid KEM operations. The provider is
+    // kept as an object, so creating another KeyGenerator does not depend on 
it still being registered in the JVM
+    private String keyGeneratorAlgorithm;
+    private Provider keyGeneratorProvider;
+    private String classicalKeyAgreementAlgorithm;
 
     // Hybrid cryptography fields
     private Signature classicalSigner;
@@ -398,6 +403,8 @@ public class PQCProducer extends DefaultProducer {
                 PQCKeyEncapsulationAlgorithms kemAlg
                         = 
PQCKeyEncapsulationAlgorithms.valueOf(getConfiguration().getKeyEncapsulationAlgorithm());
                 keyGenerator = KeyGenerator.getInstance(kemAlg.getAlgorithm(), 
kemAlg.getBcProvider());
+                keyGeneratorAlgorithm = kemAlg.getAlgorithm();
+                keyGeneratorProvider = keyGenerator.getProvider();
             }
         }
 
@@ -460,6 +467,8 @@ public class PQCProducer extends DefaultProducer {
                 PQCKeyEncapsulationAlgorithms kemAlg
                         = 
PQCKeyEncapsulationAlgorithms.valueOf(getConfiguration().getKeyEncapsulationAlgorithm());
                 keyGenerator = KeyGenerator.getInstance(kemAlg.getAlgorithm(), 
kemAlg.getBcProvider());
+                keyGeneratorAlgorithm = kemAlg.getAlgorithm();
+                keyGeneratorProvider = keyGenerator.getProvider();
             }
 
             // Initialize classical key agreement
@@ -468,7 +477,8 @@ public class PQCProducer extends DefaultProducer {
                     && 
ObjectHelper.isNotEmpty(getConfiguration().getClassicalKEMAlgorithm())) {
                 PQCClassicalKEMAlgorithms classKemAlg
                         = 
PQCClassicalKEMAlgorithms.valueOf(getConfiguration().getClassicalKEMAlgorithm());
-                classicalKeyAgreement = 
KeyAgreement.getInstance(classKemAlg.getAlgorithm());
+                classicalKeyAgreementAlgorithm = classKemAlg.getAlgorithm();
+                classicalKeyAgreement = 
KeyAgreement.getInstance(classicalKeyAgreementAlgorithm);
             }
 
             // Initialize classical key pair
@@ -625,25 +635,58 @@ public class PQCProducer extends DefaultProducer {
         }
     }
 
+    /**
+     * Returns the KEM {@link KeyGenerator} to use for a single exchange.
+     * <p>
+     * A KEM {@code KeyGenerator} keeps the spec passed to {@code init()} 
until {@code generateKey()} reads it and is
+     * not thread-safe, so the two calls of one exchange must not interleave 
with those of another. As with
+     * {@link #signerForExchange()}, an instance this producer created is 
simply created again, while a configured one -
+     * an autowired bean can be shared by every endpoint, and {@code 
PQCDefault*Material.keyGenerator} JVM-wide - is
+     * handed back as-is, and the caller serializes on whatever it gets.
+     */
+    private KeyGenerator keyGeneratorForExchange() throws 
NoSuchAlgorithmException {
+        if (keyGeneratorAlgorithm == null) {
+            return keyGenerator;
+        }
+        return KeyGenerator.getInstance(keyGeneratorAlgorithm, 
keyGeneratorProvider);
+    }
+
+    /**
+     * The {@link KeyAgreement} counterpart of {@link 
#keyGeneratorForExchange()}, used by the hybrid KEM operations.
+     */
+    private KeyAgreement classicalKeyAgreementForExchange() throws 
NoSuchAlgorithmException {
+        if (classicalKeyAgreementAlgorithm == null) {
+            return classicalKeyAgreement;
+        }
+        // Created the same way as at start, without naming a provider: the 
JDK picks a provider that accepts the key
+        // when init() is called. Unlike keyGeneratorForExchange() there is 
therefore no Provider object to keep, and
+        // naming one here would change which provider handles the classical 
keys
+        return KeyAgreement.getInstance(classicalKeyAgreementAlgorithm);
+    }
+
     private void generateEncapsulation(Exchange exchange)
-            throws InvalidAlgorithmParameterException {
-        // initialise for creating an encapsulation and shared secret.
-        keyGenerator.init(
-                new KEMGenerateSpec(
-                        keyPair.getPublic(),
-                        
getEndpoint().getConfiguration().getSymmetricKeyAlgorithm(),
-                        
getEndpoint().getConfiguration().getSymmetricKeyLength()),
-                SecureRandomHelper.getSecureRandom());
-        // SecretKeyWithEncapsulation is the class to use as the secret key, 
it has additional
-        // methods on it for recovering the encapsulation as well.
-        SecretKeyWithEncapsulation secEnc1 = (SecretKeyWithEncapsulation) 
keyGenerator.generateKey();
+            throws InvalidAlgorithmParameterException, 
NoSuchAlgorithmException {
+        KeyGenerator generator = keyGeneratorForExchange();
+        SecretKeyWithEncapsulation secEnc1;
+        synchronized (generator) {
+            // initialise for creating an encapsulation and shared secret.
+            generator.init(
+                    new KEMGenerateSpec(
+                            keyPair.getPublic(),
+                            
getEndpoint().getConfiguration().getSymmetricKeyAlgorithm(),
+                            
getEndpoint().getConfiguration().getSymmetricKeyLength()),
+                    SecureRandomHelper.getSecureRandom());
+            // SecretKeyWithEncapsulation is the class to use as the secret 
key, it has additional
+            // methods on it for recovering the encapsulation as well.
+            secEnc1 = (SecretKeyWithEncapsulation) generator.generateKey();
+        }
 
         exchange.getMessage().setBody(secEnc1, 
SecretKeyWithEncapsulation.class);
     }
 
     private void extractEncapsulation(Exchange exchange)
             throws InvalidAlgorithmParameterException,
-            InvalidPayloadException {
+            InvalidPayloadException, NoSuchAlgorithmException {
         // initialise for creating an encapsulation and shared secret.
         SecretKeyWithEncapsulation payload = 
exchange.getMessage().getMandatoryBody(SecretKeyWithEncapsulation.class);
 
@@ -651,15 +694,19 @@ public class PQCProducer extends DefaultProducer {
             throw new IllegalArgumentException("Symmetric Algorithm needs to 
be specified");
         }
 
-        keyGenerator.init(
-                new KEMExtractSpec(
-                        keyPair.getPrivate(), payload.getEncapsulation(),
-                        
PQCSymmetricAlgorithms.valueOf(getConfiguration().getSymmetricKeyAlgorithm()).getAlgorithm(),
-                        
getEndpoint().getConfiguration().getSymmetricKeyLength()),
-                SecureRandomHelper.getSecureRandom());
+        KeyGenerator generator = keyGeneratorForExchange();
+        SecretKeyWithEncapsulation secEnc2;
+        synchronized (generator) {
+            generator.init(
+                    new KEMExtractSpec(
+                            keyPair.getPrivate(), payload.getEncapsulation(),
+                            
PQCSymmetricAlgorithms.valueOf(getConfiguration().getSymmetricKeyAlgorithm()).getAlgorithm(),
+                            
getEndpoint().getConfiguration().getSymmetricKeyLength()),
+                    SecureRandomHelper.getSecureRandom());
 
-        // initialise for extracting the shared secret from the encapsulation.
-        SecretKeyWithEncapsulation secEnc2 = (SecretKeyWithEncapsulation) 
keyGenerator.generateKey();
+            // initialise for extracting the shared secret from the 
encapsulation.
+            secEnc2 = (SecretKeyWithEncapsulation) generator.generateKey();
+        }
 
         exchange.getMessage().setBody(secEnc2, 
SecretKeyWithEncapsulation.class);
     }
@@ -792,12 +839,13 @@ public class PQCProducer extends DefaultProducer {
         int keyLength = getConfiguration().getSymmetricKeyLength();
         String kdfAlgorithm = getConfiguration().getHybridKdfAlgorithm();
 
-        // Generate hybrid encapsulation
+        // Generate hybrid encapsulation. HybridKEM serializes on the 
KeyAgreement and KeyGenerator it is given, which
+        // only contends when they are configured instances; see 
keyGeneratorForExchange()
         HybridKEM.HybridEncapsulationResult result = HybridKEM.encapsulate(
                 classicalKeyPair.getPublic(),
                 keyPair.getPublic(),
-                classicalKeyAgreement,
-                keyGenerator,
+                classicalKeyAgreementForExchange(),
+                keyGeneratorForExchange(),
                 symmetricAlgorithm,
                 keyLength,
                 kdfAlgorithm);
@@ -835,13 +883,13 @@ public class PQCProducer extends DefaultProducer {
         int keyLength = getConfiguration().getSymmetricKeyLength();
         String kdfAlgorithm = getConfiguration().getHybridKdfAlgorithm();
 
-        // Extract shared secret from hybrid encapsulation
+        // Extract shared secret from hybrid encapsulation, with the same 
per-exchange instances as above
         SecretKey sharedSecret = HybridKEM.extract(
                 hybridEncapsulation,
                 classicalKeyPair.getPrivate(),
                 keyPair.getPrivate(),
-                classicalKeyAgreement,
-                keyGenerator,
+                classicalKeyAgreementForExchange(),
+                keyGeneratorForExchange(),
                 symmetricAlgorithm,
                 keyLength,
                 kdfAlgorithm);
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 e2dc94b39caa..a83afdb3a950 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
@@ -47,6 +47,10 @@ import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
  * </ul>
  * The shared secrets from both algorithms are combined using HKDF to produce 
the final shared secret.
  * <p>
+ * The {@link KeyAgreement} and {@link KeyGenerator} passed in may be shared 
by concurrent callers. Both keep state from
+ * their {@code init()} call to the call that returns the result and are not 
thread-safe, so each of those sequences
+ * runs while holding the instance's monitor.
+ * <p>
  * Wire format: [4 bytes: classical encap length][classical encap][pqc encap]
  */
 public final class HybridKEM {
@@ -87,18 +91,24 @@ public final class HybridKEM {
         KeyPair ephemeralKeyPair = 
generateEphemeralKeyPair(classicalPublicKey.getAlgorithm());
 
         // Perform classical key agreement
-        classicalKA.init(ephemeralKeyPair.getPrivate());
-        classicalKA.doPhase(classicalPublicKey, true);
-        byte[] classicalSharedSecret = classicalKA.generateSecret();
+        byte[] classicalSharedSecret;
+        synchronized (classicalKA) {
+            classicalKA.init(ephemeralKeyPair.getPrivate());
+            classicalKA.doPhase(classicalPublicKey, true);
+            classicalSharedSecret = classicalKA.generateSecret();
+        }
 
         // The classical "encapsulation" is the ephemeral public key
         byte[] classicalEncapsulation = 
ephemeralKeyPair.getPublic().getEncoded();
 
         // Perform PQC KEM encapsulation
-        pqcKemGenerator.init(
-                new KEMGenerateSpec(pqcPublicKey, symmetricAlgorithm, 
keyLength),
-                SecureRandomHelper.getSecureRandom());
-        SecretKeyWithEncapsulation pqcResult = (SecretKeyWithEncapsulation) 
pqcKemGenerator.generateKey();
+        SecretKeyWithEncapsulation pqcResult;
+        synchronized (pqcKemGenerator) {
+            pqcKemGenerator.init(
+                    new KEMGenerateSpec(pqcPublicKey, symmetricAlgorithm, 
keyLength),
+                    SecureRandomHelper.getSecureRandom());
+            pqcResult = (SecretKeyWithEncapsulation) 
pqcKemGenerator.generateKey();
+        }
         byte[] pqcSharedSecret = pqcResult.getEncoded();
         byte[] pqcEncapsulation = pqcResult.getEncapsulation();
 
@@ -161,15 +171,21 @@ public final class HybridKEM {
                 classicalPrivateKey.getAlgorithm());
 
         // Perform classical key agreement
-        classicalKA.init(classicalPrivateKey);
-        classicalKA.doPhase(ephemeralPublicKey, true);
-        byte[] classicalSharedSecret = classicalKA.generateSecret();
+        byte[] classicalSharedSecret;
+        synchronized (classicalKA) {
+            classicalKA.init(classicalPrivateKey);
+            classicalKA.doPhase(ephemeralPublicKey, true);
+            classicalSharedSecret = classicalKA.generateSecret();
+        }
 
         // Perform PQC KEM extraction
-        pqcKemGenerator.init(
-                new KEMExtractSpec(pqcPrivateKey, 
components.pqcEncapsulation(), symmetricAlgorithm, keyLength),
-                SecureRandomHelper.getSecureRandom());
-        SecretKeyWithEncapsulation pqcResult = (SecretKeyWithEncapsulation) 
pqcKemGenerator.generateKey();
+        SecretKeyWithEncapsulation pqcResult;
+        synchronized (pqcKemGenerator) {
+            pqcKemGenerator.init(
+                    new KEMExtractSpec(pqcPrivateKey, 
components.pqcEncapsulation(), symmetricAlgorithm, keyLength),
+                    SecureRandomHelper.getSecureRandom());
+            pqcResult = (SecretKeyWithEncapsulation) 
pqcKemGenerator.generateKey();
+        }
         byte[] pqcSharedSecret = pqcResult.getEncoded();
 
         // Combine shared secrets using HKDF
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 f84c8da0c821..d3bec6931753 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
@@ -163,8 +163,12 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
         try {
             // Generate KEM encapsulation and the fresh shared secret
             KeyGenerator kg = getOrCreateKeyGenerator(kemAlg);
-            kg.init(new KEMGenerateSpec(kp.getPublic(), symAlg, 
symmetricKeyLength), SecureRandomHelper.getSecureRandom());
-            SecretKeyWithEncapsulation secretKey = 
(SecretKeyWithEncapsulation) kg.generateKey();
+            SecretKeyWithEncapsulation secretKey;
+            synchronized (kg) {
+                kg.init(new KEMGenerateSpec(kp.getPublic(), symAlg, 
symmetricKeyLength),
+                        SecureRandomHelper.getSecureRandom());
+                secretKey = (SecretKeyWithEncapsulation) kg.generateKey();
+            }
             byte[] encapsulation = secretKey.getEncapsulation();
 
             // Encrypt the payload with an authenticated cipher using a fresh 
random nonce
@@ -221,9 +225,12 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
 
             // Extract the shared secret from the encapsulation
             KeyGenerator kg = getOrCreateKeyGenerator(kemAlg);
-            kg.init(new KEMExtractSpec(kp.getPrivate(), encapsulation, symAlg, 
symmetricKeyLength),
-                    SecureRandomHelper.getSecureRandom());
-            SecretKeyWithEncapsulation secretKey = 
(SecretKeyWithEncapsulation) kg.generateKey();
+            SecretKeyWithEncapsulation secretKey;
+            synchronized (kg) {
+                kg.init(new KEMExtractSpec(kp.getPrivate(), encapsulation, 
symAlg, symmetricKeyLength),
+                        SecureRandomHelper.getSecureRandom());
+                secretKey = (SecretKeyWithEncapsulation) kg.generateKey();
+            }
 
             // Decrypt. doFinal() verifies the authentication tag and throws 
on tampering; this must not be done with
             // CipherInputStream, which can silently swallow an AEAD tag 
failure and return truncated plaintext.
@@ -250,17 +257,8 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
         }
 
         if (keyEncapsulationAlgorithm != null && keyGenerator == null) {
-            PQCKeyEncapsulationAlgorithms kemEnum = 
PQCKeyEncapsulationAlgorithms.valueOf(keyEncapsulationAlgorithm);
-            String algorithm = kemEnum.getAlgorithm();
-            String bcProvider = kemEnum.getBcProvider();
-
-            if (provider != null) {
-                keyGenerator = KeyGenerator.getInstance(algorithm, provider);
-            } else if (bcProvider != null) {
-                keyGenerator = KeyGenerator.getInstance(algorithm, bcProvider);
-            } else {
-                keyGenerator = KeyGenerator.getInstance(algorithm);
-            }
+            // Fail fast on an unknown algorithm or provider. The instance is 
not kept, see getOrCreateKeyGenerator()
+            createKeyGenerator(keyEncapsulationAlgorithm);
         }
     }
 
@@ -341,12 +339,19 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
         }
     }
 
+    /**
+     * Returns the KeyGenerator for one marshal or unmarshal operation. A KEM 
{@code KeyGenerator} keeps the spec passed
+     * to {@code init()} until {@code generateKey()} reads it and is not 
thread-safe, so a new one is created for every
+     * operation. A configured KeyGenerator is returned as-is and shared by 
all operations, which serialize on it.
+     */
     private KeyGenerator getOrCreateKeyGenerator(String kemAlgorithm) throws 
Exception {
         if (keyGenerator != null) {
             return keyGenerator;
         }
+        return createKeyGenerator(kemAlgorithm);
+    }
 
-        // Create a temporary key generator for this operation
+    private KeyGenerator createKeyGenerator(String kemAlgorithm) throws 
Exception {
         PQCKeyEncapsulationAlgorithms kemEnum = 
PQCKeyEncapsulationAlgorithms.valueOf(kemAlgorithm);
         String algorithm = kemEnum.getAlgorithm();
         String bcProvider = kemEnum.getBcProvider();
@@ -426,7 +431,8 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
     }
 
     /**
-     * Sets a custom KeyGenerator for KEM operations. If not set, one will be 
created based on the KEM algorithm.
+     * Sets a custom KeyGenerator for KEM operations. If not set, a new one is 
created for every operation based on the
+     * KEM algorithm. A configured KeyGenerator is shared by all operations, 
which then run one at a time on it.
      */
     public void setKeyGenerator(KeyGenerator keyGenerator) {
         this.keyGenerator = keyGenerator;
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/InterleavingProbe.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/InterleavingProbe.java
new file mode 100644
index 000000000000..b97ba2d83b22
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/InterleavingProbe.java
@@ -0,0 +1,83 @@
+/*
+ * 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.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Test helper for stateful JCA objects (KeyGenerator, KeyAgreement) shared by 
concurrent callers. It holds one caller
+ * between the {@code init()} call and the call that returns the result, and 
records whether another caller ran
+ * {@code init()} on the same object in the meantime. Code that runs the whole 
sequence while holding the object's
+ * monitor never lets that happen: the second caller waits until the first one 
is done.
+ */
+public final class InterleavingProbe {
+
+    private static final long HOLD_MILLIS = 1000;
+
+    private final AtomicBoolean holdNext = new AtomicBoolean();
+    private final CountDownLatch holding = new CountDownLatch(1);
+    private final AtomicBoolean interleaved = new AtomicBoolean();
+    private volatile CountDownLatch initWhileHolding;
+
+    /**
+     * Holds the next caller that asks for the result, for up to a second or 
until another caller runs {@code init()}.
+     */
+    public void holdNextResult() {
+        holdNext.set(true);
+    }
+
+    /**
+     * Waits until a caller is being held.
+     */
+    public boolean awaitHolding(long timeout, TimeUnit unit) throws 
InterruptedException {
+        return holding.await(timeout, unit);
+    }
+
+    /**
+     * Whether another caller ran {@code init()} while a caller was held 
between its own {@code init()} and the result.
+     */
+    public boolean interleaved() {
+        return interleaved.get();
+    }
+
+    void afterInit() {
+        CountDownLatch latch = initWhileHolding;
+        if (latch != null) {
+            latch.countDown();
+        }
+    }
+
+    void beforeResult() {
+        if (!holdNext.compareAndSet(true, false)) {
+            return;
+        }
+        CountDownLatch latch = new CountDownLatch(1);
+        initWhileHolding = latch;
+        holding.countDown();
+        try {
+            if (latch.await(HOLD_MILLIS, TimeUnit.MILLISECONDS)) {
+                interleaved.set(true);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        } finally {
+            initWhileHolding = null;
+        }
+    }
+}
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKemConcurrentExtractionIsolationTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKemConcurrentExtractionIsolationTest.java
new file mode 100644
index 000000000000..b5f639ab26ee
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKemConcurrentExtractionIsolationTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.KeyGenerator;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
+import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
+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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * When no KeyGenerator is configured the producer creates one itself, and a 
Camel producer is a singleton invoked
+ * concurrently. Every encapsulation extracted through the same producer must 
yield its own secret, no matter how many
+ * exchanges are in flight at once.
+ */
+class PQCKemConcurrentExtractionIsolationTest extends CamelTestSupport {
+
+    private static final int THREADS = 8;
+    private static final int MESSAGES_PER_THREAD = 40;
+
+    private KeyPair keyPair;
+
+    @BeforeAll
+    static void startup() {
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+        if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == 
null) {
+            Security.addProvider(new BouncyCastlePQCProvider());
+        }
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        KeyPairGenerator kpg = 
KeyPairGenerator.getInstance(PQCKeyEncapsulationAlgorithms.MLKEM.getAlgorithm(),
+                PQCKeyEncapsulationAlgorithms.MLKEM.getBcProvider());
+        kpg.initialize(MLKEMParameterSpec.ml_kem_512, new SecureRandom());
+        keyPair = kpg.generateKeyPair();
+        registry.bind("kemKeyPair", keyPair);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:extract")
+                        
.to("pqc:extract?operation=extractSecretKeyEncapsulation&symmetricKeyAlgorithm=AES"
+                            + 
"&keyEncapsulationAlgorithm=MLKEM&keyPair=#kemKeyPair");
+            }
+        };
+    }
+
+    @Test
+    void everyConcurrentExtractionReturnsItsOwnSecret() throws Exception {
+        ExecutorService pool = Executors.newFixedThreadPool(THREADS);
+        try {
+            List<Callable<Integer>> work = new ArrayList<>();
+            for (int t = 0; t < THREADS; t++) {
+                work.add(() -> {
+                    KeyGenerator sender = 
KeyGenerator.getInstance(PQCKeyEncapsulationAlgorithms.MLKEM.getAlgorithm(),
+                            
PQCKeyEncapsulationAlgorithms.MLKEM.getBcProvider());
+                    int matching = 0;
+                    for (int i = 0; i < MESSAGES_PER_THREAD; i++) {
+                        sender.init(new KEMGenerateSpec(keyPair.getPublic(), 
"AES", 128), new SecureRandom());
+                        SecretKeyWithEncapsulation sent = 
(SecretKeyWithEncapsulation) sender.generateKey();
+                        SecretKeyWithEncapsulation extracted
+                                = template.requestBody("direct:extract", sent, 
SecretKeyWithEncapsulation.class);
+                        if (Arrays.equals(sent.getEncoded(), 
extracted.getEncoded())) {
+                            matching++;
+                        }
+                    }
+                    return matching;
+                });
+            }
+
+            int matching = 0;
+            for (Future<Integer> f : pool.invokeAll(work, 5, 
TimeUnit.MINUTES)) {
+                matching += f.get();
+            }
+
+            assertEquals(THREADS * MESSAGES_PER_THREAD, matching);
+        } finally {
+            pool.shutdownNow();
+        }
+    }
+}
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKemSharedKeyGeneratorIsolationTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKemSharedKeyGeneratorIsolationTest.java
new file mode 100644
index 000000000000..b0cf702607ab
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKemSharedKeyGeneratorIsolationTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.KeyGenerator;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
+import org.bouncycastle.jcajce.spec.KEMExtractSpec;
+import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
+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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A KeyGenerator configured on the endpoints - for example a registry bean, 
which is autowired into every pqc endpoint
+ * - is shared by all their exchanges, while a KEM KeyGenerator keeps the spec 
passed to init() until generateKey()
+ * reads it. Every exchange must get the result of its own spec, however the 
exchanges overlap.
+ */
+class PQCKemSharedKeyGeneratorIsolationTest extends CamelTestSupport {
+
+    private final InterleavingProbe probe = new InterleavingProbe();
+    private KeyPair keyPair;
+
+    @BeforeAll
+    static void startup() {
+        ensureProviders();
+    }
+
+    /**
+     * PQCEndpoint removes the BouncyCastle providers from the JVM when it 
stops, so with more than one test method in
+     * the class they must be registered again before each CamelContext is 
built.
+     */
+    private static void ensureProviders() {
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+        if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == 
null) {
+            Security.addProvider(new BouncyCastlePQCProvider());
+        }
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        ensureProviders();
+        KeyPairGenerator kpg = 
KeyPairGenerator.getInstance(PQCKeyEncapsulationAlgorithms.MLKEM.getAlgorithm(),
+                PQCKeyEncapsulationAlgorithms.MLKEM.getBcProvider());
+        kpg.initialize(MLKEMParameterSpec.ml_kem_512, new SecureRandom());
+        keyPair = kpg.generateKeyPair();
+        registry.bind("kemKeyPair", keyPair);
+        registry.bind("sharedKeyGenerator", 
ProbedKeyGenerator.wrap(newKeyGenerator(), probe));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:generate")
+                        
.to("pqc:generate?operation=generateSecretKeyEncapsulation&symmetricKeyAlgorithm=AES"
+                            + 
"&keyPair=#kemKeyPair&keyGenerator=#sharedKeyGenerator");
+                from("direct:extract")
+                        
.to("pqc:extract?operation=extractSecretKeyEncapsulation&symmetricKeyAlgorithm=AES"
+                            + 
"&keyPair=#kemKeyPair&keyGenerator=#sharedKeyGenerator");
+            }
+        };
+    }
+
+    @Test
+    void overlappingExtractionsEachReturnTheirOwnSecret() throws Exception {
+        SecretKeyWithEncapsulation first = encapsulate();
+        SecretKeyWithEncapsulation second = encapsulate();
+
+        probe.holdNextResult();
+        Future<SecretKeyWithEncapsulation> firstResult
+                = template.asyncRequestBody("direct:extract", first, 
SecretKeyWithEncapsulation.class);
+        assertTrue(probe.awaitHolding(10, TimeUnit.SECONDS));
+        Future<SecretKeyWithEncapsulation> secondResult
+                = template.asyncRequestBody("direct:extract", second, 
SecretKeyWithEncapsulation.class);
+
+        assertArrayEquals(first.getEncoded(), firstResult.get(10, 
TimeUnit.SECONDS).getEncoded());
+        assertArrayEquals(second.getEncoded(), secondResult.get(10, 
TimeUnit.SECONDS).getEncoded());
+        assertFalse(probe.interleaved(), "an init() ran between another 
exchange's init() and generateKey()");
+    }
+
+    @Test
+    void overlappingGenerationAndExtractionEachReturnTheirOwnResult() throws 
Exception {
+        SecretKeyWithEncapsulation other = encapsulate();
+
+        probe.holdNextResult();
+        Future<SecretKeyWithEncapsulation> generated
+                = template.asyncRequestBody("direct:generate", "generate", 
SecretKeyWithEncapsulation.class);
+        assertTrue(probe.awaitHolding(10, TimeUnit.SECONDS));
+        Future<SecretKeyWithEncapsulation> extracted
+                = template.asyncRequestBody("direct:extract", other, 
SecretKeyWithEncapsulation.class);
+
+        SecretKeyWithEncapsulation generatedKey = generated.get(10, 
TimeUnit.SECONDS);
+        assertFalse(Arrays.equals(other.getEncapsulation(), 
generatedKey.getEncapsulation()),
+                "the generation returned the encapsulation of the other 
exchange");
+        assertArrayEquals(generatedKey.getEncoded(), 
decapsulate(generatedKey.getEncapsulation()));
+        assertArrayEquals(other.getEncoded(), extracted.get(10, 
TimeUnit.SECONDS).getEncoded());
+        assertFalse(probe.interleaved(), "an init() ran between another 
exchange's init() and generateKey()");
+    }
+
+    private SecretKeyWithEncapsulation encapsulate() throws Exception {
+        KeyGenerator kg = newKeyGenerator();
+        kg.init(new KEMGenerateSpec(keyPair.getPublic(), "AES", 128), new 
SecureRandom());
+        return (SecretKeyWithEncapsulation) kg.generateKey();
+    }
+
+    private byte[] decapsulate(byte[] encapsulation) throws Exception {
+        KeyGenerator kg = newKeyGenerator();
+        kg.init(new KEMExtractSpec(keyPair.getPrivate(), encapsulation, "AES", 
128), new SecureRandom());
+        return kg.generateKey().getEncoded();
+    }
+
+    private static KeyGenerator newKeyGenerator() throws Exception {
+        return 
KeyGenerator.getInstance(PQCKeyEncapsulationAlgorithms.MLKEM.getAlgorithm(),
+                PQCKeyEncapsulationAlgorithms.MLKEM.getBcProvider());
+    }
+}
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/ProbedKeyAgreement.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/ProbedKeyAgreement.java
new file mode 100644
index 000000000000..dac2a1fe2396
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/ProbedKeyAgreement.java
@@ -0,0 +1,90 @@
+/*
+ * 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.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.AlgorithmParameterSpec;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.KeyAgreementSpi;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+
+/**
+ * A {@link KeyAgreement} that delegates to a real one and reports its calls 
to an {@link InterleavingProbe}.
+ */
+public final class ProbedKeyAgreement extends KeyAgreement {
+
+    private ProbedKeyAgreement(KeyAgreement delegate, InterleavingProbe probe) 
{
+        super(new Spi(delegate, probe), delegate.getProvider(), 
delegate.getAlgorithm());
+    }
+
+    public static KeyAgreement wrap(KeyAgreement delegate, InterleavingProbe 
probe) {
+        return new ProbedKeyAgreement(delegate, probe);
+    }
+
+    private static final class Spi extends KeyAgreementSpi {
+
+        private final KeyAgreement delegate;
+        private final InterleavingProbe probe;
+
+        Spi(KeyAgreement delegate, InterleavingProbe probe) {
+            this.delegate = delegate;
+            this.probe = probe;
+        }
+
+        @Override
+        protected void engineInit(Key key, SecureRandom random) throws 
InvalidKeyException {
+            delegate.init(key, random);
+            probe.afterInit();
+        }
+
+        @Override
+        protected void engineInit(Key key, AlgorithmParameterSpec params, 
SecureRandom random)
+                throws InvalidKeyException, InvalidAlgorithmParameterException 
{
+            delegate.init(key, params, random);
+            probe.afterInit();
+        }
+
+        @Override
+        protected Key engineDoPhase(Key key, boolean lastPhase) throws 
InvalidKeyException {
+            return delegate.doPhase(key, lastPhase);
+        }
+
+        @Override
+        protected byte[] engineGenerateSecret() {
+            probe.beforeResult();
+            return delegate.generateSecret();
+        }
+
+        @Override
+        protected int engineGenerateSecret(byte[] sharedSecret, int offset) 
throws ShortBufferException {
+            probe.beforeResult();
+            return delegate.generateSecret(sharedSecret, offset);
+        }
+
+        @Override
+        protected SecretKey engineGenerateSecret(String algorithm) throws 
NoSuchAlgorithmException, InvalidKeyException {
+            probe.beforeResult();
+            return delegate.generateSecret(algorithm);
+        }
+    }
+}
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/ProbedKeyGenerator.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/ProbedKeyGenerator.java
new file mode 100644
index 000000000000..f0cd84ba1da7
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/ProbedKeyGenerator.java
@@ -0,0 +1,75 @@
+/*
+ * 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.security.InvalidAlgorithmParameterException;
+import java.security.SecureRandom;
+import java.security.spec.AlgorithmParameterSpec;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.KeyGeneratorSpi;
+import javax.crypto.SecretKey;
+
+/**
+ * A {@link KeyGenerator} that delegates to a real one and reports its calls 
to an {@link InterleavingProbe}.
+ */
+public final class ProbedKeyGenerator extends KeyGenerator {
+
+    private ProbedKeyGenerator(KeyGenerator delegate, InterleavingProbe probe) 
{
+        super(new Spi(delegate, probe), delegate.getProvider(), 
delegate.getAlgorithm());
+    }
+
+    public static KeyGenerator wrap(KeyGenerator delegate, InterleavingProbe 
probe) {
+        return new ProbedKeyGenerator(delegate, probe);
+    }
+
+    private static final class Spi extends KeyGeneratorSpi {
+
+        private final KeyGenerator delegate;
+        private final InterleavingProbe probe;
+
+        Spi(KeyGenerator delegate, InterleavingProbe probe) {
+            this.delegate = delegate;
+            this.probe = probe;
+        }
+
+        @Override
+        protected void engineInit(SecureRandom random) {
+            delegate.init(random);
+            probe.afterInit();
+        }
+
+        @Override
+        protected void engineInit(AlgorithmParameterSpec params, SecureRandom 
random)
+                throws InvalidAlgorithmParameterException {
+            delegate.init(params, random);
+            probe.afterInit();
+        }
+
+        @Override
+        protected void engineInit(int keysize, SecureRandom random) {
+            delegate.init(keysize, random);
+            probe.afterInit();
+        }
+
+        @Override
+        protected SecretKey engineGenerateKey() {
+            probe.beforeResult();
+            return delegate.generateKey();
+        }
+    }
+}
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEMConcurrencyTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEMConcurrencyTest.java
new file mode 100644
index 000000000000..16adad80fd33
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/crypto/hybrid/HybridKEMConcurrencyTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+import org.apache.camel.component.pqc.InterleavingProbe;
+import org.apache.camel.component.pqc.ProbedKeyAgreement;
+import org.apache.camel.component.pqc.ProbedKeyGenerator;
+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.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * HybridKEM receives its KeyAgreement and KeyGenerator from the caller, and 
they can be shared by concurrent callers: a
+ * configured bean, or the {@code PQCDefault*Material} instances. Both keep 
state from init() to the call that returns
+ * the result, so every call must work with its own keys and encapsulation 
however the calls overlap.
+ */
+class HybridKEMConcurrencyTest {
+
+    private final InterleavingProbe agreementProbe = new InterleavingProbe();
+    private final InterleavingProbe generatorProbe = new InterleavingProbe();
+    private KeyPair classicalKeyPair;
+    private KeyPair pqcKeyPair;
+    private KeyAgreement sharedAgreement;
+    private KeyGenerator sharedGenerator;
+
+    @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());
+        }
+    }
+
+    @BeforeEach
+    void createKeys() throws Exception {
+        classicalKeyPair = KeyPairGenerator.getInstance("X25519", 
"BC").generateKeyPair();
+        KeyPairGenerator mlkemKpg = KeyPairGenerator.getInstance("ML-KEM", 
"BC");
+        mlkemKpg.initialize(MLKEMParameterSpec.ml_kem_768, new SecureRandom());
+        pqcKeyPair = mlkemKpg.generateKeyPair();
+        sharedAgreement = 
ProbedKeyAgreement.wrap(KeyAgreement.getInstance("X25519", "BC"), 
agreementProbe);
+        sharedGenerator = 
ProbedKeyGenerator.wrap(KeyGenerator.getInstance("ML-KEM", "BC"), 
generatorProbe);
+    }
+
+    @Test
+    void overlappingExtractionsOnSharedInstancesEachReturnTheirOwnSecret() 
throws Exception {
+        HybridKEM.HybridEncapsulationResult first = 
encapsulate(newAgreement(), newGenerator());
+        HybridKEM.HybridEncapsulationResult second = 
encapsulate(newAgreement(), newGenerator());
+
+        agreementProbe.holdNextResult();
+        generatorProbe.holdNextResult();
+        ExecutorService pool = Executors.newFixedThreadPool(2);
+        try {
+            Future<SecretKey> firstSecret = pool.submit(() -> 
extract(first.encapsulation()));
+            assertTrue(agreementProbe.awaitHolding(10, TimeUnit.SECONDS));
+            Future<SecretKey> secondSecret = pool.submit(() -> 
extract(second.encapsulation()));
+
+            assertArrayEquals(first.sharedSecret().getEncoded(), 
firstSecret.get(10, TimeUnit.SECONDS).getEncoded());
+            assertArrayEquals(second.sharedSecret().getEncoded(), 
secondSecret.get(10, TimeUnit.SECONDS).getEncoded());
+        } finally {
+            pool.shutdownNow();
+        }
+        assertFalse(agreementProbe.interleaved(), "an init() ran inside 
another call's KeyAgreement sequence");
+        assertFalse(generatorProbe.interleaved(), "an init() ran inside 
another call's KeyGenerator sequence");
+    }
+
+    @Test
+    void 
overlappingEncapsulationsOnSharedInstancesEachProduceAMatchingSecret() throws 
Exception {
+        agreementProbe.holdNextResult();
+        generatorProbe.holdNextResult();
+        ExecutorService pool = Executors.newFixedThreadPool(2);
+        HybridKEM.HybridEncapsulationResult first;
+        HybridKEM.HybridEncapsulationResult second;
+        try {
+            Future<HybridKEM.HybridEncapsulationResult> firstResult
+                    = pool.submit(() -> encapsulate(sharedAgreement, 
sharedGenerator));
+            assertTrue(agreementProbe.awaitHolding(10, TimeUnit.SECONDS));
+            Future<HybridKEM.HybridEncapsulationResult> secondResult
+                    = pool.submit(() -> encapsulate(sharedAgreement, 
sharedGenerator));
+            first = firstResult.get(10, TimeUnit.SECONDS);
+            second = secondResult.get(10, TimeUnit.SECONDS);
+        } finally {
+            pool.shutdownNow();
+        }
+
+        // Each encapsulation must lead the recipient to the secret its sender 
was given
+        assertArrayEquals(first.sharedSecret().getEncoded(), 
extractWithOwnInstances(first.encapsulation()).getEncoded());
+        assertArrayEquals(second.sharedSecret().getEncoded(),
+                extractWithOwnInstances(second.encapsulation()).getEncoded());
+        assertFalse(agreementProbe.interleaved(), "an init() ran inside 
another call's KeyAgreement sequence");
+        assertFalse(generatorProbe.interleaved(), "an init() ran inside 
another call's KeyGenerator sequence");
+    }
+
+    private HybridKEM.HybridEncapsulationResult encapsulate(KeyAgreement 
agreement, KeyGenerator generator)
+            throws Exception {
+        return HybridKEM.encapsulate(classicalKeyPair.getPublic(), 
pqcKeyPair.getPublic(), agreement, generator,
+                "AES", 256, "HKDF-SHA256");
+    }
+
+    private SecretKey extract(byte[] encapsulation) throws Exception {
+        return HybridKEM.extract(encapsulation, classicalKeyPair.getPrivate(), 
pqcKeyPair.getPrivate(),
+                sharedAgreement, sharedGenerator, "AES", 256, "HKDF-SHA256");
+    }
+
+    private SecretKey extractWithOwnInstances(byte[] encapsulation) throws 
Exception {
+        return HybridKEM.extract(encapsulation, classicalKeyPair.getPrivate(), 
pqcKeyPair.getPrivate(),
+                newAgreement(), newGenerator(), "AES", 256, "HKDF-SHA256");
+    }
+
+    private static KeyAgreement newAgreement() throws Exception {
+        return KeyAgreement.getInstance("X25519", "BC");
+    }
+
+    private static KeyGenerator newGenerator() throws Exception {
+        return KeyGenerator.getInstance("ML-KEM", "BC");
+    }
+}
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/dataformat/PQCDataFormatConcurrencyTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/dataformat/PQCDataFormatConcurrencyTest.java
new file mode 100644
index 000000000000..289bedfc04f1
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/dataformat/PQCDataFormatConcurrencyTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.dataformat;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.KeyGenerator;
+
+import org.apache.camel.component.pqc.InterleavingProbe;
+import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms;
+import org.apache.camel.component.pqc.ProbedKeyGenerator;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A data format instance is shared by every exchange of the routes that use 
it, while a KEM KeyGenerator keeps the spec
+ * passed to init() until generateKey() reads it. Every marshal and unmarshal 
must work with its own spec, whether the
+ * data format creates the KeyGenerator or one is configured.
+ */
+class PQCDataFormatConcurrencyTest extends CamelTestSupport {
+
+    private static final int THREADS = 8;
+    private static final int MESSAGES_PER_THREAD = 40;
+
+    @BeforeAll
+    static void startup() {
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+        if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == 
null) {
+            Security.addProvider(new BouncyCastlePQCProvider());
+        }
+    }
+
+    @Test
+    void concurrentRoundTripsThroughOneInstanceAllSucceed() throws Exception {
+        PQCDataFormat dataFormat = startedDataFormat(newKeyPair(), null);
+
+        ExecutorService pool = Executors.newFixedThreadPool(THREADS);
+        try {
+            List<Callable<Integer>> work = new ArrayList<>();
+            for (int t = 0; t < THREADS; t++) {
+                final int thread = t;
+                work.add(() -> {
+                    int matching = 0;
+                    for (int i = 0; i < MESSAGES_PER_THREAD; i++) {
+                        String message = "message-" + thread + "-" + i;
+                        if (message.equals(unmarshal(dataFormat, 
marshal(dataFormat, message)))) {
+                            matching++;
+                        }
+                    }
+                    return matching;
+                });
+            }
+
+            int matching = 0;
+            for (Future<Integer> f : pool.invokeAll(work, 5, 
TimeUnit.MINUTES)) {
+                matching += f.get();
+            }
+
+            assertEquals(THREADS * MESSAGES_PER_THREAD, matching);
+        } finally {
+            pool.shutdownNow();
+        }
+        assertNull(dataFormat.getKeyGenerator(), "the data format should not 
keep a KeyGenerator to share");
+    }
+
+    @Test
+    void 
overlappingMarshalAndUnmarshalOnConfiguredKeyGeneratorKeepTheirOwnKeys() throws 
Exception {
+        KeyPair keyPair = newKeyPair();
+        byte[] otherMessage = marshal(startedDataFormat(keyPair, null), 
"other");
+
+        InterleavingProbe probe = new InterleavingProbe();
+        PQCDataFormat dataFormat = startedDataFormat(keyPair, 
ProbedKeyGenerator.wrap(
+                
KeyGenerator.getInstance(PQCKeyEncapsulationAlgorithms.MLKEM.getAlgorithm(),
+                        PQCKeyEncapsulationAlgorithms.MLKEM.getBcProvider()),
+                probe));
+
+        probe.holdNextResult();
+        ExecutorService pool = Executors.newFixedThreadPool(2);
+        byte[] mine;
+        String other;
+        try {
+            Future<byte[]> marshalled = pool.submit(() -> marshal(dataFormat, 
"mine"));
+            assertTrue(probe.awaitHolding(10, TimeUnit.SECONDS));
+            Future<String> unmarshalled = pool.submit(() -> 
unmarshal(dataFormat, otherMessage));
+            mine = marshalled.get(10, TimeUnit.SECONDS);
+            other = unmarshalled.get(10, TimeUnit.SECONDS);
+        } finally {
+            pool.shutdownNow();
+        }
+
+        assertFalse(Arrays.equals(encapsulationOf(otherMessage), 
encapsulationOf(mine)),
+                "the marshal used the encapsulation of the other operation");
+        assertEquals("mine", unmarshal(dataFormat, mine));
+        assertEquals("other", other);
+        assertFalse(probe.interleaved(), "an init() ran between another 
operation's init() and generateKey()");
+    }
+
+    private static KeyPair newKeyPair() throws Exception {
+        KeyPairGenerator kpg = 
KeyPairGenerator.getInstance(PQCKeyEncapsulationAlgorithms.MLKEM.getAlgorithm(),
+                PQCKeyEncapsulationAlgorithms.MLKEM.getBcProvider());
+        kpg.initialize(MLKEMParameterSpec.ml_kem_512, new SecureRandom());
+        return kpg.generateKeyPair();
+    }
+
+    private static PQCDataFormat startedDataFormat(KeyPair keyPair, 
KeyGenerator keyGenerator) {
+        PQCDataFormat dataFormat = new PQCDataFormat("MLKEM", "AES", keyPair);
+        dataFormat.setKeyGenerator(keyGenerator);
+        dataFormat.start();
+        return dataFormat;
+    }
+
+    private byte[] marshal(PQCDataFormat dataFormat, String body) throws 
Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        dataFormat.marshal(new DefaultExchange(context), body, out);
+        return out.toByteArray();
+    }
+
+    private String unmarshal(PQCDataFormat dataFormat, byte[] data) throws 
Exception {
+        byte[] plaintext = (byte[]) dataFormat.unmarshal(new 
DefaultExchange(context), new ByteArrayInputStream(data));
+        return new String(plaintext, StandardCharsets.UTF_8);
+    }
+
+    private static byte[] encapsulationOf(byte[] message) {
+        ByteBuffer buffer = ByteBuffer.wrap(message);
+        byte[] encapsulation = new byte[buffer.getInt()];
+        buffer.get(encapsulation);
+        return encapsulation;
+    }
+}

Reply via email to