This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch CAMEL-22522
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/CAMEL-22522 by this push:
new 1b7dc7c49653 CAMEL-22528 - Camel-PQC: Add AWS Secrets Manager
lifecycle manager
1b7dc7c49653 is described below
commit 1b7dc7c4965358a16f3c9d35705581a46a54971b
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Oct 10 11:42:53 2025 +0200
CAMEL-22528 - Camel-PQC: Add AWS Secrets Manager lifecycle manager
Signed-off-by: Andrea Cosentino <[email protected]>
---
components/camel-pqc/pom.xml | 15 +
.../camel-pqc/src/main/docs/pqc-component.adoc | 457 ++++++++++++-
.../AwsSecretsManagerKeyLifecycleManager.java | 717 +++++++++++++++++++++
.../pqc/AwsSecretsManagerKeyLifecycleIT.java | 274 ++++++++
4 files changed, 1442 insertions(+), 21 deletions(-)
diff --git a/components/camel-pqc/pom.xml b/components/camel-pqc/pom.xml
index e4d0cd581e15..0e215b8dd49f 100644
--- a/components/camel-pqc/pom.xml
+++ b/components/camel-pqc/pom.xml
@@ -54,6 +54,14 @@
<optional>true</optional>
</dependency>
+ <!-- AWS SDK for AwsSecretsManagerKeyLifecycleManager (optional) -->
+ <dependency>
+ <groupId>software.amazon.awssdk</groupId>
+ <artifactId>secretsmanager</artifactId>
+ <version>${aws-java-sdk2-version}</version>
+ <optional>true</optional>
+ </dependency>
+
<!-- for testing -->
<dependency>
<groupId>org.apache.camel</groupId>
@@ -78,5 +86,12 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-aws-v2</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/components/camel-pqc/src/main/docs/pqc-component.adoc
b/components/camel-pqc/src/main/docs/pqc-component.adoc
index 4b8f1ea4f2f0..37f64264d863 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -899,83 +899,498 @@ vault token create -policy=pqc-app # For
applications (verification onl
vault token create -policy=pqc-app-signing-key # For specific key access
--------------------------------------------------------------------------------
-**Production Security Recommendations:**
+==== AwsSecretsManagerKeyLifecycleManager
-1. **Principle of Least Privilege**: Use the most restrictive policy possible
- - Applications that only verify signatures: Use `pqc-app` policy (public
keys only)
- - Signing services: Use `pqc-signing` policy or specific key policies
- - Key management: Use `pqc-admin` policy
+An enterprise-grade implementation that integrates with AWS Secrets Manager
for centralized secret management.
-2. **Limit Private Key Access**:
- - Create specific policies per key ID in production
- - Use Vault namespaces to isolate different environments
- - Enable audit logging: `vault audit enable file
file_path=/var/log/vault-audit.log`
+**Security Implementation:**
+
+This implementation uses industry-standard cryptographic key formats and
separation of concerns:
+
+* **Private keys**: Stored in PKCS#8 format (RFC 5208) - the standard for
private key encoding
+* **Public keys**: Stored in X.509/SubjectPublicKeyInfo format (RFC 5280) -
the standard for public key encoding
+* **Separate storage**: Private keys, public keys, and metadata are stored as
distinct AWS secrets
+* **Fine-grained IAM policies**: Enables different access policies for private
keys (restricted) vs public keys (read-only)
+
+**Security Note:** This implementation stores PQC keys in AWS Secrets Manager.
While this approach uses industry-standard formats and enables fine-grained
access control, organizations with stringent security requirements should
consider:
+
+* Using **AWS CloudHSM** for keys that must never be exportable
+* Implementing additional encryption layers using **AWS KMS Customer Master
Keys (CMKs)**
+* Applying **strict IAM policies** limiting private key access to specific
services or roles only
+* Regular **key rotation** and **CloudTrail audit log monitoring**
+* For production use, review and customize the IAM policies shown in the setup
section
+
+**Features:**
+
+* Centralized secret management via AWS Secrets Manager
+* Industry-standard key formats (PKCS#8 for private keys, X.509 for public
keys)
+* Separate storage for private and public keys (enables different IAM policies)
+* Automatic audit logging through AWS CloudTrail
+* Fine-grained access control with IAM policies
+* Encryption at rest with AWS KMS
+* Multi-region replication support
+* In-memory caching for performance
+* Uses AWS SDK v2 (software.amazon.awssdk) consistent with other Camel AWS
components
+* LocalStack endpoint override support for testing
+
+**Use Cases:**
+
+* Production environments with existing AWS infrastructure
+* Multi-region deployments
+* Enterprise security and compliance requirements
+* Centralized key management across multiple applications
+* AWS Organizations and cross-account access scenarios
+
+**Dependencies:**
+
+To use AwsSecretsManagerKeyLifecycleManager, add the following optional
dependency:
+
+[source,xml]
+--------------------------------------------------------------------------------
+<dependency>
+ <groupId>software.amazon.awssdk</groupId>
+ <artifactId>secretsmanager</artifactId>
+ <version>${aws-java-sdk2-version}</version>
+</dependency>
+--------------------------------------------------------------------------------
+
+**Example with SecretsManagerClient:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Option 1: Using existing SecretsManagerClient (recommended when using
camel-aws-secrets-manager)
+@BindToRegistry("secretsManagerClient")
+public SecretsManagerClient createSecretsManagerClient() {
+ return SecretsManagerClient.builder()
+ .region(Region.US_EAST_1)
+ .build(); // Uses default AWS credentials chain
+}
+
+@BindToRegistry("keyLifecycleManager")
+public AwsSecretsManagerKeyLifecycleManager createKeyManager() {
+ return new AwsSecretsManagerKeyLifecycleManager(
+ secretsManagerClient, // Reuse existing client
+ "pqc/keys" // Key prefix
+ );
+}
+
+// Generate a Dilithium key stored in AWS Secrets Manager
+KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM", "app-signing-key",
+ DilithiumParameterSpec.dilithium2);
+
+// Keys are stored as: pqc/keys/app-signing-key/private, /public, /metadata
+--------------------------------------------------------------------------------
+
+**Example with Direct Configuration:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Option 2: Direct configuration with AWS credentials
+AwsSecretsManagerKeyLifecycleManager keyManager =
+ new AwsSecretsManagerKeyLifecycleManager(
+ "us-east-1", // AWS region
+ "AKIAIOSFODNN7EXAMPLE", // access key (optional, uses default
credentials if null)
+ "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", // secret key (optional)
+ "pqc/keys" // key prefix (optional, defaults to "pqc/keys")
+ );
+
+// Generate and store key in AWS Secrets Manager
+KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM", "aws-key",
+ DilithiumParameterSpec.dilithium2);
+--------------------------------------------------------------------------------
+
+**Example with LocalStack (Testing):**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Option 3: Configuration for LocalStack testing
+AwsSecretsManagerKeyLifecycleManager keyManager =
+ new AwsSecretsManagerKeyLifecycleManager(
+ "us-east-1", // region
+ "test", // access key for LocalStack
+ "test", // secret key for LocalStack
+ "pqc/test-keys", // key prefix
+ "http://localhost:4566" // LocalStack endpoint
+ );
+
+// Generate and store key in LocalStack
+KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM", "test-key",
+ DilithiumParameterSpec.dilithium2);
+--------------------------------------------------------------------------------
+
+**YAML Configuration:**
+
+[source,yaml]
+--------------------------------------------------------------------------------
+camel:
+ beans:
+ # Create SecretsManagerClient using default credentials
+ secretsManagerClient:
+ type: software.amazon.awssdk.services.secretsmanager.SecretsManagerClient
+ factoryMethod: builder
+ factoryBean:
+ type:
software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder
+ properties:
+ region: "!software.amazon.awssdk.regions.Region#US_EAST_1"
+ factoryMethod: build
+
+ # Create AwsSecretsManagerKeyLifecycleManager
+ keyLifecycleManager:
+ type:
org.apache.camel.component.pqc.lifecycle.AwsSecretsManagerKeyLifecycleManager
+ constructorArgs:
+ - "#bean:secretsManagerClient"
+ - "pqc/keys"
+--------------------------------------------------------------------------------
+
+**YAML Configuration with Explicit Credentials:**
+
+[source,yaml]
+--------------------------------------------------------------------------------
+camel:
+ beans:
+ # Create AwsSecretsManagerKeyLifecycleManager with explicit configuration
+ keyLifecycleManager:
+ type:
org.apache.camel.component.pqc.lifecycle.AwsSecretsManagerKeyLifecycleManager
+ constructorArgs:
+ - "us-east-1" # region
+ - "${AWS_ACCESS_KEY}" # access key from environment
+ - "${AWS_SECRET_KEY}" # secret key from environment
+ - "pqc/keys" # key prefix
+ - null # endpoint override (null for production)
+--------------------------------------------------------------------------------
+
+**AWS Secrets Storage Structure:**
+
+Keys are stored in AWS Secrets Manager with separate secrets for private keys,
public keys, and metadata. This separation enables fine-grained access control
where applications can access public keys without having access to private keys.
+
+Each key creates three secrets:
+
+[source,text]
+--------------------------------------------------------------------------------
+pqc/keys/app-signing-key/private # PKCS#8 private key (STRICT IAM POLICY)
+ {
+ "key": "MIIEvQIBADANBg...", # Base64-encoded PKCS#8 private key
+ "format": "PKCS8",
+ "algorithm": "DILITHIUM"
+ }
+ Tags: ManagedBy=camel-pqc
-3. **Monitor and Rotate**:
- - Monitor Vault audit logs for private key access
- - Implement automated key rotation policies
- - Set up alerts for unusual access patterns
+pqc/keys/app-signing-key/public # X.509 public key (READ-ONLY IAM POLICY)
+ {
+ "key": "MIIBIjANBgkqhk...", # Base64-encoded X.509 public key
+ "format": "X509",
+ "algorithm": "DILITHIUM"
+ }
+ Tags: ManagedBy=camel-pqc
-4. **Additional Security Layers**:
- - Consider using Vault Transit engine to encrypt private keys before storage
- - Use AppRole or Kubernetes authentication instead of tokens in production
- - Enable MFA for administrative operations
+pqc/keys/app-signing-key/metadata # Key metadata
+ {
+ "metadata": "rO0ABXNyAC9vcm...", # Serialized KeyMetadata object
+ "keyId": "app-signing-key",
+ "algorithm": "DILITHIUM"
+ }
+ Tags: ManagedBy=camel-pqc
+--------------------------------------------------------------------------------
+
+**AWS Setup and IAM Policies:**
+
+To use AwsSecretsManagerKeyLifecycleManager, configure IAM policies for
appropriate access control. The implementation stores private keys, public
keys, and metadata separately to enable fine-grained access control.
+
+**Basic Policy (Full Access - Development/Testing):**
+
+[source,json]
+--------------------------------------------------------------------------------
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "PQCKeysFullAccess",
+ "Effect": "Allow",
+ "Action": [
+ "secretsmanager:CreateSecret",
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:PutSecretValue",
+ "secretsmanager:DeleteSecret",
+ "secretsmanager:ListSecrets",
+ "secretsmanager:DescribeSecret",
+ "secretsmanager:TagResource"
+ ],
+ "Resource": "arn:aws:secretsmanager:*:*:secret:pqc/keys/*"
+ },
+ {
+ "Sid": "KMSAccess",
+ "Effect": "Allow",
+ "Action": [
+ "kms:Decrypt",
+ "kms:Encrypt",
+ "kms:GenerateDataKey"
+ ],
+ "Resource": "*",
+ "Condition": {
+ "StringEquals": {
+ "kms:ViaService": "secretsmanager.*.amazonaws.com"
+ }
+ }
+ }
+ ]
+}
+--------------------------------------------------------------------------------
+
+**Production Policies (Fine-Grained Access Control):**
+
+**POLICY 1: Admin Policy (Key Management Service)**
+
+Full access to generate, rotate, and manage keys:
+
+[source,json]
+--------------------------------------------------------------------------------
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "PQCKeyManagement",
+ "Effect": "Allow",
+ "Action": [
+ "secretsmanager:CreateSecret",
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:PutSecretValue",
+ "secretsmanager:DeleteSecret",
+ "secretsmanager:UpdateSecret",
+ "secretsmanager:ListSecrets",
+ "secretsmanager:DescribeSecret",
+ "secretsmanager:TagResource"
+ ],
+ "Resource": "arn:aws:secretsmanager:*:*:secret:pqc/keys/*"
+ },
+ {
+ "Sid": "KMSFullAccess",
+ "Effect": "Allow",
+ "Action": [
+ "kms:Decrypt",
+ "kms:Encrypt",
+ "kms:GenerateDataKey",
+ "kms:DescribeKey"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+--------------------------------------------------------------------------------
+
+**POLICY 2: Signing Service Policy (Read Private Keys for Signing)**
+
+Read-only access to specific private keys for signing operations:
+
+[source,json]
+--------------------------------------------------------------------------------
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ReadPrivateKeysForSigning",
+ "Effect": "Allow",
+ "Action": [
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:DescribeSecret"
+ ],
+ "Resource": [
+ "arn:aws:secretsmanager:*:*:secret:pqc/keys/*/private-*",
+ "arn:aws:secretsmanager:*:*:secret:pqc/keys/*/public-*",
+ "arn:aws:secretsmanager:*:*:secret:pqc/keys/*/metadata-*"
+ ]
+ },
+ {
+ "Sid": "ListKeys",
+ "Effect": "Allow",
+ "Action": "secretsmanager:ListSecrets",
+ "Resource": "*",
+ "Condition": {
+ "StringLike": {
+ "secretsmanager:Name": "pqc/keys/*"
+ }
+ }
+ },
+ {
+ "Sid": "KMSDecrypt",
+ "Effect": "Allow",
+ "Action": "kms:Decrypt",
+ "Resource": "*"
+ }
+ ]
+}
+--------------------------------------------------------------------------------
+
+**POLICY 3: Application Policy (Public Keys Only)**
+
+Read-only access to public keys for signature verification:
+
+[source,json]
+--------------------------------------------------------------------------------
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ReadPublicKeysOnly",
+ "Effect": "Allow",
+ "Action": [
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:DescribeSecret"
+ ],
+ "Resource": [
+ "arn:aws:secretsmanager:*:*:secret:pqc/keys/*/public-*",
+ "arn:aws:secretsmanager:*:*:secret:pqc/keys/*/metadata-*"
+ ]
+ },
+ {
+ "Sid": "DenyPrivateKeyAccess",
+ "Effect": "Deny",
+ "Action": "secretsmanager:GetSecretValue",
+ "Resource": "arn:aws:secretsmanager:*:*:secret:pqc/keys/*/private-*"
+ },
+ {
+ "Sid": "ListKeys",
+ "Effect": "Allow",
+ "Action": "secretsmanager:ListSecrets",
+ "Resource": "*"
+ },
+ {
+ "Sid": "KMSDecrypt",
+ "Effect": "Allow",
+ "Action": "kms:Decrypt",
+ "Resource": "*"
+ }
+ ]
+}
+--------------------------------------------------------------------------------
+
+**POLICY 4: Specific Key Access (Production Best Practice)**
+
+Limit access to specific key IDs only:
+
+[source,json]
+--------------------------------------------------------------------------------
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AccessSpecificKeyOnly",
+ "Effect": "Allow",
+ "Action": [
+ "secretsmanager:GetSecretValue",
+ "secretsmanager:DescribeSecret"
+ ],
+ "Resource": [
+
"arn:aws:secretsmanager:us-east-1:123456789012:secret:pqc/keys/app-signing-key/private-*",
+
"arn:aws:secretsmanager:us-east-1:123456789012:secret:pqc/keys/app-signing-key/public-*",
+
"arn:aws:secretsmanager:us-east-1:123456789012:secret:pqc/keys/app-signing-key/metadata-*"
+ ]
+ },
+ {
+ "Sid": "KMSDecrypt",
+ "Effect": "Allow",
+ "Action": "kms:Decrypt",
+ "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
+ }
+ ]
+}
+--------------------------------------------------------------------------------
+
+**Integration with camel-aws-secrets-manager:**
+
+AwsSecretsManagerKeyLifecycleManager can share the same SecretsManagerClient
with the camel-aws-secrets-manager component:
+
+[source,java]
+--------------------------------------------------------------------------------
+// Reuse SecretsManagerClient from camel-aws-secrets-manager
+@BindToRegistry("keyLifecycleManager")
+public AwsSecretsManagerKeyLifecycleManager createKeyManager() {
+ SecretsManagerClient secretsManagerClient = context.getRegistry()
+ .lookupByNameAndType("secretsManagerClient",
SecretsManagerClient.class);
+
+ return new AwsSecretsManagerKeyLifecycleManager(
+ secretsManagerClient,
+ "pqc/keys"
+ );
+}
+--------------------------------------------------------------------------------
**Comparison of Implementations:**
[options="header"]
|===
-|Feature |FileBasedKeyLifecycleManager |InMemoryKeyLifecycleManager
|HashicorpVaultKeyLifecycleManager
+|Feature |FileBasedKeyLifecycleManager |InMemoryKeyLifecycleManager
|HashicorpVaultKeyLifecycleManager |AwsSecretsManagerKeyLifecycleManager
|Persistence
|✅ File system
|❌ Memory only
|✅ Vault backend
+|✅ AWS Secrets Manager
|Distributed
|❌ Single node
|❌ Single node
|✅ Multi-node
+|✅ Multi-region
|Audit Logging
|❌ Manual
|❌ None
-|✅ Automatic
+|✅ Automatic (Vault)
+|✅ Automatic (CloudTrail)
|Access Control
|❌ File permissions
|❌ None
|✅ Vault policies
+|✅ IAM policies
|Encryption at Rest
|❌ OS-dependent
|❌ N/A
-|✅ Always
+|✅ Always (Vault)
+|✅ Always (AWS KMS)
|High Availability
|❌ No
|❌ No
|✅ Yes (Vault HA)
+|✅ Yes (AWS Multi-AZ)
|External Dependencies
|❌ None
|❌ None
|✅ Vault + spring-vault
+|✅ AWS SDK v2
|Caching
|✅ Yes
|✅ Yes
|✅ Yes
+|✅ Yes
|Spring Integration
|❌ No
|❌ No
|✅ Yes
+|❌ No
+
+|Cloud Integration
+|❌ No
+|❌ No
+|✅ HCP Vault
+|✅ AWS Native
+
+|Multi-Region Support
+|❌ No
+|❌ No
+|❌ No
+|✅ Yes
|Use Case
|Single server
|Testing/Dev
-|Production/Enterprise
+|Production/Enterprise (Vault)
+|Production/Enterprise (AWS)
|===
=== Key Generation
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
new file mode 100644
index 000000000000..46a37c426630
--- /dev/null
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
@@ -0,0 +1,717 @@
+/*
+ * 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.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.URI;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.AlgorithmParameterSpec;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms;
+import org.apache.camel.component.pqc.PQCSignatureAlgorithms;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
+import
software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
+import
software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
+import
software.amazon.awssdk.services.secretsmanager.model.DeleteSecretRequest;
+import
software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
+import
software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
+import software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest;
+import
software.amazon.awssdk.services.secretsmanager.model.ListSecretsResponse;
+import
software.amazon.awssdk.services.secretsmanager.model.PutSecretValueRequest;
+import
software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.secretsmanager.model.SecretListEntry;
+import software.amazon.awssdk.services.secretsmanager.model.Tag;
+
+/**
+ * AWS Secrets Manager-based implementation of KeyLifecycleManager. Stores
keys and metadata in AWS Secrets Manager with
+ * centralized secret management, audit logging, and fine-grained access
control via IAM policies.
+ *
+ * Features: - Centralized secret management via AWS Secrets Manager -
Automatic audit logging through AWS CloudTrail -
+ * Fine-grained access control with IAM policies - Encryption at rest with AWS
KMS - Multi-region replication support -
+ * In-memory caching for performance - Industry-standard PKCS#8/X.509 key
formats - Separate storage for private/public
+ * keys
+ *
+ * Configuration: - region: AWS region (e.g., us-east-1) - accessKey: AWS
access key (optional, uses default credentials
+ * if not provided) - secretKey: AWS secret key (optional) - keyPrefix: Prefix
for all secret names (default: pqc/keys)
+ * - endpointOverride: Custom endpoint for testing with LocalStack (optional)
+ *
+ * This implementation uses AWS SDK v2 (software.amazon.awssdk) consistent
with other Camel AWS components.
+ */
+public class AwsSecretsManagerKeyLifecycleManager implements
KeyLifecycleManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AwsSecretsManagerKeyLifecycleManager.class);
+
+ private final SecretsManagerClient secretsManagerClient;
+ private final String keyPrefix;
+ private final ConcurrentHashMap<String, KeyPair> keyCache = new
ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, KeyMetadata> metadataCache = new
ConcurrentHashMap<>();
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Create an AwsSecretsManagerKeyLifecycleManager with an existing
SecretsManagerClient
+ *
+ * @param secretsManagerClient Configured SecretsManagerClient instance
+ * @param keyPrefix Prefix for secret names in AWS Secrets
Manager
+ */
+ public AwsSecretsManagerKeyLifecycleManager(SecretsManagerClient
secretsManagerClient, String keyPrefix) {
+ this.secretsManagerClient = secretsManagerClient;
+ this.keyPrefix = keyPrefix != null ? keyPrefix : "pqc/keys";
+
+ LOG.info("Initialized AwsSecretsManagerKeyLifecycleManager with
keyPrefix: {}", this.keyPrefix);
+
+ try {
+ loadExistingKeys();
+ } catch (Exception e) {
+ LOG.warn("Failed to load existing keys from AWS Secrets Manager",
e);
+ }
+ }
+
+ /**
+ * Create an AwsSecretsManagerKeyLifecycleManager with basic configuration
+ *
+ * @param region AWS region (e.g., us-east-1)
+ */
+ public AwsSecretsManagerKeyLifecycleManager(String region) {
+ this(region, null, null, null);
+ }
+
+ /**
+ * Create an AwsSecretsManagerKeyLifecycleManager with custom configuration
+ *
+ * @param region AWS region (e.g., us-east-1)
+ * @param accessKey AWS access key (optional, uses default credentials if
null)
+ * @param secretKey AWS secret key (optional)
+ * @param keyPrefix Prefix for secret names
+ */
+ public AwsSecretsManagerKeyLifecycleManager(String region, String
accessKey, String secretKey, String keyPrefix) {
+ this(region, accessKey, secretKey, keyPrefix, null);
+ }
+
+ /**
+ * Create an AwsSecretsManagerKeyLifecycleManager with full configuration
including endpoint override
+ *
+ * @param region AWS region (e.g., us-east-1)
+ * @param accessKey AWS access key (optional, uses default
credentials if null)
+ * @param secretKey AWS secret key (optional)
+ * @param keyPrefix Prefix for secret names
+ * @param endpointOverride Custom endpoint for testing (optional, e.g.,
http://localhost:4566 for LocalStack)
+ */
+ public AwsSecretsManagerKeyLifecycleManager(String region, String
accessKey, String secretKey, String keyPrefix,
+ String endpointOverride) {
+ this.keyPrefix = keyPrefix != null ? keyPrefix : "pqc/keys";
+
+ // Build SecretsManagerClient
+ SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
+
+ if (region != null) {
+ clientBuilder.region(Region.of(region));
+ }
+
+ if (accessKey != null && secretKey != null) {
+ AwsBasicCredentials credentials =
AwsBasicCredentials.create(accessKey, secretKey);
+
clientBuilder.credentialsProvider(StaticCredentialsProvider.create(credentials));
+ }
+
+ if (endpointOverride != null) {
+ clientBuilder.endpointOverride(URI.create(endpointOverride));
+ }
+
+ this.secretsManagerClient = clientBuilder.build();
+
+ LOG.info("Initialized AwsSecretsManagerKeyLifecycleManager with
region: {}, keyPrefix: {}, endpointOverride: {}",
+ region, this.keyPrefix, endpointOverride);
+
+ try {
+ loadExistingKeys();
+ } catch (Exception e) {
+ LOG.warn("Failed to load existing keys from AWS Secrets Manager",
e);
+ }
+ }
+
+ @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);
+
+ 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) {
+ generator.initialize((AlgorithmParameterSpec) parameterSpec,
new SecureRandom());
+ } else if (parameterSpec instanceof Integer) {
+ generator.initialize((Integer) parameterSpec, new
SecureRandom());
+ }
+ } else {
+ // Use default parameter spec for the algorithm
+ AlgorithmParameterSpec defaultSpec =
getDefaultParameterSpec(algorithm);
+ if (defaultSpec != null) {
+ generator.initialize(defaultSpec, new SecureRandom());
+ } else {
+ generator.initialize(getDefaultKeySize(algorithm), new
SecureRandom());
+ }
+ }
+
+ KeyPair keyPair = generator.generateKeyPair();
+
+ // Create metadata
+ KeyMetadata metadata = new KeyMetadata(keyId, algorithm);
+ metadata.setDescription("Generated on " + new Date());
+
+ // Store the key
+ storeKey(keyId, keyPair, metadata);
+
+ LOG.info("Generated key pair in AWS Secrets Manager: {}", metadata);
+ 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 {
+ // Try to import as private key first (which includes public key)
+ try {
+ PrivateKey privateKey =
KeyFormatConverter.importPrivateKey(keyData, format,
getAlgorithmName(algorithm));
+ 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));
+ 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);
+
+ // Get old key metadata
+ KeyMetadata oldMetadata = getKeyMetadata(oldKeyId);
+ if (oldMetadata == null) {
+ throw new IllegalArgumentException("Old key not found: " +
oldKeyId);
+ }
+
+ // Mark old key as deprecated
+ oldMetadata.setStatus(KeyMetadata.KeyStatus.DEPRECATED);
+ updateKeyMetadata(oldKeyId, oldMetadata);
+
+ // Generate new key
+ KeyPair newKeyPair = generateKeyPair(algorithm, newKeyId);
+
+ LOG.info("Key rotation completed in AWS Secrets Manager: {} -> {}",
oldKeyId, newKeyId);
+ return newKeyPair;
+ }
+
+ @Override
+ public void storeKey(String keyId, KeyPair keyPair, KeyMetadata metadata)
throws Exception {
+ // Use PKCS#8 format for private key and X.509 for public key
(industry standard)
+ byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); // PKCS#8
format
+ byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); //
X.509/SubjectPublicKeyInfo format
+ String privateKeyBase64 =
Base64.getEncoder().encodeToString(privateKeyBytes);
+ String publicKeyBase64 =
Base64.getEncoder().encodeToString(publicKeyBytes);
+ String metadataBase64 = serializeMetadata(metadata);
+
+ // Store private key separately (strict IAM policy recommended in
production)
+ String privateSecretName = getSecretName(keyId, "private");
+ String privateSecretValue = objectMapper.writeValueAsString(new
SecretData(
+ privateKeyBase64,
+ "PKCS8",
+ metadata.getAlgorithm()));
+
+ createOrUpdateSecret(privateSecretName, privateSecretValue, "PQC
Private Key: " + keyId);
+
+ // Store public key separately (can have read-only IAM policy)
+ String publicSecretName = getSecretName(keyId, "public");
+ String publicSecretValue = objectMapper.writeValueAsString(new
SecretData(
+ publicKeyBase64,
+ "X509",
+ metadata.getAlgorithm()));
+
+ createOrUpdateSecret(publicSecretName, publicSecretValue, "PQC Public
Key: " + keyId);
+
+ // Store metadata separately
+ String metadataSecretName = getSecretName(keyId, "metadata");
+ String metadataSecretValue = objectMapper.writeValueAsString(new
MetadataData(
+ metadataBase64,
+ keyId,
+ metadata.getAlgorithm()));
+
+ createOrUpdateSecret(metadataSecretName, metadataSecretValue, "PQC Key
Metadata: " + keyId);
+
+ // Update caches
+ keyCache.put(keyId, keyPair);
+ metadataCache.put(keyId, metadata);
+
+ LOG.debug("Stored private key, public key, and metadata separately in
AWS Secrets Manager for: {}", keyId);
+ }
+
+ @Override
+ public KeyPair getKey(String keyId) throws Exception {
+ // Check cache first
+ if (keyCache.containsKey(keyId)) {
+ return keyCache.get(keyId);
+ }
+
+ // Read private key from AWS Secrets Manager
+ String privateSecretName = getSecretName(keyId, "private");
+ GetSecretValueResponse privateResponse = getSecret(privateSecretName);
+
+ // Read public key from AWS Secrets Manager
+ String publicSecretName = getSecretName(keyId, "public");
+ GetSecretValueResponse publicResponse = getSecret(publicSecretName);
+
+ // Parse secret values
+ SecretData privateData =
objectMapper.readValue(privateResponse.secretString(), SecretData.class);
+ SecretData publicData =
objectMapper.readValue(publicResponse.secretString(), SecretData.class);
+
+ byte[] privateKeyBytes =
Base64.getDecoder().decode(privateData.getKey());
+ byte[] publicKeyBytes =
Base64.getDecoder().decode(publicData.getKey());
+
+ // Use KeyFormatConverter to reconstruct keys from standard formats
+ PrivateKey privateKey =
KeyFormatConverter.importPrivateKey(privateKeyBytes,
+ KeyLifecycleManager.KeyFormat.DER,
getAlgorithmName(privateData.getAlgorithm()));
+ PublicKey publicKey =
KeyFormatConverter.importPublicKey(publicKeyBytes,
+ KeyLifecycleManager.KeyFormat.DER,
getAlgorithmName(publicData.getAlgorithm()));
+
+ KeyPair keyPair = new KeyPair(publicKey, privateKey);
+
+ // Cache it
+ keyCache.put(keyId, keyPair);
+ return keyPair;
+ }
+
+ @Override
+ public KeyMetadata getKeyMetadata(String keyId) throws Exception {
+ // Check cache first
+ if (metadataCache.containsKey(keyId)) {
+ return metadataCache.get(keyId);
+ }
+
+ // Read metadata from AWS Secrets Manager
+ String metadataSecretName = getSecretName(keyId, "metadata");
+
+ try {
+ GetSecretValueResponse response = getSecret(metadataSecretName);
+ MetadataData metadataData =
objectMapper.readValue(response.secretString(), MetadataData.class);
+ KeyMetadata metadata =
deserializeMetadata(metadataData.getMetadata());
+
+ // Cache it
+ metadataCache.put(keyId, metadata);
+ return metadata;
+ } catch (ResourceNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void updateKeyMetadata(String keyId, KeyMetadata metadata) throws
Exception {
+ // Read existing key pair
+ KeyPair keyPair = getKey(keyId);
+
+ // Store updated metadata with existing key pair
+ storeKey(keyId, keyPair, metadata);
+ }
+
+ @Override
+ public void deleteKey(String keyId) throws Exception {
+ // Delete private key, public key, and metadata separately
+ deleteSecret(getSecretName(keyId, "private"));
+ deleteSecret(getSecretName(keyId, "public"));
+ deleteSecret(getSecretName(keyId, "metadata"));
+
+ keyCache.remove(keyId);
+ metadataCache.remove(keyId);
+
+ LOG.info("Deleted private key, public key, and metadata from AWS
Secrets Manager: {}", keyId);
+ }
+
+ @Override
+ public List<KeyMetadata> listKeys() throws Exception {
+ // List all secrets with the key prefix
+ List<KeyMetadata> metadataList = new ArrayList<>();
+ String nextToken = null;
+
+ do {
+ ListSecretsRequest.Builder requestBuilder =
ListSecretsRequest.builder()
+ .maxResults(100);
+
+ if (nextToken != null) {
+ requestBuilder.nextToken(nextToken);
+ }
+
+ ListSecretsResponse response =
secretsManagerClient.listSecrets(requestBuilder.build());
+
+ for (SecretListEntry secret : response.secretList()) {
+ String secretName = secret.name();
+ // Only process metadata secrets to avoid duplicates
+ if (secretName.startsWith(keyPrefix) &&
secretName.endsWith("/metadata")) {
+ String keyId = extractKeyIdFromSecretName(secretName);
+ try {
+ KeyMetadata metadata = getKeyMetadata(keyId);
+ if (metadata != null) {
+ metadataList.add(metadata);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to load metadata for key: {}", keyId,
e);
+ }
+ }
+ }
+
+ nextToken = response.nextToken();
+ } while (nextToken != null);
+
+ return metadataList;
+ }
+
+ @Override
+ public boolean needsRotation(String keyId, Duration maxAge, long maxUsage)
throws Exception {
+ KeyMetadata metadata = getKeyMetadata(keyId);
+ if (metadata == null) {
+ return false;
+ }
+
+ if (metadata.needsRotation()) {
+ return true;
+ }
+
+ if (maxAge != null && metadata.getAgeInDays() > maxAge.toDays()) {
+ return true;
+ }
+
+ if (maxUsage > 0 && metadata.getUsageCount() >= maxUsage) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void expireKey(String keyId) throws Exception {
+ KeyMetadata metadata = getKeyMetadata(keyId);
+ if (metadata != null) {
+ metadata.setStatus(KeyMetadata.KeyStatus.EXPIRED);
+ updateKeyMetadata(keyId, metadata);
+ LOG.info("Expired key in AWS Secrets Manager: {}", keyId);
+ }
+ }
+
+ @Override
+ public void revokeKey(String keyId, String reason) throws Exception {
+ KeyMetadata metadata = getKeyMetadata(keyId);
+ if (metadata != null) {
+ metadata.setStatus(KeyMetadata.KeyStatus.REVOKED);
+ metadata.setDescription((metadata.getDescription() != null ?
metadata.getDescription() + "; " : "")
+ + "Revoked: " + reason);
+ updateKeyMetadata(keyId, metadata);
+ LOG.info("Revoked key in AWS Secrets Manager: {} - {}", keyId,
reason);
+ }
+ }
+
+ private void loadExistingKeys() throws Exception {
+ List<KeyMetadata> keys = listKeys();
+ if (!keys.isEmpty()) {
+ LOG.info("Found {} existing keys in AWS Secrets Manager",
keys.size());
+ for (KeyMetadata metadata : keys) {
+ LOG.debug("Loaded existing key from AWS Secrets Manager: {}",
metadata);
+ }
+ }
+ }
+
+ private String getSecretName(String keyId, String type) {
+ return keyPrefix + "/" + keyId + "/" + type;
+ }
+
+ private String extractKeyIdFromSecretName(String secretName) {
+ // Extract keyId from pattern: pqc/keys/{keyId}/metadata
+ String withoutPrefix = secretName.substring(keyPrefix.length() + 1);
+ int lastSlash = withoutPrefix.lastIndexOf('/');
+ return withoutPrefix.substring(0, lastSlash);
+ }
+
+ private void createOrUpdateSecret(String secretName, String secretValue,
String description) {
+ try {
+ // Try to update existing secret
+ PutSecretValueRequest putRequest = PutSecretValueRequest.builder()
+ .secretId(secretName)
+ .secretString(secretValue)
+ .build();
+ secretsManagerClient.putSecretValue(putRequest);
+ LOG.debug("Updated secret: {}", secretName);
+ } catch (ResourceNotFoundException e) {
+ // Secret doesn't exist, create it
+ CreateSecretRequest createRequest = CreateSecretRequest.builder()
+ .name(secretName)
+ .secretString(secretValue)
+ .description(description)
+
.tags(Tag.builder().key("ManagedBy").value("camel-pqc").build())
+ .build();
+ secretsManagerClient.createSecret(createRequest);
+ LOG.debug("Created secret: {}", secretName);
+ }
+ }
+
+ private GetSecretValueResponse getSecret(String secretName) {
+ GetSecretValueRequest request = GetSecretValueRequest.builder()
+ .secretId(secretName)
+ .build();
+ return secretsManagerClient.getSecretValue(request);
+ }
+
+ private void deleteSecret(String secretName) {
+ try {
+ DeleteSecretRequest request = DeleteSecretRequest.builder()
+ .secretId(secretName)
+ .forceDeleteWithoutRecovery(true)
+ .build();
+ secretsManagerClient.deleteSecret(request);
+ LOG.debug("Deleted secret: {}", secretName);
+ } catch (ResourceNotFoundException e) {
+ LOG.debug("Secret not found, skipping deletion: {}", secretName);
+ }
+ }
+
+ private String serializeMetadata(KeyMetadata metadata) throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(metadata);
+ }
+ return Base64.getEncoder().encodeToString(baos.toByteArray());
+ }
+
+ private KeyMetadata deserializeMetadata(String base64) throws Exception {
+ byte[] data = Base64.getDecoder().decode(base64);
+ ByteArrayInputStream bais = new ByteArrayInputStream(data);
+ try (ObjectInputStream ois = new ObjectInputStream(bais)) {
+ return (KeyMetadata) ois.readObject();
+ }
+ }
+
+ 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":
+ return
org.bouncycastle.pqc.jcajce.spec.DilithiumParameterSpec.dilithium2;
+ case "MLDSA":
+ case "SLHDSA":
+ // These use default initialization
+ return null;
+ case "FALCON":
+ return
org.bouncycastle.pqc.jcajce.spec.FalconParameterSpec.falcon_512;
+ case "SPHINCSPLUS":
+ return
org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec.sha2_128s;
+ case "XMSS":
+ return new
org.bouncycastle.pqc.jcajce.spec.XMSSParameterSpec(
+ 10,
+
org.bouncycastle.pqc.jcajce.spec.XMSSParameterSpec.SHA256);
+ case "XMSSMT":
+ return
org.bouncycastle.pqc.jcajce.spec.XMSSMTParameterSpec.XMSSMT_SHA2_20d2_256;
+ case "LMS":
+ case "HSS":
+ return new
org.bouncycastle.pqc.jcajce.spec.LMSKeyGenParameterSpec(
+
org.bouncycastle.pqc.crypto.lms.LMSigParameters.lms_sha256_n32_h10,
+
org.bouncycastle.pqc.crypto.lms.LMOtsParameters.sha256_n32_w4);
+ case "MLKEM":
+ case "KYBER":
+ // These use default initialization
+ return null;
+ case "NTRU":
+ return
org.bouncycastle.pqc.jcajce.spec.NTRUParameterSpec.ntruhps2048509;
+ case "NTRULPRime":
+ return
org.bouncycastle.pqc.jcajce.spec.NTRULPRimeParameterSpec.ntrulpr653;
+ case "SNTRUPrime":
+ return
org.bouncycastle.pqc.jcajce.spec.SNTRUPrimeParameterSpec.sntrup761;
+ case "SABER":
+ return
org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.lightsaberkem128r3;
+ case "FRODO":
+ return
org.bouncycastle.pqc.jcajce.spec.FrodoParameterSpec.frodokem640aes;
+ case "BIKE":
+ return
org.bouncycastle.pqc.jcajce.spec.BIKEParameterSpec.bike128;
+ case "HQC":
+ return
org.bouncycastle.pqc.jcajce.spec.HQCParameterSpec.hqc128;
+ case "CMCE":
+ return
org.bouncycastle.pqc.jcajce.spec.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;
+ }
+
+ /**
+ * Get the underlying SecretsManagerClient for advanced operations
+ */
+ public SecretsManagerClient getSecretsManagerClient() {
+ return secretsManagerClient;
+ }
+
+ /**
+ * Helper class for storing secret data in JSON format
+ */
+ private static class SecretData {
+ private String key;
+ private String format;
+ private String algorithm;
+
+ public SecretData() {
+ }
+
+ public SecretData(String key, String format, String algorithm) {
+ this.key = key;
+ this.format = format;
+ this.algorithm = algorithm;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ public void setFormat(String format) {
+ this.format = format;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+ }
+
+ /**
+ * Helper class for storing metadata in JSON format
+ */
+ private static class MetadataData {
+ private String metadata;
+ private String keyId;
+ private String algorithm;
+
+ public MetadataData() {
+ }
+
+ public MetadataData(String metadata, String keyId, String algorithm) {
+ this.metadata = metadata;
+ this.keyId = keyId;
+ this.algorithm = algorithm;
+ }
+
+ public String getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(String metadata) {
+ this.metadata = metadata;
+ }
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(String keyId) {
+ this.keyId = keyId;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+ }
+}
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/AwsSecretsManagerKeyLifecycleIT.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/AwsSecretsManagerKeyLifecycleIT.java
new file mode 100644
index 000000000000..5aab9f12ee04
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/AwsSecretsManagerKeyLifecycleIT.java
@@ -0,0 +1,274 @@
+/*
+ * 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.Security;
+import java.time.Duration;
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import
org.apache.camel.component.pqc.lifecycle.AwsSecretsManagerKeyLifecycleManager;
+import org.apache.camel.component.pqc.lifecycle.KeyLifecycleManager;
+import org.apache.camel.component.pqc.lifecycle.KeyMetadata;
+import org.apache.camel.test.infra.aws.common.services.AWSService;
+import org.apache.camel.test.infra.aws2.services.AWSServiceFactory;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
+import org.bouncycastle.pqc.jcajce.spec.DilithiumParameterSpec;
+import org.bouncycastle.pqc.jcajce.spec.FalconParameterSpec;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * End-to-end integration test for AwsSecretsManagerKeyLifecycleManager. Tests
key generation, storage, retrieval,
+ * rotation, and usage in Camel routes with a real AWS Secrets Manager
instance via testcontainers (LocalStack).
+ */
+public class AwsSecretsManagerKeyLifecycleIT extends CamelTestSupport {
+
+ @RegisterExtension
+ public static AWSService service =
AWSServiceFactory.createSecretsManagerService();
+
+ private AwsSecretsManagerKeyLifecycleManager keyManager;
+
+ @EndpointInject("mock:signed")
+ private MockEndpoint mockSigned;
+
+ @EndpointInject("mock:verified")
+ private MockEndpoint mockVerified;
+
+ @BeforeAll
+ public 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 CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+
+ // Create AwsSecretsManagerKeyLifecycleManager using AWS test
infrastructure
+ String accessKey =
service.getConnectionProperties().getProperty("aws.access.key");
+ String secretKey =
service.getConnectionProperties().getProperty("aws.secret.key");
+ String region =
service.getConnectionProperties().getProperty("aws.region");
+ String protocol =
service.getConnectionProperties().getProperty("aws.protocol");
+ String host =
service.getConnectionProperties().getProperty("aws.host");
+ String endpointOverride = protocol + "://" + host;
+
+ keyManager = new AwsSecretsManagerKeyLifecycleManager(
+ region,
+ accessKey,
+ secretKey,
+ "pqc/test-keys",
+ endpointOverride);
+
+ // Register the manager in the registry
+ context.getRegistry().bind("keyLifecycleManager", keyManager);
+
+ return context;
+ }
+
+ @Test
+ public void testGenerateAndStoreKeyInSecretsManager() throws Exception {
+ // Generate a Dilithium key
+ KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM",
"test-dilithium-key", DilithiumParameterSpec.dilithium2);
+
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPublic());
+ assertNotNull(keyPair.getPrivate());
+
+ // Verify metadata was created
+ KeyMetadata metadata = keyManager.getKeyMetadata("test-dilithium-key");
+ assertNotNull(metadata);
+ assertEquals("test-dilithium-key", metadata.getKeyId());
+ assertEquals("DILITHIUM", metadata.getAlgorithm());
+ assertEquals(KeyMetadata.KeyStatus.ACTIVE, metadata.getStatus());
+ }
+
+ @Test
+ public void testRetrieveKeyFromSecretsManager() throws Exception {
+ // Generate and store key
+ keyManager.generateKeyPair("FALCON", "test-falcon-key",
FalconParameterSpec.falcon_512);
+
+ // Clear cache to force AWS Secrets Manager read
+ // (In production this would simulate a different process/server
accessing the key)
+
+ // Retrieve key from AWS Secrets Manager
+ KeyPair retrieved = keyManager.getKey("test-falcon-key");
+ assertNotNull(retrieved);
+ assertNotNull(retrieved.getPublic());
+ assertNotNull(retrieved.getPrivate());
+
+ // Verify metadata
+ KeyMetadata metadata = keyManager.getKeyMetadata("test-falcon-key");
+ assertEquals("FALCON", metadata.getAlgorithm());
+ }
+
+ @Test
+ public void testKeyRotation() throws Exception {
+ // Generate initial key
+ keyManager.generateKeyPair("DILITHIUM", "rotation-key-old",
DilithiumParameterSpec.dilithium2);
+
+ KeyMetadata oldMetadata =
keyManager.getKeyMetadata("rotation-key-old");
+ assertEquals(KeyMetadata.KeyStatus.ACTIVE, oldMetadata.getStatus());
+
+ // Rotate the key
+ KeyPair newKeyPair = keyManager.rotateKey("rotation-key-old",
"rotation-key-new", "DILITHIUM");
+ assertNotNull(newKeyPair);
+
+ // Verify old key is deprecated
+ oldMetadata = keyManager.getKeyMetadata("rotation-key-old");
+ assertEquals(KeyMetadata.KeyStatus.DEPRECATED,
oldMetadata.getStatus());
+
+ // Verify new key is active
+ KeyMetadata newMetadata =
keyManager.getKeyMetadata("rotation-key-new");
+ assertEquals(KeyMetadata.KeyStatus.ACTIVE, newMetadata.getStatus());
+ }
+
+ @Test
+ public void testNeedsRotation() throws Exception {
+ keyManager.generateKeyPair("DILITHIUM", "rotation-check-key",
DilithiumParameterSpec.dilithium2);
+
+ // New key should not need rotation
+ assertFalse(keyManager.needsRotation("rotation-check-key",
Duration.ofDays(90), 10000));
+
+ // Simulate old key by setting next rotation time in the past
+ KeyMetadata metadata = keyManager.getKeyMetadata("rotation-check-key");
+ metadata.setNextRotationAt(java.time.Instant.now().minusSeconds(1));
+ keyManager.updateKeyMetadata("rotation-check-key", metadata);
+
+ // Now it should need rotation
+ assertTrue(keyManager.needsRotation("rotation-check-key",
Duration.ofDays(90), 10000));
+ }
+
+ @Test
+ public void testListKeys() throws Exception {
+ // Generate multiple keys
+ keyManager.generateKeyPair("DILITHIUM", "list-key-1",
DilithiumParameterSpec.dilithium2);
+ keyManager.generateKeyPair("FALCON", "list-key-2",
FalconParameterSpec.falcon_512);
+ keyManager.generateKeyPair("DILITHIUM", "list-key-3",
DilithiumParameterSpec.dilithium3);
+
+ // List all keys
+ List<KeyMetadata> keys = keyManager.listKeys();
+ assertTrue(keys.size() >= 3, "Should have at least 3 keys");
+
+ // Verify all our keys are present
+ assertTrue(keys.stream().anyMatch(k ->
k.getKeyId().equals("list-key-1")));
+ assertTrue(keys.stream().anyMatch(k ->
k.getKeyId().equals("list-key-2")));
+ assertTrue(keys.stream().anyMatch(k ->
k.getKeyId().equals("list-key-3")));
+ }
+
+ @Test
+ public void testExpireAndRevokeKey() throws Exception {
+ // Test expiration
+ keyManager.generateKeyPair("DILITHIUM", "expire-key",
DilithiumParameterSpec.dilithium2);
+ keyManager.expireKey("expire-key");
+
+ KeyMetadata expiredMetadata = keyManager.getKeyMetadata("expire-key");
+ assertEquals(KeyMetadata.KeyStatus.EXPIRED,
expiredMetadata.getStatus());
+
+ // Test revocation
+ keyManager.generateKeyPair("DILITHIUM", "revoke-key",
DilithiumParameterSpec.dilithium2);
+ keyManager.revokeKey("revoke-key", "Key compromised in test");
+
+ KeyMetadata revokedMetadata = keyManager.getKeyMetadata("revoke-key");
+ assertEquals(KeyMetadata.KeyStatus.REVOKED,
revokedMetadata.getStatus());
+ assertTrue(revokedMetadata.getDescription().contains("Revoked: Key
compromised in test"));
+ }
+
+ @Test
+ public void testDeleteKey() throws Exception {
+ keyManager.generateKeyPair("DILITHIUM", "delete-key",
DilithiumParameterSpec.dilithium2);
+ assertNotNull(keyManager.getKey("delete-key"));
+
+ keyManager.deleteKey("delete-key");
+
+ // Should throw exception when trying to get deleted key
+ assertThrows(Exception.class, () -> keyManager.getKey("delete-key"));
+ }
+
+ @Test
+ public void testExportAndImportKey() throws Exception {
+ KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM",
"export-key", DilithiumParameterSpec.dilithium2);
+
+ // Export public key as PEM
+ byte[] exported = keyManager.exportPublicKey(keyPair,
KeyLifecycleManager.KeyFormat.PEM);
+ assertNotNull(exported);
+ assertTrue(exported.length > 0);
+
+ String pemString = new String(exported);
+ assertTrue(pemString.contains("-----BEGIN PUBLIC KEY-----"));
+ assertTrue(pemString.contains("-----END PUBLIC KEY-----"));
+
+ // Import the key
+ KeyPair imported = keyManager.importKey(exported,
KeyLifecycleManager.KeyFormat.PEM, "DILITHIUM");
+ assertNotNull(imported);
+ assertNotNull(imported.getPublic());
+ }
+
+ @Test
+ public void testMetadataTracking() throws Exception {
+ // Generate key
+ keyManager.generateKeyPair("DILITHIUM", "tracking-key",
DilithiumParameterSpec.dilithium2);
+
+ // Get initial metadata
+ KeyMetadata metadata = keyManager.getKeyMetadata("tracking-key");
+ assertEquals(0, metadata.getUsageCount());
+ assertEquals(KeyMetadata.KeyStatus.ACTIVE, metadata.getStatus());
+
+ // Simulate usage by updating metadata
+ for (int i = 0; i < 5; i++) {
+ metadata.updateLastUsed();
+ }
+ keyManager.updateKeyMetadata("tracking-key", metadata);
+
+ // Verify usage was tracked
+ metadata = keyManager.getKeyMetadata("tracking-key");
+ assertEquals(5, metadata.getUsageCount());
+ assertNotNull(metadata.getLastUsedAt());
+
+ // Verify age calculation
+ long ageInDays = metadata.getAgeInDays();
+ assertEquals(0, ageInDays); // Should be 0 for a newly created key
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ // Signing route using PQC component with AWS Secrets
Manager-stored key
+ from("direct:sign")
+
.to("pqc:sign?operation=sign&signatureAlgorithm=DILITHIUM")
+ .to("mock:signed")
+
.to("pqc:verify?operation=verify&signatureAlgorithm=DILITHIUM")
+ .to("mock:verified");
+ }
+ };
+ }
+}