Skip to content

test(threshold): independent DKG and reshare transcript checker; Details: TC-2223 - #494

Draft
samgbafa wants to merge 6 commits into
skgbafa/tc-2221-threshold-resharefrom
skgbafa/tc-2223-s05-transcript-checker
Draft

samgbafa wants to merge 6 commits into
skgbafa/tc-2221-threshold-resharefrom
skgbafa/tc-2223-s05-transcript-checker

Conversation

@samgbafa

@samgbafa samgbafa commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Independent checker for threshold DKG and resharing transcripts (S0.5). Stacked on #493 (TC-2221), which sits on #492 (TC-2220); retarget to main after those merge.

tinycloud-threshold/vectors/check now recomputes a whole genesis DKG and a whole reshare/refresh outcome from the raw signed records, using only the published SPEC. It covers the H0 start link and transcript chain, every closure and member signature and binding, the complaint and answer rules, QUAL/D selection, mpk and public shares, identity rejections, the 4 MiB bound and the malformed-message rules. It reuses lane A's primitives (TH, BLS12-381 decoders, @noble/curves 2.4.0) and Ed25519 from the same package with zip215: false (strict RFC 8032). No new dependency; bun.lock is unchanged.

SPEC changes (owner rulings, 2026-10-30)

The independent review approved this PR at ee0d4d3d. A follow-up commit applies the owner's SPEC rulings to SPEC.md and enforces them in the checker:

  1. New section "Verifier conformance". Outcomes are normative: accept/reject, QUAL or D, the final mpk/commitments/public shares, and the excluded dealers. Error codes and their precedence are informative. This settles proposal 2.
  2. Sealed dealings (S0.3 appendix, reused by S0.4). The ciphertext must be at least 16 bytes (the AEAD tag); a shorter one makes that R1 MalformedDeal. This supersedes proposal 7.
  3. §3.9 and the S0.2 certificate paragraph. Genesis requires genesis_t = t and n ≥ 2t−1. Every successor requires epoch ≥ 1 and rekey ≤ epoch. A re-key must change mpk (previous mpk ≠ mpk), have rekey ≥ 1, and, like genesis, satisfy genesis_t = t and n ≥ 2t−1 (UnsupportedFaultBudget). The re-key clauses were added in a second rulings commit after the delta review; they match verify_successor's "genesis t = new t" and S0.2's "re-key uses the genesis budget".
  4. S0.4 authorization. A stale or absent current-certificate digest is InvalidCeremony (authorized against the pinned certificate). This settles proposal 9. Rust's resharing reports a different code for this input, but it also rejects it, and codes are informative, so no Rust change is needed (Naomi, 2026-10-30).

Clean-room statement

Clean-room statement. tinycloud-threshold is written from published papers and standards only: Shamir 1979; Feldman 1987; Pedersen 1991; Gennaro–Jarecki–Krawczyk–Rabin 1999/2007; Boneh–Franklin 2001/2003; Fujisaki–Okamoto 1999; Desmedt–Jajodia 1997; Wong–Wang–Wing 2002; Herzberg–Jarecki–Krawczyk–Yung 1995; CHURP (ePrint 2019/017); vetKeys (ePrint 2023/616); RFC 9380; RFC 9180; the IRTF pairing-friendly-curves draft; and TinyCloud's own design and plan. No source code, tests, issues or other material from TACo, nucypher, nucypher-core, ferveo, umbral or taco-web (GPL-3.0/AGPL-3.0) was opened, fetched, cloned, searched or used. Dependencies are permissively licensed and listed in the PR. Each PR keeps a provenance log of the sources consulted.

