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)