This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new e0d28b661f94 CAMEL-23845: Camel-PQC - implement
InMemoryKeyLifecycleManager (#24676)
e0d28b661f94 is described below
commit e0d28b661f9471d2e88fea3e0a9ede0474ad6956
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Jul 14 14:39:10 2026 +0200
CAMEL-23845: Camel-PQC - implement InMemoryKeyLifecycleManager (#24676)
* CAMEL-23845: Camel-PQC implement InMemoryKeyLifecycleManager
The documentation referenced an InMemoryKeyLifecycleManager in eight places
(a
dedicated subsection, code examples, the implementation comparison table,
the
testing strategy and a migration example) but the class was never
committed, so
those examples did not compile.
Implement it: a ConcurrentHashMap-backed, thread-safe KeyLifecycleManager
that
performs no I/O, plus the documented clear() and size() helpers. It is a
good fit
for tests, development and ephemeral workloads.
To avoid duplicating the algorithm mapping, extract the shared
key-generation
logic (JCE algorithm name, BouncyCastle provider and default parameter set)
from
FileBasedKeyLifecycleManager into a new KeyAlgorithmSupport helper, which
both
implementations now use. FileBasedKeyLifecycleManager behaviour is
unchanged.
The existing documentation examples already match this API, so no doc
changes are
needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* CAMEL-23845: Address review feedback - align key lifecycle log levels
Harmonize InMemoryKeyLifecycleManager log levels with the three existing
KeyLifecycleManager implementations (FileBased, AwsSecretsManager,
HashicorpVault), which all log key generation and deletion at INFO and
reserve DEBUG for internal storage plumbing.
Key generation and deletion are security-relevant lifecycle events, and
the implementations are meant to be interchangeable: swapping FileBased
for InMemory would otherwise silently drop these events from an
INFO-level audit log.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.../lifecycle/FileBasedKeyLifecycleManager.java | 132 +------------
.../pqc/lifecycle/InMemoryKeyLifecycleManager.java | 198 +++++++++++++++++++
.../pqc/lifecycle/KeyAlgorithmSupport.java | 167 ++++++++++++++++
.../lifecycle/InMemoryKeyLifecycleManagerTest.java | 209 +++++++++++++++++++++
4 files changed, 581 insertions(+), 125 deletions(-)
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
index 2b4cba9949c8..19750a941857 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
@@ -26,10 +26,8 @@ import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.KeyFactory;
import java.security.KeyPair;
-import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
-import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
@@ -45,15 +43,6 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
-import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms;
-import org.apache.camel.component.pqc.PQCSignatureAlgorithms;
-import org.apache.camel.util.SecureRandomHelper;
-import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
-import org.bouncycastle.jcajce.spec.MLKEMParameterSpec;
-import org.bouncycastle.jcajce.spec.SLHDSAParameterSpec;
-import org.bouncycastle.pqc.crypto.lms.LMOtsParameters;
-import org.bouncycastle.pqc.crypto.lms.LMSigParameters;
-import org.bouncycastle.pqc.jcajce.spec.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,33 +81,7 @@ public class FileBasedKeyLifecycleManager implements
KeyLifecycleManager {
public KeyPair generateKeyPair(String algorithm, String keyId, Object
parameterSpec) throws Exception {
LOG.info("Generating key pair for algorithm: {}, keyId: {}",
algorithm, keyId);
- KeyPairGenerator generator;
- String provider = determineProvider(algorithm);
-
- if (provider != null) {
- generator =
KeyPairGenerator.getInstance(getAlgorithmName(algorithm), provider);
- } else {
- generator =
KeyPairGenerator.getInstance(getAlgorithmName(algorithm));
- }
-
- // Initialize with parameter spec if provided
- if (parameterSpec != null) {
- if (parameterSpec instanceof AlgorithmParameterSpec
algorithmParamSpec) {
- generator.initialize(algorithmParamSpec,
SecureRandomHelper.getSecureRandom());
- } else if (parameterSpec instanceof Integer keySize) {
- generator.initialize(keySize,
SecureRandomHelper.getSecureRandom());
- }
- } else {
- // Use default parameter spec for the algorithm
- AlgorithmParameterSpec defaultSpec =
getDefaultParameterSpec(algorithm);
- if (defaultSpec != null) {
- generator.initialize(defaultSpec,
SecureRandomHelper.getSecureRandom());
- } else {
- generator.initialize(getDefaultKeySize(algorithm),
SecureRandomHelper.getSecureRandom());
- }
- }
-
- KeyPair keyPair = generator.generateKeyPair();
+ KeyPair keyPair = KeyAlgorithmSupport.generateKeyPair(algorithm,
parameterSpec);
// Create metadata
KeyMetadata metadata = new KeyMetadata(keyId, algorithm);
@@ -145,14 +108,16 @@ public class FileBasedKeyLifecycleManager implements
KeyLifecycleManager {
public KeyPair importKey(byte[] keyData, KeyFormat format, String
algorithm) throws Exception {
// Try to import as private key first (which includes public key)
try {
- PrivateKey privateKey =
KeyFormatConverter.importPrivateKey(keyData, format,
getAlgorithmName(algorithm));
+ PrivateKey privateKey =
KeyFormatConverter.importPrivateKey(keyData, format,
+ KeyAlgorithmSupport.getAlgorithmName(algorithm));
// For PQC algorithms, we need to derive the public key
// This is algorithm-specific and may require regeneration
LOG.warn("Importing private key only - public key derivation may
be needed");
return new KeyPair(null, privateKey);
} catch (Exception e) {
// Try as public key only
- PublicKey publicKey = KeyFormatConverter.importPublicKey(keyData,
format, getAlgorithmName(algorithm));
+ PublicKey publicKey = KeyFormatConverter.importPublicKey(keyData,
format,
+ KeyAlgorithmSupport.getAlgorithmName(algorithm));
return new KeyPair(publicKey, null);
}
}
@@ -347,8 +312,8 @@ public class FileBasedKeyLifecycleManager implements
KeyLifecycleManager {
X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes);
String algorithm = privateData.algorithm();
- String algorithmName = getAlgorithmName(algorithm);
- String provider = determineProvider(algorithm);
+ String algorithmName = KeyAlgorithmSupport.getAlgorithmName(algorithm);
+ String provider = KeyAlgorithmSupport.determineProvider(algorithm);
KeyFactory keyFactory;
if (provider != null) {
@@ -463,89 +428,6 @@ public class FileBasedKeyLifecycleManager implements
KeyLifecycleManager {
return false;
}
- private String determineProvider(String algorithm) {
- try {
- PQCSignatureAlgorithms sigAlg =
PQCSignatureAlgorithms.valueOf(algorithm);
- return sigAlg.getBcProvider();
- } catch (IllegalArgumentException e1) {
- try {
- PQCKeyEncapsulationAlgorithms kemAlg =
PQCKeyEncapsulationAlgorithms.valueOf(algorithm);
- return kemAlg.getBcProvider();
- } catch (IllegalArgumentException e2) {
- return null;
- }
- }
- }
-
- private String getAlgorithmName(String algorithm) {
- try {
- return PQCSignatureAlgorithms.valueOf(algorithm).getAlgorithm();
- } catch (IllegalArgumentException e1) {
- try {
- return
PQCKeyEncapsulationAlgorithms.valueOf(algorithm).getAlgorithm();
- } catch (IllegalArgumentException e2) {
- return algorithm;
- }
- }
- }
-
- private AlgorithmParameterSpec getDefaultParameterSpec(String algorithm) {
- // Provide default parameter specs for PQC algorithms
- try {
- switch (algorithm) {
- case "DILITHIUM":
- case "MLDSA":
- return MLDSAParameterSpec.ml_dsa_44;
- case "SLHDSA":
- case "SPHINCSPLUS":
- return SLHDSAParameterSpec.slh_dsa_sha2_128s;
- case "FALCON":
- return FalconParameterSpec.falcon_512;
- case "XMSS":
- return new XMSSParameterSpec(
- 10,
- XMSSParameterSpec.SHA256);
- case "XMSSMT":
- return XMSSMTParameterSpec.XMSSMT_SHA2_20d2_256;
- case "LMS":
- case "HSS":
- return new LMSKeyGenParameterSpec(
- LMSigParameters.lms_sha256_n32_h10,
- LMOtsParameters.sha256_n32_w4);
- case "MLKEM":
- case "KYBER":
- return MLKEMParameterSpec.ml_kem_768;
- case "NTRU":
- return NTRUParameterSpec.ntruhps2048509;
- case "NTRULPRime":
- return NTRULPRimeParameterSpec.ntrulpr653;
- case "SNTRUPrime":
- return SNTRUPrimeParameterSpec.sntrup761;
- case "SABER":
- return SABERParameterSpec.lightsaberkem128r3;
- case "FRODO":
- return FrodoParameterSpec.frodokem640aes;
- case "BIKE":
- return BIKEParameterSpec.bike128;
- case "HQC":
- return HQCParameterSpec.hqc128;
- case "CMCE":
- return CMCEParameterSpec.mceliece348864;
- default:
- return null;
- }
- } catch (Exception e) {
- LOG.warn("Failed to create default parameter spec for algorithm:
{}", algorithm, e);
- return null;
- }
- }
-
- private int getDefaultKeySize(String algorithm) {
- // Default key sizes for different algorithms
- // For PQC algorithms, key size is usually determined by parameter
specs
- return 256;
- }
-
/**
* JSON structure for storing key data (private or public) in files.
*/
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/InMemoryKeyLifecycleManager.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/InMemoryKeyLifecycleManager.java
new file mode 100644
index 000000000000..625dfc20bf55
--- /dev/null
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/InMemoryKeyLifecycleManager.java
@@ -0,0 +1,198 @@
+/*
+ * 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.lifecycle;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * In-memory implementation of {@link KeyLifecycleManager}. Keys and metadata
are held in {@link ConcurrentHashMap}s and
+ * are lost when the application stops - nothing is written to disk or to a
remote store.
+ * <p/>
+ * Because it performs no I/O it is a good fit for tests, development and
ephemeral workloads. All operations are
+ * thread-safe. In addition to the {@link KeyLifecycleManager} contract it
offers {@link #clear()} and {@link #size()},
+ * which are convenient for resetting state between tests.
+ */
+public class InMemoryKeyLifecycleManager implements KeyLifecycleManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(InMemoryKeyLifecycleManager.class);
+
+ private final Map<String, KeyPair> keys = new ConcurrentHashMap<>();
+ private final Map<String, KeyMetadata> metadata = new
ConcurrentHashMap<>();
+
+ @Override
+ public KeyPair generateKeyPair(String algorithm, String keyId) throws
Exception {
+ return generateKeyPair(algorithm, keyId, null);
+ }
+
+ @Override
+ public KeyPair generateKeyPair(String algorithm, String keyId, Object
parameterSpec) throws Exception {
+ LOG.info("Generating key pair for algorithm: {}, keyId: {}",
algorithm, keyId);
+
+ KeyPair keyPair = KeyAlgorithmSupport.generateKeyPair(algorithm,
parameterSpec);
+
+ KeyMetadata keyMetadata = new KeyMetadata(keyId, algorithm);
+ keyMetadata.setDescription("Generated on " + new Date());
+ storeKey(keyId, keyPair, keyMetadata);
+
+ LOG.info("Generated key pair: {}", keyMetadata);
+ return keyPair;
+ }
+
+ @Override
+ public byte[] exportKey(KeyPair keyPair, KeyFormat format, boolean
includePrivate) throws Exception {
+ return KeyFormatConverter.exportKeyPair(keyPair, format,
includePrivate);
+ }
+
+ @Override
+ public byte[] exportPublicKey(KeyPair keyPair, KeyFormat format) throws
Exception {
+ return KeyFormatConverter.exportPublicKey(keyPair.getPublic(), format);
+ }
+
+ @Override
+ public KeyPair importKey(byte[] keyData, KeyFormat format, String
algorithm) throws Exception {
+ String algorithmName = KeyAlgorithmSupport.getAlgorithmName(algorithm);
+ // Try to import as a private key first, and fall back to a public key
+ try {
+ PrivateKey privateKey =
KeyFormatConverter.importPrivateKey(keyData, format, algorithmName);
+ LOG.warn("Importing private key only - public key derivation may
be needed");
+ return new KeyPair(null, privateKey);
+ } catch (Exception e) {
+ PublicKey publicKey = KeyFormatConverter.importPublicKey(keyData,
format, algorithmName);
+ return new KeyPair(publicKey, null);
+ }
+ }
+
+ @Override
+ public KeyPair rotateKey(String oldKeyId, String newKeyId, String
algorithm) throws Exception {
+ LOG.info("Rotating key from {} to {}", oldKeyId, newKeyId);
+
+ KeyMetadata oldMetadata = getKeyMetadata(oldKeyId);
+ if (oldMetadata == null) {
+ throw new IllegalArgumentException("Old key not found: " +
oldKeyId);
+ }
+
+ // Deprecate the old key, then generate its replacement
+ oldMetadata.setStatus(KeyMetadata.KeyStatus.DEPRECATED);
+ updateKeyMetadata(oldKeyId, oldMetadata);
+
+ KeyPair newKeyPair = generateKeyPair(algorithm, newKeyId);
+
+ LOG.info("Key rotation completed: {} -> {}", oldKeyId, newKeyId);
+ return newKeyPair;
+ }
+
+ @Override
+ public void storeKey(String keyId, KeyPair keyPair, KeyMetadata
keyMetadata) {
+ keys.put(keyId, keyPair);
+ metadata.put(keyId, keyMetadata);
+ }
+
+ @Override
+ public KeyPair getKey(String keyId) {
+ KeyPair keyPair = keys.get(keyId);
+ if (keyPair == null) {
+ throw new IllegalArgumentException("Key not found: " + keyId);
+ }
+ return keyPair;
+ }
+
+ @Override
+ public KeyMetadata getKeyMetadata(String keyId) {
+ return metadata.get(keyId);
+ }
+
+ @Override
+ public void updateKeyMetadata(String keyId, KeyMetadata keyMetadata) {
+ metadata.put(keyId, keyMetadata);
+ }
+
+ @Override
+ public void deleteKey(String keyId) {
+ keys.remove(keyId);
+ metadata.remove(keyId);
+ LOG.info("Deleted key: {}", keyId);
+ }
+
+ @Override
+ public List<KeyMetadata> listKeys() {
+ return new ArrayList<>(metadata.values());
+ }
+
+ @Override
+ public boolean needsRotation(String keyId, Duration maxAge, long maxUsage)
{
+ KeyMetadata keyMetadata = metadata.get(keyId);
+ if (keyMetadata == null) {
+ return false;
+ }
+ if (keyMetadata.needsRotation()) {
+ return true;
+ }
+ if (maxAge != null && keyMetadata.getAgeInDays() > maxAge.toDays()) {
+ return true;
+ }
+ return maxUsage > 0 && keyMetadata.getUsageCount() >= maxUsage;
+ }
+
+ @Override
+ public void expireKey(String keyId) {
+ KeyMetadata keyMetadata = metadata.get(keyId);
+ if (keyMetadata != null) {
+ keyMetadata.setStatus(KeyMetadata.KeyStatus.EXPIRED);
+ updateKeyMetadata(keyId, keyMetadata);
+ LOG.info("Expired key: {}", keyId);
+ }
+ }
+
+ @Override
+ public void revokeKey(String keyId, String reason) {
+ KeyMetadata keyMetadata = metadata.get(keyId);
+ if (keyMetadata != null) {
+ keyMetadata.setStatus(KeyMetadata.KeyStatus.REVOKED);
+ keyMetadata.setDescription(
+ (keyMetadata.getDescription() != null ?
keyMetadata.getDescription() + "; " : "")
+ + "Revoked: " + reason);
+ updateKeyMetadata(keyId, keyMetadata);
+ LOG.info("Revoked key: {} - {}", keyId, reason);
+ }
+ }
+
+ /**
+ * Removes every key and its metadata. Convenient for resetting the
manager between tests.
+ */
+ public void clear() {
+ keys.clear();
+ metadata.clear();
+ }
+
+ /**
+ * The number of keys currently held by this manager.
+ */
+ public int size() {
+ return metadata.size();
+ }
+}
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyAlgorithmSupport.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyAlgorithmSupport.java
new file mode 100644
index 000000000000..018f1d469298
--- /dev/null
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyAlgorithmSupport.java
@@ -0,0 +1,167 @@
+/*
+ * 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.lifecycle;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.spec.AlgorithmParameterSpec;
+
+import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms;
+import org.apache.camel.component.pqc.PQCSignatureAlgorithms;
+import org.apache.camel.util.SecureRandomHelper;
+import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
+import org.bouncycastle.jcajce.spec.MLKEMParameterSpec;
+import org.bouncycastle.jcajce.spec.SLHDSAParameterSpec;
+import org.bouncycastle.pqc.crypto.lms.LMOtsParameters;
+import org.bouncycastle.pqc.crypto.lms.LMSigParameters;
+import org.bouncycastle.pqc.jcajce.spec.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Shared key-generation support for the {@link KeyLifecycleManager}
implementations: resolves the JCE algorithm name,
+ * the BouncyCastle provider and the default parameter set for a PQC
algorithm, and generates key pairs from them.
+ */
+public final class KeyAlgorithmSupport {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(KeyAlgorithmSupport.class);
+
+ /**
+ * Fallback key size, used only when an algorithm has no default parameter
set. For PQC algorithms the key size is
+ * determined by the parameter set.
+ */
+ private static final int DEFAULT_KEY_SIZE = 256;
+
+ private KeyAlgorithmSupport() {
+ }
+
+ /**
+ * Generates a key pair for the given PQC algorithm.
+ *
+ * @param algorithm the Camel PQC algorithm name (see
PQCSignatureAlgorithms / PQCKeyEncapsulationAlgorithms)
+ * @param parameterSpec an {@link AlgorithmParameterSpec} or an {@link
Integer} key size; when {@code null} the
+ * algorithm's default parameter set is used
+ * @return the generated key pair
+ */
+ public static KeyPair generateKeyPair(String algorithm, Object
parameterSpec) throws Exception {
+ KeyPairGenerator generator;
+ String provider = determineProvider(algorithm);
+ if (provider != null) {
+ generator =
KeyPairGenerator.getInstance(getAlgorithmName(algorithm), provider);
+ } else {
+ generator =
KeyPairGenerator.getInstance(getAlgorithmName(algorithm));
+ }
+
+ if (parameterSpec != null) {
+ if (parameterSpec instanceof AlgorithmParameterSpec
algorithmParamSpec) {
+ generator.initialize(algorithmParamSpec,
SecureRandomHelper.getSecureRandom());
+ } else if (parameterSpec instanceof Integer keySize) {
+ generator.initialize(keySize,
SecureRandomHelper.getSecureRandom());
+ }
+ } else {
+ AlgorithmParameterSpec defaultSpec =
getDefaultParameterSpec(algorithm);
+ if (defaultSpec != null) {
+ generator.initialize(defaultSpec,
SecureRandomHelper.getSecureRandom());
+ } else {
+ generator.initialize(DEFAULT_KEY_SIZE,
SecureRandomHelper.getSecureRandom());
+ }
+ }
+
+ return generator.generateKeyPair();
+ }
+
+ /**
+ * The BouncyCastle provider for the given algorithm, or {@code null} when
the algorithm is unknown.
+ */
+ public static String determineProvider(String algorithm) {
+ try {
+ return PQCSignatureAlgorithms.valueOf(algorithm).getBcProvider();
+ } catch (IllegalArgumentException e1) {
+ try {
+ return
PQCKeyEncapsulationAlgorithms.valueOf(algorithm).getBcProvider();
+ } catch (IllegalArgumentException e2) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * The JCE algorithm name for the given algorithm, or the algorithm itself
when it is unknown.
+ */
+ public static String getAlgorithmName(String algorithm) {
+ try {
+ return PQCSignatureAlgorithms.valueOf(algorithm).getAlgorithm();
+ } catch (IllegalArgumentException e1) {
+ try {
+ return
PQCKeyEncapsulationAlgorithms.valueOf(algorithm).getAlgorithm();
+ } catch (IllegalArgumentException e2) {
+ return algorithm;
+ }
+ }
+ }
+
+ /**
+ * The default parameter set for the given algorithm, or {@code null} when
it has none.
+ */
+ public static AlgorithmParameterSpec getDefaultParameterSpec(String
algorithm) {
+ try {
+ switch (algorithm) {
+ case "DILITHIUM":
+ case "MLDSA":
+ return MLDSAParameterSpec.ml_dsa_44;
+ case "SLHDSA":
+ case "SPHINCSPLUS":
+ return SLHDSAParameterSpec.slh_dsa_sha2_128s;
+ case "FALCON":
+ return FalconParameterSpec.falcon_512;
+ case "XMSS":
+ return new XMSSParameterSpec(10, XMSSParameterSpec.SHA256);
+ case "XMSSMT":
+ return XMSSMTParameterSpec.XMSSMT_SHA2_20d2_256;
+ case "LMS":
+ case "HSS":
+ return new LMSKeyGenParameterSpec(
+ LMSigParameters.lms_sha256_n32_h10,
+ LMOtsParameters.sha256_n32_w4);
+ case "MLKEM":
+ case "KYBER":
+ return MLKEMParameterSpec.ml_kem_768;
+ case "NTRU":
+ return NTRUParameterSpec.ntruhps2048509;
+ case "NTRULPRime":
+ return NTRULPRimeParameterSpec.ntrulpr653;
+ case "SNTRUPrime":
+ return SNTRUPrimeParameterSpec.sntrup761;
+ case "SABER":
+ return SABERParameterSpec.lightsaberkem128r3;
+ case "FRODO":
+ return FrodoParameterSpec.frodokem640aes;
+ case "BIKE":
+ return BIKEParameterSpec.bike128;
+ case "HQC":
+ return HQCParameterSpec.hqc128;
+ case "CMCE":
+ return CMCEParameterSpec.mceliece348864;
+ default:
+ return null;
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to create default parameter spec for algorithm:
{}", algorithm, e);
+ return null;
+ }
+ }
+}
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/InMemoryKeyLifecycleManagerTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/InMemoryKeyLifecycleManagerTest.java
new file mode 100644
index 000000000000..de5b8cccbd6b
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/InMemoryKeyLifecycleManagerTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.lifecycle;
+
+import java.security.KeyPair;
+import java.security.Security;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class InMemoryKeyLifecycleManagerTest {
+
+ private InMemoryKeyLifecycleManager keyManager;
+
+ @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());
+ }
+ }
+
+ @BeforeEach
+ void setup() {
+ keyManager = new InMemoryKeyLifecycleManager();
+ }
+
+ @Test
+ void testGenerateAndRetrieveKey() throws Exception {
+ KeyPair keyPair = keyManager.generateKeyPair("MLDSA", "app-key");
+
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPublic());
+ assertNotNull(keyPair.getPrivate());
+ assertArrayEquals(keyPair.getPublic().getEncoded(),
keyManager.getKey("app-key").getPublic().getEncoded());
+
+ KeyMetadata metadata = keyManager.getKeyMetadata("app-key");
+ assertNotNull(metadata);
+ assertEquals("app-key", metadata.getKeyId());
+ assertEquals("MLDSA", metadata.getAlgorithm());
+ assertEquals(KeyMetadata.KeyStatus.ACTIVE, metadata.getStatus());
+ assertEquals(0, metadata.getUsageCount());
+ assertEquals(1, keyManager.size());
+ }
+
+ @Test
+ void testGenerateKeyForOtherAlgorithm() throws Exception {
+ // the documented example uses FALCON
+ assertNotNull(keyManager.generateKeyPair("FALCON", "test-key"));
+ assertEquals(1, keyManager.size());
+ }
+
+ @Test
+ void testMissingKey() {
+ assertThrows(IllegalArgumentException.class, () ->
keyManager.getKey("nope"));
+ assertNull(keyManager.getKeyMetadata("nope"));
+ }
+
+ @Test
+ void testRotateKey() throws Exception {
+ KeyPair oldKey = keyManager.generateKeyPair("MLDSA", "old-key");
+ KeyPair newKey = keyManager.rotateKey("old-key", "new-key", "MLDSA");
+
+ assertEquals(KeyMetadata.KeyStatus.DEPRECATED,
keyManager.getKeyMetadata("old-key").getStatus());
+ assertEquals(KeyMetadata.KeyStatus.ACTIVE,
keyManager.getKeyMetadata("new-key").getStatus());
+ assertFalse(Arrays.equals(oldKey.getPublic().getEncoded(),
newKey.getPublic().getEncoded()));
+ assertEquals(2, keyManager.size());
+ }
+
+ @Test
+ void testRotateUnknownKeyFails() {
+ assertThrows(IllegalArgumentException.class, () ->
keyManager.rotateKey("nope", "new", "MLDSA"));
+ }
+
+ @Test
+ void testExpireAndRevokeKey() throws Exception {
+ keyManager.generateKeyPair("MLDSA", "expire-key");
+ keyManager.expireKey("expire-key");
+ assertEquals(KeyMetadata.KeyStatus.EXPIRED,
keyManager.getKeyMetadata("expire-key").getStatus());
+
+ keyManager.generateKeyPair("MLDSA", "revoke-key");
+ keyManager.revokeKey("revoke-key", "compromised");
+ KeyMetadata revoked = keyManager.getKeyMetadata("revoke-key");
+ assertEquals(KeyMetadata.KeyStatus.REVOKED, revoked.getStatus());
+ assertTrue(revoked.getDescription().contains("compromised"));
+ }
+
+ @Test
+ void testListAndDeleteKeys() throws Exception {
+ keyManager.generateKeyPair("MLDSA", "k1");
+ keyManager.generateKeyPair("FALCON", "k2");
+
+ List<KeyMetadata> keys = keyManager.listKeys();
+ assertEquals(2, keys.size());
+ assertTrue(keys.stream().anyMatch(k -> k.getKeyId().equals("k1")));
+ assertTrue(keys.stream().anyMatch(k -> k.getKeyId().equals("k2")));
+
+ keyManager.deleteKey("k1");
+ assertEquals(1, keyManager.size());
+ assertNull(keyManager.getKeyMetadata("k1"));
+ }
+
+ @Test
+ void testNeedsRotation() throws Exception {
+ keyManager.generateKeyPair("MLDSA", "usage-key");
+ assertFalse(keyManager.needsRotation("usage-key",
Duration.ofDays(365), 0));
+
+ KeyMetadata metadata = keyManager.getKeyMetadata("usage-key");
+ for (int i = 0; i < 100; i++) {
+ metadata.updateLastUsed();
+ }
+ keyManager.updateKeyMetadata("usage-key", metadata);
+
+ assertTrue(keyManager.needsRotation("usage-key", null, 50));
+ assertFalse(keyManager.needsRotation("usage-key", null, 150));
+ // an unknown key never needs rotation
+ assertFalse(keyManager.needsRotation("nope", null, 1));
+ }
+
+ @Test
+ void testExportAndImportPublicKey() throws Exception {
+ KeyPair keyPair = keyManager.generateKeyPair("MLDSA", "export-key");
+
+ byte[] pem = keyManager.exportPublicKey(keyPair,
KeyLifecycleManager.KeyFormat.PEM);
+ assertNotNull(pem);
+ assertTrue(new String(pem).contains("-----BEGIN PUBLIC KEY-----"));
+
+ KeyPair imported = keyManager.importKey(pem,
KeyLifecycleManager.KeyFormat.PEM, "MLDSA");
+ assertNotNull(imported);
+ assertArrayEquals(keyPair.getPublic().getEncoded(),
imported.getPublic().getEncoded());
+ }
+
+ @Test
+ void testClearAndSize() throws Exception {
+ assertEquals(0, keyManager.size());
+
+ keyManager.generateKeyPair("MLDSA", "a");
+ keyManager.generateKeyPair("MLDSA", "b");
+ assertEquals(2, keyManager.size());
+
+ keyManager.clear();
+ assertEquals(0, keyManager.size());
+ assertTrue(keyManager.listKeys().isEmpty());
+ assertNull(keyManager.getKeyMetadata("a"));
+ }
+
+ @Test
+ void testConcurrentKeyGenerationIsThreadSafe() throws Exception {
+ int threads = 8;
+ ExecutorService pool = Executors.newFixedThreadPool(threads);
+ CountDownLatch done = new CountDownLatch(threads);
+ AtomicInteger failures = new AtomicInteger();
+ try {
+ for (int i = 0; i < threads; i++) {
+ final int index = i;
+ pool.submit(() -> {
+ try {
+ keyManager.generateKeyPair("MLDSA", "key-" + index);
+ } catch (Exception e) {
+ failures.incrementAndGet();
+ } finally {
+ done.countDown();
+ }
+ });
+ }
+ assertTrue(done.await(60, TimeUnit.SECONDS), "concurrent key
generation did not finish in time");
+ } finally {
+ pool.shutdownNow();
+ }
+
+ assertEquals(0, failures.get(), "concurrent key generation should not
fail");
+ assertEquals(threads, keyManager.size());
+ }
+}