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-jbang-examples.git
The following commit(s) were added to refs/heads/main by this push:
new 02fe96c Added Java example for PQC Signing with Hashicorp Vault Key
Lifecycle Management (#41)
02fe96c is described below
commit 02fe96c01ac1227159b954efeaa9ba3521e6709f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Oct 13 14:11:23 2025 +0200
Added Java example for PQC Signing with Hashicorp Vault Key Lifecycle
Management (#41)
* Added Java example for PQC Signing with Hashicorp Vault Key Lifecycle
Management
Signed-off-by: Andrea Cosentino <[email protected]>
* Added Java example for PQC Signing with Hashicorp Vault Key Lifecycle
Management
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
---
pqc-document-signing/PQCDocumentSigningRoutes.java | 316 +++++++++++++++++++
pqc-document-signing/README.adoc | 349 +++++++++++++++++++++
pqc-document-signing/application.properties | 40 +++
3 files changed, 705 insertions(+)
diff --git a/pqc-document-signing/PQCDocumentSigningRoutes.java
b/pqc-document-signing/PQCDocumentSigningRoutes.java
new file mode 100644
index 0000000..94f0f70
--- /dev/null
+++ b/pqc-document-signing/PQCDocumentSigningRoutes.java
@@ -0,0 +1,316 @@
+/*
+ * 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.example.pqc;
+
+import java.security.KeyPair;
+import java.security.Security;
+import java.util.Base64;
+
+import org.apache.camel.builder.RouteBuilder;
+import
org.apache.camel.component.pqc.lifecycle.HashicorpVaultKeyLifecycleManager;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultTemplate;
+
+/**
+ * RouteBuilder for PQC Document Signing with HashiCorp Vault integration.
+ *
+ * This class defines 12 routes:
+ * 1. Initialize PQC signing key on startup
+ * 2. REST API - Sign a document
+ * 3. REST API - Verify a document signature
+ * 4. REST API - Get key metadata
+ * 5. REST API - List all keys
+ * 6. REST API - Rotate signing key
+ * 7. Scheduled job - Check key rotation needs
+ * 8. Helper - Update key usage metadata
+ * 9. Helper - Get key metadata
+ * 10. Helper - Check key expiration and auto-rotate
+ * 11. Helper - Automatic key rotation on expiration
+ */
+public class PQCDocumentSigningRoutes extends RouteBuilder {
+
+ @Override
+ public void configure() throws Exception {
+
+ // Register BouncyCastle providers for PQC algorithms
+ if (Security.getProvider("BC") == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ if (Security.getProvider("BCPQC") == null) {
+ Security.addProvider(new BouncyCastlePQCProvider());
+ }
+
+ // Register beans for HashiCorp Vault integration
+ registerVaultBeans();
+
+ // Route 1: Initialize PQC signing key on startup
+ from("timer:init?repeatCount=1")
+ .routeId("initialize-signing-key")
+ .log("Initializing PQC signing key in Vault...")
+ .bean("keyLifecycleManager", "generateKeyPair('DILITHIUM',
'document-signing-key')")
+ .log("PQC signing key initialized successfully")
+ .process(exchange -> {
+ // Register the KeyPair bean directly for autowiring
+ HashicorpVaultKeyLifecycleManager keyManager =
exchange.getContext().getRegistry()
+ .lookupByNameAndType("keyLifecycleManager",
HashicorpVaultKeyLifecycleManager.class);
+ KeyPair keyPair = keyManager.getKey("document-signing-key");
+
+ // Register as KeyPair type so PQC component can autowire it
+ bindToRegistry("signingKey", keyPair);
+ })
+ .to("direct:get-key-metadata");
+
+ // Route 2: REST API - Sign a document
+ from("platform-http:/api/sign")
+ .routeId("sign-document-api")
+ .log("Received document signing request: ${body}")
+ .setHeader("originalBody", simple("${body}"))
+ // Sign the document using PQC
+
.toD("pqc:sign?operation=sign&signatureAlgorithm=DILITHIUM&keyPair=#signingKey")
+ // Convert binary signature to base64
+ .process(exchange -> {
+ byte[] signature =
exchange.getMessage().getHeader("CamelPQCSignature", byte[].class);
+ if (signature != null) {
+ String base64Signature =
Base64.getEncoder().encodeToString(signature);
+ exchange.getMessage().setHeader("CamelPQCSignature",
base64Signature);
+ }
+ })
+ .log("Document signed with quantum-resistant signature")
+ // Update key usage metadata
+ .to("direct:update-key-usage")
+ // Check if key needs rotation or has expired
+ .to("direct:check-key-expiration")
+ // Prepare response with signature and key metadata
+ .setBody(simple("{\n" +
+ " \"status\": \"signed\",\n" +
+ " \"document\": \"${header.originalBody}\",\n" +
+ " \"signature\": \"${header.CamelPQCSignature}\",\n" +
+ " \"signatureAlgorithm\": \"DILITHIUM\",\n" +
+ " \"keyId\": \"document-signing-key\",\n" +
+ " \"keyMetadata\": ${body}\n" +
+ "}"))
+ .setHeader("Content-Type", constant("application/json"))
+ .log("Response: ${body}");
+
+ // Route 3: REST API - Verify a document signature
+ from("platform-http:/api/verify")
+ .routeId("verify-document-api")
+ .log("Received document verification request: ${body}")
+ // Get signature from X-Signature header (workaround for header
filtering)
+ .process(exchange -> {
+ String base64Signature =
exchange.getIn().getHeader("X-Signature", String.class);
+ if (base64Signature == null || base64Signature.isEmpty()) {
+ throw new IllegalArgumentException("X-Signature header is
missing or empty");
+ }
+ byte[] signature = Base64.getDecoder().decode(base64Signature);
+ exchange.getIn().setHeader("CamelPQCSignature", signature);
+ exchange.getIn().setHeader("signatureLength",
signature.length);
+ })
+ .log("Verifying signature of length: ${header.signatureLength}
bytes")
+ // Verify the document signature using PQC
+ .doTry()
+
.toD("pqc:verify?operation=verify&signatureAlgorithm=DILITHIUM&keyPair=#signingKey")
+ .log("Verification completed. Result:
${header.CamelPQCVerify}")
+ .doCatch(Exception.class)
+ .log("ERROR during verification: ${exception.message}")
+ .setHeader("CamelPQCVerify", constant(false))
+ .end()
+ .choice()
+ .when(simple("${header.CamelPQCVerify} == true"))
+ .setBody(simple("{\n" +
+ " \"status\": \"verified\",\n" +
+ " \"valid\": true,\n" +
+ " \"message\": \"Document signature is valid\",\n" +
+ " \"signatureAlgorithm\": \"DILITHIUM\"\n" +
+ "}"))
+ .when(simple("${header.CamelPQCVerify} == false"))
+ .setBody(simple("{\n" +
+ " \"status\": \"verified\",\n" +
+ " \"valid\": false,\n" +
+ " \"message\": \"Document signature is invalid\",\n" +
+ " \"signatureAlgorithm\": \"DILITHIUM\"\n" +
+ "}"))
+ .end()
+ .setHeader("Content-Type", constant("application/json"))
+ .log("Verification result: ${body}");
+
+ // Route 4: REST API - Get key metadata
+ from("platform-http:/api/key/metadata")
+ .routeId("get-key-metadata-api")
+ .log("Fetching key metadata...")
+ .to("direct:get-key-metadata");
+
+ // Route 5: REST API - List all keys
+ from("platform-http:/api/keys")
+ .routeId("list-keys-api")
+ .log("Listing all PQC keys...")
+ .bean("keyLifecycleManager", "listKeys")
+ .setBody(simple("{\n" +
+ " \"keys\": ${body}\n" +
+ "}"))
+ .setHeader("Content-Type", constant("application/json"))
+ .log("Keys listed: ${body}");
+
+ // Route 6: REST API - Rotate signing key
+ from("platform-http:/api/key/rotate")
+ .routeId("rotate-key-api")
+ .log("Rotating signing key...")
+ .bean("keyLifecycleManager", "rotateKey('document-signing-key',
'document-signing-key-v2', 'DILITHIUM')")
+ .log("Key rotated successfully: old key deprecated, new key
active")
+ .setBody(simple("{\n" +
+ " \"status\": \"rotated\",\n" +
+ " \"oldKey\": \"document-signing-key\",\n" +
+ " \"newKey\": \"document-signing-key-v2\",\n" +
+ " \"message\": \"Key rotation completed successfully\"\n" +
+ "}"))
+ .setHeader("Content-Type", constant("application/json"));
+
+ // Route 7: Scheduled job - Check key rotation needs
+ from("timer:checkRotation?period={{key.rotation.check.period}}")
+ .routeId("check-rotation-schedule")
+ .log("Checking if key needs rotation...")
+ .bean("keyLifecycleManager",
"needsRotation('document-signing-key', 'P{{key.max.age.days}}D',
{{key.max.usage.count}})")
+ .choice()
+ .when(simple("${body} == true"))
+ .log("WARNING: Key 'document-signing-key' needs rotation!")
+ .to("direct:get-key-metadata")
+ .log("Current key metadata: ${body}")
+ .otherwise()
+ .log("Key rotation not needed yet")
+ .end();
+
+ // Route 8: Helper - Update key usage metadata
+ from("direct:update-key-usage")
+ .routeId("update-key-usage")
+ .bean("keyLifecycleManager",
"getKeyMetadata('document-signing-key')")
+ .setProperty("metadata", simple("${body}"))
+ .process(exchange -> {
+ // Update last used timestamp and increment usage count
+ Object metadata = exchange.getProperty("metadata");
+ if (metadata != null) {
+ // Call updateLastUsed on the metadata object
+
metadata.getClass().getMethod("updateLastUsed").invoke(metadata);
+ exchange.getMessage().setBody(metadata);
+ }
+ })
+ .bean("keyLifecycleManager",
"updateKeyMetadata('document-signing-key', ${body})")
+ .log("Key usage updated. Usage count: ${body.usageCount}");
+
+ // Route 9: Helper - Get key metadata
+ from("direct:get-key-metadata")
+ .routeId("get-key-metadata-helper")
+ .bean("keyLifecycleManager",
"getKeyMetadata('document-signing-key')")
+ .setBody(simple("{\n" +
+ " \"keyId\": \"${body.keyId}\",\n" +
+ " \"algorithm\": \"${body.algorithm}\",\n" +
+ " \"status\": \"${body.status}\",\n" +
+ " \"createdAt\": \"${body.createdAt}\",\n" +
+ " \"lastUsedAt\": \"${body.lastUsedAt}\",\n" +
+ " \"usageCount\": ${body.usageCount},\n" +
+ " \"ageInDays\": ${body.ageInDays},\n" +
+ " \"expiresAt\": \"${body.expiresAt}\",\n" +
+ " \"nextRotationAt\": \"${body.nextRotationAt}\",\n" +
+ " \"expired\": ${body.expired},\n" +
+ " \"needsRotation\": ${body.needsRotation}\n" +
+ "}"))
+ .setHeader("Content-Type", constant("application/json"))
+ .log("Key metadata: ${body}");
+
+ // Route 10: Helper - Check key expiration and auto-rotate
+ from("direct:check-key-expiration")
+ .routeId("check-key-expiration")
+ .bean("keyLifecycleManager",
"getKeyMetadata('document-signing-key')")
+ .choice()
+ .when(simple("${body.usageCount} >= {{key.max.usage.count}}"))
+ .log("WARNING: Key has reached maximum usage count
(${body.usageCount} >= {{key.max.usage.count}})")
+ .setProperty("expirationReason", simple("usage count
(${body.usageCount} uses)"))
+ .to("direct:auto-rotate-key")
+ .when(simple("${body.ageInDays} >= {{key.max.age.days}}"))
+ .log("WARNING: Key has reached maximum age
(${body.ageInDays} >= {{key.max.age.days}} days)")
+ .setProperty("expirationReason", simple("age
(${body.ageInDays} days)"))
+ .to("direct:auto-rotate-key")
+ .otherwise()
+ .setBody(simple("Key is valid (usage:
${body.usageCount}/{{key.max.usage.count}}, age:
${body.ageInDays}/{{key.max.age.days}} days)"))
+ .end();
+
+ // Route 11: Helper - Automatic key rotation on expiration
+ from("direct:auto-rotate-key")
+ .routeId("auto-rotate-key")
+ .log(">>> AUTOMATIC KEY ROTATION TRIGGERED <<<")
+ .log("Reason: Key expired due to
${exchangeProperty.expirationReason}")
+ // Generate new key ID with timestamp
+ .setProperty("timestamp", simple("${date:now:yyyyMMdd-HHmmss}"))
+ .setProperty("newKeyId", simple("document-signing-key"))
+ .log("Rotating key from 'document-signing-key' to
'${exchangeProperty.newKeyId}'")
+ // Perform rotation
+ .bean("keyLifecycleManager", "rotateKey('document-signing-key',
'${exchangeProperty.newKeyId}', 'DILITHIUM')")
+ .log(">>> KEY ROTATION COMPLETED <<<")
+ .log("Old key 'document-signing-key' status: DEPRECATED")
+ .log("New key '${exchangeProperty.newKeyId}' status: ACTIVE")
+ // Update the active key reference for future operations
+ .setProperty("activeKeyId", simple("${exchangeProperty.newKeyId}"))
+ // Get metadata of new key
+ .bean("keyLifecycleManager",
"getKeyMetadata('${exchangeProperty.newKeyId}')")
+ .setBody(simple("{\n" +
+ " \"rotationTriggered\": true,\n" +
+ " \"reason\": \"${exchangeProperty.expirationReason}\",\n" +
+ " \"oldKey\": \"document-signing-key\",\n" +
+ " \"oldKeyStatus\": \"DEPRECATED\",\n" +
+ " \"newKey\": \"${exchangeProperty.newKeyId}\",\n" +
+ " \"newKeyStatus\": \"ACTIVE\",\n" +
+ " \"newKeyCreatedAt\": \"${body.createdAt}\",\n" +
+ " \"message\": \"Key automatically rotated due to
${exchangeProperty.expirationReason}\"\n" +
+ "}"))
+ .log("Rotation details: ${body}");
+ }
+
+ /**
+ * Register beans for HashiCorp Vault integration
+ */
+ private void registerVaultBeans() throws Exception {
+ // Get configuration properties
+ String vaultHost =
getContext().resolvePropertyPlaceholders("{{vault.host}}");
+ int vaultPort =
Integer.parseInt(getContext().resolvePropertyPlaceholders("{{vault.port}}"));
+ String vaultScheme =
getContext().resolvePropertyPlaceholders("{{vault.scheme}}");
+ String vaultToken =
getContext().resolvePropertyPlaceholders("{{vault.token}}");
+ String secretsEngine =
getContext().resolvePropertyPlaceholders("{{vault.secrets.engine}}");
+ String keyPrefix =
getContext().resolvePropertyPlaceholders("{{vault.keys.prefix}}");
+
+ // Create VaultEndpoint
+ VaultEndpoint vaultEndpoint = new VaultEndpoint();
+ vaultEndpoint.setHost(vaultHost);
+ vaultEndpoint.setPort(vaultPort);
+ vaultEndpoint.setScheme(vaultScheme);
+ bindToRegistry("vaultEndpoint", vaultEndpoint);
+
+ // Create TokenAuthentication
+ TokenAuthentication tokenAuthentication = new
TokenAuthentication(vaultToken);
+ bindToRegistry("tokenAuthentication", tokenAuthentication);
+
+ // Create VaultTemplate
+ VaultTemplate vaultTemplate = new VaultTemplate(vaultEndpoint,
tokenAuthentication);
+ bindToRegistry("vaultTemplate", vaultTemplate);
+
+ // Create HashicorpVaultKeyLifecycleManager using constructor with all
parameters
+ HashicorpVaultKeyLifecycleManager keyLifecycleManager =
+ new HashicorpVaultKeyLifecycleManager(vaultHost, vaultPort,
vaultScheme, vaultToken, secretsEngine, keyPrefix);
+ bindToRegistry("keyLifecycleManager", keyLifecycleManager);
+ }
+}
diff --git a/pqc-document-signing/README.adoc b/pqc-document-signing/README.adoc
new file mode 100644
index 0000000..09b313e
--- /dev/null
+++ b/pqc-document-signing/README.adoc
@@ -0,0 +1,349 @@
+= Post-Quantum Cryptography Document Signing with HashiCorp Vault
+
+This example demonstrates how to build a secure document signing service using
Apache Camel with Post-Quantum Cryptography (PQC) and HashiCorp Vault for key
lifecycle management.
+
+== Features
+
+* **Quantum-Resistant Signatures** - Signs documents using DILITHIUM, a
NIST-standardized Post-Quantum Cryptographic algorithm
+* **HashiCorp Vault Integration** - Stores and manages PQC keys in HashiCorp
Vault for enterprise-grade security
+* **Key Lifecycle Management** - Automatic tracking of key usage, age,
rotation, and expiration
+* **Key Usage Tracking** - Demonstrates key expiration after a predefined
number of usages (default: 10 signatures)
+* **Automatic Key Rotation** - When a key expires, a new key is
**automatically generated and rotated**
+* **Key Metadata Tracking** - Real-time visibility into key status, usage
count, age, and lifecycle events
+* **REST API** - HTTP endpoints for signing, verifying documents, and managing
keys
+* **Scheduled Monitoring** - Periodic checks for key rotation needs based on
age and usage thresholds
+* **Multi-Key Support** - Deprecated keys are retained for signature
verification of old documents
+
+== Prerequisites
+
+* JBang installed (https://www.jbang.dev)
+* Docker installed for running HashiCorp Vault
+
+== Running HashiCorp Vault
+
+Run HashiCorp Vault using Docker:
+
+[source,sh]
+----
+$ docker run -d \
+ --name vault \
+ -p 8201:8200 \
+ -e VAULT_DEV_ROOT_TOKEN_ID=myroot \
+ -e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \
+ hashicorp/vault:latest
+----
+
+This will start HashiCorp Vault configured with:
+
+* Root token: `myroot`
+* Port: `8201` (mapped from container port 8200)
+* Secrets engine: `secret` (KV v2)
+
+Wait a few seconds for Vault to fully start before proceeding.
+
+== Project Structure
+
+The example consists of the following files:
+
+[source,text]
+----
+pqc-document-signing/
+├── PQCDocumentSigningRoutes.java # Main RouteBuilder with 12 routes
+├── application.properties # Configuration file
+└── README.adoc # This file
+----
+
+== Running the Example
+
+After Vault is configured and running, start the Camel application:
+
+[source,sh]
+----
+$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel run
PQCDocumentSigningRoutes.java
+----
+
+Or using Maven:
+
+[source,sh]
+----
+$ mvn compile exec:java
+----
+
+The application will:
+
+1. Connect to HashiCorp Vault
+2. Generate a DILITHIUM Post-Quantum Cryptographic key pair
+3. Store the key securely in Vault at path:
`secret/data/pqc/keys/document-signing-key`
+4. Start REST API endpoints on port 8080
+5. Begin monitoring key lifecycle (usage count, age, rotation needs)
+
+== API Endpoints
+
+The application exposes the following REST endpoints:
+
+=== 1. Sign a Document
+
+Signs a document with a quantum-resistant signature.
+
+[source,sh]
+----
+$ curl -X POST http://localhost:8080/api/sign \
+ -H "Content-Type: text/plain" \
+ -d "This is a confidential document that needs quantum-resistant protection"
+----
+
+**Response:**
+[source,json]
+----
+{
+ "status": "signed",
+ "document": "This is a confidential document...",
+ "signature": "UMyKJt21oIYWNq0jDe6v63Xw8bs7JZ9hg9KEYXdFPIMxXS3AHwOa...",
+ "signatureAlgorithm": "DILITHIUM",
+ "keyId": "document-signing-key",
+ "keyMetadata": {
+ "keyId": "document-signing-key",
+ "algorithm": "DILITHIUM",
+ "status": "ACTIVE",
+ "usageCount": 1,
+ "ageInDays": 0
+ }
+}
+----
+
+Note: The `signature` field contains a base64-encoded quantum-resistant
signature.
+
+=== 2. Verify a Document Signature
+
+Verifies the authenticity of a signed document using the signature from the
sign response.
+
+[source,sh]
+----
+$ curl -X POST http://localhost:8080/api/verify \
+ -H "Content-Type: text/plain" \
+ -H "X-Signature: UMyKJt21oIYWNq0jDe6v63Xw8bs7JZ9hg9KEYXdFPIMxXS3AHwOa..." \
+ -d "This is a confidential document that needs quantum-resistant protection"
+----
+
+Note: Pass the base64-encoded signature from the sign response in the
`X-Signature` header. The `X-Signature` header is used instead of
`CamelPQCSignature` to avoid header filtering by REST frameworks.
+
+**Response:**
+[source,json]
+----
+{
+ "status": "verified",
+ "valid": true,
+ "message": "Document signature is valid",
+ "signatureAlgorithm": "DILITHIUM"
+}
+----
+
+=== 3. Get Key Metadata
+
+Retrieves detailed metadata about the signing key, including usage count and
lifecycle status.
+
+[source,sh]
+----
+$ curl http://localhost:8080/api/key/metadata
+----
+
+**Response:**
+[source,json]
+----
+{
+ "keyId": "document-signing-key",
+ "algorithm": "DILITHIUM",
+ "status": "ACTIVE",
+ "createdAt": "2024-10-13T10:30:00Z",
+ "lastUsedAt": "2024-10-13T10:35:00Z",
+ "usageCount": 5,
+ "ageInDays": 0,
+ "expiresAt": null,
+ "nextRotationAt": null,
+ "expired": false,
+ "needsRotation": false
+}
+----
+
+=== 4. List All Keys
+
+Lists all PQC keys stored in Vault.
+
+[source,sh]
+----
+$ curl http://localhost:8080/api/keys
+----
+
+**Response:**
+[source,json]
+----
+{
+ "keys": [
+ {
+ "keyId": "document-signing-key",
+ "algorithm": "DILITHIUM",
+ "status": "ACTIVE",
+ "usageCount": 5
+ }
+ ]
+}
+----
+
+=== 5. Rotate Signing Key
+
+Manually rotates the signing key (creates a new key and deprecates the old
one).
+
+[source,sh]
+----
+$ curl -X POST http://localhost:8080/api/key/rotate
+----
+
+**Response:**
+[source,json]
+----
+{
+ "status": "rotated",
+ "oldKey": "document-signing-key",
+ "newKey": "document-signing-key",
+ "message": "Key rotation completed successfully"
+}
+----
+
+== Automatic Key Rotation
+
+The example demonstrates automatic key rotation when keys reach expiration
criteria:
+
+* **Usage-based**: After 10 signatures (configurable via `key.max.usage.count`)
+* **Age-based**: After 90 days (configurable via `key.max.age.days`)
+
+When a key expires, the system automatically rotates to a new key, marking the
old key as `DEPRECATED` for verification of previously signed documents.
+
+**Test rotation:**
+[source,sh]
+----
+# Sign document 10 times to trigger automatic rotation
+$ for i in {1..10}; do
+ curl -X POST http://localhost:8080/api/sign \
+ -H "Content-Type: text/plain" \
+ -d "Document number $i"
+done
+
+# View both old and new keys
+$ curl http://localhost:8080/api/keys
+----
+
+== Key Storage in HashiCorp Vault
+
+The PQC keys are stored in Vault's KV v2 secrets engine with the following
structure:
+
+[source,text]
+----
+secret/
+└── data/
+ └── pqc/
+ └── keys/
+ └── document-signing-key/
+ ├── private/ # PKCS#8 private key
+ ├── public/ # X.509 public key
+ └── metadata/ # Key metadata
+----
+
+This separation enables fine-grained access control where applications can
access public keys for verification without having access to private keys for
signing.
+
+== Configuration
+
+Configuration is managed in `application.properties`:
+
+[source,properties]
+----
+# Vault connection
+vault.host=localhost
+vault.port=8201
+vault.token=myroot
+
+# Key rotation policy
+key.max.usage.count=10 # Max signatures before rotation
+key.max.age.days=90 # Max key age in days
+key.rotation.check.period=60000 # Check interval (ms)
+----
+
+== Troubleshooting
+
+=== Connection to Vault Failed
+
+* Ensure Vault is running: `docker ps`
+* Verify the port in `application.properties` is set to `8201`
+* Check the Vault token is correct (default: `myroot`)
+
+=== Key Not Found in Vault
+
+* Check Vault UI at http://localhost:8201 (token: `myroot`)
+* Verify the secrets engine is enabled: `vault secrets list`
+* Check the key path: `secret/data/pqc/keys/document-signing-key`
+
+=== Signature Verification Failed
+
+* Ensure you're using the same document content for both signing and
verification
+* Include the `X-Signature` header with the base64-encoded signature from the
sign response
+* The signature must be passed exactly as received from the sign endpoint
(base64 string)
+* Verify the key hasn't been rotated or revoked
+* Check logs for "ERROR during verification" messages which indicate signature
format issues
+
+== Stopping
+
+To stop the Camel application, press `Ctrl+C`.
+
+To stop HashiCorp Vault:
+
+[source,sh]
+----
+$ docker stop vault
+$ docker rm vault
+----
+
+== Architecture
+
+This example demonstrates:
+
+1. **Post-Quantum Cryptography (PQC)** - Quantum-resistant digital signatures
using DILITHIUM
+2. **HashiCorp Vault Integration** - Enterprise secret management for PQC keys
+3. **Key Lifecycle Management** - Automated tracking of key usage, rotation,
and expiration
+4. **RESTful API** - Multiple endpoints for signing, verification, and key
management
+5. **Platform HTTP Component** - Lightweight HTTP server for REST APIs
+6. **Java-based RouteBuilder** - Routes defined using Camel Java DSL
+7. **Bean Configuration** - Vault and key manager beans configured
programmatically in Java
+8. **Scheduled Jobs** - Periodic key rotation checks
+9. **Dynamic Routing** - Uses `.toD()` for runtime endpoint creation with
autowired KeyPair
+10. **Base64 Encoding** - Binary signatures converted to base64 for JSON
transport
+
+=== Implementation Details
+
+The `PQCDocumentSigningRoutes.java` RouteBuilder:
+
+- Registers BouncyCastle security providers (BC, BCPQC)
+- Creates and binds Vault-related beans to the registry
+- Defines 11 Camel routes using Java DSL
+- Handles KeyPair registration after key generation
+- Converts binary signatures to/from base64 for JSON transport
+- Uses dynamic routing (`.toD()`) to create PQC endpoints at runtime
+- Implements automatic key rotation based on usage count and age
+
+== Security Notice
+
+This example uses **development settings**. For production:
+
+* Use HTTPS for Vault (`vault.scheme=https`)
+* Use AppRole or similar authentication instead of root tokens
+* Configure Vault access policies
+* Increase `key.max.usage.count` to realistic values (e.g., 100,000)
+* Enable Vault audit logging
+
+== Help and Contributions
+
+If you hit any problem using Camel or have some feedback, then please
+https://camel.apache.org/community/support/[let us know].
+
+We also love contributors, so
+https://camel.apache.org/community/contributing/[get involved] :-)
+
+The Camel riders!
diff --git a/pqc-document-signing/application.properties
b/pqc-document-signing/application.properties
new file mode 100644
index 0000000..74b80cc
--- /dev/null
+++ b/pqc-document-signing/application.properties
@@ -0,0 +1,40 @@
+# 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.
+
+# Application Configuration
+camel.main.name = PQCDocumentSigningExample
+
+# HashiCorp Vault Configuration
+# Port 8200 is used when running Vault via: camel infra run hashicorp-vault
+# Port 8201 is used when running Vault manually via Docker (see README.adoc)
+vault.host=localhost
+vault.port=8201
+vault.scheme=http
+vault.token=myroot
+vault.secrets.engine=secret
+vault.keys.prefix=pqc/keys
+
+# PQC Key Lifecycle Configuration
+# Maximum number of times a key can be used before expiration
+key.max.usage.count=10
+
+# Maximum age of a key in days before rotation is needed
+key.max.age.days=90
+
+# How often to check if key rotation is needed (in milliseconds)
+# 60000 = 1 minute (for demo purposes, use longer intervals in production)
+key.rotation.check.period=60000