Files read for this checker (verbatim list). No Rust source under tinycloud-threshold/src/, no Rust git history and no Rust diffs were opened.

  • tinycloud-threshold/SPEC.md (whole file, including the S0.2, S0.3 DKG and S0.4 redistribution appendices)
  • tinycloud-threshold/vectors/v1/dkg-genesis.json, reshare.json, cert-core.json (layouts, and the published Ed25519 fixture seeds for tests), primitives.json
  • tinycloud-threshold/vectors/check/* (lane A's checker: README.md, package.json, bun.lock, bunfig.toml, tsconfig.json, .gitignore, isolate.ts, cli.ts, schema.ts, vectors.ts, protocol.ts and the four existing *.test.ts)
  • /var/tmp/tc2092-plan.J13a/plan-r2.md (sha256 f701d2be…4d14 verified): §0, §§3.0–3.11, §5 (allowed by Naomi for error names; it names none), §6
  • @noble/curves 2.4.0 README.md and the ed25519.d.ts/abstract/edwards.d.ts declarations (API only)
  • .github/workflows/rust.yml (only to confirm the CI lane)
  • Host notes: /var/tmp/naomi-brief-lines.txt, host-hygiene.md

Vector coverage

Every committed case agrees byte for byte (bun run vectors: primitives 18/18, DKG 6/6, reshare 5/5).

Vector Verified
genesis_2of3 ceremony digest, H0, 5-round chain, QUAL [1,2,3], A_k, mpk, pk_1..3, transcript hash
genesis_2of3_selective_extraction upheld R5 complaint (Pedersen-valid, Feldman-invalid) → R6 reconstruction of dealer 3, which stays in QUAL; 6-round chain
genesis_3of5_two_faulty rejected (Pedersen-invalid) R5 entry ignored; R6 from 3 of 5 members suffices
genesis_3of5_answers_and_disqualification answered complaint replaces value; Equivocated{round:1} (dealer 4); MissingAnswer (dealer 5)
insufficient_reconstruction_values InsufficientRecovery with R6 as last record
insufficient_qualified_dealers InsufficientQualified with R3 as last record
reshare_2of3_to_3of4 certificate verification, ceremony binding, D = [1,2], B′_k with λ^D, mpk preserved, pk′_1..4
refresh_2of3_answered_complaint refresh shape checks; valid R3 answer replaces the complainer's value
reshare_wrong_constant B_10 ≠ pk_1 → MalformedDeal; D = [2,3]
reshare_t_prime_complaints t′ complainers → Complaints; D in R1 order skips dealer 3
reshare_insufficient_dealers InsufficientQualified

Synthetic transcripts (testkit.ts, signed with the published fixture seeds) cover each rule the vectors do not. In DKG: every disqualification reason, the order of rules 5–8 pairwise, self-complaints, R2/R3 index bounds, unrequired valid and invalid answers, missing/conflicting/malformed/wrong-count R4, R5 entries that are upheld, self-directed against a Feldman-invalid dealer, Pedersen-invalid, or against a Feldman-valid dealer, R6 shortfall and conflicts, and 3-of-5 with two silent members. Genesis ceremony rules: anchor, kind, current digest, counters, budget, directory contiguity and duplicate DIDs, option marker, signature cap. In reshare: every exclusion reason, the order of rules 4–7, R1 owner-order selection, other-cohort envelopes ignored, no self-exclusion, and entry checks. In both: identity mpk, top coefficient and public share; byte-identical and alternate-signature retransmissions; envelope, closure and certificate signature caps; record count; closure binding; list reordering. Strict RFC 8032: a ZIP-215-only signature (non-canonical R), S + L and a small-order public key are rejected. s02.test.ts runs every cert-core.json certificate (genesis, reshare successor, re-key), ceremony, decode refusal and closed-round record through the same S0.2 layer; all agree. It also covers the §3.9 decoded-certificate rules: constant = mpk, top coefficient, evaluated shares, predecessor combinations, genesis t, option markers, anchor and signer.

Checker-order reason codes (corruption probe)

Each probe flips one byte of one committed signed record, in every field class, for all 11 cases (first and last message of each round). 1309/1309 are rejected, each with the reason code this checker's order produces. Rejection is normative; the reason codes are informative (SPEC "Verifier conformance").

Record Field Reason
ceremony version, kind→0 InvalidEncoding
ceremony ceremony id, last HPKE key byte, signature R/S InvalidSignature
ceremony network InvalidCeremony
certificate (reshare) version InvalidEncoding
certificate network InvalidCertificate
certificate transcript hash, signature InvalidSignature
closure (each round) version, ceremony digest, round, previous hash InvalidRound
closure ordered-list hash, signature InvalidSignature
member envelope version, payload length InvalidEncoding
member envelope ceremony digest, round InvalidRound
member envelope cohort→0, index outside its cohort InvalidIndex
member envelope index inside its cohort, payload first/last byte, signature R/S InvalidSignature

Re-signed one-byte payload flips reach the rule-level reason. Dealer commitment byte → MalformedDeal (QUAL [2,3]). R2 version byte → malformed R2 contributes nothing (mpk unchanged). R4 byte → extraction fault, so a 5-record transcript is InvalidRound. Corrupted upheld R5 pair → nothing to reconstruct, so R6 is an extra record (InvalidRound). Reshare B_0 byte → MalformedDeal, D [2,3]. R3 value byte → InvalidAnswer. Complaint version byte → MissingAnswer. One byte over 4 MiB → LimitExceeded before any signature.

Error names chosen as defaults (not from SPEC, plan §5 or vector reason fields)

Plan §5 names the error types but no variants. The defaults below follow Naomi's 2026-10-30 rulings and sit in one table (DEFAULT_ERROR, ceremony.ts):

  • InvalidSignature: owner or member signature fails strict RFC 8032, or the DID has no key.
  • InvalidRound: closure non-canonical, ordered-list mismatch, envelope names another ceremony/round. (SPEC names InvalidRound for the closure's ceremony/round/previous-hash binding and for record counts; the rest is a default.)
  • InvalidIndex: envelope cohort/index does not resolve in its signed cohort (including any cohort-2 envelope in genesis, and unknown cohort bytes).
  • InvalidCeremony: ceremony owner/network differs from the anchor; genesis with a current digest or non-zero counters; successor epoch ≠ checked current + 1 or re-key counter changed. (Stale or absent current digest is now SPEC-named InvalidCeremony.)
  • InvalidCertificate: current certificate owner/network differs from the anchor.
  • ContextMismatch: reshare B′_0 ≠ current mpk.
  • InvalidEncoding: envelope over 64 KiB, any signature over 512 bytes; certificate commitment[0] ≠ mpk, genesis t outside 2..=16, and the §3.9 field-combination rules (including the new genesis_t = t, successor epoch/rekey and re-key mpk/rekey rules). A genesis certificate over budget reports the SPEC-named UnsupportedFaultBudget.

SPEC clarifications proposed

  1. Name the errors above in SPEC.
  2. Fix the check order inside a closed round. Settled: codes and their precedence are informative (SPEC "Verifier conformance"). This checker's order, for reference: closure ceremony/round/previous → owner closure signature → each envelope (decode, ceremony/round, signer resolution, signature) → ordered-list commitment.
  3. State that an abort (InsufficientQualified, InsufficientRecovery) must be the last record; extra records are InvalidRound (Naomi ruling).
  4. State the injected verifier's exact acceptance set. Here: noble zip215: false, which rejects non-canonical A/R encodings, S ≥ L and (observed) small-order public keys.
  5. DKG R3 with a recipient above n is not used, so rule 7 is not evaluated on it (Naomi ruling).
  6. Reshare complaints have no self-exclusion (Naomi ruling).
  7. Sealed dealing decoded by layout only. Superseded: SPEC now requires a ciphertext of at least 16 bytes.
  8. Two rules are unobservable by a public verifier. "t lowest R6 senders": any t Pedersen-valid values interpolate the same f_i unless log_g h is known. The reshare B′_0 = mpk check cannot fail once the certificate verifies. The checker keeps both, as SPEC requires.
  9. A stale current digest has two names. Settled: InvalidCeremony (SPEC S0.4 now says so; the checker follows).
  10. "Any single message ≤ 64 KiB" is applied to the whole member envelope.
  11. A re-key DKG (kind 4) is outside S0.3 (§3.7's precondition is kind genesis), so dkg_verify_transcript here returns InvalidCeremony for it.

Verification

  • bun install --frozen-lockfile && bun run check: all tests, tsc --noEmit, and the real CLI over the three committed documents pass.
  • Mutation runs on a temp copy, each caught by at least one test. Rules: self-complaint exclusion removed; ≥ t → > t; R3 index bound removed; R6 record-count rule removed; R5 i ≠ j removed; DKG rules 5–8 and reshare rules 4–7 reordered pairwise; reshare B_0 check removed; R1-order selection removed. Checks: > → ≥ on the 4 MiB bound; ZIP-215 enabled; certificate constant/top/public-share/combination/genesis-t/option-marker checks removed; envelope, closure and certificate signature caps removed; directory contiguity removed; genesis re-key-counter check removed. The "lowest t R6 senders" mutation survives because it is unobservable (see 8).
  • Owner-ruling mutants, each caught: the 16-byte sealed minimum removed; genesis genesis_t = t removed; genesis budget removed; successor epoch ≥ 1 removed; rekey ≤ epoch removed; re-key mpk change removed; re-key rekey ≥ 1 removed; re-key genesis_t = t removed; re-key budget removed; stale/absent digest back to ContextMismatch. New tests: 0- and 15-byte ciphertexts exclude the dealer and change QUAL (DKG) or D (reshare); a 16-byte tag-only ciphertext is accepted; a re-key with t=2, n=3, genesis_t=3 is rejected; a 3-of-4 re-key is UnsupportedFaultBudget.
  • No existing repository test or fixture was edited or deleted; lane A's tests pass unchanged. Within this PR's own new tests, the two reshare digest assertions moved from ContextMismatch to InvalidCeremony per ruling 4.

Internal review (builder's subagent)

This is an internal check, not the independent review; Naomi has seated a separate independent Opus reviewer for this PR. A fresh-context Opus subagent read SPEC and the checker line by line. It found no P0. Its four P1 findings were all test gaps: rule precedence, the R5 i ≠ j rule, untested §3.9 certificate rules and caps, and genesis counter rules. All four are fixed in the follow-up commit, with each reported surviving mutant now killed. Its P2 items are applied: every default is in DEFAULT_ERROR, the README wording is corrected, and clarifications 4 and 9–10 are added.

Subagent clean-room disclosure (accepted by Naomi, 2026-10-30): one subagent grep over tinycloud-threshold for error names also printed about 40 lines of Rust error-variant snippets from src/*.rs. The subagent opened no Rust file, history or diff, made no finding that relies on those lines, and declined to rule on directory-rule error names because of them. The builder (author of this PR) did not see those lines; no code here depends on them.

The independent reviewer (abf01a5d) approved ee0d4d3d, requested one P2 change at 3e61e74c (re-key certificate rules), and approved 6d1883ae with no remaining P1/P2/P3 findings. The head is frozen at 6d1883ae; retarget to main only after #492 and #493 merge.

Provenance log

Source Use
SPEC §§3.0, 3.2, 3.3, 3.7–3.12, S0.2/S0.3/S0.4 appendices All layouts, tags, rules and named errors
plan-r2 §§3.0–3.11, §5, §6 Rules cross-check; error type names; S2/S4/S5 test properties
Committed vectors (dkg-genesis.json, reshare.json, cert-core.json seeds) Expected outputs, reason fields, fixture signing seeds
RFC 8032 §5.1 Ed25519 encodings for the strict-verification tests
noble-curves 2.4.0 README Ed25519 zip215: false API
Naomi rulings, 2026-10-30 Defaults listed above

Details: TC-2223

…2223-s05-transcript-checker; Details: TC-2223
@samgbafa samgbafa mentioned this pull request Nov 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant