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 da9e101cae34 CAMEL-25603: camel-pqc - do not share a KeyGenerator or 
KeyAgreement across concurrent KEM operations (#27768)
da9e101cae34 is described below

commit da9e101cae347dd085a04f9925c06c9b830abd94
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 14 21:20:24 2026 +0200

    CAMEL-25603: camel-pqc - do not share a KeyGenerator or KeyAgreement across 
concurrent KEM operations (#27768)
    
    Co-Authored-By: Claude Opus 5.5 <[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 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    |  12 ++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  13 ++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 17 files changed, 1021 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 3c57e80e1da0..93295b5eea4a 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 78aa9f4d61ff..b8bd8cf22697 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
@@ -143,7 +143,7 @@ YAML::
 |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
@@ -411,21 +411,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 3c57e80e1da0..93295b5eea4a 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 78aa9f4d61ff..b8bd8cf22697 100644
--- a/components/camel-pqc/src/main/docs/pqc-dataformat.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-dataformat.adoc
@@ -143,7 +143,7 @@ YAML::
 |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
@@ -411,21 +411,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;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index db3e7ab13a97..81866e1a0496 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -81,6 +81,18 @@ A `BasicMessageFilter` created in application code applies a 
`SpringWebserviceHe
 created with another strategy. A route that relies on the previous behaviour 
can supply a custom
 `headerFilterStrategy` on the `spring-ws` endpoint.
 
+=== camel-pqc - a configured KeyGenerator is used by one exchange at a time
+
+When Camel creates the instance itself, the KEM operations of the `pqc` 
component and the PQC data format now use a
+new `KeyGenerator` for every operation. A configured instance is still shared: 
a registry bean, which is autowired into
+every `pqc` endpoint, a `KeyGenerator` set on the data format, or the default 
material used when no key material is
+configured. Each operation now holds its lock from `init` until the result is 
returned, so concurrent exchanges that
+use it run one at a time.
+
+A started PQC data format no longer keeps a `KeyGenerator` of its own: 
`getKeyGenerator()` returns only a configured
+instance and, unless one is configured, the `CamelPQCKemAlgorithm` header now 
selects the KEM algorithm of each
+operation.
+
 === camel-undertow - WebSocket endpoints apply the security provider, allowed 
roles and handlers
 
 WebSocket endpoints (`ws://` and `wss://`) now apply the 
`UndertowSecurityProvider` (the `securityConfiguration` or
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index de32f08a20b4..73fc892e03fb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -90,6 +90,19 @@ Code that creates key material itself has to follow the same 
move: the `CMCEPara
 `CMCEParameterSpec`, `FrodoKEMParameterSpec`, `MLKEMParameterSpec`, 
`MLDSAParameterSpec` and
 `SLHDSAParameterSpec` of `org.bouncycastle.jcajce.spec` with the `BC` provider 
instead.
 
+=== camel-pqc - a configured KeyGenerator or KeyAgreement is used by one 
exchange at a time
+
+When Camel creates the instance itself, the KEM operations of the `pqc` 
component (including the hybrid KEM
+operations) and the PQC data format now use a new `KeyGenerator`, and for the 
hybrid operations a new `KeyAgreement`,
+for every operation. A configured instance is still shared: a registry bean, 
which is autowired into every `pqc`
+endpoint, a `KeyGenerator` set on the data format, or the default material 
used when no key material is configured.
+Each operation now holds its lock from `init` until the result is returned, so 
concurrent exchanges that use it run
+one at a time.
+
+A started PQC data format no longer keeps a `KeyGenerator` of its own: 
`getKeyGenerator()` returns only a configured
+instance and, unless one is configured, the `CamelPQCKemAlgorithm` header now 
selects the KEM algorithm of each
+operation.
+
 === camel-netty-http - the roles of a security constraint are matched by role 
name
 
 The roles of a `SecurityConstraintMapping` inclusion are a comma-separated 
list of role names, and an authenticated
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 3a4a8f50cf7d..5b24443dca94 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
@@ -544,6 +544,19 @@ The `get` operation on a folder (producer and consumer) 
now downloads every file
 downloaded only the files of the first page of the folder listing. As before, 
the content of all the files is in the
 message body, so a large folder needs more memory than it did when only the 
first page was read.
 
+=== camel-pqc - a configured KeyGenerator or KeyAgreement is used by one 
exchange at a time
+
+When Camel creates the instance itself, the KEM operations of the `pqc` 
component (including the hybrid KEM
+operations) and the PQC data format now use a new `KeyGenerator`, and for the 
hybrid operations a new `KeyAgreement`,
+for every operation. A configured instance is still shared: a registry bean, 
which is autowired into every `pqc`
+endpoint, a `KeyGenerator` set on the data format, or the default material 
used when no key material is configured.
+Each operation now holds its lock from `init` until the result is returned, so 
concurrent exchanges that use it run
+one at a time.
+
+A started PQC data format no longer keeps a `KeyGenerator` of its own: 
`getKeyGenerator()` returns only a configured
+instance and, unless one is configured, the `CamelPQCKemAlgorithm` header now 
selects the KEM algorithm of each
+operation.
+
 === camel-box - getEnterpriseEvents returns the events of every chunk
 
 The `getEnterpriseEvents` operation of the `event-logs` API now returns all 
the events between `after` and `before`.

Reply via email to