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