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)

Reply via email to