This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 5efa1952f61c CAMEL-22522 - Camel-PQC: Add Hashicorp-vault lifecycle
manager (#19501)
5efa1952f61c is described below
commit 5efa1952f61c3cf5d2768b7c91018cca5900edc7
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 9 20:28:16 2025 +0200
CAMEL-22522 - Camel-PQC: Add Hashicorp-vault lifecycle manager (#19501)
* CAMEL-22522 - Camel-PQC: Add Hashicorp-vault lifecycle manager
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-22522 - Camel-PQC: Add Hashicorp-vault lifecycle manager
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
---
components/camel-pqc/pom.xml | 15 +
.../camel-pqc/src/main/docs/pqc-component.adoc | 504 +++++++++++++++-
.../HashicorpVaultKeyLifecycleManager.java | 642 +++++++++++++++++++++
.../pqc/HashicorpVaultKeyLifecycleIT.java | 268 +++++++++
4 files changed, 1428 insertions(+), 1 deletion(-)
diff --git a/components/camel-pqc/pom.xml b/components/camel-pqc/pom.xml
index 6dd24cfad661..e4d0cd581e15 100644
--- a/components/camel-pqc/pom.xml
+++ b/components/camel-pqc/pom.xml
@@ -46,6 +46,14 @@
<version>${bouncycastle-version}</version>
</dependency>
+ <!-- Spring Vault for HashicorpVaultKeyLifecycleManager (optional) -->
+ <dependency>
+ <groupId>org.springframework.vault</groupId>
+ <artifactId>spring-vault-core</artifactId>
+ <version>${spring-vault-core-version}</version>
+ <optional>true</optional>
+ </dependency>
+
<!-- for testing -->
<dependency>
<groupId>org.apache.camel</groupId>
@@ -63,5 +71,12 @@
<version>${bouncycastle-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-hashicorp-vault</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 8cb124ecb9d8..4b8f1ea4f2f0 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -408,7 +408,7 @@ The `KeyLifecycleManager` interface provides the following
operations:
=== Available Implementations
-The component provides two implementations of `KeyLifecycleManager`:
+The component provides three implementations of `KeyLifecycleManager`:
==== FileBasedKeyLifecycleManager
@@ -476,6 +476,508 @@ KeyPair keyPair = keyManager.generateKeyPair("FALCON",
"test-key");
keyManager.clear();
--------------------------------------------------------------------------------
+==== HashicorpVaultKeyLifecycleManager
+
+An enterprise-grade implementation that integrates with HashiCorp Vault for
centralized secret management.
+
+**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 in
distinct Vault paths
+* **Fine-grained ACLs**: Enables different access policies for private keys
(restricted) vs public keys (read-only)
+
+**Security Note:** This implementation stores PQC keys in Vault's KV secrets
engine. While this approach uses industry-standard formats and enables
fine-grained access control, organizations with stringent security requirements
should consider:
+
+* Using **Hardware Security Modules (HSMs)** via PKCS#11 for keys that must
never be exportable
+* Implementing additional encryption layers using **Vault's Transit engine**
to encrypt key material before storage
+* Applying **strict Vault policies** limiting private key access to specific
services or key IDs only
+* Regular **key rotation** and **audit log monitoring**
+* For production use, review and customize the Vault ACL policies shown in the
setup section
+
+**Features:**
+
+* Centralized secret management via HashiCorp Vault
+* Industry-standard key formats (PKCS#8 for private keys, X.509 for public
keys)
+* Separate storage for private and public keys (enables different ACLs)
+* Automatic audit logging of all key operations
+* Fine-grained access control with Vault policies
+* Encryption at rest
+* High availability support (Vault HA clusters)
+* In-memory caching for performance
+* Uses Spring Vault (spring-vault-core) consistent with camel-hashicorp-vault
+* HashiCorp Cloud Platform (HCP) Vault support
+
+**Use Cases:**
+
+* Production environments with existing Vault infrastructure
+* Multi-node/distributed deployments
+* Enterprise security and compliance requirements
+* Centralized key management across multiple applications
+* Audit and compliance mandates
+
+**Dependencies:**
+
+To use HashicorpVaultKeyLifecycleManager, add the following optional
dependency:
+
+[source,xml]
+--------------------------------------------------------------------------------
+<dependency>
+ <groupId>org.springframework.vault</groupId>
+ <artifactId>spring-vault-core</artifactId>
+ <version>${spring-vault-core-version}</version>
+</dependency>
+--------------------------------------------------------------------------------
+
+**Example with VaultTemplate:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Option 1: Using existing VaultTemplate (recommended when using
camel-hashicorp-vault)
+@BindToRegistry("vaultTemplate")
+public VaultTemplate createVaultTemplate() {
+ VaultEndpoint vaultEndpoint = new VaultEndpoint();
+ vaultEndpoint.setHost("localhost");
+ vaultEndpoint.setPort(8200);
+ vaultEndpoint.setScheme("https");
+
+ return new VaultTemplate(vaultEndpoint, new
TokenAuthentication("s.token"));
+}
+
+@BindToRegistry("keyLifecycleManager")
+public HashicorpVaultKeyLifecycleManager createKeyManager() {
+ return new HashicorpVaultKeyLifecycleManager(
+ vaultTemplate, // Reuse existing VaultTemplate
+ "secret", // Secrets engine name
+ "pqc/keys" // Key prefix in Vault
+ );
+}
+
+// Generate a Dilithium key stored in Vault
+KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM", "app-signing-key",
+ DilithiumParameterSpec.dilithium2);
+
+// Key is stored in Vault at: secret/data/pqc/keys/app-signing-key
+--------------------------------------------------------------------------------
+
+**Example with Direct Configuration:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Option 2: Direct configuration with connection parameters
+HashicorpVaultKeyLifecycleManager keyManager =
+ new HashicorpVaultKeyLifecycleManager(
+ "vault.example.com", // host
+ 8200, // port
+ "https", // scheme
+ "s.your-token", // Vault token
+ "secret", // secrets engine (optional, defaults to
"secret")
+ "pqc/keys" // key prefix (optional, defaults to "pqc/keys")
+ );
+
+// Generate and store key in Vault
+KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM", "vault-key",
+ DilithiumParameterSpec.dilithium2);
+--------------------------------------------------------------------------------
+
+**Example with HashiCorp Cloud Platform (HCP) Vault:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Option 3: Configuration for HCP Vault with namespace
+HashicorpVaultKeyLifecycleManager keyManager =
+ new HashicorpVaultKeyLifecycleManager(
+ "your-cluster.vault.hashicorp.cloud", // HCP Vault host
+ 8200, // port
+ "https", // scheme
+ "s.your-hcp-token", // HCP Vault token
+ "secret", // secrets engine
+ "pqc/keys", // key prefix
+ true, // cloud=true for HCP Vault
+ "admin" // namespace (required for
HCP)
+ );
+
+// Generate and store key in HCP Vault
+KeyPair keyPair = keyManager.generateKeyPair("DILITHIUM", "hcp-key",
+ DilithiumParameterSpec.dilithium2);
+
+// Key is stored in HCP Vault at: admin/secret/data/pqc/keys/hcp-key
+--------------------------------------------------------------------------------
+
+**YAML Configuration:**
+
+[source,yaml]
+--------------------------------------------------------------------------------
+camel:
+ beans:
+ # Create VaultTemplate
+ vaultEndpoint:
+ type: org.springframework.vault.client.VaultEndpoint
+ properties:
+ host: "vault.example.com"
+ port: 8200
+ scheme: "https"
+
+ tokenAuthentication:
+ type: org.springframework.vault.authentication.TokenAuthentication
+ constructorArgs:
+ - "${VAULT_TOKEN}"
+
+ vaultTemplate:
+ type: org.springframework.vault.core.VaultTemplate
+ constructorArgs:
+ - "#bean:vaultEndpoint"
+ - "#bean:tokenAuthentication"
+
+ # Create HashicorpVaultKeyLifecycleManager
+ keyLifecycleManager:
+ type:
org.apache.camel.component.pqc.lifecycle.HashicorpVaultKeyLifecycleManager
+ constructorArgs:
+ - "#bean:vaultTemplate"
+ - "secret"
+ - "pqc/keys"
+ - false # cloud
+ - null # namespace
+--------------------------------------------------------------------------------
+
+**YAML Configuration for HCP Vault:**
+
+[source,yaml]
+--------------------------------------------------------------------------------
+camel:
+ beans:
+ # Create VaultTemplate for HCP
+ vaultEndpoint:
+ type: org.springframework.vault.client.VaultEndpoint
+ properties:
+ host: "your-cluster.vault.hashicorp.cloud"
+ port: 8200
+ scheme: "https"
+
+ tokenAuthentication:
+ type: org.springframework.vault.authentication.TokenAuthentication
+ constructorArgs:
+ - "${HCP_VAULT_TOKEN}"
+
+ vaultTemplate:
+ type: org.springframework.vault.core.VaultTemplate
+ constructorArgs:
+ - "#bean:vaultEndpoint"
+ - "#bean:tokenAuthentication"
+
+ # Create HashicorpVaultKeyLifecycleManager for HCP Vault
+ keyLifecycleManager:
+ type:
org.apache.camel.component.pqc.lifecycle.HashicorpVaultKeyLifecycleManager
+ constructorArgs:
+ - "#bean:vaultTemplate"
+ - "secret"
+ - "pqc/keys"
+ - true # cloud=true for HCP Vault
+ - "admin" # namespace (required for HCP)
+--------------------------------------------------------------------------------
+
+**Vault Storage Structure:**
+
+Keys are stored in Vault's KV v2 secrets engine with separate paths 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.
+
+*On-Premise Vault:*
+
+[source,text]
+--------------------------------------------------------------------------------
+secret/ # Secrets engine
+├── data/
+│ └── pqc/
+│ └── keys/
+│ ├── app-signing-key/
+│ │ ├── private # Private key path (STRICT ACL)
+│ │ │ ├── key # Base64-encoded PKCS#8 private key
+│ │ │ ├── format # "PKCS8"
+│ │ │ └── algorithm # "DILITHIUM"
+│ │ ├── public # Public key path (READ-ONLY ACL)
+│ │ │ ├── key # Base64-encoded X.509 public key
+│ │ │ ├── format # "X509"
+│ │ │ └── algorithm # "DILITHIUM"
+│ │ └── metadata # Metadata path
+│ │ ├── metadata # Serialized KeyMetadata
+│ │ ├── keyId # "app-signing-key"
+│ │ └── algorithm # "DILITHIUM"
+│ └── app-signing-key-v2/
+│ ├── private/
+│ ├── public/
+│ └── metadata/
+└── metadata/
+ └── pqc/
+ └── keys/
+ ├── app-signing-key/ # Vault metadata entry
+ └── app-signing-key-v2/
+--------------------------------------------------------------------------------
+
+*HCP Vault (with namespace):*
+
+[source,text]
+--------------------------------------------------------------------------------
+admin/ # Namespace
+└── secret/ # Secrets engine
+ ├── data/
+ │ └── pqc/
+ │ └── keys/
+ │ ├── app-signing-key/
+ │ │ ├── private/ # PKCS#8 private key (STRICT ACL)
+ │ │ ├── public/ # X.509 public key (READ-ONLY ACL)
+ │ │ └── metadata/ # Key metadata
+ │ └── app-signing-key-v2/
+ │ ├── private/
+ │ ├── public/
+ │ └── metadata/
+ └── metadata/
+ └── pqc/
+ └── keys/
+ ├── app-signing-key/
+ └── app-signing-key-v2/
+--------------------------------------------------------------------------------
+
+**Integration with camel-hashicorp-vault:**
+
+HashicorpVaultKeyLifecycleManager can share the same VaultTemplate with the
camel-hashicorp-vault component:
+
+[source,java]
+--------------------------------------------------------------------------------
+// Reuse VaultTemplate from camel-hashicorp-vault
+@BindToRegistry("keyLifecycleManager")
+public HashicorpVaultKeyLifecycleManager createKeyManager() {
+ VaultTemplate vaultTemplate = context.getRegistry()
+ .lookupByNameAndType("vaultTemplate", VaultTemplate.class);
+
+ return new HashicorpVaultKeyLifecycleManager(
+ vaultTemplate,
+ "secret",
+ "pqc/keys"
+ );
+}
+--------------------------------------------------------------------------------
+
+**Vault Setup and Security Policies:**
+
+To use HashicorpVaultKeyLifecycleManager, configure Vault with appropriate
policies. The implementation stores private keys, public keys, and metadata
separately to enable fine-grained access control.
+
+**Basic Policy (Full Access - Development/Testing):**
+
+[source,bash]
+--------------------------------------------------------------------------------
+# Enable KV v2 secrets engine (usually enabled by default)
+vault secrets enable -path=secret kv-v2
+
+# Create basic policy with full access to all key paths
+cat > pqc-policy-full.hcl <<EOF
+# Full access to private keys
+path "secret/data/pqc/keys/*/private" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+# Full access to public keys
+path "secret/data/pqc/keys/*/public" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+# Full access to metadata
+path "secret/data/pqc/keys/*/metadata" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+# List access to key paths
+path "secret/metadata/pqc/keys/*" {
+ capabilities = ["list", "read", "delete"]
+}
+EOF
+
+# Apply policy
+vault policy write pqc-keys-full pqc-policy-full.hcl
+
+# Create token with policy
+vault token create -policy=pqc-keys-full
+--------------------------------------------------------------------------------
+
+**Production Policies (Fine-Grained Access Control):**
+
+[source,bash]
+--------------------------------------------------------------------------------
+# POLICY 1: Admin Policy (Key Management Service)
+# Full access to generate, rotate, and manage keys
+cat > pqc-policy-admin.hcl <<EOF
+path "secret/data/pqc/keys/*/private" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+path "secret/data/pqc/keys/*/public" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+path "secret/data/pqc/keys/*/metadata" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+path "secret/metadata/pqc/keys/*" {
+ capabilities = ["list", "read", "delete"]
+}
+EOF
+
+vault policy write pqc-admin pqc-policy-admin.hcl
+
+# POLICY 2: Signing Service Policy (Read Private Keys for Signing)
+# Read-only access to specific private keys for signing operations
+cat > pqc-policy-signing.hcl <<EOF
+# Read-only access to private keys (for signing operations)
+path "secret/data/pqc/keys/*/private" {
+ capabilities = ["read"]
+}
+
+# Read access to public keys
+path "secret/data/pqc/keys/*/public" {
+ capabilities = ["read"]
+}
+
+# Read access to metadata
+path "secret/data/pqc/keys/*/metadata" {
+ capabilities = ["read"]
+}
+
+# List keys
+path "secret/metadata/pqc/keys/*" {
+ capabilities = ["list", "read"]
+}
+EOF
+
+vault policy write pqc-signing pqc-policy-signing.hcl
+
+# POLICY 3: Application Policy (Public Keys Only)
+# Read-only access to public keys for signature verification
+cat > pqc-policy-app.hcl <<EOF
+# NO access to private keys
+# Read-only access to public keys
+path "secret/data/pqc/keys/*/public" {
+ capabilities = ["read"]
+}
+
+# Read access to metadata
+path "secret/data/pqc/keys/*/metadata" {
+ capabilities = ["read"]
+}
+
+# List keys
+path "secret/metadata/pqc/keys/*" {
+ capabilities = ["list", "read"]
+}
+EOF
+
+vault policy write pqc-app pqc-policy-app.hcl
+
+# POLICY 4: Specific Key Access (Production Best Practice)
+# Limit access to specific key IDs only
+cat > pqc-policy-specific.hcl <<EOF
+# Access only to app-signing-key private key
+path "secret/data/pqc/keys/app-signing-key/private" {
+ capabilities = ["read"]
+}
+
+path "secret/data/pqc/keys/app-signing-key/public" {
+ capabilities = ["read"]
+}
+
+path "secret/data/pqc/keys/app-signing-key/metadata" {
+ capabilities = ["read"]
+}
+EOF
+
+vault policy write pqc-app-signing-key pqc-policy-specific.hcl
+
+# Create tokens with different policies
+vault token create -policy=pqc-admin # For key management service
+vault token create -policy=pqc-signing # For signing service
+vault token create -policy=pqc-app # For applications (verification
only)
+vault token create -policy=pqc-app-signing-key # For specific key access
+--------------------------------------------------------------------------------
+
+**Production Security Recommendations:**
+
+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
+
+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`
+
+3. **Monitor and Rotate**:
+ - Monitor Vault audit logs for private key access
+ - Implement automated key rotation policies
+ - Set up alerts for unusual access patterns
+
+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
+
+**Comparison of Implementations:**
+
+[options="header"]
+|===
+|Feature |FileBasedKeyLifecycleManager |InMemoryKeyLifecycleManager
|HashicorpVaultKeyLifecycleManager
+
+|Persistence
+|✅ File system
+|❌ Memory only
+|✅ Vault backend
+
+|Distributed
+|❌ Single node
+|❌ Single node
+|✅ Multi-node
+
+|Audit Logging
+|❌ Manual
+|❌ None
+|✅ Automatic
+
+|Access Control
+|❌ File permissions
+|❌ None
+|✅ Vault policies
+
+|Encryption at Rest
+|❌ OS-dependent
+|❌ N/A
+|✅ Always
+
+|High Availability
+|❌ No
+|❌ No
+|✅ Yes (Vault HA)
+
+|External Dependencies
+|❌ None
+|❌ None
+|✅ Vault + spring-vault
+
+|Caching
+|✅ Yes
+|✅ Yes
+|✅ Yes
+
+|Spring Integration
+|❌ No
+|❌ No
+|✅ Yes
+
+|Use Case
+|Single server
+|Testing/Dev
+|Production/Enterprise
+|===
+
=== Key Generation
The key lifecycle manager supports all PQC algorithms with sensible default
parameter specifications.
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
new file mode 100644
index 000000000000..b607becbfad3
--- /dev/null
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
@@ -0,0 +1,642 @@
+/*
+ * 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.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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms;
+import org.apache.camel.component.pqc.PQCSignatureAlgorithms;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultKeyValueOperations;
+import org.springframework.vault.core.VaultKeyValueOperationsSupport;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultResponse;
+
+/**
+ * HashiCorp Vault-based implementation of KeyLifecycleManager using Spring
Vault. Stores keys and metadata in Vault's
+ * KV secrets engine with centralized secret management, audit logging, and
fine-grained access control.
+ *
+ * Features: - Centralized secret management via HashiCorp Vault - Automatic
audit logging - Fine-grained access control
+ * with Vault policies - Encryption at rest - High availability support -
In-memory caching for performance
+ *
+ * Configuration: - host: Vault server host (e.g., localhost) - port: Vault
server port (default: 8200) - scheme:
+ * http/https (default: https) - token: Vault authentication token -
secretsEngine: KV secrets engine name (default:
+ * secret) - keyPrefix: Prefix for all key paths in Vault (default: pqc/keys)
+ *
+ * This implementation uses Spring Vault (spring-vault-core) consistent with
the camel-hashicorp-vault component.
+ */
+public class HashicorpVaultKeyLifecycleManager implements KeyLifecycleManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(HashicorpVaultKeyLifecycleManager.class);
+
+ private final VaultTemplate vaultTemplate;
+ private final String secretsEngine;
+ private final String keyPrefix;
+ private final boolean cloud;
+ private final String namespace;
+ private final ConcurrentHashMap<String, KeyPair> keyCache = new
ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, KeyMetadata> metadataCache = new
ConcurrentHashMap<>();
+
+ /**
+ * Create a HashicorpVaultKeyLifecycleManager with an existing
VaultTemplate
+ *
+ * @param vaultTemplate Configured VaultTemplate instance
+ * @param secretsEngine KV secrets engine name
+ * @param keyPrefix Prefix for key paths in Vault
+ */
+ public HashicorpVaultKeyLifecycleManager(VaultTemplate vaultTemplate,
String secretsEngine, String keyPrefix) {
+ this(vaultTemplate, secretsEngine, keyPrefix, false, null);
+ }
+
+ /**
+ * Create a HashicorpVaultKeyLifecycleManager with an existing
VaultTemplate including HCP Vault support
+ *
+ * @param vaultTemplate Configured VaultTemplate instance
+ * @param secretsEngine KV secrets engine name
+ * @param keyPrefix Prefix for key paths in Vault
+ * @param cloud Whether Vault is deployed on HashiCorp Cloud
Platform
+ * @param namespace Namespace for HCP Vault (required if cloud is true)
+ */
+ public HashicorpVaultKeyLifecycleManager(VaultTemplate vaultTemplate,
String secretsEngine, String keyPrefix,
+ boolean cloud, String namespace) {
+ this.vaultTemplate = vaultTemplate;
+ this.secretsEngine = secretsEngine != null ? secretsEngine : "secret";
+ this.keyPrefix = keyPrefix != null ? keyPrefix : "pqc/keys";
+ this.cloud = cloud;
+ this.namespace = namespace;
+
+ LOG.info(
+ "Initialized HashicorpVaultKeyLifecycleManager with
secretsEngine: {}, keyPrefix: {}, cloud: {}, namespace: {}",
+ this.secretsEngine, this.keyPrefix, this.cloud,
this.namespace);
+
+ try {
+ loadExistingKeys();
+ } catch (Exception e) {
+ LOG.warn("Failed to load existing keys from Vault", e);
+ }
+ }
+
+ /**
+ * Create a HashicorpVaultKeyLifecycleManager with default settings
+ *
+ * @param host Vault server host
+ * @param port Vault server port
+ * @param scheme Vault scheme (http/https)
+ * @param token Vault token for authentication
+ */
+ public HashicorpVaultKeyLifecycleManager(String host, int port, String
scheme, String token) {
+ this(host, port, scheme, token, "secret", "pqc/keys", false, null);
+ }
+
+ /**
+ * Create a HashicorpVaultKeyLifecycleManager with custom settings
+ *
+ * @param host Vault server host
+ * @param port Vault server port
+ * @param scheme Vault scheme (http/https)
+ * @param token Vault token for authentication
+ * @param secretsEngine KV secrets engine name
+ * @param keyPrefix Prefix for key paths in Vault
+ */
+ public HashicorpVaultKeyLifecycleManager(String host, int port, String
scheme, String token, String secretsEngine,
+ String keyPrefix) {
+ this(host, port, scheme, token, secretsEngine, keyPrefix, false, null);
+ }
+
+ /**
+ * Create a HashicorpVaultKeyLifecycleManager with full settings including
HCP Vault support
+ *
+ * @param host Vault server host
+ * @param port Vault server port
+ * @param scheme Vault scheme (http/https)
+ * @param token Vault token for authentication
+ * @param secretsEngine KV secrets engine name
+ * @param keyPrefix Prefix for key paths in Vault
+ * @param cloud Whether Vault is deployed on HashiCorp Cloud
Platform
+ * @param namespace Namespace for HCP Vault (required if cloud is true)
+ */
+ public HashicorpVaultKeyLifecycleManager(String host, int port, String
scheme, String token, String secretsEngine,
+ String keyPrefix, boolean cloud,
String namespace) {
+ this.secretsEngine = secretsEngine != null ? secretsEngine : "secret";
+ this.keyPrefix = keyPrefix != null ? keyPrefix : "pqc/keys";
+ this.cloud = cloud;
+ this.namespace = namespace;
+
+ // Create VaultEndpoint
+ VaultEndpoint vaultEndpoint = new VaultEndpoint();
+ vaultEndpoint.setHost(host);
+ vaultEndpoint.setPort(port);
+ vaultEndpoint.setScheme(scheme != null ? scheme : "https");
+
+ // Create VaultTemplate with TokenAuthentication
+ this.vaultTemplate = new VaultTemplate(vaultEndpoint, new
TokenAuthentication(token));
+
+ LOG.info(
+ "Initialized HashicorpVaultKeyLifecycleManager with Vault at:
{}://{}:{}, secretsEngine: {}, keyPrefix: {}, cloud: {}, namespace: {}",
+ scheme, host, port, this.secretsEngine, this.keyPrefix,
this.cloud, this.namespace);
+
+ try {
+ loadExistingKeys();
+ } catch (Exception e) {
+ LOG.warn("Failed to load existing keys from Vault", 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 Vault: {}", 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 Vault: {} -> {}", 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)
+ // This is more secure than Java serialization
+ 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);
+
+ VaultKeyValueOperations keyValue =
vaultTemplate.opsForKeyValue(secretsEngine,
+ VaultKeyValueOperationsSupport.KeyValueBackend.versioned());
+
+ // Store private key separately (strict ACL recommended in production)
+ Map<String, Object> privateKeyData = new HashMap<>();
+ privateKeyData.put("key", privateKeyBase64);
+ privateKeyData.put("format", "PKCS8");
+ privateKeyData.put("algorithm", metadata.getAlgorithm());
+ keyValue.put(getKeyPath(keyId) + "/private", privateKeyData);
+
+ // Store public key separately (can have read-only ACL)
+ Map<String, Object> publicKeyData = new HashMap<>();
+ publicKeyData.put("key", publicKeyBase64);
+ publicKeyData.put("format", "X509");
+ publicKeyData.put("algorithm", metadata.getAlgorithm());
+ keyValue.put(getKeyPath(keyId) + "/public", publicKeyData);
+
+ // Store metadata separately
+ Map<String, Object> metadataData = new HashMap<>();
+ metadataData.put("metadata", metadataBase64);
+ metadataData.put("keyId", keyId);
+ metadataData.put("algorithm", metadata.getAlgorithm());
+ keyValue.put(getKeyPath(keyId) + "/metadata", metadataData);
+
+ // Update caches
+ keyCache.put(keyId, keyPair);
+ metadataCache.put(keyId, metadata);
+
+ LOG.debug("Stored private key, public key, and metadata separately in
Vault 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 Vault
+ String privateKeyPath = buildDataPath(getKeyPath(keyId) + "/private");
+ VaultResponse privateResponse = vaultTemplate.read(privateKeyPath);
+
+ if (privateResponse == null || privateResponse.getData() == null) {
+ throw new IllegalArgumentException("Private key not found in
Vault: " + keyId);
+ }
+
+ // Read public key from Vault
+ String publicKeyPath = buildDataPath(getKeyPath(keyId) + "/public");
+ VaultResponse publicResponse = vaultTemplate.read(publicKeyPath);
+
+ if (publicResponse == null || publicResponse.getData() == null) {
+ throw new IllegalArgumentException("Public key not found in Vault:
" + keyId);
+ }
+
+ // Reconstruct KeyPair from PKCS#8 private key and X.509 public key
+ Map<String, Object> privateData = privateResponse.getData();
+ Map<String, Object> publicData = publicResponse.getData();
+
+ String privateKeyBase64 = (String) privateData.get("key");
+ String publicKeyBase64 = (String) publicData.get("key");
+ String algorithm = (String) privateData.get("algorithm");
+
+ byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
+ byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
+
+ // Use KeyFormatConverter to reconstruct keys from standard formats
+ PrivateKey privateKey =
KeyFormatConverter.importPrivateKey(privateKeyBytes,
+ KeyLifecycleManager.KeyFormat.DER,
getAlgorithmName(algorithm));
+ PublicKey publicKey =
KeyFormatConverter.importPublicKey(publicKeyBytes,
+ KeyLifecycleManager.KeyFormat.DER,
getAlgorithmName(algorithm));
+
+ 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 Vault
+ String metadataPath = buildDataPath(getKeyPath(keyId) + "/metadata");
+ VaultResponse response = vaultTemplate.read(metadataPath);
+
+ if (response == null || response.getData() == null) {
+ return null;
+ }
+
+ Map<String, Object> data = response.getData();
+ String metadataBase64 = (String) data.get("metadata");
+ KeyMetadata metadata = deserializeMetadata(metadataBase64);
+
+ // Cache it
+ metadataCache.put(keyId, metadata);
+ return metadata;
+ }
+
+ @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 {
+ VaultKeyValueOperations keyValue =
vaultTemplate.opsForKeyValue(secretsEngine,
+ VaultKeyValueOperationsSupport.KeyValueBackend.versioned());
+
+ // Delete private key, public key, and metadata separately
+ keyValue.delete(getKeyPath(keyId) + "/private");
+ keyValue.delete(getKeyPath(keyId) + "/public");
+ keyValue.delete(getKeyPath(keyId) + "/metadata");
+
+ keyCache.remove(keyId);
+ metadataCache.remove(keyId);
+
+ LOG.info("Deleted private key, public key, and metadata from Vault:
{}", keyId);
+ }
+
+ @Override
+ public List<KeyMetadata> listKeys() throws Exception {
+ // List all keys under the key prefix
+ String metadataPath = buildMetadataPath(keyPrefix);
+ List<String> keyIds = vaultTemplate.list(metadataPath);
+
+ List<KeyMetadata> metadataList = new ArrayList<>();
+ if (keyIds != null) {
+ for (String keyId : keyIds) {
+ try {
+ // Remove trailing slash if present
+ String cleanKeyId = keyId.endsWith("/") ?
keyId.substring(0, keyId.length() - 1) : keyId;
+ KeyMetadata metadata = getKeyMetadata(cleanKeyId);
+ if (metadata != null) {
+ metadataList.add(metadata);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to load metadata for key: {}", keyId, e);
+ }
+ }
+ }
+
+ 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 Vault: {}", 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 Vault: {} - {}", keyId, reason);
+ }
+ }
+
+ private void loadExistingKeys() throws Exception {
+ String metadataPath = buildMetadataPath(keyPrefix);
+ List<String> keyIds = vaultTemplate.list(metadataPath);
+
+ if (keyIds != null) {
+ LOG.info("Found {} existing keys in Vault", keyIds.size());
+
+ for (String keyId : keyIds) {
+ try {
+ // Remove trailing slash if present
+ String cleanKeyId = keyId.endsWith("/") ?
keyId.substring(0, keyId.length() - 1) : keyId;
+ KeyMetadata metadata = getKeyMetadata(cleanKeyId);
+ if (metadata != null) {
+ LOG.debug("Loaded existing key from Vault: {}",
metadata);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to load key from Vault: {}", keyId, e);
+ }
+ }
+ }
+ }
+
+ private String getKeyPath(String keyId) {
+ return keyPrefix + "/" + keyId;
+ }
+
+ /**
+ * Build the data path for reading/writing secrets, following HCP Vault
pattern from camel-hashicorp-vault
+ */
+ private String buildDataPath(String secretPath) {
+ if (!cloud) {
+ return secretsEngine + "/data/" + secretPath;
+ } else {
+ if (namespace != null && !namespace.isEmpty()) {
+ return namespace + "/" + secretsEngine + "/data/" + secretPath;
+ } else {
+ return secretsEngine + "/data/" + secretPath;
+ }
+ }
+ }
+
+ /**
+ * Build the metadata path for listing secrets, following HCP Vault
pattern from camel-hashicorp-vault
+ */
+ private String buildMetadataPath(String secretPath) {
+ if (!cloud) {
+ return secretsEngine + "/metadata/" + secretPath;
+ } else {
+ if (namespace != null && !namespace.isEmpty()) {
+ return namespace + "/" + secretsEngine + "/metadata/" +
secretPath;
+ } else {
+ return secretsEngine + "/metadata/" + secretPath;
+ }
+ }
+ }
+
+ 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 VaultTemplate for advanced operations
+ */
+ public VaultTemplate getVaultTemplate() {
+ return vaultTemplate;
+ }
+}
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/HashicorpVaultKeyLifecycleIT.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/HashicorpVaultKeyLifecycleIT.java
new file mode 100644
index 000000000000..58592fceee56
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/HashicorpVaultKeyLifecycleIT.java
@@ -0,0 +1,268 @@
+/*
+ * 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.HashicorpVaultKeyLifecycleManager;
+import org.apache.camel.component.pqc.lifecycle.KeyLifecycleManager;
+import org.apache.camel.component.pqc.lifecycle.KeyMetadata;
+import
org.apache.camel.test.infra.hashicorp.vault.services.HashicorpServiceFactory;
+import
org.apache.camel.test.infra.hashicorp.vault.services.HashicorpVaultService;
+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 HashicorpVaultKeyLifecycleManager. Tests
key generation, storage, retrieval,
+ * rotation, and usage in Camel routes with a real Vault instance via
testcontainers.
+ */
+public class HashicorpVaultKeyLifecycleIT extends CamelTestSupport {
+
+ @RegisterExtension
+ public static HashicorpVaultService service =
HashicorpServiceFactory.createService();
+
+ private HashicorpVaultKeyLifecycleManager 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 HashicorpVaultKeyLifecycleManager using Vault test
infrastructure
+ keyManager = new HashicorpVaultKeyLifecycleManager(
+ service.host(),
+ service.port(),
+ "http", // Test container uses http
+ service.token(),
+ "secret",
+ "pqc/test-keys");
+
+ // Register the manager in the registry
+ context.getRegistry().bind("keyLifecycleManager", keyManager);
+
+ return context;
+ }
+
+ @Test
+ public void testGenerateAndStoreKeyInVault() 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 testRetrieveKeyFromVault() throws Exception {
+ // Generate and store key
+ keyManager.generateKeyPair("FALCON", "test-falcon-key",
FalconParameterSpec.falcon_512);
+
+ // Clear cache to force Vault read
+ // (In production this would simulate a different process/server
accessing the key)
+
+ // Retrieve key from Vault
+ 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(IllegalArgumentException.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 Vault-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");
+ }
+ };
+ }
+}