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