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

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


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 2248e8c4954c [backport camel-4.18.x] CAMEL-25603: camel-pqc - do not 
share a KeyGenerator or KeyAgreement across concurrent KEM operations (#27884)
2248e8c4954c is described below

commit 2248e8c4954cacc15935bcf5f7c41f0b273178d8
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 15 12:31:56 2026 +0200

    [backport camel-4.18.x] CAMEL-25603: camel-pqc - do not share a 
KeyGenerator or KeyAgreement across concurrent KEM operations (#27884)
    
    CAMEL-25603: camel-pqc - do not share a KeyGenerator or KeyAgreement across 
concurrent KEM operations (#27768)
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../camel-pqc/src/main/docs/pqc-component.adoc     |   2 +
 .../apache/camel/component/pqc/PQCProducer.java    |  72 ++++++---
 .../component/pqc/dataformat/PQCDataFormat.java    |  47 +++---
 .../camel/component/pqc/InterleavingProbe.java     |  83 ++++++++++
 .../PQCKemConcurrentExtractionIsolationTest.java   | 123 +++++++++++++++
 .../pqc/PQCKemSharedKeyGeneratorIsolationTest.java | 151 ++++++++++++++++++
 .../camel/component/pqc/ProbedKeyGenerator.java    |  75 +++++++++
 .../dataformat/PQCDataFormatConcurrencyTest.java   | 172 +++++++++++++++++++++
 8 files changed, 683 insertions(+), 42 deletions(-)

diff --git a/components/camel-pqc/src/main/docs/pqc-component.adoc 
b/components/camel-pqc/src/main/docs/pqc-component.adoc
index 1ef61422c394..1e51e985c4df 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -297,6 +297,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` 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 run one at a time. Registering a 
`KeyGenerator` bean is optional: [...]
+
 This will be true for standardized algorithms and for experimental ones.
 
 A possible flow of the operation could be the following:
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 1a1346fa59c4..f5b1098b1e2b 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
@@ -52,6 +52,10 @@ public class PQCProducer extends DefaultProducer {
     private String signerAlgorithm;
     private String signerProvider;
     private KeyGenerator keyGenerator;
+    // The same for the KEM KeyGenerator. 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 KeyPair keyPair;
 
     public PQCProducer(Endpoint endpoint) {
@@ -122,6 +126,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();
             }
         }
 
@@ -190,25 +196,45 @@ public class PQCProducer extends DefaultProducer {
         exchange.getMessage().setHeader(PQCConstants.VERIFY, verified);
     }
 
+    /**
+     * 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);
+    }
+
     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()),
-                new SecureRandom());
-        // 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()),
+                    new SecureRandom());
+            // 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);
 
@@ -216,15 +242,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()),
-                new SecureRandom());
-
-        // initialise for extracting the shared secret from the encapsulation.
-        SecretKeyWithEncapsulation secEnc2 = (SecretKeyWithEncapsulation) 
keyGenerator.generateKey();
+        KeyGenerator generator = keyGeneratorForExchange();
+        SecretKeyWithEncapsulation secEnc2;
+        synchronized (generator) {
+            generator.init(
+                    new KEMExtractSpec(
+                            keyPair.getPrivate(), payload.getEncapsulation(),
+                            
PQCSymmetricAlgorithms.valueOf(getConfiguration().getSymmetricKeyAlgorithm()).getAlgorithm(),
+                            
getEndpoint().getConfiguration().getSymmetricKeyLength()),
+                    new SecureRandom());
+
+            // initialise for extracting the shared secret from the 
encapsulation.
+            secEnc2 = (SecretKeyWithEncapsulation) generator.generateKey();
+        }
 
         exchange.getMessage().setBody(secEnc2, 
SecretKeyWithEncapsulation.class);
     }
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 32d4e68335e6..db4be8f7f931 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
@@ -134,11 +134,14 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
         try {
             // Generate KEM encapsulation and shared secret
             KeyGenerator kg = getOrCreateKeyGenerator(kemAlg);
-            kg.init(
-                    new KEMGenerateSpec(kp.getPublic(), symAlg, 
symmetricKeyLength),
-                    new SecureRandom());
+            SecretKeyWithEncapsulation secretKey;
+            synchronized (kg) {
+                kg.init(
+                        new KEMGenerateSpec(kp.getPublic(), symAlg, 
symmetricKeyLength),
+                        new SecureRandom());
 
-            SecretKeyWithEncapsulation secretKey = 
(SecretKeyWithEncapsulation) kg.generateKey();
+                secretKey = (SecretKeyWithEncapsulation) kg.generateKey();
+            }
             byte[] encapsulation = secretKey.getEncapsulation();
 
             // Write encapsulation length and data
@@ -203,11 +206,14 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
 
             // Extract secret key from encapsulation
             KeyGenerator kg = getOrCreateKeyGenerator(kemAlg);
-            kg.init(
-                    new KEMExtractSpec(kp.getPrivate(), encapsulation, symAlg, 
symmetricKeyLength),
-                    new SecureRandom());
+            SecretKeyWithEncapsulation secretKey;
+            synchronized (kg) {
+                kg.init(
+                        new KEMExtractSpec(kp.getPrivate(), encapsulation, 
symAlg, symmetricKeyLength),
+                        new SecureRandom());
 
-            SecretKeyWithEncapsulation secretKey = 
(SecretKeyWithEncapsulation) kg.generateKey();
+                secretKey = (SecretKeyWithEncapsulation) kg.generateKey();
+            }
 
             // Decrypt data with the extracted secret
             Cipher cipher = Cipher.getInstance(symAlg);
@@ -232,17 +238,8 @@ public class PQCDataFormat extends ServiceSupport 
implements DataFormat, DataFor
     @Override
     protected void doStart() throws Exception {
         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);
         }
     }
 
@@ -284,12 +281,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();
@@ -379,7 +383,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..0069d3c31de7
--- /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.junit5.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..b1416c1aaafd
--- /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.junit5.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/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/dataformat/PQCDataFormatConcurrencyTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/dataformat/PQCDataFormatConcurrencyTest.java
new file mode 100644
index 000000000000..bd1d2c2d7357
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/dataformat/PQCDataFormatConcurrencyTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.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.Exchange;
+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.junit5.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 {
+        // On this branch unmarshal returns a stream cache rather than a byte 
array
+        Exchange exchange = new DefaultExchange(context);
+        Object plaintext = dataFormat.unmarshal(exchange, new 
ByteArrayInputStream(data));
+        return context.getTypeConverter().mandatoryConvertTo(String.class, 
exchange, plaintext);
+    }
+
+    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