Repository navigation
CAMEL-25796: camel-pqc - persist the stateful key index before the signature is published - #28099
Conversation
gnodet-bot
left a comment
There was a problem hiding this comment.
Review: persist-before-publish ordering + usage-count tracking
Ordering fix ✅ — recordKeyUsageAfterSign() is now called before setHeader(PQCConstants.SIGNATURE, …) in both signature() and hybridSignature(). The stateful key index (and usageCount) is therefore persisted before the signature is visible to the caller. Correct.
Test ✅ — The new test correctly discriminates the ordered from the out-of-order path.
Inline comment — one gap spotted in the usage-count path (see below).
This review was generated by an AI agent, Hermès on behalf of @gnodet.
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
4a986d9 to
1b7f1bc
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
The persist-before-publish reordering is correct, the previous finding on non-stateful key warning coverage is resolved, and no new issues were introduced. ✅
This review was generated by an AI agent, Hermès on behalf of @gnodet.
davsclaus
left a comment
There was a problem hiding this comment.
Thanks @oscerd. Reviewing the two CAMEL-25796 commits only, as you asked. Moving the persist before the headers is right. The gnodet-bot thread is addressed by warnKeyUsageNotRecorded, and I resolved it.
Three things to change:
-
The warning for stateless keys without a manager (inline). See the inline comment on
PQCProducer.java. -
What changes, in the upgrade guide and the description (inline). Before this PR a persist failure already failed the exchange:
recordKeyUsageAfterSignthrew out ofsignature(). What changes is that the failed message no longer carries the signature headers, so anonException(...).handled(true)or a dead letter channel can no longer pass the signature on. The same applies to a crash (kill -9): the header set before the persist never left the processor, so the order does not change that case. Please describe the error-handling case instead. Also, the last paragraph of the entry still speaks of stateful keys only, while the warning now covers every key. -
Stateless keys and the new order. With #28094 a stateless key records its usage too, so after this reordering a vault error while counting an ML-DSA signature drops the signature and fails the exchange. Losing the signature is the right trade for a one-time index, but not for a usage count. As on #28094, I would only fail the exchange for a stateful key, and log for the others.
The tests are clear. theSignatureIsNotPublishedWhenPersistenceFails is the one that fails on the base. CI is still running on 1b7f1bc.
Claude Code on behalf of Claus Ibsen
1b7f1bc to
c38801d
Compare
davsclaus
left a comment
There was a problem hiding this comment.
Thanks, both threads are fixed and I resolved them:
- With no
keyLifecycleManager, only a stateful key warns now, so a plainpqc:signstays quiet. - The upgrade note now says the right thing: the exchange already failed, and what is new is that the failed message no longer carries the signature headers.
One part of the third point is still open. For a stateless key, the updateKeyMetadata write is now caught and logged, but the klm.getKeyMetadata(keyId) read before it is not. On main, a stateless key returns before any call to the manager. With this stack, a vault that cannot be read fails the exchange. Because the usage is now recorded before the header is set, that exchange also loses a good signature. Catching the read for a stateless key, the same way as the write, would close it. That code comes from 28094, so the fix may belong there.
This branch now carries the three commits of #28094 (CAMEL-25797). Please merge it after #28094, or rebase it once #28094 is on main, so this PR only shows its own commit.
Claude Code on behalf of Claus Ibsen
|
🧪 CI tested the following changed modules:
🔬 Scalpel shadow comparison — Scalpel: 8 of 707 tested, 26 compile-only — current: 8 all testedMaveniverse Scalpel detected 8 affected modules (current approach: 8). Skip-tests mode would test 8 modules (2 direct + 7 downstream), skip tests for 26 (generated code, meta-modules) Modules Scalpel would test (8)
Modules with tests skipped (26)
All tested modules (35 modules, 5m 33s total)Total reactor time: 5m 33s
Top 20 slowest modules:
|
gnodet-bot
left a comment
There was a problem hiding this comment.
Thank you for this PR — the overall approach is solid. One bug to address before merge:
In recordKeyUsageAfterSign, the call to klm.getKeyMetadata(keyId) at line 1199 is not wrapped in a try-catch, yet the interface declares throws Exception and remote implementations (Hashicorp Vault, Azure Key Vault) perform a network read that can fail. If that read throws, the exception propagates up through signature() (no catch there) or hybridSignature() (rethrown as RuntimeCamelException), which fails the exchange and discards a perfectly good signature.
This directly contradicts the intent expressed in the try-catch around updateKeyMetadata just below (lines 1215-1223): "Failing the exchange over it would throw away a perfectly good signature to protect a counter whose loss merely delays a rotation." The same rationale applies to the read that precedes the write.
The fix is to extend the non-stateful try-catch to cover getKeyMetadata as well — either by moving the null-check + getKeyMetadata call inside the else branch's try block, or by wrapping both calls together.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
|
Rebased onto the updated #28094 (this branch carries its commits, so merge #28094 first, or this one shows only its own commit once #28094 is on
Verification: camel-pqc on JDK 21 ( Claude Code on behalf of oscerd |
c38801d to
a42a589
Compare
davsclaus
left a comment
There was a problem hiding this comment.
LGTM, thanks. The open point is fixed. The getKeyMetadata read for a non-stateful key is now caught (that comes from #28094's 71322b5, which this branch carries). The rebase resolved recordKeyUsageAfterSign correctly: the stateful/no-manager warning comes first, then the guarded read, then warnKeyUsageNotRecorded when there is no metadata. The persist still runs before the signature headers are set in signature() and hybridSignature(). I resolved the gnodet-bot thread.
- Merge order: please merge #28094 first, after its upgrade-guide fix (two of its paragraphs ended up in the hybridKdfAlgorithm section, see #28094). Then rebase this branch, so it shows only its own commit and does not bring the misplaced paragraphs along. Until then it conflicts with #28089 in the upgrade guide.
- Nit, inline: the last paragraph of the upgrade-guide note.
Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
gnodet-bot
left a comment
There was a problem hiding this comment.
Previous findings are all addressed. LGTM.
The getKeyMetadata call is now correctly wrapped in a try-catch with the right stateful/non-stateful split: re-throw for stateful keys (a persist failure on an unrecoverable index must fail the exchange), log-and-return for non-stateful keys (a usage-counter read failure should not discard a valid signature). All other exit paths consistently apply the stateful flag. The usageRecordFailureWarned AtomicBoolean one-time-warning pattern is the correct per-producer design.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
a42a589 to
af4a55e
Compare
|
Rebased onto the updated #28094 (previous head a42a589 → af4a55e).
Claude Code on behalf of oscerd |
…gnature is published sign() consumes the one-time index inside an LMS, HSS, XMSS or XMSSMT private key. The signature was placed on the message first and the advanced key persisted afterwards, so a crash, a kill -9, or any failure inside the persist step left an emitted signature in the world while the stored key still sat at the old index. On restart the key resumes there and signs a second time with an index that is already in use, which is precisely what the one-time property of these schemes forbids. The order is now reversed in both signature() and hybridSignature(): the key state is recorded first, and only then are the signature headers set. The window cannot be closed completely - BouncyCastle advances the index inside sign() and exposes no reserve-then-sign API - but a failure now fails the exchange instead of releasing a signature whose index advance was never stored. Losing a signature is recoverable; reusing an index is not. The three paths that quietly skipped persistence for a stateful key - no keyLifecycleManager, no CamelPQCKeyId header, no stored metadata - now log a warning naming the reason. It is emitted once per producer so a high volume route does not flood the log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
…ich keys warn without a keyLifecycleManager Only a stateful key warns when no keyLifecycleManager is configured; the note read as if every key did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
af4a55e to
1389b72
Compare
|
Rebased onto Claude Code on behalf of oscerd |
davsclaus
left a comment
There was a problem hiding this comment.
Re-review after the rebase (a42a589 -> 1389b72). Thanks Andrea.
With #28094 on main, the branch now shows only its own two commits. PQCProducer and PQCStatefulKeyPersistOrderTest are byte-identical to the version I approved. The new commit fixes the last paragraph of the upgrade-guide note: it now says that only a stateful key warns without a keyLifecycleManager, which matches the code. The section sits after the #28094 one, so the earlier conflict is gone.
LGTM once CI is green.
This review was generated by an AI agent (Claude Code on behalf of davsclaus) and may contain inaccuracies. Please verify all suggestions before applying.
Fixes CAMEL-25796.
Problem
sign()consumes the one-time index inside an LMS, HSS, XMSS or XMSSMT private key. The signature was publishedfirst and the advanced key persisted afterwards:
A crash, a
kill -9, or any failure inside the persist step leaves an emitted signature in the world while thestored key still sits at the old index. On restart the key resumes there and signs a second time with an index
already in use — exactly what the one-time property of these schemes forbids.
Three further paths skipped persistence silently for a stateful key: no
keyLifecycleManager, noCamelPQCKeyIdheader, and no stored metadata. Only the last one warned.
Fix
The order is reversed in both
signature()andhybridSignature()— key state recorded first, signature headersset afterwards.
The window cannot be closed completely: BouncyCastle advances the index inside
sign()and exposes noreserve-then-sign API, so a true reservation is not available. What this buys is that a persistence failure now
fails the exchange instead of releasing a signature whose index advance was never stored. Losing a signature is
recoverable; reusing a one-time index is not.
The three silent paths now log a warning naming the specific reason, once per producer so a high-volume route does
not flood the log.
Tests
New
PQCStatefulKeyPersistOrderTest, using anInMemoryKeyLifecycleManagerwhosestoreKeycan be made to fail(standing in for an unreachable vault):
aFailureToPersistTheIndexFailsTheExchangetheSignatureIsNotPublishedWhenPersistenceFails— the discriminating test: against the pre-change baseline asignature is published despite the failure
aSuccessfulSignStillPersistsAndPublishes— guards the happy path, asserting exactly onestoreKeyand a usagecount of 1
Full
camel-pqcbuild green: 244 tests, 0 failures, no regenerated-file drift.Upgrade guide
Added a 4.24 entry — an exchange that previously returned a signature with unpersisted key state now fails, which is
a visible behaviour change even though it is the safer outcome.
Reported, analysed and fixed by Claude Code (AI agent) on behalf of @oscerd.
🤖 Generated with Claude Code