Repository navigation
Sign and verify ledger signatures with every service signing identity - #8766
Draft
Max (maxtropets) wants to merge 4 commits into
Draft
Max (maxtropets) wants to merge 4 commits into
Max (maxtropets) wants to merge 4 commits into
Conversation
11 of 23 tasks
Max (maxtropets)
force-pushed
the
maxtropets-sturdy-bassoon
branch
from
November 13, 2026 15:38
610c884 to
3e43911
Compare
Max (maxtropets)
force-pushed
the
maxtropets-sturdy-bassoon
branch
from
November 13, 2026 15:46
3e43911 to
a02fc1f
Compare
Max (maxtropets)
changed the base branch from
maxtropets-scaling-fishstick
to
maxtropets-fluffy-fiesta
November 13, 2026 15:47
Max (maxtropets)
force-pushed
the
maxtropets-sturdy-bassoon
branch
from
November 13, 2026 18:13
a02fc1f to
8c1c6da
Compare
Signature transactions carry one COSE signature per service signing key: CLASSICAL with the EC key and, for a service with an ML-DSA identity, PQ with the ML-DSA key, with the same protected header except alg and kid. verify_root_signatures verifies every COSE signature written by the transaction with the signing identity of its type, keeping one cached verifier per type, and requires a signature for every identity published in signing_identities. A signature of a type the build cannot verify throws UnverifiableSignatureType, and public recovery stops at that transaction. Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4440247d330d4afc9d8d425064fa47be
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4440247d330d4afc9d8d425064fa47be
Raft logs exceptions from apply() at DEBUG, so the cause is logged by verify_root_signatures for every caller, and public recovery logs only that it aborts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4440247d330d4afc9d8d425064fa47be
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4440247d330d4afc9d8d425064fa47be
Amaury Chamayou (achamayou)
force-pushed
the
maxtropets-sturdy-bassoon
branch
from
November 14, 2026 12:25
6e9b5cc to
9429b12
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relates to #7848. Stacked on #8768.
Every signature transaction carries one COSE signature per service signing key, and nodes verify all of them when they replay the ledger. This delivers the MVP goals "every signature transaction carries a CLASSICAL and a PQ COSE_Sign1" and "C++ verifies both on replay".
MerkleTreeHistoryPendingTx::callsigns the root with the EC key intocose_signatures[CLASSICAL]and, when the history holds an ML-DSA key, with that key intocose_signatures[PQ]. Both share the protected header (iat,iss,sub,txid,vds);algfollows the key andkidiskid_from_keyof its SPKI DER.verify_root_signaturesverifies everycose_signaturesentry written at the version withget_service_signing_identity(tx, type)(CLASSICAL keeps theservice.info.certfallback), through one cached verifier per type, and requires a signature for every type published insigning_identities.ccf::kv::UnverifiableSignatureType, and public recovery rethrows it, so the node aborts at that transaction.Choices to review
signing_identities[CLASSICAL]is published, a signature transaction carrying only a node signature fails. 7.x publishes that entry and, like 8.x, always writes a CLASSICAL COSE signature; the checked-intests/testdataledgers publish no signing identities; no 8.x release exists. AL4 ledgers written bymainbetween Publish the PQ service signing identity #8696 and this PR publishsigning_identities[PQ]with CLASSICAL-only signatures, and fail replay from here on.Safety and compatibility
select_described_cose_signature),resolve_latest_sig_view, the last signed seqno ofGET /node/state, and the Python SDK, which reads key 0 ofcose_signatures.Validation (scoped to this layer, at the reviewer's request for the draft stack)
Ran:
history_teston an ML-DSA build (OpenSSL 3.5.4,-DTEST_MLDSA=ON) and on a build without ML-DSA (OpenSSL 3.3.5): 16/16 pass on both. New cases cover the shared header, tampered and foreign PQ signatures, strict presence for PQ and CLASSICAL, a PQ signature without a PQ identity, and unknown identity types; on the build without ML-DSA a PQ signature raisesUnverifiableSignatureType.snapshotter_teston the build without ML-DSA: 21/21 pass.historical_queries_teston the ML-DSA build: 45 pass, 2 skipped by design.recovery_testthrough./tests.shon the ML-DSA build: passed in 210 s. Every signature transaction in the ledgers it wrote carriescose_signatureskeys 0 and 1, and the teardown ledger validation passed.src/node/recovery_decision_protocol.cpp(includeshistory.h,node_state.h,kv_types.h) with the ML-DSA compile database: one finding, the existing ML-DSA-only dead storeprev_service_has_pq_certincreate_service.cpp-format-checks.sh,doc-newline-checks.sh,ascii-checks.sh,todo-checks.sh,release-notes-checks.sh, prettier on the change note.Not run: e2e on a build without ML-DSA, other e2e on the ML-DSA build, clang-tidy with the AL3 compile database, Sphinx build, full
scripts/ci-checks.sh. The recovery abort inrecover_public_ledger_entrieshas no automated test: it needs a build without ML-DSA recovering a PQ-signed ledger.