This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-23248 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2e8837a7ad452efdf3e217a511c6f999ba59b170 Author: Andrea Cosentino <[email protected]> AuthorDate: Wed Mar 25 12:23:39 2026 +0100 CAMEL-23248 - Camel-PQC: Add stateful key usage tracking and warnings for XMSS/LMS Add pre-sign remaining signature checks for stateful hash-based signature schemes (XMSS, XMSSMT, LMS/HSS). When remaining signatures reach zero, an IllegalStateException is thrown to prevent key reuse. When below a configurable threshold (default 10%), a WARN log is emitted. Add PQCStatefulKeyHealthCheck extending AbstractHealthCheck to report stateful key capacity via Camel's health API (remaining signatures, total capacity, exhaustion status). Add statefulKeyWarningThreshold config parameter to PQCConfiguration. Persist key state through KeyLifecycleManager after each signing to prevent index reuse across restarts. Add camel-health dependency to pom.xml. Add PQCStatefulKeyTrackingTest with 4 unit tests covering signature count decrease, key exhaustion, and StatefulKeyState model behavior. Signed-off-by: Andrea Cosentino <[email protected]> --- components/camel-pqc/pom.xml | 4 + .../component/pqc/PQCComponentConfigurer.java | 6 + .../camel/component/pqc/PQCEndpointConfigurer.java | 6 + .../camel/component/pqc/PQCEndpointUriFactory.java | 3 +- .../org/apache/camel/component/pqc/pqc.json | 22 +-- .../camel/component/pqc/PQCConfiguration.java | 22 +++ .../apache/camel/component/pqc/PQCProducer.java | 141 ++++++++++++++- .../component/pqc/PQCStatefulKeyHealthCheck.java | 103 +++++++++++ .../component/pqc/PQCStatefulKeyTrackingTest.java | 194 +++++++++++++++++++++ 9 files changed, 489 insertions(+), 12 deletions(-) diff --git a/components/camel-pqc/pom.xml b/components/camel-pqc/pom.xml index fcc695c27b7f..e641acefda96 100644 --- a/components/camel-pqc/pom.xml +++ b/components/camel-pqc/pom.xml @@ -42,6 +42,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-health</artifactId> + </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> 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 9044003da182..39e46f037f72 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 @@ -69,6 +69,8 @@ public class PQCComponentConfigurer extends PropertyConfigurerSupport implements case "signaturealgorithm": case "signatureAlgorithm": getOrCreateConfiguration(target).setSignatureAlgorithm(property(camelContext, java.lang.String.class, value)); return true; case "signer": getOrCreateConfiguration(target).setSigner(property(camelContext, java.security.Signature.class, value)); return true; + case "statefulkeywarningthreshold": + case "statefulKeyWarningThreshold": getOrCreateConfiguration(target).setStatefulKeyWarningThreshold(property(camelContext, double.class, value)); return true; case "storeextractedsecretkeyasheader": case "storeExtractedSecretKeyAsHeader": getOrCreateConfiguration(target).setStoreExtractedSecretKeyAsHeader(property(camelContext, boolean.class, value)); return true; case "strictkeylifecycle": @@ -128,6 +130,8 @@ public class PQCComponentConfigurer extends PropertyConfigurerSupport implements case "signaturealgorithm": case "signatureAlgorithm": return java.lang.String.class; case "signer": return java.security.Signature.class; + case "statefulkeywarningthreshold": + case "statefulKeyWarningThreshold": return double.class; case "storeextractedsecretkeyasheader": case "storeExtractedSecretKeyAsHeader": return boolean.class; case "strictkeylifecycle": @@ -183,6 +187,8 @@ public class PQCComponentConfigurer extends PropertyConfigurerSupport implements case "signaturealgorithm": case "signatureAlgorithm": return getOrCreateConfiguration(target).getSignatureAlgorithm(); case "signer": return getOrCreateConfiguration(target).getSigner(); + case "statefulkeywarningthreshold": + case "statefulKeyWarningThreshold": return getOrCreateConfiguration(target).getStatefulKeyWarningThreshold(); case "storeextractedsecretkeyasheader": case "storeExtractedSecretKeyAsHeader": return getOrCreateConfiguration(target).isStoreExtractedSecretKeyAsHeader(); case "strictkeylifecycle": 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 fe426f7b55dd..97181137be25 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 @@ -55,6 +55,8 @@ public class PQCEndpointConfigurer extends PropertyConfigurerSupport implements case "signaturealgorithm": case "signatureAlgorithm": target.getConfiguration().setSignatureAlgorithm(property(camelContext, java.lang.String.class, value)); return true; case "signer": target.getConfiguration().setSigner(property(camelContext, java.security.Signature.class, value)); return true; + case "statefulkeywarningthreshold": + case "statefulKeyWarningThreshold": target.getConfiguration().setStatefulKeyWarningThreshold(property(camelContext, double.class, value)); return true; case "storeextractedsecretkeyasheader": case "storeExtractedSecretKeyAsHeader": target.getConfiguration().setStoreExtractedSecretKeyAsHeader(property(camelContext, boolean.class, value)); return true; case "strictkeylifecycle": @@ -107,6 +109,8 @@ public class PQCEndpointConfigurer extends PropertyConfigurerSupport implements case "signaturealgorithm": case "signatureAlgorithm": return java.lang.String.class; case "signer": return java.security.Signature.class; + case "statefulkeywarningthreshold": + case "statefulKeyWarningThreshold": return double.class; case "storeextractedsecretkeyasheader": case "storeExtractedSecretKeyAsHeader": return boolean.class; case "strictkeylifecycle": @@ -155,6 +159,8 @@ public class PQCEndpointConfigurer extends PropertyConfigurerSupport implements case "signaturealgorithm": case "signatureAlgorithm": return target.getConfiguration().getSignatureAlgorithm(); case "signer": return target.getConfiguration().getSigner(); + case "statefulkeywarningthreshold": + case "statefulKeyWarningThreshold": return target.getConfiguration().getStatefulKeyWarningThreshold(); case "storeextractedsecretkeyasheader": case "storeExtractedSecretKeyAsHeader": return target.getConfiguration().isStoreExtractedSecretKeyAsHeader(); case "strictkeylifecycle": 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 1f1bfe4747fe..c151e1f44691 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<>(22); + Set<String> props = new HashSet<>(23); props.add("classicalKEMAlgorithm"); props.add("classicalKeyAgreement"); props.add("classicalKeyPair"); @@ -42,6 +42,7 @@ public class PQCEndpointUriFactory extends org.apache.camel.support.component.En props.add("operation"); props.add("signatureAlgorithm"); props.add("signer"); + props.add("statefulKeyWarningThreshold"); props.add("storeExtractedSecretKeyAsHeader"); props.add("strictKeyLifecycle"); props.add("symmetricKeyAlgorithm"); 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 04323a8b95c0..c1e44735e9ae 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 @@ -43,12 +43,13 @@ "keyStorePassword": { "index": 16, "kind": "property", "displayName": "Key Store Password", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration", "configurationField": "configuration", "description": "The KeyStore password to use in combination with KeyStore Parameter" }, "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 [...] - "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 [...] + "statefulKeyWarningThreshold": { "index": 19, "kind": "property", "displayName": "Stateful Key Warning Threshold", "group": "advanced", "label": "advanced", "required": false, "type": "number", "javaType": "double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0.1, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration", "configurationField": "configuration", "description": "The warning threshold for stateful key exh [...] + "storeExtractedSecretKeyAsHeader": { "index": 20, "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 [...] + "strictKeyLifecycle": { "index": 21, "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": 22, "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": 23, "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": 24, "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": 25, "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,9 +95,10 @@ "keyStorePassword": { "index": 15, "kind": "parameter", "displayName": "Key Store Password", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration", "configurationField": "configuration", "description": "The KeyStore password to use in combination with KeyStore Parameter" }, "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 [...] - "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" } + "statefulKeyWarningThreshold": { "index": 18, "kind": "parameter", "displayName": "Stateful Key Warning Threshold", "group": "advanced", "label": "advanced", "required": false, "type": "number", "javaType": "double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0.1, "configurationClass": "org.apache.camel.component.pqc.PQCConfiguration", "configurationField": "configuration", "description": "The warning threshold for stateful key ex [...] + "storeExtractedSecretKeyAsHeader": { "index": 19, "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 [...] + "strictKeyLifecycle": { "index": 20, "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": 21, "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": 22, "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/java/org/apache/camel/component/pqc/PQCConfiguration.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCConfiguration.java index 938e2f706866..bfac40af1cb2 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 @@ -114,6 +114,14 @@ public class PQCConfiguration implements Cloneable { @Metadata(label = "advanced") private boolean strictKeyLifecycle = true; + @UriParam(defaultValue = "0.1", + description = "The warning threshold for stateful key exhaustion as a fraction of total signatures (0.0 to 1.0). " + + "When the remaining signatures for a stateful key (XMSS, XMSSMT, LMS/HSS) drop below this " + + "fraction of the total capacity, a WARN log is emitted. When remaining signatures reach zero, " + + "an exception is thrown to prevent key reuse. Set to 0 to disable warnings.") + @Metadata(label = "advanced") + private double statefulKeyWarningThreshold = 0.1; + public PQCOperations getOperation() { return operation; } @@ -339,6 +347,20 @@ public class PQCConfiguration implements Cloneable { this.strictKeyLifecycle = strictKeyLifecycle; } + public double getStatefulKeyWarningThreshold() { + return statefulKeyWarningThreshold; + } + + /** + * The warning threshold for stateful key exhaustion as a fraction of total signatures (0.0 to 1.0). When the + * remaining signatures for a stateful key (XMSS, XMSSMT, LMS/HSS) drop below this fraction of the total capacity, a + * WARN log is emitted. When remaining signatures reach zero, an exception is thrown to prevent key reuse. Set to 0 + * to disable warnings. + */ + public void setStatefulKeyWarningThreshold(double statefulKeyWarningThreshold) { + this.statefulKeyWarningThreshold = statefulKeyWarningThreshold; + } + // ************************************************* // // ************************************************* 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 d3e7ef4c029a..ed169ae65e7f 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 @@ -36,6 +36,9 @@ import org.apache.camel.component.pqc.crypto.hybrid.HybridSignature; import org.apache.camel.component.pqc.lifecycle.KeyLifecycleManager; import org.apache.camel.component.pqc.lifecycle.KeyMetadata; import org.apache.camel.component.pqc.stateful.StatefulKeyState; +import org.apache.camel.health.HealthCheck; +import org.apache.camel.health.HealthCheckHelper; +import org.apache.camel.health.WritableHealthCheckRepository; import org.apache.camel.support.DefaultProducer; import org.apache.camel.util.ObjectHelper; import org.bouncycastle.jcajce.SecretKeyWithEncapsulation; @@ -108,6 +111,10 @@ public class PQCProducer extends DefaultProducer { private KeyAgreement classicalKeyAgreement; private KeyPair classicalKeyPair; + // Health check fields + private HealthCheck producerHealthCheck; + private WritableHealthCheckRepository healthCheckRepository; + public PQCProducer(Endpoint endpoint) { super(endpoint); } @@ -381,10 +388,34 @@ public class PQCProducer extends DefaultProducer { // Initialize classical key pair classicalKeyPair = getConfiguration().getClassicalKeyPair(); } + + // Register health check for stateful key monitoring + healthCheckRepository = HealthCheckHelper.getHealthCheckRepository( + getEndpoint().getCamelContext(), + "producers", + WritableHealthCheckRepository.class); + + if (ObjectHelper.isNotEmpty(healthCheckRepository)) { + String id = getEndpoint().getId(); + producerHealthCheck = new PQCStatefulKeyHealthCheck(getEndpoint(), id); + producerHealthCheck.setEnabled(getEndpoint().getComponent().isHealthCheckProducerEnabled()); + healthCheckRepository.addHealthCheck(producerHealthCheck); + } + } + + @Override + protected void doStop() throws Exception { + if (ObjectHelper.isNotEmpty(healthCheckRepository) && ObjectHelper.isNotEmpty(producerHealthCheck)) { + healthCheckRepository.removeHealthCheck(producerHealthCheck); + producerHealthCheck = null; + } + super.doStop(); } private void signature(Exchange exchange) - throws InvalidPayloadException, InvalidKeyException, SignatureException { + throws Exception { + checkStatefulKeyBeforeSign(); + String payload = exchange.getMessage().getMandatoryBody(String.class); signer.initSign(keyPair.getPrivate()); @@ -392,6 +423,8 @@ public class PQCProducer extends DefaultProducer { byte[] signature = signer.sign(); exchange.getMessage().setHeader(PQCConstants.SIGNATURE, signature); + + persistStatefulKeyStateAfterSign(exchange); } private void verification(Exchange exchange) @@ -812,6 +845,112 @@ public class PQCProducer extends DefaultProducer { } } + /** + * Checks whether the current key is a stateful signature key (XMSS, XMSSMT, LMS/HSS) and if so, validates that it + * has remaining signatures available. Logs a warning when remaining signatures fall below the configured threshold. + * + * @throws IllegalStateException if the key has zero remaining signatures + */ + private void checkStatefulKeyBeforeSign() { + if (keyPair == null || keyPair.getPrivate() == null) { + return; + } + + PrivateKey privateKey = keyPair.getPrivate(); + long remaining = getStatefulKeyRemaining(privateKey); + if (remaining < 0) { + // Not a stateful key + return; + } + + if (remaining <= 0) { + throw new IllegalStateException( + "Stateful key (" + privateKey.getAlgorithm() + ") is exhausted with 0 remaining signatures. " + + "The key must not be reused — generate a new key pair."); + } + + double threshold = getConfiguration().getStatefulKeyWarningThreshold(); + if (threshold > 0) { + long totalCapacity = getStatefulKeyIndex(privateKey) + remaining; + if (totalCapacity > 0) { + double fractionRemaining = (double) remaining / totalCapacity; + if (fractionRemaining <= threshold) { + LOG.warn( + "Stateful key ({}) is approaching exhaustion: {} signatures remaining out of {} total ({} remaining). " + + "Consider generating a new key pair.", + privateKey.getAlgorithm(), remaining, totalCapacity, + String.format("%.1f%%", fractionRemaining * 100)); + } + } + } + } + + /** + * Persists stateful key state after a signing operation through the KeyLifecycleManager, if configured. This + * ensures the key index is tracked across restarts. + */ + private void persistStatefulKeyStateAfterSign(Exchange exchange) throws Exception { + if (keyPair == null || keyPair.getPrivate() == null) { + return; + } + + PrivateKey privateKey = keyPair.getPrivate(); + long remaining = getStatefulKeyRemaining(privateKey); + if (remaining < 0) { + // Not a stateful key + return; + } + + KeyLifecycleManager klm = getConfiguration().getKeyLifecycleManager(); + if (klm == null) { + return; + } + + String keyId = exchange.getMessage().getHeader(PQCConstants.KEY_ID, String.class); + if (ObjectHelper.isEmpty(keyId)) { + return; + } + + // Update metadata with current usage + KeyMetadata metadata = klm.getKeyMetadata(keyId); + if (metadata != null) { + metadata.updateLastUsed(); + klm.updateKeyMetadata(keyId, metadata); + } + + // Persist the updated key (with new index) so state survives restarts + klm.storeKey(keyId, keyPair, metadata); + } + + /** + * Returns the remaining signatures for a stateful private key, or -1 if the key is not stateful. + */ + private long getStatefulKeyRemaining(PrivateKey privateKey) { + if (privateKey instanceof XMSSPrivateKey) { + return ((XMSSPrivateKey) privateKey).getUsagesRemaining(); + } else if (privateKey instanceof XMSSMTPrivateKey) { + return ((XMSSMTPrivateKey) privateKey).getUsagesRemaining(); + } else if (privateKey instanceof LMSPrivateKey) { + return ((LMSPrivateKey) privateKey).getUsagesRemaining(); + } + return -1; + } + + /** + * Returns the current index (number of signatures already produced) for a stateful private key, or 0 if the key is + * not stateful. + */ + private long getStatefulKeyIndex(PrivateKey privateKey) { + if (privateKey instanceof XMSSPrivateKey) { + return ((XMSSPrivateKey) privateKey).getIndex(); + } else if (privateKey instanceof XMSSMTPrivateKey) { + return ((XMSSMTPrivateKey) privateKey).getIndex(); + } else if (privateKey instanceof LMSPrivateKey) { + return ((LMSPrivateKey) privateKey).getIndex(); + } + return 0; + } + // ========== Configuration Validation ========== /** diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCStatefulKeyHealthCheck.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCStatefulKeyHealthCheck.java new file mode 100644 index 000000000000..23acc4e43a6c --- /dev/null +++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCStatefulKeyHealthCheck.java @@ -0,0 +1,103 @@ +/* + * 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.PrivateKey; +import java.util.Map; + +import org.apache.camel.health.HealthCheckResultBuilder; +import org.apache.camel.impl.health.AbstractHealthCheck; +import org.bouncycastle.pqc.jcajce.interfaces.LMSPrivateKey; +import org.bouncycastle.pqc.jcajce.interfaces.XMSSMTPrivateKey; +import org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey; + +/** + * Health check that reports the state of stateful PQC signature keys (XMSS, XMSSMT, LMS/HSS). These hash-based + * signature schemes have a finite number of signatures. This health check reports DOWN when a key is exhausted and + * includes remaining signature capacity as a detail. + */ +public class PQCStatefulKeyHealthCheck extends AbstractHealthCheck { + + private final PQCEndpoint endpoint; + + public PQCStatefulKeyHealthCheck(PQCEndpoint endpoint, String clientId) { + super("camel", "producer:pqc-stateful-key-" + clientId); + this.endpoint = endpoint; + } + + @Override + protected void doCall(HealthCheckResultBuilder builder, Map<String, Object> options) { + PQCConfiguration configuration = endpoint.getConfiguration(); + KeyPair keyPair = configuration.getKeyPair(); + + if (keyPair == null || keyPair.getPrivate() == null) { + builder.detail("stateful_key", false); + builder.up(); + return; + } + + PrivateKey privateKey = keyPair.getPrivate(); + long remaining = -1; + long index = 0; + String algorithm = privateKey.getAlgorithm(); + + if (privateKey instanceof XMSSPrivateKey) { + XMSSPrivateKey xmssKey = (XMSSPrivateKey) privateKey; + remaining = xmssKey.getUsagesRemaining(); + index = xmssKey.getIndex(); + } else if (privateKey instanceof XMSSMTPrivateKey) { + XMSSMTPrivateKey xmssmtKey = (XMSSMTPrivateKey) privateKey; + remaining = xmssmtKey.getUsagesRemaining(); + index = xmssmtKey.getIndex(); + } else if (privateKey instanceof LMSPrivateKey) { + LMSPrivateKey lmsKey = (LMSPrivateKey) privateKey; + remaining = lmsKey.getUsagesRemaining(); + index = lmsKey.getIndex(); + } + + if (remaining < 0) { + // Not a stateful key - always healthy + builder.detail("stateful_key", false); + builder.detail("algorithm", algorithm); + builder.up(); + return; + } + + builder.detail("stateful_key", true); + builder.detail("algorithm", algorithm); + builder.detail("remaining_signatures", remaining); + builder.detail("signatures_used", index); + builder.detail("total_capacity", index + remaining); + + if (remaining <= 0) { + builder.message("Stateful key (" + algorithm + ") is exhausted with 0 remaining signatures"); + builder.down(); + return; + } + + double threshold = configuration.getStatefulKeyWarningThreshold(); + long totalCapacity = index + remaining; + if (threshold > 0 && totalCapacity > 0) { + double fractionRemaining = (double) remaining / totalCapacity; + builder.detail("fraction_remaining", String.format("%.4f", fractionRemaining)); + builder.detail("warning_threshold", String.valueOf(threshold)); + } + + builder.up(); + } +} diff --git a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCStatefulKeyTrackingTest.java b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCStatefulKeyTrackingTest.java new file mode 100644 index 000000000000..fc7c133e96dd --- /dev/null +++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCStatefulKeyTrackingTest.java @@ -0,0 +1,194 @@ +/* + * 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.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.Signature; + +import org.apache.camel.BindToRegistry; +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.stateful.StatefulKeyState; +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.XMSSParameterSpec; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for stateful key usage tracking with XMSS. Uses a very small tree height (2) so the key has only 4 total + * signatures, making it feasible to test exhaustion and threshold warnings. + */ +public class PQCStatefulKeyTrackingTest extends CamelTestSupport { + + @EndpointInject("mock:signed") + protected MockEndpoint resultSigned; + + @EndpointInject("mock:state") + protected MockEndpoint resultState; + + @Produce("direct:sign") + protected ProducerTemplate templateSign; + + @Produce("direct:getState") + protected ProducerTemplate templateGetState; + + public PQCStatefulKeyTrackingTest() throws NoSuchAlgorithmException { + } + + 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()); + } + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:sign") + .to("pqc:sign?operation=sign&statefulKeyWarningThreshold=0.5") + .to("mock:signed"); + + from("direct:getState") + .to("pqc:state?operation=getKeyState") + .to("mock:state"); + } + }; + } + + @BindToRegistry("Keypair") + public KeyPair setKeyPair() throws Exception { + ensureProviders(); + KeyPairGenerator kpGen = KeyPairGenerator.getInstance( + PQCSignatureAlgorithms.XMSS.getAlgorithm(), + PQCSignatureAlgorithms.XMSS.getBcProvider()); + kpGen.initialize(new XMSSParameterSpec(2, XMSSParameterSpec.SHA256), new SecureRandom()); + return kpGen.generateKeyPair(); + } + + @BindToRegistry("Signer") + public Signature getSigner() throws NoSuchAlgorithmException, NoSuchProviderException { + ensureProviders(); + return Signature.getInstance( + PQCSignatureAlgorithms.XMSS.getAlgorithm(), + PQCSignatureAlgorithms.XMSS.getBcProvider()); + } + + @Test + void testSignDecreasesRemainingSignatures() throws Exception { + // Get initial state + resultState.expectedMessageCount(1); + templateGetState.sendBody("check"); + resultState.assertIsSatisfied(); + + StatefulKeyState initialState = resultState.getExchanges().get(0) + .getMessage().getHeader(PQCConstants.KEY_STATE, StatefulKeyState.class); + assertNotNull(initialState); + long initialRemaining = initialState.getUsagesRemaining(); + assertTrue(initialRemaining > 0, "Initial key should have remaining signatures"); + + // Sign once + resultSigned.expectedMessageCount(1); + templateSign.sendBody("Hello"); + resultSigned.assertIsSatisfied(); + + // Get state after signing + resultState.reset(); + resultState.expectedMessageCount(1); + templateGetState.sendBody("check"); + resultState.assertIsSatisfied(); + + StatefulKeyState afterState = resultState.getExchanges().get(0) + .getMessage().getHeader(PQCConstants.KEY_STATE, StatefulKeyState.class); + assertNotNull(afterState); + assertEquals(initialRemaining - 1, afterState.getUsagesRemaining(), + "Remaining signatures should decrease by 1 after signing"); + } + + @Test + void testKeyExhaustion() throws Exception { + ensureProviders(); + + // Create a fresh key with height=2 (4 signatures) + KeyPairGenerator kpGen = KeyPairGenerator.getInstance( + PQCSignatureAlgorithms.XMSS.getAlgorithm(), + PQCSignatureAlgorithms.XMSS.getBcProvider()); + kpGen.initialize(new XMSSParameterSpec(2, XMSSParameterSpec.SHA256), new SecureRandom()); + KeyPair exhaustionKeyPair = kpGen.generateKeyPair(); + + // Sign 4 times to exhaust the key + Signature xmssSigner = Signature.getInstance( + PQCSignatureAlgorithms.XMSS.getAlgorithm(), + PQCSignatureAlgorithms.XMSS.getBcProvider()); + + for (int i = 0; i < 4; i++) { + xmssSigner.initSign(exhaustionKeyPair.getPrivate()); + xmssSigner.update(("message" + i).getBytes()); + xmssSigner.sign(); + } + + // Now the key should be exhausted + org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey xmssPriv + = (org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey) exhaustionKeyPair.getPrivate(); + assertEquals(0, xmssPriv.getUsagesRemaining(), "Key should be exhausted after 4 signatures with height=2"); + } + + @Test + void testStatefulKeyStateNotExhausted() throws Exception { + ensureProviders(); + + KeyPairGenerator kpGen = KeyPairGenerator.getInstance( + PQCSignatureAlgorithms.XMSS.getAlgorithm(), + PQCSignatureAlgorithms.XMSS.getBcProvider()); + kpGen.initialize(new XMSSParameterSpec(2, XMSSParameterSpec.SHA256), new SecureRandom()); + KeyPair freshKeyPair = kpGen.generateKeyPair(); + + org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey xmssPriv + = (org.bouncycastle.pqc.jcajce.interfaces.XMSSPrivateKey) freshKeyPair.getPrivate(); + + StatefulKeyState state = new StatefulKeyState( + xmssPriv.getAlgorithm(), xmssPriv.getIndex(), xmssPriv.getUsagesRemaining()); + + assertFalse(state.isExhausted(), "Fresh key should not be exhausted"); + assertEquals(4, state.getUsagesRemaining(), "Fresh XMSS key with height=2 should have 4 remaining"); + assertEquals(0, state.getIndex(), "Fresh key should have index 0"); + } + + @Test + void testStatefulKeyStateExhausted() { + StatefulKeyState state = new StatefulKeyState("XMSS", 4, 0); + + assertTrue(state.isExhausted(), "Key with 0 remaining should be exhausted"); + assertEquals(0, state.getUsagesRemaining()); + assertEquals(4, state.getIndex()); + } +}
