This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-22528 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9a8e1062960219539d9e5aa028f1dfc2cd4a3aab 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 47a6a4f0602e..ced4cf138cdf 100644 --- a/components/camel-pqc/pom.xml +++ b/components/camel-pqc/pom.xml @@ -55,6 +55,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> @@ -79,5 +87,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"); + } + }; + } +}
