This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-22522 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 587a554e9d80e6761027bf6b71407020c661e20a Author: Andrea Cosentino <[email protected]> AuthorDate: Thu Oct 9 18:18:00 2025 +0200 CAMEL-22522 - Camel-PQC: Add Hashicorp-vault lifecycle manager Signed-off-by: Andrea Cosentino <[email protected]> --- components/camel-pqc/pom.xml | 15 + .../camel-pqc/src/main/docs/pqc-component.adoc | 331 ++++++++++- .../HashicorpVaultKeyLifecycleManager.java | 642 +++++++++++++++++++++ .../pqc/HashicorpVaultKeyLifecycleIT.java | 268 +++++++++ 4 files changed, 1255 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..ae64dcce316d 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,335 @@ KeyPair keyPair = keyManager.generateKeyPair("FALCON", "test-key"); keyManager.clear(); -------------------------------------------------------------------------------- +==== HashicorpVaultKeyLifecycleManager + +An enterprise-grade implementation that integrates with HashiCorp Vault for centralized secret management. + +**Features:** + +* Centralized secret management via HashiCorp Vault +* 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 + +**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 the following structure: + +*On-Premise Vault:* + +[source,text] +-------------------------------------------------------------------------------- +secret/ # Secrets engine +├── data/ +│ └── pqc/ +│ └── keys/ +│ ├── app-signing-key +│ │ ├── keyPair # Serialized KeyPair (Base64) +│ │ ├── metadata # Serialized KeyMetadata (Base64) +│ │ ├── keyId # Key identifier +│ │ └── algorithm # Algorithm name +│ └── app-signing-key-v2 +└── metadata/ + └── pqc/ + └── keys/ + ├── app-signing-key/ # Metadata entry + └── app-signing-key-v2/ +-------------------------------------------------------------------------------- + +*HCP Vault (with namespace):* + +[source,text] +-------------------------------------------------------------------------------- +admin/ # Namespace +└── secret/ # Secrets engine + ├── data/ + │ └── pqc/ + │ └── keys/ + │ ├── app-signing-key + │ └── app-signing-key-v2 + └── 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:** + +To use HashicorpVaultKeyLifecycleManager, configure Vault with appropriate policies: + +[source,bash] +-------------------------------------------------------------------------------- +# Enable KV v2 secrets engine (usually enabled by default) +vault secrets enable -path=secret kv-v2 + +# Create policy for PQC key management +cat > pqc-policy.hcl <<EOF +path "secret/data/pqc/keys/*" { + capabilities = ["create", "read", "update", "delete", "list"] +} + +path "secret/metadata/pqc/keys/*" { + capabilities = ["list", "read", "delete"] +} +EOF + +# Apply policy +vault policy write pqc-keys pqc-policy.hcl + +# Create token with policy +vault token create -policy=pqc-keys +-------------------------------------------------------------------------------- + +**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"); + } + }; + } +}
