This is an automated email from the ASF dual-hosted git repository.

acosentino pushed a commit to branch pqc-nodet
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 65b03fd1a5dbf8b3c21c0e13223e5bafd770c5e5
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Mar 26 11:30:10 2026 +0100

    CAMEL-PQC: Address Guillaume Nodet's review findings on PQC stateful key 
tracking
    
    Fix all 8 findings from post-merge review on PR #22264:
    - Simplify redundant remaining<=0 to remaining==0 after remaining<0 check
    - Health check reads runtime keyPair from producer instead of configuration
    - Health check reports DOWN with warning detail when capacity below 
threshold
    - Validate statefulKeyWarningThreshold is between 0.0 and 1.0
    - Guard null metadata in persistStatefulKeyStateAfterSign
    - Add producer-level exhaustion test exercising the Camel route
    - Extract duplicated instanceof dispatch into shared static helpers
    - Add checkStatefulKeyBeforeSign to hybridSignature method
    
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/component/pqc/PQCConfiguration.java      |  4 ++
 .../apache/camel/component/pqc/PQCProducer.java    | 54 +++++++++++-----------
 .../component/pqc/PQCStatefulKeyHealthCheck.java   | 54 +++++++++++-----------
 .../component/pqc/PQCStatefulKeyTrackingTest.java  | 40 ++++++++++++++++
 4 files changed, 97 insertions(+), 55 deletions(-)

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 bfac40af1cb2..d54f21e01d9f 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
@@ -358,6 +358,10 @@ public class PQCConfiguration implements Cloneable {
      * to disable warnings.
      */
     public void setStatefulKeyWarningThreshold(double 
statefulKeyWarningThreshold) {
+        if (statefulKeyWarningThreshold < 0.0 || statefulKeyWarningThreshold > 
1.0) {
+            throw new IllegalArgumentException(
+                    "statefulKeyWarningThreshold must be between 0.0 and 1.0, 
but was: " + 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 ed169ae65e7f..bc433c8a6f90 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
@@ -119,6 +119,14 @@ public class PQCProducer extends DefaultProducer {
         super(endpoint);
     }
 
+    /**
+     * Returns the runtime key pair used by this producer. Used by the health 
check to read actual key state rather than
+     * the configuration snapshot.
+     */
+    KeyPair getRuntimeKeyPair() {
+        return keyPair;
+    }
+
     @Override
     public void process(Exchange exchange) throws Exception {
         PQCOperations operation = determineOperation(exchange);
@@ -397,7 +405,7 @@ public class PQCProducer extends DefaultProducer {
 
         if (ObjectHelper.isNotEmpty(healthCheckRepository)) {
             String id = getEndpoint().getId();
-            producerHealthCheck = new PQCStatefulKeyHealthCheck(getEndpoint(), 
id);
+            producerHealthCheck = new PQCStatefulKeyHealthCheck(getEndpoint(), 
this, id);
             
producerHealthCheck.setEnabled(getEndpoint().getComponent().isHealthCheckProducerEnabled());
             healthCheckRepository.addHealthCheck(producerHealthCheck);
         }
@@ -500,7 +508,9 @@ public class PQCProducer extends DefaultProducer {
     // ========== Hybrid Signature Operations ==========
 
     private void hybridSignature(Exchange exchange)
-            throws InvalidPayloadException, InvalidKeyException, 
SignatureException {
+            throws Exception {
+        checkStatefulKeyBeforeSign();
+
         String payload = exchange.getMessage().getMandatoryBody(String.class);
         byte[] data = payload.getBytes();
 
@@ -526,6 +536,8 @@ public class PQCProducer extends DefaultProducer {
         exchange.getMessage().setHeader(PQCConstants.HYBRID_SIGNATURE, 
hybridSig);
         exchange.getMessage().setHeader(PQCConstants.CLASSICAL_SIGNATURE, 
components.classicalSignature());
         exchange.getMessage().setHeader(PQCConstants.PQC_SIGNATURE, 
components.pqcSignature());
+
+        persistStatefulKeyStateAfterSign(exchange);
     }
 
     private void hybridVerification(Exchange exchange)
@@ -789,15 +801,9 @@ public class PQCProducer extends DefaultProducer {
         }
 
         PrivateKey privateKey = keyPair.getPrivate();
-        long remaining;
+        long remaining = getStatefulKeyRemaining(privateKey);
 
-        if (privateKey instanceof XMSSPrivateKey) {
-            remaining = ((XMSSPrivateKey) privateKey).getUsagesRemaining();
-        } else if (privateKey instanceof XMSSMTPrivateKey) {
-            remaining = ((XMSSMTPrivateKey) privateKey).getUsagesRemaining();
-        } else if (privateKey instanceof LMSPrivateKey) {
-            remaining = ((LMSPrivateKey) privateKey).getUsagesRemaining();
-        } else {
+        if (remaining < 0) {
             throw new IllegalArgumentException(
                     "getRemainingSignatures is only supported for stateful 
signature schemes (XMSS, XMSSMT, LMS/HSS). "
                                                + "Key type: " + 
privateKey.getClass().getName());
@@ -812,23 +818,17 @@ public class PQCProducer extends DefaultProducer {
         }
 
         PrivateKey privateKey = keyPair.getPrivate();
-        StatefulKeyState state;
+        long remaining = getStatefulKeyRemaining(privateKey);
 
-        if (privateKey instanceof XMSSPrivateKey) {
-            XMSSPrivateKey xmssKey = (XMSSPrivateKey) privateKey;
-            state = new StatefulKeyState(privateKey.getAlgorithm(), 
xmssKey.getIndex(), xmssKey.getUsagesRemaining());
-        } else if (privateKey instanceof XMSSMTPrivateKey) {
-            XMSSMTPrivateKey xmssmtKey = (XMSSMTPrivateKey) privateKey;
-            state = new StatefulKeyState(privateKey.getAlgorithm(), 
xmssmtKey.getIndex(), xmssmtKey.getUsagesRemaining());
-        } else if (privateKey instanceof LMSPrivateKey) {
-            LMSPrivateKey lmsKey = (LMSPrivateKey) privateKey;
-            state = new StatefulKeyState(privateKey.getAlgorithm(), 
lmsKey.getIndex(), lmsKey.getUsagesRemaining());
-        } else {
+        if (remaining < 0) {
             throw new IllegalArgumentException(
                     "getKeyState is only supported for stateful signature 
schemes (XMSS, XMSSMT, LMS/HSS). "
                                                + "Key type: " + 
privateKey.getClass().getName());
         }
 
+        long index = getStatefulKeyIndex(privateKey);
+        StatefulKeyState state = new 
StatefulKeyState(privateKey.getAlgorithm(), index, remaining);
+
         exchange.getMessage().setHeader(PQCConstants.KEY_STATE, state);
     }
 
@@ -863,7 +863,7 @@ public class PQCProducer extends DefaultProducer {
             return;
         }
 
-        if (remaining <= 0) {
+        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.");
@@ -916,16 +916,16 @@ public class PQCProducer extends DefaultProducer {
         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);
+            // 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) {
+    static long getStatefulKeyRemaining(PrivateKey privateKey) {
         if (privateKey instanceof XMSSPrivateKey) {
             return ((XMSSPrivateKey) privateKey).getUsagesRemaining();
         } else if (privateKey instanceof XMSSMTPrivateKey) {
@@ -940,7 +940,7 @@ public class PQCProducer extends DefaultProducer {
      * 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) {
+    static long getStatefulKeyIndex(PrivateKey privateKey) {
         if (privateKey instanceof XMSSPrivateKey) {
             return ((XMSSPrivateKey) privateKey).getIndex();
         } else if (privateKey instanceof XMSSMTPrivateKey) {
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
index 23acc4e43a6c..2b3448865b64 100644
--- 
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
@@ -22,28 +22,29 @@ 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.
+ * signature schemes have a finite number of signatures. This health check 
reports DOWN when a key is exhausted,
+ * DEGRADED when remaining signatures fall below the warning threshold, and 
includes remaining signature capacity as a
+ * detail.
  */
 public class PQCStatefulKeyHealthCheck extends AbstractHealthCheck {
 
     private final PQCEndpoint endpoint;
+    private final PQCProducer producer;
 
-    public PQCStatefulKeyHealthCheck(PQCEndpoint endpoint, String clientId) {
+    public PQCStatefulKeyHealthCheck(PQCEndpoint endpoint, PQCProducer 
producer, String clientId) {
         super("camel", "producer:pqc-stateful-key-" + clientId);
         this.endpoint = endpoint;
+        this.producer = producer;
     }
 
     @Override
     protected void doCall(HealthCheckResultBuilder builder, Map<String, 
Object> options) {
-        PQCConfiguration configuration = endpoint.getConfiguration();
-        KeyPair keyPair = configuration.getKeyPair();
+        // Read key state from the producer's runtime keyPair, not from 
configuration,
+        // to avoid stale state when the key has been rotated or updated at 
runtime
+        KeyPair keyPair = producer.getRuntimeKeyPair();
 
         if (keyPair == null || keyPair.getPrivate() == null) {
             builder.detail("stateful_key", false);
@@ -52,23 +53,9 @@ public class PQCStatefulKeyHealthCheck extends 
AbstractHealthCheck {
         }
 
         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();
-        }
+        long remaining = PQCProducer.getStatefulKeyRemaining(privateKey);
+        long index = PQCProducer.getStatefulKeyIndex(privateKey);
 
         if (remaining < 0) {
             // Not a stateful key - always healthy
@@ -78,24 +65,35 @@ public class PQCStatefulKeyHealthCheck extends 
AbstractHealthCheck {
             return;
         }
 
+        long totalCapacity = index + remaining;
+
         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);
+        builder.detail("total_capacity", totalCapacity);
 
-        if (remaining <= 0) {
+        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;
+        double threshold = 
endpoint.getConfiguration().getStatefulKeyWarningThreshold();
         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));
+
+            if (fractionRemaining <= threshold) {
+                builder.message(
+                        "Stateful key (" + algorithm + ") is approaching 
exhaustion: " + remaining
+                                + " signatures remaining out of " + 
totalCapacity + " total ("
+                                + String.format("%.1f%%", fractionRemaining * 
100) + " remaining)");
+                builder.detail("warning", true);
+                builder.down();
+                return;
+            }
         }
 
         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
index fc7c133e96dd..0bd8a1f53186 100644
--- 
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
@@ -25,6 +25,7 @@ import java.security.Security;
 import java.security.Signature;
 
 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;
@@ -191,4 +192,43 @@ public class PQCStatefulKeyTrackingTest extends 
CamelTestSupport {
         assertEquals(0, state.getUsagesRemaining());
         assertEquals(4, state.getIndex());
     }
+
+    @Test
+    void testProducerExhaustionThrowsException() throws Exception {
+        // Sign 4 times to exhaust the key (XMSS height=2 allows exactly 4)
+        for (int i = 0; i < 4; i++) {
+            resultSigned.reset();
+            resultSigned.expectedMessageCount(1);
+            templateSign.sendBody("message" + i);
+            resultSigned.assertIsSatisfied();
+        }
+
+        // The 5th sign attempt through the producer should throw 
IllegalStateException
+        CamelExecutionException ex = 
assertThrows(CamelExecutionException.class,
+                () -> templateSign.sendBody("message4"));
+        assertInstanceOf(IllegalStateException.class, ex.getCause());
+        assertTrue(ex.getCause().getMessage().contains("exhausted"),
+                "Exception message should mention exhaustion");
+    }
+
+    @Test
+    void testWarningThresholdValidation() {
+        PQCConfiguration config = new PQCConfiguration();
+
+        // Valid values
+        config.setStatefulKeyWarningThreshold(0.0);
+        assertEquals(0.0, config.getStatefulKeyWarningThreshold());
+
+        config.setStatefulKeyWarningThreshold(0.5);
+        assertEquals(0.5, config.getStatefulKeyWarningThreshold());
+
+        config.setStatefulKeyWarningThreshold(1.0);
+        assertEquals(1.0, config.getStatefulKeyWarningThreshold());
+
+        // Invalid values
+        assertThrows(IllegalArgumentException.class,
+                () -> config.setStatefulKeyWarningThreshold(-0.1));
+        assertThrows(IllegalArgumentException.class,
+                () -> config.setStatefulKeyWarningThreshold(1.1));
+    }
 }

Reply via email to