Repository navigation
PQ endorsement chain: endorse the previous ML-DSA service identity when a service opens - #8765
Draft
Max (maxtropets) wants to merge 5 commits into
Draft
Max (maxtropets) wants to merge 5 commits into
Max (maxtropets) wants to merge 5 commits into
Conversation
11 of 23 tasks
Max (maxtropets)
force-pushed
the
maxtropets-scaling-fishstick
branch
from
November 13, 2026 13:39
24c1cd8 to
ba8615f
Compare
Max (maxtropets)
force-pushed
the
maxtropets-scaling-fishstick
branch
2 times, most recently
from
November 13, 2026 15:42
fdecd93 to
fd1caf4
Compare
Max (maxtropets)
force-pushed
the
maxtropets-scaling-fishstick
branch
from
November 13, 2026 17:37
1fef7c9 to
0d78360
Compare
A service with an ML-DSA identity signs, when it opens, a COSE endorsement of the previous PQ service identity with its ML-DSA key, in previous_service_identity_endorsement[PQ] next to the CLASSICAL one. A PQ back-endorsement has the epochs, previous version and previous root of the CLASSICAL endorsement written with it. The PQ chain starts with a self-endorsement when the previous service had no PQ identity. - cose::sign_endorsement over MLDSAKeyPair, with the EC protected header except alg - InternalTablesAccess::endorse_previous_identity takes the nullable PQ service key, checks it against signing_identities[PQ], and signs every identity type before writing any - open_service and open_recovered_service take the PQ service key, which NodeState passes from NetworkIdentity::get_pq_key_pair() - Unit tests, e2e recovery tests of the PQ chain, audit and migration docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e71570e226fd4a1ea74eecf57a790614
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e71570e226fd4a1ea74eecf57a790614
get_service_signing_identity checks the published identity of each type against its certificate, on every path of endorse_previous_identity, so the whole-table check before it is dropped. A test covers a mismatched published identity of each type, and of PQ with and without a PQ key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e71570e226fd4a1ea74eecf57a790614
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e71570e226fd4a1ea74eecf57a790614
The transaction holds the mismatched published identity and is never committed, so the test checks in it that no endorsement was written. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e71570e226fd4a1ea74eecf57a790614
Amaury Chamayou (achamayou)
force-pushed
the
maxtropets-scaling-fishstick
branch
from
November 14, 2026 12:25
ef7df95 to
882579f
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 #8764.
Receipts for PQ-signed transactions have to stay verifiable across disaster recoveries (goal 5 of #7848), so each ML-DSA service identity endorses the previous one, as each classical identity already does. When a service with an ML-DSA identity opens, it writes
previous_service_identity_endorsement[PQ]in the transaction that writes[CLASSICAL]: a COSE endorsement, signed with the ML-DSA service key, of the key that signed the previous PQ endorsement. A PQ back-endorsement has the epochs,previous_versionandepoch.end.merkle.rootof the CLASSICAL one, and its protected header differs only inalg, so the two chains line up and the PQ chain can be walked through the same historical states as the CLASSICAL one. A service whose previous service had no PQ identity (a service started on an ML-DSA build, or a classical-only ledger recovered on one) starts the PQ chain with a self-endorsement.Reading of the plan item
cose::sign_endorsementoverMLDSAKeyPair: the EC and ML-DSA overloads share one protected-header builder, withalgfrom the parameter set (RFC 9964). The EC header bytes are unchanged.endorse_previous_identityper identity type:endorse_previous_identity(tx, service_key, pq_service_key)takes a nullableMLDSAKeyPairPtr, asTxHistory::set_service_signing_identitydoes, and so doopen_serviceandopen_recovered_service. Each endorsing key is checked againstsigning_identities[type], whichget_service_signing_identitychecks against its certificate; a published PQ identity with no PQ key throws. Both endorsements are signed before either is written, so an opening writes both or neither. They share oneiat.previous_service_identity_endorsementstate as after a ledger recovery, because snapshots keep per-key write versions. The startup check of the stapled receipt stays CLASSICAL inverify_recovery_snapshot_candidate_unsafe, because the receipt is CLASSICAL and the configured previous identity is the classical certificate. Its ledger scan collects CLASSICAL writes only, so PQ writes in the same entries leave it unchanged. The difference for PQ is that its chain can start after the snapshot (a classical-only ledger recovered on AL4). Startup verification of a PQ snapshot receipt ("Carry classical and PQ snapshot receipts") will therefore need to handle a chain that begins with a self-endorsement inside the suffix, whichvalidate_recovery_snapshot_endorsement_chainrejects today. The new e2e test recovers from a snapshot older than the previous identity, with both types in the suffix.previous_versionlinks, CLASSICAL epochs and header, CDDL, newest key matchingpq_service_cert.pem). The classical-only test ledgers ofrecovery_from_*start the PQ chain with exactly one self-endorsement on ML-DSA builds and have no PQ endorsement on AL3. Unit tests cover a classical-only service followed by PQ services, the restart, and a published identity of each type that does not match its certificate.If the item meant something else, say so and this layer will follow.
Safety and compatibility
On builds without ML-DSA, openings pass no PQ key and write the same
[CLASSICAL]endorsement. On ML-DSA builds, each opening transaction gains one PQ endorsement (about 13 KB of JSON with ML-DSA-65). Readers of[CLASSICAL], the receipt endorsement chain and the startup snapshot scan, read the same entries. The error for a mismatched endorsing key now names the identity type.doc/audit/builtin_maps.rstdocuments the identity-type key of the table, which still said "Sentinel value 0".Validation
ML-DSA build (OpenSSL 3.5.4,
-DTEST_MLDSA=ON, Debug) on Azure Linux 3:internal_tables_access_test,open_service_test,node_frontend_test,cose_test, and theML-DSA COSE_Sign1*cases ofcrypto_test: pass.recovery.py, the sub-testsrecovery_pq_endorsement_chain(3 PQ endorsements; the last recovery logs the in-memory validation of the older snapshot) andrecovery_from_{expired,double_sealed,sgx,cose_flipflop,acme_containing}_service(1 PQ self-endorsement each);recovery_snapshot_endorsements_test: pass.src/crypto/cose.cpp,src/node/recovery_decision_protocol.cpp,src/host/run.cppandsrc/node/local_sealing.cpp: no finding in changed code. The one finding, a dead store increate_service, is pre-existing and appears only on ML-DSA builds, wheremldsa_supported()is constant.doc-newline,ascii,todo,builtin-maps,copyrightandrelease-noteschecks: clean.internal_tables_access_test,open_service_testand therecovery_pq_endorsement_chainsub-test: pass; clang-format, black and ruff: clean.Not run, left to PR CI: any default (AL3) build or test, including the AL3 runs of the new e2e checks; the other
recovery.pysub-tests and other e2e suites;scripts/ci-checks.sh;clang-tidier.py; the Sphinx docs build.ci-al4.yml(ML-DSA) runs on schedule orworkflow_dispatch.