Andrea Cosentino created CAMEL-25797:
----------------------------------------

             Summary: camel-pqc - the usage-count and PENDING_ROTATION key 
rotation signals never fire
                 Key: CAMEL-25797
                 URL: https://issues.apache.org/jira/browse/CAMEL-25797
             Project: Camel
          Issue Type: Bug
          Components: camel-pqc
            Reporter: Andrea Cosentino


{{KeyRotationScheduler}} documents three rotation signals: key age, usage 
count, and a {{PENDING_ROTATION}} status on the key metadata. Two of the three 
cannot trigger.

h3. 1. {{usageCount}} is only ever advanced for stateful keys

{{KeyMetadata.updateLastUsed()}} is the only thing that increments the counter 
— {{KeyMetadata.java:81-84}}:
{code:java}
    public void updateLastUsed() {
        this.lastUsedAt = Instant.now();
        this.usageCount++;
{code}

It has exactly one production caller, {{PQCProducer.java:1185}}, inside 
{{persistStatefulKeyStateAfterSign}}, which returns early for anything that is 
not XMSS/XMSSMT/LMS/HSS — {{PQCProducer.java:1159-1162}}:
{code:java}
        long remaining = getStatefulKeyRemaining(privateKey);
        if (remaining < 0) {
            // Not a stateful key
            return;
        }
{code}

So for ML-DSA, SLH-DSA, Falcon and every KEM key — the overwhelming majority — 
{{usageCount}} stays {{0}} forever, while all five managers gate rotation on 
it, e.g. {{InMemoryKeyLifecycleManager.java:158}}:
{code:java}
        return maxUsage > 0 && keyMetadata.getUsageCount() >= maxUsage;
{code}

{{KeyRotationScheduler.setMaxKeyUsage}} and the documented 
{{camel.component.pqc.key-rotation-max-usage}} property 
({{pqc-key-lifecycle.adoc:975}}, "rotate keys used more than 1,000,000 times") 
are therefore inert.

h3. 2. {{PENDING_ROTATION}} is filtered out before it is consulted

{{KeyRotationScheduler.java:79}}:
{code:java}
    private Predicate<KeyMetadata> keyFilter = metadata -> metadata.getStatus() 
== KeyMetadata.KeyStatus.ACTIVE;
{code}

{{KeyRotationScheduler.java:130-137}}:
{code:java}
            for (KeyMetadata metadata : keys) {
                if (metadata == null || !keyFilter.test(metadata)) {
                    continue;
                }
                String keyId = metadata.getKeyId();
                try {
                    if (keyManager.needsRotation(keyId, maxKeyAge, 
maxKeyUsage)) {
{code}

{{KeyMetadata.needsRotation()}} ({{:130-132}}) does check the status:
{code:java}
    public boolean needsRotation() {
        return status == KeyStatus.PENDING_ROTATION
                || (nextRotationAt != null && 
Instant.now().isAfter(nextRotationAt));
{code}

but a key whose status an operator set to {{PENDING_ROTATION}} is no longer 
{{ACTIVE}}, so it is {{continue}}d at {{:132}} before {{needsRotation}} ever 
runs. Only {{nextRotationAt}} on an otherwise-ACTIVE key works today.

The scheduler javadoc ({{:62-64}}) and {{pqc-key-lifecycle.adoc:929-931}} both 
advertise {{PENDING_ROTATION}} as a supported signal.

h3. Suggested fix

Increment {{usageCount}} on every successful sign/verify/KEM operation, not 
just the stateful path. Widen the default {{keyFilter}} to {{ACTIVE}} or 
{{PENDING_ROTATION}}, or test {{needsRotation()}} before applying the filter.

h3. Test gap

{{KeyRotationSchedulerTest}} increments {{usageCount}} by hand ({{:87}}), which 
hides that production never does, and never asserts that a {{PENDING_ROTATION}} 
key rotates.

----
_Reported by Claude Code (AI agent) on behalf of [~oscerd]. Every code 
reference was verified against commit {{7aae27315946}} on {{main}}._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to