Repository navigation
PQ receipts: serve and verify COSE receipts of the ML-DSA service identity - #8770
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-bookish-happiness
branch
from
November 13, 2026 18:36
5fa4cdb to
a68b3ef
Compare
Historical receipts carry the COSE signature and endorsement chain of each service identity type, and the receipt endpoints select one with an optional identity_type query parameter, CLASSICAL by default or PQ. - describe_cose_receipt_v1, describe_cose_signature_v1 and describe_cose_endorsements_v1 take the identity type, CLASSICAL by default; TxReceiptImpl holds an endorsement chain per identity type - populate_cose_service_endorsements adds the PQ chain of a receipt with a PQ signature, when the PQ identities are endorsed from its epoch - verify_self_issued_receipt verifies an ML-DSA receipt with the trusted PQ key of its seqno - GET /receipt/cose (node, app and gov frontends) and the logging sample's /log/public/cose_receipt, cose_signature and cose_endorsements take identity_type; an unknown value is a 400, and a transaction without a receipt of that type a 404. Node API 5.0.11, governance API 5.0.2, logging app API 2.8.7 - ccf.cose.verify_cose_sign1_mldsa verifies pure ML-DSA COSE_Sign1 messages, and verify_cose_sign1_with_key dispatches ML-DSA keys to it - ccf.receipt.verify_cose accepts ML-DSA keys, and with endorsements verifies a receipt of a previous service identity through its chain; ccf.receipt.verify_cose_endorsements verifies the chain - Unit tests, Python tests, e2e coverage of PQ receipts in e2e_logging and across recoveries in recovery_test, docs and change note Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bc4ac502844f069dc9238bf2de0fb7
- verify_cose_sign1_mldsa registers a decoder for the COSE_Sign1 tag, so that cbor2 decodes the message contents as a mutable list and dict. It no longer refers to cbor2.frozendict, which cbor2 only exports before Python 3.15, so ML-DSA verification works on 3.15 too - verify_cose_endorsements and verify_cose raise TypeError when endorsements is a single message (bytes, bytearray, memoryview or str) rather than a sequence of COSE_Sign1 messages - verify_cose_endorsements requires each epoch to end two views before the start of the next one, as the node does Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bc4ac502844f069dc9238bf2de0fb7
describe_cose_receipt_v1, describe_cose_signature_v1 and describe_cose_endorsements_v1 keep their signature, and describe the CLASSICAL receipt. describe_cose_receipt_v2, describe_cose_signature_v2 and describe_cose_endorsements_v2 take the identity type, and the receipt endpoints and the logging sample call them. Relates to #7848. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bc4ac502844f069dc9238bf2de0fb7
cbor2.CBORDecodeError derives from Exception only, so a truncated message or protected header escaped the documented ValueError. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 25bc4ac502844f069dc9238bf2de0fb7
Amaury Chamayou (achamayou)
force-pushed
the
maxtropets-bookish-happiness
branch
from
November 14, 2026 12:25
acebd5f to
3a99101
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 #8766.
A service with an ML-DSA identity signs every signature transaction with its classical and its ML-DSA service keys, and each ML-DSA identity endorses the previous one, but receipts were classical only. This PR serves the ML-DSA COSE receipt of a transaction and verifies it offline, also across disaster recoveries through the ML-DSA endorsement chain fetched by #8768 (goal 5 and the "PQ receipts" item of #7848).
Endpoint shape (TBD in #7848, please redirect if needed): an optional
identity_typequery parameter,CLASSICAL(the default and the current behaviour) orPQ, for exampleGET /node/receipt/cose?transaction_id=2.643&identity_type=PQ./receipt/coseof the node, app and governance frontends, and to the logging sample's/log/public/cose_receipt,/log/public/cose_signatureand/log/public/cose_endorsements.idparameter: an unknown value returns400 InvalidQueryParameterValueonce the transaction's state is available. A transaction with no receipt of that type returns404, which includes every transaction signed before the first service with an ML-DSA identity./log/public/verify_cose_receiptselects the identity type from the receipt'salg.Building blocks:
describe_cose_receipt_v2,describe_cose_signature_v2anddescribe_cose_endorsements_v2take the identity type, and the_v1functions keep describing the CLASSICAL receipt. Historical receipts carry the endorsement chain of each identity type, andverify_self_issued_receiptverifies ML-DSA receipts with the trusted PQ key of their seqno.ccf.receipt.verify_coseaccepts ML-DSA keys and an optionalendorsementschain, whichccf.receipt.verify_cose_endorsementsverifies from the trusted current key (signatures, key linkage, and epochs that follow each other as recoveries leave them); the receipt must then be signed for a transaction in the epoch of the endorsed key.ccf.cose.verify_cose_sign1_mldsa(spki_der, cose_sign1, payload), the signature planned in ML-DSA signatures in CCF #7848, verifies pure ML-DSA COSE_Sign1 messages (RFC 9964).Alternatives
/node/receipt/cose/pq), or one response with both receipts: rejected, as a query parameter is additive, keeps the default response, and selects receipts, signatures and endorsements the same way.describe_cose_*_v1functions: rejected, as a versioned name denotes a fixed contract, asdescribe_receipt_v1anddescribe_receipt_v2show, so the_v2functions take it.404for those receipts, and with PQ endorsement fetching: fetch, validate and serve the ML-DSA endorsement chain and signing keys #8768 failing on an inconsistent PQ chain, a receipt with an ML-DSA signature lacks a PQ chain only after an unsupported 7.x recovery between ML-DSA services. Verifying such a receipt against the current ML-DSA key fails closed on itskid.Safety and compatibility
identity_typeget the same CLASSICAL receipts. Builds without ML-DSA have no PQ signatures, soidentity_type=PQreturns 404 there.doc/schemasregenerated. Thedescribe_cose_*_v1functions are unchanged.cryptography >= 46: ML-DSA is imported where it is used, so classical receipts verify with 46 and 47, and PQ receipts need 48, which also loads the key frompq_service_certificate. The floor moves with the "Python SDK verifies every signature present" item of ML-DSA signatures in CCF #7848./log/transparent_statementand the commit-time receipt ofmake_respond_with_receipt_on_commitstay CLASSICAL.Validation (scoped to this layer, at the reviewer's request for the draft stack)
ML-DSA build (OpenSSL 3.5.4,
-DTEST_MLDSA=ON, Debug).At acebd5f (after review round 2, Python only):
cd python && pytest, 713 passed withcryptography50.0.2, including a test that malformed CBOR raisesValueErrorfromverify_cose_sign1_mldsa.test_receipt_identity_types.pyandtest_receipt.pypass withcryptography48.0.0 and 48.0.1, and with 46.0.7, where the ML-DSA tests skip.cryptography48.0.0 and 48.0.1, thepq_service_cert.pemof an ML-DSAe2e_loggingrun loads, and its key verifies the run's PQ receipt withccf.receipt.verify_cose.At cef7c59 (after review round 1):
historical_queries_test46/46, with cases for the_v1and_v2descriptions and for a PQ-signed historical receipt that verifies with the ML-DSA key and not with the EC key.cd python && pytest, 712 passed, including 14 tests of EC and ML-DSA receipts and endorsement chains, one of which removesfrozendictfromcbor2(as on Python 3.15) and verifies ML-DSA messages.cpprun ofe2e_logging(all 33 tests; the PQ receipt test checks the receipt and signature CDDL, self-verification, the sample endpoints, 400 and 404), and therecoveryrun ofrecovery_test.historical_queries_adapter.cpp; clang-format, black, ruff, pyright, doc-newline and prettier checks.At 5fa4cdb (the first commit, on a02fc1f):
recovery,recovery_from_double_sealed_serviceandrecovery_from_acme_containing_serviceruns ofrecovery_test: across two recoveries the PQ receipts and chains verify in Python exactly as the CLASSICAL ones do, the classical-only history has no PQ receipt, and the recovered service's transactions have one.schema_test.Not run: any build or test without ML-DSA (AL3), other e2e suites, the Sphinx build,
scripts/ci-checks.sh.