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 cef8d588fa71 CAMEL-23204 - CAMEL-PQC: Enforce key status checks before
cryptographic operations in PQCProducer (#22052)
cef8d588fa71 is described below
commit cef8d588fa71eda487bcbfdb64ffebe84c9a9e2e
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Mar 17 15:41:52 2026 +0100
CAMEL-23204 - CAMEL-PQC: Enforce key status checks before cryptographic
operations in PQCProducer (#22052)
PQCProducer previously allowed cryptographic operations (signing,
verification,
KEM encapsulation/extraction) to proceed regardless of the key's lifecycle
status. A key that had been explicitly revoked or expired via the
KeyLifecycleManager could still be used for all operations, defeating the
purpose of key lifecycle management and creating a potential security gap.
This commit adds key status enforcement to PQCProducer that gates all
cryptographic operations based on the key's metadata status:
- REVOKED keys are now rejected for ALL operations (sign, verify,
encapsulate,
extract) with an IllegalStateException. Revoked keys indicate a compromise
and must never be used, not even for verification.
- EXPIRED keys are rejected for producing operations (sign, encapsulate,
hybridSign, hybridGenerateSecretKeyEncapsulation) but remain allowed for
consuming operations (verify, extract). This preserves the ability to
verify
old signatures or extract secrets from existing encapsulations while
preventing new cryptographic material from being generated with stale
keys.
- DEPRECATED keys (typically set during rotation) continue to function for
all operations but now emit a WARN-level log advising the user to rotate
to an active key. This provides a graceful transition period.
- ACTIVE and PENDING_ROTATION keys are always permitted without restriction.
A new configuration flag 'strictKeyLifecycle' (default: true) controls this
enforcement. Setting it to false restores the previous permissive behavior.
Enforcement is also naturally skipped when no KeyLifecycleManager is
configured
or when the exchange does not carry a CamelPQCKeyId header, ensuring full
backward compatibility with existing routes that do not use lifecycle
management.
The enforcement is implemented as a pre-check in PQCProducer.process() that
runs before the operation dispatch. It uses two helper methods to classify
operations: isCryptographicOperation() filters out lifecycle and stateful
management operations that should not be gated, and isProducingOperation()
distinguishes key-producing operations (sign, encapsulate) from
key-consuming
ones (verify, extract) to apply the differentiated EXPIRED key policy.
Includes 12 new unit tests in PQCKeyStatusEnforcementTest covering every
status/operation combination: REVOKED rejection for sign and verify, EXPIRED
rejection for sign with allowance for verify, DEPRECATED warning behavior,
ACTIVE and PENDING_ROTATION acceptance, strictKeyLifecycle=false bypass, and
the no-KEY_ID-header skip path. All 126 camel-pqc tests pass.
---
.../org/apache/camel/catalog/components/pqc.json | 14 +-
components/camel-pqc/pom.xml | 5 +
.../component/pqc/PQCComponentConfigurer.java | 6 +
.../camel/component/pqc/PQCEndpointConfigurer.java | 6 +
.../camel/component/pqc/PQCEndpointUriFactory.java | 3 +-
.../org/apache/camel/component/pqc/pqc.json | 14 +-
.../camel-pqc/src/main/docs/pqc-component.adoc | 129 ++++++-
.../camel/component/pqc/PQCConfiguration.java | 23 ++
.../apache/camel/component/pqc/PQCProducer.java | 100 +++++-
.../component/pqc/PQCKeyStatusEnforcementTest.java | 378 +++++++++++++++++++++
.../component/dsl/PqcComponentBuilderFactory.java | 23 ++
.../endpoint/dsl/PQCEndpointBuilderFactory.java | 40 +++
12 files changed, 723 insertions(+), 18 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pqc.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pqc.json
index a7ad60e9a84c..04323a8b95c0 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pqc.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pqc.json
@@ -44,10 +44,11 @@
"signatureAlgorithm": { "index": 17, "kind": "property", "displayName":
"Signature Algorithm", "group": "advanced", "label": "advanced", "required":
false, "type": "enum", "javaType": "java.lang.String", "enum": [ "MLDSA",
"SLHDSA", "LMS", "HSS", "XMSS", "XMSSMT", "DILITHIUM", "FALCON", "PICNIC",
"SNOVA", "MAYO", "SPHINCSPLUS" ], "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "c [...]
"signer": { "index": 18, "kind": "property", "displayName": "Signer",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "java.security.Signature", "deprecated": false, "deprecationNote":
"", "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "The Signer to be used" },
"storeExtractedSecretKeyAsHeader": { "index": 19, "kind": "property",
"displayName": "Store Extracted Secret Key As Header", "group": "advanced",
"label": "advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "In the context of extractSec [...]
- "symmetricKeyAlgorithm": { "index": 20, "kind": "property", "displayName":
"Symmetric Key Algorithm", "group": "advanced", "label": "advanced",
"required": false, "type": "enum", "javaType": "java.lang.String", "enum": [
"AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6", "CHACHA7539",
"DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128", "HC256",
"SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "co [...]
- "symmetricKeyLength": { "index": 21, "kind": "property", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" },
- "healthCheckConsumerEnabled": { "index": 22, "kind": "property",
"displayName": "Health Check Consumer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all consumer based health checks
from this component" },
- "healthCheckProducerEnabled": { "index": 23, "kind": "property",
"displayName": "Health Check Producer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all producer based health checks
from this component. Notice: Camel has by default disabled all producer based
health-checks. You can turn on produce [...]
+ "strictKeyLifecycle": { "index": 20, "kind": "property", "displayName":
"Strict Key Lifecycle", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
true, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "Whether to enforce key
status checks before cryptographic [...]
+ "symmetricKeyAlgorithm": { "index": 21, "kind": "property", "displayName":
"Symmetric Key Algorithm", "group": "advanced", "label": "advanced",
"required": false, "type": "enum", "javaType": "java.lang.String", "enum": [
"AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6", "CHACHA7539",
"DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128", "HC256",
"SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "co [...]
+ "symmetricKeyLength": { "index": 22, "kind": "property", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" },
+ "healthCheckConsumerEnabled": { "index": 23, "kind": "property",
"displayName": "Health Check Consumer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all consumer based health checks
from this component" },
+ "healthCheckProducerEnabled": { "index": 24, "kind": "property",
"displayName": "Health Check Producer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all producer based health checks
from this component. Notice: Camel has by default disabled all producer based
health-checks. You can turn on produce [...]
},
"headers": {
"CamelPQCOperation": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The operation we want to perform", "constantName":
"org.apache.camel.component.pqc.PQCConstants#OPERATION" },
@@ -94,7 +95,8 @@
"signatureAlgorithm": { "index": 16, "kind": "parameter", "displayName":
"Signature Algorithm", "group": "advanced", "label": "advanced", "required":
false, "type": "enum", "javaType": "java.lang.String", "enum": [ "MLDSA",
"SLHDSA", "LMS", "HSS", "XMSS", "XMSSMT", "DILITHIUM", "FALCON", "PICNIC",
"SNOVA", "MAYO", "SPHINCSPLUS" ], "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", " [...]
"signer": { "index": 17, "kind": "parameter", "displayName": "Signer",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "java.security.Signature", "deprecated": false, "deprecationNote":
"", "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "The Signer to be used" },
"storeExtractedSecretKeyAsHeader": { "index": 18, "kind": "parameter",
"displayName": "Store Extracted Secret Key As Header", "group": "advanced",
"label": "advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "In the context of extractSe [...]
- "symmetricKeyAlgorithm": { "index": 19, "kind": "parameter",
"displayName": "Symmetric Key Algorithm", "group": "advanced", "label":
"advanced", "required": false, "type": "enum", "javaType": "java.lang.String",
"enum": [ "AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6",
"CHACHA7539", "DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128",
"HC256", "SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "c [...]
- "symmetricKeyLength": { "index": 20, "kind": "parameter", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" }
+ "strictKeyLifecycle": { "index": 19, "kind": "parameter", "displayName":
"Strict Key Lifecycle", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
true, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "Whether to enforce key
status checks before cryptographic [...]
+ "symmetricKeyAlgorithm": { "index": 20, "kind": "parameter",
"displayName": "Symmetric Key Algorithm", "group": "advanced", "label":
"advanced", "required": false, "type": "enum", "javaType": "java.lang.String",
"enum": [ "AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6",
"CHACHA7539", "DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128",
"HC256", "SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "c [...]
+ "symmetricKeyLength": { "index": 21, "kind": "parameter", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" }
}
}
diff --git a/components/camel-pqc/pom.xml b/components/camel-pqc/pom.xml
index 17c0dca90481..fcc695c27b7f 100644
--- a/components/camel-pqc/pom.xml
+++ b/components/camel-pqc/pom.xml
@@ -99,5 +99,10 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCComponentConfigurer.java
b/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCComponentConfigurer.java
index a4da0eb7933c..9044003da182 100644
---
a/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCComponentConfigurer.java
+++
b/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCComponentConfigurer.java
@@ -71,6 +71,8 @@ public class PQCComponentConfigurer extends
PropertyConfigurerSupport implements
case "signer":
getOrCreateConfiguration(target).setSigner(property(camelContext,
java.security.Signature.class, value)); return true;
case "storeextractedsecretkeyasheader":
case "storeExtractedSecretKeyAsHeader":
getOrCreateConfiguration(target).setStoreExtractedSecretKeyAsHeader(property(camelContext,
boolean.class, value)); return true;
+ case "strictkeylifecycle":
+ case "strictKeyLifecycle":
getOrCreateConfiguration(target).setStrictKeyLifecycle(property(camelContext,
boolean.class, value)); return true;
case "symmetrickeyalgorithm":
case "symmetricKeyAlgorithm":
getOrCreateConfiguration(target).setSymmetricKeyAlgorithm(property(camelContext,
java.lang.String.class, value)); return true;
case "symmetrickeylength":
@@ -128,6 +130,8 @@ public class PQCComponentConfigurer extends
PropertyConfigurerSupport implements
case "signer": return java.security.Signature.class;
case "storeextractedsecretkeyasheader":
case "storeExtractedSecretKeyAsHeader": return boolean.class;
+ case "strictkeylifecycle":
+ case "strictKeyLifecycle": return boolean.class;
case "symmetrickeyalgorithm":
case "symmetricKeyAlgorithm": return java.lang.String.class;
case "symmetrickeylength":
@@ -181,6 +185,8 @@ public class PQCComponentConfigurer extends
PropertyConfigurerSupport implements
case "signer": return getOrCreateConfiguration(target).getSigner();
case "storeextractedsecretkeyasheader":
case "storeExtractedSecretKeyAsHeader": return
getOrCreateConfiguration(target).isStoreExtractedSecretKeyAsHeader();
+ case "strictkeylifecycle":
+ case "strictKeyLifecycle": return
getOrCreateConfiguration(target).isStrictKeyLifecycle();
case "symmetrickeyalgorithm":
case "symmetricKeyAlgorithm": return
getOrCreateConfiguration(target).getSymmetricKeyAlgorithm();
case "symmetrickeylength":
diff --git
a/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointConfigurer.java
b/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointConfigurer.java
index 1d15c2a783b7..fe426f7b55dd 100644
---
a/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointConfigurer.java
+++
b/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointConfigurer.java
@@ -57,6 +57,8 @@ public class PQCEndpointConfigurer extends
PropertyConfigurerSupport implements
case "signer":
target.getConfiguration().setSigner(property(camelContext,
java.security.Signature.class, value)); return true;
case "storeextractedsecretkeyasheader":
case "storeExtractedSecretKeyAsHeader":
target.getConfiguration().setStoreExtractedSecretKeyAsHeader(property(camelContext,
boolean.class, value)); return true;
+ case "strictkeylifecycle":
+ case "strictKeyLifecycle":
target.getConfiguration().setStrictKeyLifecycle(property(camelContext,
boolean.class, value)); return true;
case "symmetrickeyalgorithm":
case "symmetricKeyAlgorithm":
target.getConfiguration().setSymmetricKeyAlgorithm(property(camelContext,
java.lang.String.class, value)); return true;
case "symmetrickeylength":
@@ -107,6 +109,8 @@ public class PQCEndpointConfigurer extends
PropertyConfigurerSupport implements
case "signer": return java.security.Signature.class;
case "storeextractedsecretkeyasheader":
case "storeExtractedSecretKeyAsHeader": return boolean.class;
+ case "strictkeylifecycle":
+ case "strictKeyLifecycle": return boolean.class;
case "symmetrickeyalgorithm":
case "symmetricKeyAlgorithm": return java.lang.String.class;
case "symmetrickeylength":
@@ -153,6 +157,8 @@ public class PQCEndpointConfigurer extends
PropertyConfigurerSupport implements
case "signer": return target.getConfiguration().getSigner();
case "storeextractedsecretkeyasheader":
case "storeExtractedSecretKeyAsHeader": return
target.getConfiguration().isStoreExtractedSecretKeyAsHeader();
+ case "strictkeylifecycle":
+ case "strictKeyLifecycle": return
target.getConfiguration().isStrictKeyLifecycle();
case "symmetrickeyalgorithm":
case "symmetricKeyAlgorithm": return
target.getConfiguration().getSymmetricKeyAlgorithm();
case "symmetrickeylength":
diff --git
a/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointUriFactory.java
b/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointUriFactory.java
index 60e66d94b1e8..1f1bfe4747fe 100644
---
a/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointUriFactory.java
+++
b/components/camel-pqc/src/generated/java/org/apache/camel/component/pqc/PQCEndpointUriFactory.java
@@ -23,7 +23,7 @@ public class PQCEndpointUriFactory extends
org.apache.camel.support.component.En
private static final Set<String> SECRET_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(21);
+ Set<String> props = new HashSet<>(22);
props.add("classicalKEMAlgorithm");
props.add("classicalKeyAgreement");
props.add("classicalKeyPair");
@@ -43,6 +43,7 @@ public class PQCEndpointUriFactory extends
org.apache.camel.support.component.En
props.add("signatureAlgorithm");
props.add("signer");
props.add("storeExtractedSecretKeyAsHeader");
+ props.add("strictKeyLifecycle");
props.add("symmetricKeyAlgorithm");
props.add("symmetricKeyLength");
PROPERTY_NAMES = Collections.unmodifiableSet(props);
diff --git
a/components/camel-pqc/src/generated/resources/META-INF/org/apache/camel/component/pqc/pqc.json
b/components/camel-pqc/src/generated/resources/META-INF/org/apache/camel/component/pqc/pqc.json
index a7ad60e9a84c..04323a8b95c0 100644
---
a/components/camel-pqc/src/generated/resources/META-INF/org/apache/camel/component/pqc/pqc.json
+++
b/components/camel-pqc/src/generated/resources/META-INF/org/apache/camel/component/pqc/pqc.json
@@ -44,10 +44,11 @@
"signatureAlgorithm": { "index": 17, "kind": "property", "displayName":
"Signature Algorithm", "group": "advanced", "label": "advanced", "required":
false, "type": "enum", "javaType": "java.lang.String", "enum": [ "MLDSA",
"SLHDSA", "LMS", "HSS", "XMSS", "XMSSMT", "DILITHIUM", "FALCON", "PICNIC",
"SNOVA", "MAYO", "SPHINCSPLUS" ], "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "c [...]
"signer": { "index": 18, "kind": "property", "displayName": "Signer",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "java.security.Signature", "deprecated": false, "deprecationNote":
"", "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "The Signer to be used" },
"storeExtractedSecretKeyAsHeader": { "index": 19, "kind": "property",
"displayName": "Store Extracted Secret Key As Header", "group": "advanced",
"label": "advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "In the context of extractSec [...]
- "symmetricKeyAlgorithm": { "index": 20, "kind": "property", "displayName":
"Symmetric Key Algorithm", "group": "advanced", "label": "advanced",
"required": false, "type": "enum", "javaType": "java.lang.String", "enum": [
"AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6", "CHACHA7539",
"DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128", "HC256",
"SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "co [...]
- "symmetricKeyLength": { "index": 21, "kind": "property", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" },
- "healthCheckConsumerEnabled": { "index": 22, "kind": "property",
"displayName": "Health Check Consumer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all consumer based health checks
from this component" },
- "healthCheckProducerEnabled": { "index": 23, "kind": "property",
"displayName": "Health Check Producer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all producer based health checks
from this component. Notice: Camel has by default disabled all producer based
health-checks. You can turn on produce [...]
+ "strictKeyLifecycle": { "index": 20, "kind": "property", "displayName":
"Strict Key Lifecycle", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
true, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "Whether to enforce key
status checks before cryptographic [...]
+ "symmetricKeyAlgorithm": { "index": 21, "kind": "property", "displayName":
"Symmetric Key Algorithm", "group": "advanced", "label": "advanced",
"required": false, "type": "enum", "javaType": "java.lang.String", "enum": [
"AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6", "CHACHA7539",
"DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128", "HC256",
"SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "co [...]
+ "symmetricKeyLength": { "index": 22, "kind": "property", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" },
+ "healthCheckConsumerEnabled": { "index": 23, "kind": "property",
"displayName": "Health Check Consumer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all consumer based health checks
from this component" },
+ "healthCheckProducerEnabled": { "index": 24, "kind": "property",
"displayName": "Health Check Producer Enabled", "group": "health", "label":
"health", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true,
"description": "Used for enabling or disabling all producer based health checks
from this component. Notice: Camel has by default disabled all producer based
health-checks. You can turn on produce [...]
},
"headers": {
"CamelPQCOperation": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The operation we want to perform", "constantName":
"org.apache.camel.component.pqc.PQCConstants#OPERATION" },
@@ -94,7 +95,8 @@
"signatureAlgorithm": { "index": 16, "kind": "parameter", "displayName":
"Signature Algorithm", "group": "advanced", "label": "advanced", "required":
false, "type": "enum", "javaType": "java.lang.String", "enum": [ "MLDSA",
"SLHDSA", "LMS", "HSS", "XMSS", "XMSSMT", "DILITHIUM", "FALCON", "PICNIC",
"SNOVA", "MAYO", "SPHINCSPLUS" ], "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", " [...]
"signer": { "index": 17, "kind": "parameter", "displayName": "Signer",
"group": "advanced", "label": "advanced", "required": false, "type": "object",
"javaType": "java.security.Signature", "deprecated": false, "deprecationNote":
"", "autowired": true, "secret": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "The Signer to be used" },
"storeExtractedSecretKeyAsHeader": { "index": 18, "kind": "parameter",
"displayName": "Store Extracted Secret Key As Header", "group": "advanced",
"label": "advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.pqc.PQCConfiguration", "configurationField":
"configuration", "description": "In the context of extractSe [...]
- "symmetricKeyAlgorithm": { "index": 19, "kind": "parameter",
"displayName": "Symmetric Key Algorithm", "group": "advanced", "label":
"advanced", "required": false, "type": "enum", "javaType": "java.lang.String",
"enum": [ "AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6",
"CHACHA7539", "DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128",
"HC256", "SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "c [...]
- "symmetricKeyLength": { "index": 20, "kind": "parameter", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" }
+ "strictKeyLifecycle": { "index": 19, "kind": "parameter", "displayName":
"Strict Key Lifecycle", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
true, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "Whether to enforce key
status checks before cryptographic [...]
+ "symmetricKeyAlgorithm": { "index": 20, "kind": "parameter",
"displayName": "Symmetric Key Algorithm", "group": "advanced", "label":
"advanced", "required": false, "type": "enum", "javaType": "java.lang.String",
"enum": [ "AES", "ARIA", "RC2", "RC5", "CAMELLIA", "CAST5", "CAST6",
"CHACHA7539", "DSTU7624", "GOST28147", "GOST3412_2015", "GRAIN128", "HC128",
"HC256", "SALSA20", "SEED", "SM4", "DESEDE" ], "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "c [...]
+ "symmetricKeyLength": { "index": 21, "kind": "parameter", "displayName":
"Symmetric Key Length", "group": "advanced", "label": "advanced", "required":
false, "type": "integer", "javaType": "int", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue":
128, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration",
"configurationField": "configuration", "description": "The required length of
the symmetric key used" }
}
}
diff --git a/components/camel-pqc/src/main/docs/pqc-component.adoc
b/components/camel-pqc/src/main/docs/pqc-component.adoc
index d5e1fbeaf1d3..21f05bf34c01 100644
--- a/components/camel-pqc/src/main/docs/pqc-component.adoc
+++ b/components/camel-pqc/src/main/docs/pqc-component.adoc
@@ -1880,7 +1880,7 @@ from("direct:sign")
**Hybrid Signature Wire Format:**
-The hybrid signature is encoded as: `[4 bytes: classical sig length][classical
sig][pqc sig]`
+Since Camel 4.19, hybrid signatures use wire format v2 which includes magic
bytes, version, and algorithm identifiers (see <<Wire Format v2 and Algorithm
Identification>>). The v1 format `[4 bytes: classical sig length][classical
sig][pqc sig]` is still accepted on input for backward compatibility.
**Headers set by hybrid signature operations:**
@@ -1950,7 +1950,7 @@ from("direct:encapsulate")
**Hybrid KEM Wire Format:**
-The hybrid encapsulation is encoded as: `[4 bytes: classical encap
length][classical encap][pqc encap]`
+Since Camel 4.19, hybrid encapsulations use wire format v2 which includes
magic bytes, version, and algorithm identifiers (see <<Wire Format v2 and
Algorithm Identification>>). The v1 format `[4 bytes: classical encap
length][classical encap][pqc encap]` is still accepted on input for backward
compatibility.
**Headers set by hybrid KEM operations:**
@@ -2022,6 +2022,123 @@ Based on NIST recommendations for the transition period:
5. **Algorithm agility**: Design your system to allow algorithm upgrades as
standards evolve.
+== Wire Format v2 and Algorithm Identification
+
+Starting with Camel 4.19, the hybrid signature, hybrid KEM, and PQC DataFormat
wire formats include self-describing algorithm metadata. This makes the binary
output portable and verifiable without out-of-band knowledge of which
algorithms were used.
+
+=== PQCAlgorithmId
+
+The `PQCAlgorithmId` enum maps every supported algorithm — classical
signature, PQC signature, classical KEM, PQC KEM, and symmetric — to a unique
16-bit identifier used in the wire format header.
+
+**Algorithm ID ranges:**
+
+|===
+|Range |Category |Examples
+
+|`0x0100–0x01FF`
+|Classical signature
+|SHA256withECDSA (`0x0101`), Ed25519 (`0x0104`), SHA256withRSA (`0x0106`)
+
+|`0x0200–0x02FF`
+|Post-quantum signature
+|ML-DSA (`0x0201`), SLH-DSA (`0x0202`), LMS (`0x0203`), FALCON (`0x0207`)
+
+|`0x0300–0x03FF`
+|Classical KEM / key agreement
+|EC (`0x0301`), X25519 (`0x0303`), X448 (`0x0305`)
+
+|`0x0400–0x04FF`
+|Post-quantum KEM
+|ML-KEM (`0x0401`), BIKE (`0x0402`), NTRU (`0x0407`), KYBER (`0x040A`)
+
+|`0x0500–0x05FF`
+|Symmetric encryption
+|AES (`0x0501`), ARIA (`0x0502`), Camellia (`0x0503`)
+|===
+
+**Lookup by JCA name:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Resolve from a JCA algorithm string (case-insensitive)
+PQCAlgorithmId algId = PQCAlgorithmId.fromJcaName("ML-DSA"); // ML_DSA
(0x0201)
+PQCAlgorithmId kemId = PQCAlgorithmId.fromJcaName("ML-KEM"); // ML_KEM
(0x0401)
+PQCAlgorithmId aesId = PQCAlgorithmId.fromJcaName("AES"); // AES
(0x0501)
+
+// Returns UNKNOWN (0x0000) for unrecognized names
+PQCAlgorithmId unknown = PQCAlgorithmId.fromJcaName("FutureAlg"); // UNKNOWN
+--------------------------------------------------------------------------------
+
+**Lookup by wire format ID:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// Resolve from a 16-bit identifier read from the wire
+PQCAlgorithmId alg = PQCAlgorithmId.fromId(0x0201); // ML_DSA
+int id = alg.getId(); // 0x0201
+String jca = alg.getJcaName(); // "ML-DSA"
+--------------------------------------------------------------------------------
+
+**Inspecting algorithm metadata from a parsed hybrid output:**
+
+[source,java]
+--------------------------------------------------------------------------------
+// After parsing a v2 hybrid signature
+HybridSignature.HybridSignatureComponents components =
HybridSignature.parse(hybridSig);
+
+// v2 components carry algorithm identifiers
+PQCAlgorithmId classical = components.classicalAlgorithmId(); // e.g.
SHA256_WITH_ECDSA
+PQCAlgorithmId pqc = components.pqcAlgorithmId(); // e.g. ML_DSA
+int version = components.version(); // 2
+
+// v1 components (backward-compatible) return null algorithm IDs and version 1
+--------------------------------------------------------------------------------
+
+=== Wire Format v2 Layout
+
+All three output types (hybrid signature, hybrid KEM, PQC DataFormat) share
the same 8-byte header:
+
+[source,text]
+--------------------------------------------------------------------------------
+[2 bytes: magic 0x50 0x51 ("PQ")]
+[1 byte: version — 0x02]
+[1 byte: format type]
+[2 bytes: first algorithm ID]
+[2 bytes: second algorithm ID]
+[4 bytes: first payload length]
+[N bytes: first payload]
+[M bytes: second payload]
+--------------------------------------------------------------------------------
+
+**Format types:**
+
+|===
+|Value |Type |First algorithm ID |Second algorithm ID
+
+|`0x01`
+|Hybrid signature
+|Classical signature algorithm
+|PQC signature algorithm
+
+|`0x02`
+|Hybrid KEM
+|Classical KEM algorithm
+|PQC KEM algorithm
+
+|`0x03`
+|KEM DataFormat
+|KEM algorithm
+|Symmetric algorithm
+|===
+
+=== Backward Compatibility
+
+Wire format v2 is fully backward-compatible with v1:
+
+* **Writing** — `sign()`, `encapsulate()`, and `marshal()` always produce v2
output.
+* **Reading** — `parse()`, `verify()`, `extract()`, and `unmarshal()`
auto-detect the format by checking the first two bytes for the magic bytes
`0x50 0x51`. If absent, they fall back to v1 parsing.
+* **Safety** — The magic bytes `0x50 0x51`, interpreted as the first two bytes
of a big-endian 32-bit integer, produce values above 1.3 billion. No valid v1
payload has a classical signature or encapsulation of that size, so false
positives are impossible in practice.
+
== PQC DataFormat
The PQC component also provides a DataFormat for quantum-resistant encryption
and decryption of message payloads using Key Encapsulation Mechanisms (KEM).
@@ -2046,14 +2163,18 @@ This approach provides **defense-in-depth**: even if
the symmetric algorithm is
=== Wire Format
-The encrypted message format is:
+Since Camel 4.19, the PQC DataFormat uses wire format v2 which includes magic
bytes, version, and algorithm identifiers (see <<Wire Format v2 and Algorithm
Identification>>). The v1 format `[4 bytes: encapsulation length][N bytes:
encapsulation][M bytes: encrypted data]` is still accepted on input for
backward compatibility.
+
+The v2 encrypted message format is:
[source,text]
--------------------------------------------------------------------------------
+[2 bytes: magic "PQ"] [1 byte: version] [1 byte: format type]
+[2 bytes: KEM algorithm ID] [2 bytes: symmetric algorithm ID]
[4 bytes: encapsulation length] [N bytes: encapsulation] [M bytes: encrypted
data]
--------------------------------------------------------------------------------
-This format allows the receiver to extract the encapsulation, decapsulate the
shared secret using their private key, and decrypt the data.
+This format allows the receiver to identify the algorithms used, extract the
encapsulation, decapsulate the shared secret using their private key, and
decrypt the data.
=== Basic Usage
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCConfiguration.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCConfiguration.java
index be91d6ffb789..938e2f706866 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCConfiguration.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCConfiguration.java
@@ -105,6 +105,15 @@ public class PQCConfiguration implements Cloneable {
@Metadata(label = "advanced", autowired = true)
private KeyLifecycleManager keyLifecycleManager;
+ @UriParam(defaultValue = "true",
+ description = "Whether to enforce key status checks before
cryptographic operations. "
+ + "When enabled, REVOKED keys are rejected for all
operations, "
+ + "EXPIRED keys are rejected for
signing/encapsulation but allowed for verification/extraction, "
+ + "and DEPRECATED keys produce a warning but still
function. "
+ + "Requires a KeyLifecycleManager and a
CamelPQCKeyId header to be set.")
+ @Metadata(label = "advanced")
+ private boolean strictKeyLifecycle = true;
+
public PQCOperations getOperation() {
return operation;
}
@@ -316,6 +325,20 @@ public class PQCConfiguration implements Cloneable {
this.keyLifecycleManager = keyLifecycleManager;
}
+ public boolean isStrictKeyLifecycle() {
+ return strictKeyLifecycle;
+ }
+
+ /**
+ * Whether to enforce key status checks before cryptographic operations.
When enabled, REVOKED keys are rejected for
+ * all operations, EXPIRED keys are rejected for signing/encapsulation but
allowed for verification/extraction, and
+ * DEPRECATED keys produce a warning but still function. Requires a
KeyLifecycleManager and a CamelPQCKeyId header
+ * to be set.
+ */
+ public void setStrictKeyLifecycle(boolean strictKeyLifecycle) {
+ this.strictKeyLifecycle = strictKeyLifecycle;
+ }
+
// *************************************************
//
// *************************************************
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
index 5ab1c2d89a0a..d2d7993551ff 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java
@@ -41,12 +41,16 @@ import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
import org.bouncycastle.pqc.jcajce.interfaces.LMSPrivateKey;
import org.bouncycastle.pqc.jcajce.interfaces.XMSSMTPrivateKey;
import org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* A Producer which sign or verify a payload
*/
public class PQCProducer extends DefaultProducer {
+ private static final Logger LOG =
LoggerFactory.getLogger(PQCProducer.class);
+
private Signature signer;
private KeyGenerator keyGenerator;
private KeyPair keyPair;
@@ -62,7 +66,9 @@ public class PQCProducer extends DefaultProducer {
@Override
public void process(Exchange exchange) throws Exception {
- switch (determineOperation(exchange)) {
+ PQCOperations operation = determineOperation(exchange);
+ enforceKeyStatus(exchange, operation);
+ switch (operation) {
case sign:
signature(exchange);
break;
@@ -143,6 +149,98 @@ public class PQCProducer extends DefaultProducer {
return getEndpoint().getConfiguration();
}
+ /**
+ * Enforces key status checks before cryptographic operations when strict
key lifecycle mode is enabled. Looks up
+ * the key metadata via the configured {@link KeyLifecycleManager} using
the {@code CamelPQCKeyId} header.
+ *
+ * <ul>
+ * <li>REVOKED keys are rejected for all operations (sign, verify,
encapsulate, extract).</li>
+ * <li>EXPIRED keys are rejected for signing/encapsulation but allowed for
verification/extraction.</li>
+ * <li>DEPRECATED keys produce a WARN log but still function (transition
period).</li>
+ * <li>ACTIVE and PENDING_ROTATION keys are always allowed.</li>
+ * </ul>
+ */
+ private void enforceKeyStatus(Exchange exchange, PQCOperations operation)
throws Exception {
+ if (!getConfiguration().isStrictKeyLifecycle()) {
+ return;
+ }
+
+ KeyLifecycleManager klm = getConfiguration().getKeyLifecycleManager();
+ if (klm == null) {
+ return;
+ }
+
+ if (!isCryptographicOperation(operation)) {
+ return;
+ }
+
+ String keyId = exchange.getMessage().getHeader(PQCConstants.KEY_ID,
String.class);
+ if (ObjectHelper.isEmpty(keyId)) {
+ return;
+ }
+
+ KeyMetadata metadata = klm.getKeyMetadata(keyId);
+ if (metadata == null) {
+ return;
+ }
+
+ KeyMetadata.KeyStatus status = metadata.getStatus();
+
+ switch (status) {
+ case REVOKED:
+ throw new IllegalStateException(
+ "Key '" + keyId + "' has been revoked and cannot be
used for any cryptographic operation. "
+ + "Reason: " +
(metadata.getDescription() != null
+ ?
metadata.getDescription() : "not specified"));
+ case EXPIRED:
+ if (isProducingOperation(operation)) {
+ throw new IllegalStateException(
+ "Key '" + keyId + "' has expired and cannot be
used for " + operation
+ + ". Expired keys can only
be used for verification or extraction operations.");
+ }
+ LOG.info("Using expired key '{}' for {} operation
(verification/extraction of existing data)", keyId,
+ operation);
+ break;
+ case DEPRECATED:
+ LOG.warn("Key '{}' is deprecated. Consider rotating to an
active key. Operation: {}", keyId, operation);
+ break;
+ case ACTIVE:
+ case PENDING_ROTATION:
+ default:
+ break;
+ }
+ }
+
+ private boolean isCryptographicOperation(PQCOperations operation) {
+ switch (operation) {
+ case sign:
+ case verify:
+ case hybridSign:
+ case hybridVerify:
+ case generateSecretKeyEncapsulation:
+ case extractSecretKeyEncapsulation:
+ case extractSecretKeyFromEncapsulation:
+ case hybridGenerateSecretKeyEncapsulation:
+ case hybridExtractSecretKeyEncapsulation:
+ case hybridExtractSecretKeyFromEncapsulation:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private boolean isProducingOperation(PQCOperations operation) {
+ switch (operation) {
+ case sign:
+ case hybridSign:
+ case generateSecretKeyEncapsulation:
+ case hybridGenerateSecretKeyEncapsulation:
+ return true;
+ default:
+ return false;
+ }
+ }
+
@Override
public PQCEndpoint getEndpoint() {
return (PQCEndpoint) super.getEndpoint();
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyStatusEnforcementTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyStatusEnforcementTest.java
new file mode 100644
index 000000000000..3df374710beb
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyStatusEnforcementTest.java
@@ -0,0 +1,378 @@
+/*
+ * 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.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.List;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Produce;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.component.pqc.lifecycle.FileBasedKeyLifecycleManager;
+import org.apache.camel.component.pqc.lifecycle.KeyLifecycleManager;
+import org.apache.camel.component.pqc.lifecycle.KeyMetadata;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
+import org.bouncycastle.pqc.jcajce.spec.DilithiumParameterSpec;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for key status enforcement in PQCProducer. Validates that REVOKED
keys are rejected for all operations, EXPIRED
+ * keys are rejected for signing/encapsulation but allowed for
verification/extraction, DEPRECATED keys produce a WARN
+ * but still function, and the strictKeyLifecycle flag controls enforcement.
+ */
+public class PQCKeyStatusEnforcementTest extends CamelTestSupport {
+
+ @TempDir
+ Path tempDir;
+
+ @EndpointInject("mock:signed")
+ protected MockEndpoint mockSigned;
+
+ @EndpointInject("mock:verified")
+ protected MockEndpoint mockVerified;
+
+ @EndpointInject("mock:signStrict")
+ protected MockEndpoint mockSignStrict;
+
+ @EndpointInject("mock:verifyStrict")
+ protected MockEndpoint mockVerifyStrict;
+
+ @EndpointInject("mock:signDisabled")
+ protected MockEndpoint mockSignDisabled;
+
+ @Produce("direct:sign")
+ protected ProducerTemplate signTemplate;
+
+ @Produce("direct:signAndVerify")
+ protected ProducerTemplate signAndVerifyTemplate;
+
+ @Produce("direct:signDisabledStrict")
+ protected ProducerTemplate signDisabledStrictTemplate;
+
+ private KeyLifecycleManager keyManager;
+ private KeyPair sharedKeyPair;
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ // Sign-only route with strict lifecycle (default: true)
+ from("direct:sign")
+
.to("pqc:sign?operation=sign&signatureAlgorithm=DILITHIUM")
+ .to("mock:signed");
+
+ // Sign then verify route with strict lifecycle
+ from("direct:signAndVerify")
+
.to("pqc:sign?operation=sign&signatureAlgorithm=DILITHIUM")
+ .to("mock:signStrict")
+
.to("pqc:verify?operation=verify&signatureAlgorithm=DILITHIUM")
+ .to("mock:verifyStrict");
+
+ // Sign route with strict lifecycle disabled
+ from("direct:signDisabledStrict")
+
.to("pqc:sign?operation=sign&signatureAlgorithm=DILITHIUM&strictKeyLifecycle=false")
+ .to("mock:signDisabled");
+ }
+ };
+ }
+
+ @AfterEach
+ public void cleanup() throws Exception {
+ if (keyManager != null) {
+ List<KeyMetadata> keys = keyManager.listKeys();
+ for (KeyMetadata metadata : keys) {
+ try {
+ keyManager.deleteKey(metadata.getKeyId());
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+ }
+ }
+
+ // -- REVOKED key tests --
+
+ @Test
+ void testRevokedKeyRejectedForSign() throws Exception {
+ // Revoke the key
+ keyManager.revokeKey("test-key", "Compromised");
+
+ // Attempt to sign with a revoked key should fail
+ assertThatThrownBy(() -> signTemplate.sendBodyAndHeader("Hello",
PQCConstants.KEY_ID, "test-key"))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContainingAll("revoked", "test-key");
+ }
+
+ @Test
+ void testRevokedKeyRejectedForVerify() throws Exception {
+ // First sign with an active key (no KEY_ID, so no enforcement)
+ mockSigned.expectedMessageCount(1);
+ signTemplate.sendBody("Hello");
+ mockSigned.assertIsSatisfied();
+
+ byte[] signature =
mockSigned.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class);
+ assertNotNull(signature);
+
+ // Now revoke the key and try to verify
+ keyManager.revokeKey("test-key", "Key compromise detected");
+
+ // Attempt to verify with a revoked key should also fail
+ assertThatThrownBy(() ->
signAndVerifyTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key"))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("revoked");
+ }
+
+ // -- EXPIRED key tests --
+
+ @Test
+ void testExpiredKeyRejectedForSign() throws Exception {
+ // Expire the key
+ keyManager.expireKey("test-key");
+
+ // Attempt to sign with an expired key should fail
+ assertThatThrownBy(() -> signTemplate.sendBodyAndHeader("Hello",
PQCConstants.KEY_ID, "test-key"))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContainingAll("expired", "test-key");
+ }
+
+ @Test
+ void testExpiredKeyAllowedForVerify() throws Exception {
+ // First sign with the active key (no KEY_ID to skip enforcement for
the sign step)
+ mockSignStrict.expectedMessageCount(1);
+ mockVerifyStrict.expectedMessageCount(1);
+ signAndVerifyTemplate.sendBody("Hello");
+ mockSignStrict.assertIsSatisfied();
+ mockVerifyStrict.assertIsSatisfied();
+
+ byte[] signature
+ =
mockSignStrict.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class);
+ assertNotNull(signature);
+
+ // Now expire the key
+ keyManager.expireKey("test-key");
+
+ // Verify should still work (expired keys allowed for verification)
+ mockVerifyStrict.reset();
+ mockVerifyStrict.expectedMessageCount(1);
+ mockSignStrict.reset();
+ mockSignStrict.expectedMessageCount(1);
+
+ // Sign without enforcement (no KEY_ID), then verify with expired key
(KEY_ID set)
+ signAndVerifyTemplate.sendBody("Hello");
+ mockVerifyStrict.assertIsSatisfied();
+
+ // Verification with an expired key should succeed (no exception)
+ assertTrue(
+
mockVerifyStrict.getExchanges().get(0).getMessage().getHeader(PQCConstants.VERIFY,
Boolean.class));
+ }
+
+ // -- DEPRECATED key tests --
+
+ @Test
+ void testDeprecatedKeyAllowedForSignWithWarning() throws Exception {
+ // Deprecate the key (simulating a rotation)
+ KeyMetadata metadata = keyManager.getKeyMetadata("test-key");
+ metadata.setStatus(KeyMetadata.KeyStatus.DEPRECATED);
+ keyManager.updateKeyMetadata("test-key", metadata);
+
+ // Sign should still work with a deprecated key (just logs a warning)
+ mockSigned.expectedMessageCount(1);
+ signTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key");
+ mockSigned.assertIsSatisfied();
+
+ assertNotNull(
+
mockSigned.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
+ }
+
+ @Test
+ void testDeprecatedKeyAllowedForVerifyWithWarning() throws Exception {
+ // First sign with active key
+ mockSignStrict.expectedMessageCount(1);
+ mockVerifyStrict.expectedMessageCount(1);
+ signAndVerifyTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key");
+ mockSignStrict.assertIsSatisfied();
+ mockVerifyStrict.assertIsSatisfied();
+
+ // Now deprecate the key
+ KeyMetadata metadata = keyManager.getKeyMetadata("test-key");
+ metadata.setStatus(KeyMetadata.KeyStatus.DEPRECATED);
+ keyManager.updateKeyMetadata("test-key", metadata);
+
+ // Verify with deprecated key should work
+ mockSignStrict.reset();
+ mockSignStrict.expectedMessageCount(1);
+ mockVerifyStrict.reset();
+ mockVerifyStrict.expectedMessageCount(1);
+
+ signAndVerifyTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key");
+ mockVerifyStrict.assertIsSatisfied();
+ assertTrue(
+
mockVerifyStrict.getExchanges().get(0).getMessage().getHeader(PQCConstants.VERIFY,
Boolean.class));
+ }
+
+ // -- strictKeyLifecycle=false tests --
+
+ @Test
+ void testStrictKeyLifecycleDisabledAllowsRevokedKey() throws Exception {
+ // Revoke the key
+ keyManager.revokeKey("test-key", "Compromised");
+
+ // With strictKeyLifecycle=false, even revoked keys should work
+ mockSignDisabled.expectedMessageCount(1);
+ signDisabledStrictTemplate.sendBodyAndHeader("Hello",
PQCConstants.KEY_ID, "test-key");
+ mockSignDisabled.assertIsSatisfied();
+
+ assertNotNull(
+
mockSignDisabled.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
+ }
+
+ @Test
+ void testStrictKeyLifecycleDisabledAllowsExpiredKey() throws Exception {
+ // Expire the key
+ keyManager.expireKey("test-key");
+
+ // With strictKeyLifecycle=false, expired keys should work for signing
+ mockSignDisabled.expectedMessageCount(1);
+ signDisabledStrictTemplate.sendBodyAndHeader("Hello",
PQCConstants.KEY_ID, "test-key");
+ mockSignDisabled.assertIsSatisfied();
+
+ assertNotNull(
+
mockSignDisabled.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
+ }
+
+ // -- No KEY_ID header tests --
+
+ @Test
+ void testNoKeyIdHeaderSkipsEnforcement() throws Exception {
+ // Even if key is revoked, without KEY_ID header enforcement is skipped
+ keyManager.revokeKey("test-key", "Compromised");
+
+ mockSigned.expectedMessageCount(1);
+ // Send without KEY_ID header
+ signTemplate.sendBody("Hello");
+ mockSigned.assertIsSatisfied();
+
+ assertNotNull(
+
mockSigned.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
+ }
+
+ // -- ACTIVE key tests --
+
+ @Test
+ void testActiveKeyAllowedForSign() throws Exception {
+ mockSigned.expectedMessageCount(1);
+ signTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key");
+ mockSigned.assertIsSatisfied();
+
+ assertNotNull(
+
mockSigned.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
+ }
+
+ @Test
+ void testActiveKeyAllowedForSignAndVerify() throws Exception {
+ mockSignStrict.expectedMessageCount(1);
+ mockVerifyStrict.expectedMessageCount(1);
+ signAndVerifyTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key");
+ mockSignStrict.assertIsSatisfied();
+ mockVerifyStrict.assertIsSatisfied();
+ assertTrue(
+
mockVerifyStrict.getExchanges().get(0).getMessage().getHeader(PQCConstants.VERIFY,
Boolean.class));
+ }
+
+ // -- PENDING_ROTATION key tests --
+
+ @Test
+ void testPendingRotationKeyAllowedForSign() throws Exception {
+ KeyMetadata metadata = keyManager.getKeyMetadata("test-key");
+ metadata.setStatus(KeyMetadata.KeyStatus.PENDING_ROTATION);
+ keyManager.updateKeyMetadata("test-key", metadata);
+
+ mockSigned.expectedMessageCount(1);
+ signTemplate.sendBodyAndHeader("Hello", PQCConstants.KEY_ID,
"test-key");
+ mockSigned.assertIsSatisfied();
+
+ assertNotNull(
+
mockSigned.getExchanges().get(0).getMessage().getHeader(PQCConstants.SIGNATURE,
byte[].class));
+ }
+
+ // -- Registry bindings --
+
+ @BindToRegistry("Keypair")
+ public KeyPair setKeyPair() throws Exception {
+ ensureProviders();
+ if (sharedKeyPair == null) {
+ initKeyManager();
+ }
+ return sharedKeyPair;
+ }
+
+ @BindToRegistry("KeyLifecycleManager")
+ public KeyLifecycleManager getKeyLifecycleManager() throws Exception {
+ ensureProviders();
+ if (keyManager == null) {
+ initKeyManager();
+ }
+ return keyManager;
+ }
+
+ private static void ensureProviders() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) ==
null) {
+ Security.addProvider(new BouncyCastlePQCProvider());
+ }
+ }
+
+ private void initKeyManager() throws Exception {
+ keyManager = new FileBasedKeyLifecycleManager(tempDir.toString());
+
+ // Generate a key pair using KeyPairGenerator directly and store it
via the manager
+ KeyPairGenerator kpGen = KeyPairGenerator.getInstance(
+ PQCSignatureAlgorithms.DILITHIUM.getAlgorithm(),
+ PQCSignatureAlgorithms.DILITHIUM.getBcProvider());
+ kpGen.initialize(DilithiumParameterSpec.dilithium2, new
SecureRandom());
+ sharedKeyPair = kpGen.generateKeyPair();
+
+ // Store the key in the lifecycle manager so we can test status
enforcement
+ KeyMetadata metadata = new KeyMetadata("test-key", "DILITHIUM");
+ keyManager.storeKey("test-key", sharedKeyPair, metadata);
+ }
+}
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/PqcComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/PqcComponentBuilderFactory.java
index 23be92c87793..ef5b2cd6758c 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/PqcComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/PqcComponentBuilderFactory.java
@@ -388,6 +388,28 @@ public interface PqcComponentBuilderFactory {
return this;
}
+
+ /**
+ * Whether to enforce key status checks before cryptographic
operations.
+ * When enabled, REVOKED keys are rejected for all operations, EXPIRED
+ * keys are rejected for signing/encapsulation but allowed for
+ * verification/extraction, and DEPRECATED keys produce a warning but
+ * still function. Requires a KeyLifecycleManager and a CamelPQCKeyId
+ * header to be set.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: true
+ * Group: advanced
+ *
+ * @param strictKeyLifecycle the value to set
+ * @return the dsl builder
+ */
+ default PqcComponentBuilder strictKeyLifecycle(boolean
strictKeyLifecycle) {
+ doSetProperty("strictKeyLifecycle", strictKeyLifecycle);
+ return this;
+ }
+
/**
* In case we are using KEM operations, we need a Symmetric algorithm
to
* be defined for the flow to work.
@@ -499,6 +521,7 @@ public interface PqcComponentBuilderFactory {
case "signatureAlgorithm": getOrCreateConfiguration((PQCComponent)
component).setSignatureAlgorithm((java.lang.String) value); return true;
case "signer": getOrCreateConfiguration((PQCComponent)
component).setSigner((java.security.Signature) value); return true;
case "storeExtractedSecretKeyAsHeader":
getOrCreateConfiguration((PQCComponent)
component).setStoreExtractedSecretKeyAsHeader((boolean) value); return true;
+ case "strictKeyLifecycle": getOrCreateConfiguration((PQCComponent)
component).setStrictKeyLifecycle((boolean) value); return true;
case "symmetricKeyAlgorithm":
getOrCreateConfiguration((PQCComponent)
component).setSymmetricKeyAlgorithm((java.lang.String) value); return true;
case "symmetricKeyLength": getOrCreateConfiguration((PQCComponent)
component).setSymmetricKeyLength((int) value); return true;
case "healthCheckConsumerEnabled": ((PQCComponent)
component).setHealthCheckConsumerEnabled((boolean) value); return true;
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PQCEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PQCEndpointBuilderFactory.java
index 2e0bf7b2c35d..c88f80bcbc4c 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PQCEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PQCEndpointBuilderFactory.java
@@ -508,6 +508,46 @@ public interface PQCEndpointBuilderFactory {
doSetProperty("storeExtractedSecretKeyAsHeader",
storeExtractedSecretKeyAsHeader);
return this;
}
+ /**
+ * Whether to enforce key status checks before cryptographic
operations.
+ * When enabled, REVOKED keys are rejected for all operations, EXPIRED
+ * keys are rejected for signing/encapsulation but allowed for
+ * verification/extraction, and DEPRECATED keys produce a warning but
+ * still function. Requires a KeyLifecycleManager and a CamelPQCKeyId
+ * header to be set.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: true
+ * Group: advanced
+ *
+ * @param strictKeyLifecycle the value to set
+ * @return the dsl builder
+ */
+ default AdvancedPQCEndpointBuilder strictKeyLifecycle(boolean
strictKeyLifecycle) {
+ doSetProperty("strictKeyLifecycle", strictKeyLifecycle);
+ return this;
+ }
+ /**
+ * Whether to enforce key status checks before cryptographic
operations.
+ * When enabled, REVOKED keys are rejected for all operations, EXPIRED
+ * keys are rejected for signing/encapsulation but allowed for
+ * verification/extraction, and DEPRECATED keys produce a warning but
+ * still function. Requires a KeyLifecycleManager and a CamelPQCKeyId
+ * header to be set.
+ *
+ * The option will be converted to a <code>boolean</code> type.
+ *
+ * Default: true
+ * Group: advanced
+ *
+ * @param strictKeyLifecycle the value to set
+ * @return the dsl builder
+ */
+ default AdvancedPQCEndpointBuilder strictKeyLifecycle(String
strictKeyLifecycle) {
+ doSetProperty("strictKeyLifecycle", strictKeyLifecycle);
+ return this;
+ }
/**
* In case we are using KEM operations, we need a Symmetric algorithm
to
* be defined for the flow to work.