Andrea Cosentino created CAMEL-25796:
----------------------------------------
Summary: camel-pqc - persist the stateful signature key index
before the signature is emitted
Key: CAMEL-25796
URL: https://issues.apache.org/jira/browse/CAMEL-25796
Project: Camel
Issue Type: Bug
Components: camel-pqc
Reporter: Andrea Cosentino
For the stateful hash-based signature schemes (LMS, HSS, XMSS, XMSSMT) each
private key index may be used exactly once. {{camel-pqc}} advances the index by
signing and persists the advanced key *afterwards*.
{{PQCProducer.java:523-537}}:
{code:java}
private void signature(Exchange exchange)
throws Exception {
checkStatefulKeyBeforeSign();
Signature signerForExchange = signerForExchange();
byte[] signature;
synchronized (signerForExchange) {
signerForExchange.initSign(keyPair.getPrivate());
updateSignatureFromBody(signerForExchange, exchange.getMessage());
signature = signerForExchange.sign();
}
exchange.getMessage().setHeader(PQCConstants.SIGNATURE, signature);
persistStatefulKeyStateAfterSign(exchange);
}
{code}
{{sign()}} consumes the index inside the Bouncy Castle private key and the
signature is published on the message. Only then is the advanced key written to
the store. Any failure in between — a crash, a {{kill -9}}, an exception inside
{{persistStatefulKeyStateAfterSign}} — leaves the emitted signature in the
world while the persisted key still sits at the old index. On restart the same
index is used again, which defeats the one-time-use property the whole scheme
rests on. The same ordering applies to hybrid signing at
{{PQCProducer.java:779}}.
h3. Silent skips make the window wider
{{persistStatefulKeyStateAfterSign}} ({{PQCProducer.java:1152-1190}}) returns
without any log when there is no {{KeyLifecycleManager}}:
{code:java}
KeyLifecycleManager klm = getConfiguration().getKeyLifecycleManager();
if (klm == null) {
return;
}
{code}
and again when the {{CamelPQCKeyId}} header is absent ({{:1170-1172}}). Only
the {{metadata == null}} case warns. So a stateful route with no key id header
advances the index on every exchange and persists nothing, silently.
h3. Not atomic with the exhaustion check either
{{checkStatefulKeyBeforeSign()}} runs at {{:525}}, outside the {{synchronized}}
block, and the lock is on a *per-exchange* {{Signature}} instance. Two
concurrent exchanges can both observe {{remaining == 1}}.
h3. Suggested fix
Reserve and persist the next index before signing, then sign. Where the backing
store cannot do that atomically, at minimum persist before publishing the
signature on the exchange, make the two silent {{return}} paths log a warning,
and serialise the check-and-sign for stateful algorithms.
h3. Test gap
{{PQCStatefulKeyTrackingTest}} covers only sequential signing and two
hand-constructed {{StatefulKeyState}} objects. Nothing covers crash-restart
index resumption or concurrent signing.
----
_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)