Repository navigation
PQ endorsement fetching: fetch, validate and serve the ML-DSA endorsement chain and signing keys - #8768
Draft
Max (maxtropets) wants to merge 4 commits into
Draft
PQ endorsement fetching: fetch, validate and serve the ML-DSA endorsement chain and signing keys#8768Max (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-fluffy-fiesta
branch
from
November 13, 2026 15:43
1657eb4 to
92e9966
Compare
This was referenced Nov 13, 2026
Max (maxtropets)
force-pushed
the
maxtropets-fluffy-fiesta
branch
from
November 13, 2026 17:54
92e9966 to
43cb4f1
Compare
The network identity subsystem fetches the PQ endorsement chain with the CLASSICAL one, from the same historical states: a PQ back-endorsement is written with a CLASSICAL one, over the same epochs and with the same previous version, and the chain ends with the PQ self-endorsement of the first service with a PQ identity. The PQ chain is validated as the CLASSICAL one is (epochs, signatures, key linkage, current PQ service key). An inconsistent PQ chain is dropped and logged, which leaves the CLASSICAL chain and its readers unchanged. - NetworkIdentitySubsystemInterface gains get_cose_endorsements_chain(seqno, identity_type), get_trusted_key_der_for(seqno, identity_type) and get_trusted_keys_der(identity_type); for CLASSICAL they return what the existing getters return - ccf::IdentityType moves to the public header ccf/service/identity_type.h - read_current_service_identity also returns the PQ endorsement written with the CLASSICAL one, and IHistoricalStateAccessor::get_endorsements_at returns the endorsements of both types written at a version - Builds without ML-DSA skip the PQ chain Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bc4ac502844f069dc9238bf2de0fb7
The network identity subsystem validates the PQ endorsement chain as it validates the CLASSICAL one, and fails fetching if either is inconsistent. The getters serve validated chains only, and std::nullopt from a PQ getter means that no validated PQ chain covers the sequence number. - A PQ endorsement written when the current service opened, on a node without a PQ key, fails fetching - The interface documentation states where the PQ chain starts, including restarts after a service without a PQ identity, and that chains are newest endorsement first - Tests: each inconsistency fails fetching for its own reason, in Done and in Partial; the PQ chain restarts after a service without a PQ identity; a joiner whose live KV lags reads the PQ chain once the KV reaches the current service; the services of the CLASSICAL tests have no PQ identity Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b2873f93c5a4e22adff30641d412183
Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b2873f93c5a4e22adff30641d412183
clang-tidy (bugprone-unchecked-optional-access) rejects the unchecked current_service_from accesses in start_pq_chain, complete_pq_chain and build_pq_trusted_key_chain, in every library translation unit that includes the subsystem. The callers pass the TxID they have checked. Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b2873f93c5a4e22adff30641d412183
Amaury Chamayou (achamayou)
force-pushed
the
maxtropets-fluffy-fiesta
branch
from
November 14, 2026 12:25
43cb4f1 to
7601090
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 #8765.
The network identity subsystem fetches and validates the CLASSICAL service endorsement chain once a node is part of an open service, and serves it with the trusted service keys to build receipts for transactions signed by earlier service identities. #8765 writes a PQ chain next to it. This layer gives the PQ chain the same subsystem support, which PQ receipts need (goal 5 of #7848), and serves the chain and the trusted keys of each identity type through
ccf::NetworkIdentitySubsystemInterface:get_cose_endorsements_chain(seqno, identity_type)get_trusted_key_der_for(seqno, identity_type)get_trusted_keys_der(identity_type), withTrustedKeysDer = std::map<SeqNo, std::vector<uint8_t>>For CLASSICAL they return what
get_cose_endorsements_chain(seqno),get_trusted_identity_for(seqno)andget_trusted_keys()return. For PQ they returnstd::nullopt, or no keys, for a service without a PQ identity and for sequence numbers before the PQ chain starts: at the latest service which had a PQ identity when the service before it, if any, did not.ccf::IdentityTypemoves to the public headerccf/service/identity_type.h, which the interface needs.The PQ chain is walked in lockstep with the CLASSICAL one, from the same historical states, so it costs no extra fetch.
get_endorsements_at(seqno)returns the endorsements of both types written at a version, each PQ back-endorsement must carry the epochs andprevious_versionof the CLASSICAL endorsement written with it, and the walk ends at the PQ self-endorsement of the service which started the PQ chain. On completion the PQ chain gets the CLASSICAL checks: epoch contiguity, connection to the current service, signatures, key linkage, and the newest endorsing key equal to the node's PQ service key.This layer has no plan item of its own in #7848; it was inserted after #8765 at the reviewer's request. Its first version was written in the PQ receipts layer, which rebases onto it.
Choices for review
fail_fetching, which stops the node). Readers then see validated chains only, andstd::nulloptfrom a PQ getter means that no PQ identity covers the sequence number, or inPartialthat it is below the validated suffix. No supported ledger reaches these checks: PQ endorsement chain: endorse the previous ML-DSA service identity when a service opens #8765 writes both endorsements in one transaction over the same epochs, and restarts the PQ chain with a self-endorsement when the previous[PQ]entry was not written with the previous[CLASSICAL]one; snapshots keep per-key write versions; every node of a PQ service holds the PQ key; and a live upgrade of a 7.0.24 service that has an ML-DSA certificate stops earlier, atcheck_signing_identity_matches_service_cert, before any endorsement is read. A failure therefore means a corrupted ledger or a bug. The first version dropped the PQ chain with aLOG_FAILand kept serving CLASSICAL; going back to that changes one function, if CLASSICAL availability should win.make_cose_verifier_from_key()takes for EC and ML-DSA keys alike. The alternative was typed getters returningMLDSAPublicKeyPtr, mirroringTrustedKeys, with one more map type per algorithm.Safety and compatibility
Builds without ML-DSA skip the PQ chain, and the CLASSICAL getters are unchanged.
NetworkIdentitySubsystemInterfacegains three pure virtual methods, as it did forget_trusted_keys()(#7690): applications calling it are unaffected, and an implementation of the interface outside CCF would need them.read_current_service_identityreturns the PQ endorsement only when it was written at the version of the CLASSICAL one, so the[PQ]entry of an older service is ignored. On ML-DSA builds a node with an inconsistent PQ chain stops, as it does with an inconsistent CLASSICAL chain.Validation
ML-DSA build (OpenSSL 3.5.4,
-DTEST_MLDSA=ON, Debug) on Azure Linux 3:network_identity_subsystem_test47/47, including 11 PQ cases. Each of the 10 inconsistencies of "An inconsistent PQ chain fails fetching", and itsPartialvariant, asserts its own error message.logging,logging_cose_only,logging_cose_only_allow_join_dualandjs_generic(all of libccf); e2e (tests.sh)recovery_test,recovery_stale_snapshot_join_test,recovery_intermediate_snapshot_join_testandrecovery_snapshot_endorsements_testpass. In their node logs, all 38 nodes which settled fetching validated a PQ chain of 0 to 5 PQ endorsements, and none failed..clang-tidyonsrc/node/historical_queries_adapter.cpp, which includes the subsystem header: clean.clang-tidier.pyon it and on the unit test: clean.includes,release-notes,doc-newline,ascii,copyrightandtodochecks: clean.Not run, left to PR CI: any default (AL3) build or test; other unit tests and e2e suites;
scripts/ci-checks.sh; clang-tidy on other translation units; the Sphinx docs build.ci-al4.yml(ML-DSA) runs on schedule orworkflow_dispatch.