Skip to content

Integrate dev hardening set into master - #37

Draft
piotr-roslaniec wants to merge 167 commits into
masterfrom
dev
Draft

piotr-roslaniec wants to merge 167 commits into
masterfrom
dev

Conversation

@piotr-roslaniec

@piotr-roslaniec piotr-roslaniec commented Sep 30, 2026 •

Copy link
Copy Markdown

Tracking PR for everything integrated on dev since master (1cd3f0b). Draft: do not merge until the merge gates below are cleared. master is protected: merging needs one approving review (not the author's) and passing Test, Vet, Keygen units and Go fmt project checks.

Scope at 9ff573c: 152 commits, 137 files, +14,583 / −602. Already on master and not repeated here: #2, #4, #5, #6, #7 (BNB hardening base stack) and #22 (release workflow). Per-change detail, compatibility notes, and provenance live in CHANGELOG.md.

Breaking changes for callers (introduced on dev)

Numbers refer to CHANGELOG.md → Breaking changes. keep-core currently pins 2e712689, so it also absorbs the earlier breaks already on master (Breaking changes 1–7).

Change Kind PR
Protocol mode must be selected before constructing a party; legacy refuses a session nonce, security-v2 requires one Runtime #9
Security-v2 signing SSID binds the message and fullBytesLen Wire (security-v2 only) #16
Constant-time arithmetic on by default (8) Performance #17, #23
Signing end channel carries *common.SignatureData (9) Compile #39
tss.S256() concrete curve type is btcec/v2 / Decred (10) Source/runtime for type assertions and curve-object comparison #21
Keygen UnmarshalFactorProof, UnmarshalFactorProofTilde, UnmarshalProofInts return errors (11) Compile #28
Go 1.25.7 minimum (12) Build #20

PRs merged into dev

Tooling and dependencies

PR Change
#24 CI runs on every pull request, not only those targeting master
#19 Protobuf runtime updated to v1.33.0
#18 Protobuf generator version checked before regeneration
#20 Go crypto dependencies updated; Go toolchain aligned (go 1.25.7, toolchain go1.26.8)
#21 btcd upgraded; migrated to btcec/v2
#25 CI: PR-scoped cancellation, post-merge push triggers, fast-fail split, committed Paillier fixtures, weekly race workflow
#30 Docs and CI fixes from the holistic review (usage example, rollout guidance, consistent toolchain, duplicate gofmt job removed)

Constant-time hardening

PR Change
#11 Constant-time hardening backport rebased and verified on current master
#17 Bounded bigmod constant-time operations enabled by default (supersedes #8 and #10, both closed unmerged)
#23 Constant-time coverage extended to Schnorr proofs and signing rounds 3–5
#27 Regression test for unequal-width MtA constant-time arithmetic
#31 Paillier performance: binomial identity for (N+1)^m, exponent width narrowed to public N.BitLen(), sync.Pool boxing allocation removed

Input validation and lifecycle

PR Change
#12 Decoder lengths and nullable input fields guarded
#13 Helper cancellation and save-data ownership made explicit
#15 Random sampling domains and Paillier challenge widths bounded
#14 Party updates stop after fatal lifecycle errors
#26 Concurrent fatal lifecycle transition tests
#28 Input-bound hardening completed; malformed proof encodings return errors instead of panicking
#29 Native fuzz targets for untrusted decoder boundaries
#34 One shared 2,048..65,536-bit modulus policy enforced before expensive work in every exported unknown-order verifier; ModProof reuses constant-time contexts across its 80 iterations

Protocol transcript

PR Change
#9 Immutable per-party ProtocolModeLegacy / ProtocolModeSecurityV2 transcript selection; exact historical legacy compatibility; default-off, rollout-only historical Bob compatibility switch
#16 Security-v2 signing SSID binds the message and its fixed-width encoding (fullBytesLen)
#36 Signing rounds 5 and 7 store local state before emitting outbound messages (data race found by the final race run)
#39 Breaking API: signing delivers *common.SignatureData (protobuf message with an embedded mutex) instead of a by-value copy; matches upstream BNB fbb0ef7, with a deep copy on send
#38 Removes the private-key Paillier decryption cache added by the review fixes (no measured gain; kept unzeroed secret copies in global state); CI checks that go.mod's go directive matches the documented minimum

Assurance and docs

PR Change
#33 Signing boundary regressions: mixed-context ceremony rejection, m = q / q−1, out-of-order round readiness
#32 Live mixed-binary harness: current binary exchanges real messages with the pinned historical 2e712689 binary
#35 Changelog records all integration follow-ups
#40 Changelog and README completed: every composing PR listed, breaking changes 10–12, new exported API, corrected constant-time gap description, realistic release test commands

Review fixes pushed directly to dev

Made in response to the review of this PR; the review record and decisions are in the PR comments. dev has since been protected, so future changes arrive by PR only.

Commit Change
873b8ad ModProof encodes its invariant secret exponents once per proof and wipes them on completion
3b609ae Bob/BobWC/Alice provers reject out-of-domain witnesses with an error instead of panicking
20d66f8 Paillier operations validate keys and return errors instead of panicking; public-key N² cache (the private-key half was removed in #38)
c0978ef Mixed-binary harness requires both peers to reach round 8 and checks each accept-run proof at both bounds
98f642d Regression test for mixed-curve ZKV constructor rejection
fa6ef4d go.mod minimum restored to the documented Go 1.25.7

Verification on dev f8bbaff (plus #39 at 6056170, #40 at 9ff573c)

  • GitHub CI (Test, Vet, Keygen units, Go-fmt): pass.
  • Local go test -race -shuffle=on: common, crypto/..., tss, ecdsa/signing and ecdsa/keygen (561 s) pass.
  • Live mixed-binary harness: with the switch off (default), the current party rejects the historical peer in round 3 on both the Bob and BobWC checks and sends nothing for round 3. With the switch on, each captured proof rejects at the tight bound and verifies at the compatibility bound, and both peers reach round 8.
  • govulncheck: 0 reachable vulnerabilities. 5 advisories exist in required modules, but their code isn't called.
  • feat(signing)!: deliver signature results by pointer #39 (6056170): CI pass; ecdsa/signing race tests, mixed-binary harness and transcript oracle pass.
  • docs: complete changelog and README for the dev integration #40 (9ff573c): documentation only; CI pass.
  • keep-core migration (feat(tecdsa): adopt hardened tss-lib in legacy mode keep-core#4349), now pinned to 6056170: pkg/tecdsa/... (full DKG and signing protocol tests), pkg/tbtc/..., pkg/tbtcpg/..., cmd/... pass.
Earlier verification on `e7be9a0` (before the review fixes)
  • GitHub Go Test, including the bidirectional historical transcript oracle: pass.
  • Local repository-wide go test -race -shuffle=on ./...: pass (keygen 2058 s, signing 397 s).
  • Live mixed-binary harness. With the switch off (default), the current party rejects the historical peer in round 3 on both the Bob and BobWC checks, and sends nothing for round 3. With the switch on, both proofs are accepted and the exchange reaches round 8.
  • govulncheck: 0 reachable vulnerabilities. 5 advisories exist in required modules, but their code isn't called.
  • keep-core pkg/tecdsa/... compiled against dev; its runtime tests failed closed until the migration in feat(tecdsa): adopt hardened tss-lib in legacy mode keep-core#4349.

Merge gates

1. keep-core migration: PR open, tests pass

threshold-network/keep-core#4349 (draft) pins tss-lib 6056170 and meets the caller obligations:

  1. Select a protocol mode before constructing each party: it hardcodes ProtocolModeLegacy for its first release.
  2. Set a session nonce only in security-v2: legacy leaves it unset.
  3. Pass fullBytesLen to every signing constructor: the 32-byte sighash width.
  4. Enable historical Bob compatibility only during the mixed-binary window: the tbtc.legacyHistoricalBobCompatibility flag, off by default.

It also receives signing results as *common.SignatureData (#39) and sets GOTOOLCHAIN=auto in its Docker build stages for the Go 1.25.7 minimum. Its DKG, signing, tbtc, tbtcpg and cmd tests pass. The migration also uncovered and fixed a keep-core issue caused by tss-lib's btcec/v2 move: wallet keys from tss-lib and from keep-core's own parser used different secp256k1 curve objects, so identical keys compared unequal. Remaining: review and merge threshold-network/keep-core#4349, then move its pin to a tagged tss-lib release after this PR merges.

2. Rollout plan: decided for the first release; security-v2 cutover deferred

  1. Release keep-core hardcoded to ProtocolModeLegacy. It speaks the exact historical transcript, so upgraded and old nodes can still sign together.
  2. While any pre-upgrade signer remains, operators enable tbtc.legacyHistoricalBobCompatibility. The node logs a startup warning while it is on.
  3. Turn the flag off once every signer is upgraded. The next keep-core release removes it.
  4. Switching to ProtocolModeSecurityV2 is a wire break that needs every signer to switch together. Deferred; options tracked in Design: coordinated cutover to tss-lib ProtocolModeSecurityV2 keep-core#4350.

3. Latency: within budget at 10 members; production size not yet measured

Agreed budget: full keep-core signing on dev at most 2x slower than on master. Measured in keep-core with 10 members and threshold 6, 10 signatures per side:

Signing median Range
current pin (2e712689) 7.05 s 6.83–7.24 s
dev f8bbaff 9.19 s 8.65–11.45 s

That's 1.30x. DKG time is unchanged (about 33 s). The constant-time cost grows with group size, so at 100 members the ratio could move toward the per-operation Paillier decryption ratio (about 70 ms vs 33 ms, roughly 2.1x). Before release: re-measure at production size on dedicated hardware.

Next steps

In order. Steps 1–3 can run in parallel; 4 follows the merge; 5 is required before mainnet.

Known risks and residual gaps

  • Historical Bob compatibility weakens verification. Turning it on re-admits the historical unbounded-witness proof shape: a malicious peer can use the wider witness range (the documented y = N − T wrap family). It is default-off, legacy-only, and frozen at party construction. keep-core bounds its lifetime: a node flag, a startup warning while enabled, and removal in the following release.
  • Constant-time coverage is not end to end. Modular exponentiation and multiplication on secrets use the constant-time bigmod path. Surrounding math/big conversion and reduction remain variable-time; known instances are the round-5 reduction of rx and the lack of response-time normalization around Paillier decryption. An independent side-channel review is advisable before mainnet.
  • No human cryptographic audit yet. All review on this stack was agent-driven. The transcript and verifier-bound changes (Add immutable per-party GG20 transcript modes #9, Bind security-v2 signing SSIDs to the immutable message context #16, fix(crypto): complete input-bound hardening #28, fix(proofs): bound unknown-order verification #34) are the highest-value audit targets.
  • The mixed-binary harness stops at round 8. Its fixture holds only 2 shares of a threshold-10 key, so the final aggregate check cannot pass. It proves live interoperability through every per-peer MtA and Schnorr check, but not full signature completion.

Follow-ups (non-blocking)

  • Full mixed-version signature. A threshold-matched fixture (for example 3-of-5) would let the mixed-binary harness complete a signature and check it.
  • Dependency advisories. Bump golang.org/x/crypto from v0.52.0 to v0.56.0 or later (GO-2026-6354/6355/6303). Replace gogo/protobuf v1.2.1 (GO-2021-0053): bump to v1.3.2, or better, migrate to the google.golang.org/protobuf runtime the repo already uses.
  • CI runtime. Under the race detector the keygen package took 561 s on a quiet host and 2,058 s on a loaded one; the weekly race workflow allows 120 minutes. Shard the slow keygen tests or cache the precomputed parameters if PR CI gains a race job.
  • Weekly race workflow. GitHub runs scheduled workflows only from the default branch, so the weekly race workflow on dev stays inactive until this PR merges.
  • Continuous fuzzing. The test: add native fuzz targets for untrusted decoder boundaries #29 fuzz targets run only on demand. Add a nightly fuzz job or an OSS-Fuzz integration.
  • Lint debt. Remaining staticcheck findings are deprecated elliptic.Curve.ScalarBaseMult calls in tests and one ST1005 error-string style warning.

lrsaturnino and others added 30 commits July 28, 2026 23:19
Port the upstream constant-time framework (common/constant_time.go, built on
filippo.io/bigmod) and wire it, behind a default-off runtime toggle, into the
secret-exponent modular exponentiations in Paillier Decrypt/Proof and the DLN,
factor, and Paillier-Blum modulus proofs. Mitigates the timing side-channel gap
(CVE-2023-26557 class) flagged in the threshold-ECDSA variant analysis.

- common: EnableConstantTimeOps / DisableConstantTimeOps / IsConstantTimeEnabled
  toggle + CTModInt (ExpCT / ModInverseCT / MulCT) over filippo.io/bigmod.
- Wired ops use odd moduli only; phi(N)-even inverses stay on math/big.
- Default off: zero behavior/perf change unless EnableConstantTimeOps() is called.
- Bump go directive 1.16 -> 1.23 (filippo.io/bigmod floor).
- Tests: framework equivalence, per-proof byte-identical/verifies equivalence,
  and full constant-time keygen + signing end-to-end.
Close the secret-exponent coverage gap and fix correctness/robustness
issues in the opt-in constant-time path:

- Harden the remaining secret-exponent modexps: MtA ProveBobWC (h1^x,
  h1^y), ProveRangeAlice (h1^m), the ring-Pedersen trapdoor setup in
  keygen (h1i^alpha), and Paillier Encrypt (gamma^m) / HomoMult (c1^m).
- Pad the exponent to a fixed width in ExpCT so its running time no
  longer leaks the secret exponent's magnitude.
- ModInverseCT returns nil for non-coprime inputs (matching
  math/big.ModInverse) instead of a silent wrong value.
- reduceToPaddedBytes reduces unconditionally (no secret-dependent branch).
- Assert odd modulus at CTModInt construction (fail at build, not at Exp).
- Remove the unused TimingProtection / ConstantTimeCompare / Mod() API
  (the jitter helper also discarded a rand error -> nil-deref panic path).
- Document the hardening coverage scope in the package doc.
- Add equivalence/regression tests for every new site.
Follow-up to the go.mod merge conflict resolution when rebasing the
constant-time-hardening branch onto current master: go mod tidy moves
the indirect dependencies (previously listed inline from the branch's
older base) into their own require block with versions resolved
against master's current module graph. No functional change.
The constant-time-hardening branch's tests were written against an
older base, before this fork's own independent hardening added: (1)
the 2048-bit floor on dlnproof.Verify's modulus (verifyMinModulusBitLen),
(2) the mandatory per-ceremony SetSessionNonce on ECDSA keygen/signing
Start(), and (3) the mandatory positive fullBytesLen argument on ECDSA
signing constructors. Without these adaptations the ported tests
fail/panic against current master even though the underlying
constant-time crypto is unaffected:

- crypto/dlnproof/constant_time_equiv_test.go: use 1024-bit safe primes
  (NTilde ~2048 bits) instead of 512-bit, matching the
  verifyMinModulusBitLen=2048 floor and this fork's own convention
  (ecdsa/keygen/prepare.go safePrimeBitLen=1024).
- ecdsa/keygen/constant_time_e2e_test.go: call
  params.SetSessionNonce(big.NewInt(1)) before Start(), as required by
  ecdsa/keygen/round_1.go's fail-closed session-nonce check.
- ecdsa/signing/constant_time_e2e_test.go: same SetSessionNonce call,
  plus pass the now-mandatory fullBytesLen=32 argument to NewLocalParty
  (ecdsa/signing/local_party.go's validateFullBytesLen).
Two small zero-risk backports bundled together (same file, adjacent
sections):

- Size errCh to concurrency instead of concurrency*numPrimes (backport
  of upstream bnb-chain/tss-lib commit 4c83ace). At most 'concurrency'
  goroutines run at once, each sending at most one error, so the
  larger buffer was wasted allocation. No functional change.
- Fix p=2q+1 typo in GetRandomSafePrimesConcurrent doc comment
  (backport of upstream commit 27922e0). Doc-only; no functional
  change.
…_vss.Create

Backport of upstream bnb-chain/tss-lib commit b7b73a0. samplePolynomial
already sets v[0] = secret (crypto/vss/feldman_vss.go), so this
assignment was a no-op. Verified against fork's current
samplePolynomial before applying.
…g style consistent

Backport of upstream bnb-chain/tss-lib commit 0629cff. Behaviorally
identical (skip nil errs vs. include non-nil errs); pure style.
Adapt the generator check from bnb-chain/tss-lib commit d3c1af5, pinning v1.30.0 to match the four generated files in this fork.
Adapt the result handoff in common/safe_prime.go from public upstream
commit 24bb7d3.

Add bounded worker lifecycle tests for cancellation with no receiver and
a full result buffer, plus ordinary result delivery. Test cleanup drains
pending results and joins workers.
Adapt Xi and ShareID copying from public upstream commit
04bb840 and the roster-content guard
from 1693884, limited to
ecdsa/keygen/save_data.go.

Preserve nil local secrets and existing pre-parameter and per-party
pointer sharing. The nil saved-key diagnostic and focused ownership,
subset-selection, and input-guard unit tests are additions for this branch.
Adapt the common prime-size and unit-domain guards from public upstream
dc9b957.

Adapt the Paillier sampling changes from public upstream
b64213a to this base. Use an 18-bit
minimum for the local safe-prime candidate range and separation condition.
Propagate empty sampler results through direct callers while preserving
function signatures and the ordinary 2048-bit challenge output.

These are source-level adaptations, not cherry-picks. This base has no
quadratic-non-residue sampling helper, and its ModProof is outside the
scope of this change.

Add bounded domain checks, modulus-width controls, and a fixed 2048-bit
challenge digest captured from base 86bd1a3.
Validate helper inputs before indexed reads and coordinate-buffer allocation,
require a commitment payload, and propagate nil signing point results through
the existing error paths. Preserve base proof arities, optional-mode proof
serialization, nil-key parameter skips, and valid routing and point encodings.

Adapted from upstream commits:
f1588ae (WC decoder arity)
8deab46 (sender metadata guards)
1a8e277 (embedded PartyID guards)
8069c6c (SortedPartyIDs.Keys guard)
6e0bcd4 (shared nullable-input guards)
58a131c (commitment minimum parts)
cbb8fe4 (commitment boundary coverage)
e65fb36 (Gob coordinate allocation bounds)
8acae9a (signing nil-result guards)
1693884 (PartyID.String content guard)
Include the message integer and fixed byte width in security-v2 signing
SSIDs. Preserve legacy transcript selection and the 32-byte SSID encoding.
Snapshot the caller's message when constructing a signing party so its
context remains stable. Add context separation, matching-peer, fixed-width,
message ownership, and legacy tests.

SSID binding adapted from the ECDSA signing changes in upstream commits
71dad22 and
b73eea7.
Validate ECDSA keygen decommitment part counts before hashing and point decoding.

Adapted from public upstream commit 1693884.
Port the zero-exponent path, public exponent bounds, and toggle snapshot
corrections from public PR #10 commits
3169e56 and
cf15377. Adapt the default-on behavior
from upstream PR bnb-chain#332 commit 44a95b2
on top of public PR #11 at 68686af.

Keep the existing exported method and toggle signatures, add an explicit
public exponent bound, and snapshot the mode across each multi-step
operation. Use LambdaN for decryption's inverse so keys without PhiN
remain supported. Restore prior toggle state in tests and select disabled
baselines explicitly. Document the limited coverage of the bigmod path.
Raise the minimum Go version to 1.17 as required by the patched runtime. Preserve generated schemas and add binary encoding controls captured on the previous runtime.
Select x/crypto v0.52.0 and its required x/sys v0.45.0. Align the minimum Go version to 1.25.7 and development and CI toolchain to 1.26.8 while preserving runtime source and existing compatibility vectors.
…t v1.4.0 notes

NOT ready to merge/push. Exploratory work pending reconciliation with the
live #8/#10/#11/#17 stack (mswilkison) which already rebases+hardens the CT
backport more rigorously than this commit (bounded exponent widths, overflow
rejection, mode snapshotting) and already flips constantTimeEnabled's default
directly. This commit's own contribution -- CT coverage for
crypto/schnorr/schnorr_proof.go and ecdsa/signing/round_3-5.go, which none of
#8/#10/#11/#17 touch -- is real and not yet duplicated elsewhere, but should
land as an addition on top of that stack, not a competing branch.

Kept locally, unpushed, for reference pending user/maintainer coordination.
…tion gap

- Remove common/constant_time_init.go: PR #17 (mswilkison, stacked on #11)
  already flips constantTimeEnabled's default to 1 directly in
  common/constant_time.go, making a separate init() redundant. It was also
  actively wrong: dlnproof/constant_time_equiv_test.go (and this commit's own
  new schnorr test) generate their 'non-CT baseline' proof before calling
  EnableConstantTimeOps(), assuming an ambient disabled default -- an
  unconditional init() would silently make that baseline vacuous instead of
  failing loudly.
- Update the COVERAGE doc comment in common/constant_time.go to list the
  Schnorr proof responses and ECDSA signing rounds 3-5 now covered, and to
  record crypto/mta.AliceEnd/AliceEndWC's Paillier-decrypt timing protection
  (upstream BNB 3709c25) as a known, deliberately-NOT-ported gap: upstream
  uses a sleep-based response-time normalizer (NewTimingProtection, ~200ms
  target + jitter), a different mechanism entirely from the bigmod
  constant-time path used everywhere else in this file. That primitive
  exists nowhere in this fork's lineage (checked master, both CT branches,
  and PRs #12-17). Porting it would inject a fixed ~200ms delay into every
  MtA share round -- a real latency cost that needs its own sign-off, not a
  mechanical extension of the existing pattern.
- Fix CHANGELOG.md's now-stale 'has not yet published its own tagged
  release' line (contradicted by the new [1.4.0] section added earlier).
…_init.go

Two spots still described CT-enablement as happening via a package init()
in common/constant_time_init.go. That file was removed before this branch
was ever pushed (superseded by PR #17's direct default-value change,
constantTimeEnabled = 1 in common/constant_time.go) -- these two CHANGELOG
lines were never updated to match and got carried forward by the cherry-pick
onto this branch. Correct the mechanism description in both places.
Address review findings on PR #23:

- The equivalence tests built their non-CT baseline without disabling
  constant-time mode. Since this branch defaults constantTimeEnabled to 1,
  the baseline actually exercised the CT path, and the bare deferred
  DisableConstantTimeOps leaked disabled state into subsequent Schnorr
  tests, making coverage order-dependent and dropping default-path
  coverage. Save, set, and restore the ambient mode via a withCTMode
  helper, and assert the baseline really is running with CT off.

- Restore the changelog hardening entries to [Unreleased]. This fork has
  no tags or published releases, so a dated [1.4.0] heading advertised
  the security work as obtainable, and both comparison links pointed at
  a tag that does not exist.
Both workflows filtered pull_request events to base branch master. This fork
stacks pull requests on each other's branches, so every stacked PR and every
PR targeting dev ran with no checks at all: #17 and #23 currently report no
checks, and the whole in-flight batch targeting dev is covered only by
locally-run tests.

Drop the pull_request branch filter so any PR gets CI regardless of base, and
add dev to the push trigger so the integration branch is covered on merge.
Addresses review findings on PR #23's constant-time hardening entry:

- CHANGELOG.md: tag Breaking Change #8 and the Added CT-symbols entry
  with PR #17/#23, extend the Composing PRs list (F2)
- CHANGELOG.md: soften 'closing the timing side-channel' framing to
  scope it to the operations covered, cross-reference the mta
  AliceEnd/AliceEndWC gap instead of implying full closure (F6)
- CHANGELOG.md: note the coverage broadening from secret-exponent-only
  to secret-operand operations (F7)
- CHANGELOG.md: restore a 'Not ported / deferred' bullet for the mta
  gap so the dangling '(see below)' cross-reference resolves again (F8)
- CHANGELOG.md: fix the benchmark command to actually run both
  BenchmarkExpCT and BenchmarkExpStandard, correct the mismatched
  sample-count claim (F10)
- common/constant_time.go: note in the reduceToPaddedBytes NOTE that
  round_5's rx (a field-prime coordinate) is the one operand reduced
  into group-order space, and why that's still safe (F3)
- common/constant_time_test.go: add BenchmarkMulCT/BenchmarkModInverseCT
  on a 256-bit-class modulus so the CHANGELOG's performance claim can
  cite the operations this PR's stack actually added, not just the
  2048-bit ExpCT benchmark (F5)
Addresses review findings on PR #23's constant-time hardening in
ecdsa/signing:

- round_5.go: correct the SECURITY comment's operand list -- m is the
  public message hash, not secret; name rx = R.X() instead, the
  operand that actually needed flagging (F9)
- constant_time_equiv_test.go (new): add targeted equivalence tests
  for round_3's thelta/sigma, round_4's thetaInverse, and round_5's si
  terms -- the five new MulCT/ModInverseCT call sites this PR's stack
  added had only e2e coverage before this (F13)
- constant_time_e2e_test.go: expand TestE2EConcurrentConstantTime's
  doc comment to name the Schnorr and round 3/4/5 CT paths it now
  also exercises, not just the pre-existing Paillier/MtA path (F14)
@piotr-roslaniec

Copy link
Copy Markdown
Author

Review record and decisions (2026-10-02)

The review of this PR was not posted on GitHub. Its fixes were pushed directly to dev as 873b8ad..fa6ef4d. This comment records them so the findings stay traceable. If the original review notes exist elsewhere, please link them here.

Fixes pushed to dev

Commit Fix
873b8ad ModProof encodes its invariant secret exponents once per proof and wipes them on completion; canonical-operand constant-time helpers
3b609ae ProveBobWC / ProveRangeAlice reject out-of-domain witnesses with an error instead of panicking; compatibility-enabled Bob verification checks nil inputs first
20d66f8 Paillier Encrypt / HomoMult / Decrypt validate keys and return errors instead of panicking; per-key constant-time state cache
c0978ef Mixed-binary harness requires both peers to reach round 8 and checks each accept-run proof at both bounds
98f642d Regression test for mixed-curve ZKV constructor rejection
fa6ef4d go.mod minimum restored to the documented Go 1.25.7 (PR #33 had lowered it to 1.25.6)

Verified on fa6ef4d: CI Test, Vet, Keygen units and Go-fmt pass. Local race and shuffle tests pass on common, crypto/... and tss. The mixed-binary harness passes in both directions.

Decisions

  1. Paillier private-key cache: removed in fix(paillier): stop caching private-key decryption state; review follow-ups #38. It gave no measured Decrypt gain (70.7 → 70.6 ms) and kept unzeroed copies of LambdaN-derived secrets in package-global state. Key validation and the public-key cache stay.
  2. Changes reach dev only through PRs. Branch protection is now on: PR required, the Test / Vet / Keygen units / Go fmt project checks must pass, and it applies to admins too. The changelog now attributes these fixes to their commits instead of "(PR Integrate dev hardening set into master #37)".
  3. keep-core's first release hardcodes ProtocolModeLegacy. Historical Bob compatibility is enabled through node config.
  4. The compatibility flag logs a startup warning while enabled and is removed in the next keep-core release.
  5. Latency gate: at most a 2x slowdown. Measured as full keep-core signing time on dev vs master, at the same group size on the same hardware.

…ow-ups (#38)

fix(paillier): stop caching private-key decryption state; review follow-ups
common.SignatureData is a protobuf message and embeds a mutex, so delivering it by value over the signing end channel made every receiver copy a lock (go vet copylocks). The end channel is now chan<- *common.SignatureData and the party stores its data by pointer, matching upstream BNB fbb0ef7. finalize sends a proto.Clone so the receiver owns a deep copy.
piotr-roslaniec and others added 3 commits October 2, 2026 14:31
feat(signing)!: deliver signature results by pointer
List every composing PR, add breaking changes for the btcec/v2 curve type, keygen decoder signatures and the Go 1.25.7 minimum, record the new exported API, correct the AliceEnd constant-time gap description and the Paillier cache entry, and fix the README release test commands and audit scope.
piotr-roslaniec and others added 10 commits October 2, 2026 15:09
docs: complete changelog and README for the dev integration
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M003 [P3] common/constant_time.go:292: GetCTModInt keeps an exported, unbounded, never-evicted global cache keyed by modulus
- M004 [P3] common/constant_time.go:310: NewCTModIntWithPhi duplicates NewCTModInt's body and has one caller
- M007 [P3] common/validation.go:21-30,39-41: The 65,536-bit modulus ceiling does not bound verifier CPU as its comment claims (ProbablyPrime is ...
- M008 [P3] common/validation.go:33-48: Modulus-policy check is a two-call protocol (ceiling before primality) hand-ordered at each of 6 verifiers
- M010 [P3] common/constant_time.go:13-14,34-38: common/constant_time.go header: garbled COVERAGE sentence, change-narration with no issue link, stale Encrypt ...
- M014 [P3] common/constant_time_test.go:431-555: TestMulCTTimingConsistency, TestModInverseCTTimingConsistency and TestExpCTTimingConsistency can never fail
- M017 [P3] common/random.go:80,98,123-130: New nil-return contracts are undocumented
- M019 [P3] common/validation.go:13-18,24-26,33-38: common/validation.go modulus-policy comments: illogical Min rationale and false O(bitLen) claims
- M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling
- M093 [P3] ecdsa/signing/round_5.go:77: CHANGELOG, constant_time.go and round_5.go disagree on whether rx is secret; it is derived from public values ...
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M002 [P3] common/constant_time.go:207-215: The secret-wipe behavior advertised by 873b8ad has no assertion
- M020 [P3] crypto/paillier/mod_proof.go:96: ModProof exponent wipe is not deferred, so a panic skips it; its doc comment says it also runs on abort
- M021 [P2] crypto/paillier/paillier.go:194: Constant-time exponents are padded to N.BitLen() (2048) instead of public protocol bounds: HomoMult and the ...
- M023 [P3] crypto/paillier/key_reuse_regression_test.go:81-119: TestKeyReuseAfterKeyValuesReplaced and TestByValueKeyCopyIsIndependent use the public-key N² cache only for ...
- M026 [P3] crypto/paillier/mod_proof.go:368-405: Four standalone QR helpers are test-only copies of the context methods
- M027 [P3] crypto/paillier/paillier.go:121-123,170-180,233-239: No test references ErrMessageTooLong or ErrMessageMalFormed
- M035 [P3] crypto/paillier/mod_proof.go:65-70,232-236: mod_proof.go comments narrate the change history instead of describing the current design
- M037 [P3] crypto/paillier/paillier.go:157,193,196: runtime.KeepAlive(publicKey) appears three times with no comment
- M038 [P3] crypto/paillier/paillier.go:200-212: HomoAdd lacks the checkPaillierModulus guard the other Paillier operations gained
- M039 [P3] crypto/paillier/paillier.go:259-271: The ErrMalformedKey doc says a non-invertible decryption coefficient is reported "consistently by both" modes
- M040 [P3] crypto/paillier/paillier.go:284-285: The Proof doc runs two sentences together with no break: "...square-free from 3.1" then "It panics if..."
- M050 [P2] crypto/mta/share_protocol_test.go:183-203: "Overwide ... must fail before exponentiation" MtA tests cannot tell the cheap bound check from the later ...
- M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M021 [P2] crypto/paillier/paillier.go:194: Constant-time exponents are padded to N.BitLen() (2048) instead of public protocol bounds: HomoMult and the ...
- M042 [P2] crypto/mta/proof_bob_verifier.go:74-79: Nothing tests the upper edge of the opt-in historical-compat bound T1 < (q+1)*N
- M043 [P2] crypto/mta/legacy_bob_compatibility_test.go:119-138: The security-v2 Bob T1 bound (q^7 + 1) is never exercised by a test
- M044 [P3] crypto/mta/proof_bob_verifier.go:44-60: Bob and RangeAlice verifiers duplicate the same modulus/generator/ciphertext preamble
- M045 [P3] crypto/mta/proof_bob_verifier.go:84-95: The default tight Bob bound is justified with the wrong prover, and "legacy" names two different provers
- M046 [P3] crypto/mta/proofs.go:40-52,61-62,76-78,116: Constant-time path panics on even/nil/degenerate moduli in exported provers; 3b609ae no-panic claim overstated
- M048 [P3] crypto/mta/share_protocol.go:105-185: Four near-identical "verify, then decrypt, then reduce mod q" functions
- M049 [P3] crypto/mta/share_protocol.go:144-148: AliceEndLegacy doc comment has a truncated sentence about the historicalBobCompat=false case
- M050 [P2] crypto/mta/share_protocol_test.go:183-203: "Overwide ... must fail before exponentiation" MtA tests cannot tell the cheap bound check from the later ...
- M056 [P3] crypto/mta/proofs.go:92: Review-process labels appear with no definition in production comments, CHANGELOG, READMEs and CI: "PRIOR", ...
- M058 [P3] crypto/mta/share_protocol.go:149-185: AliceEndLegacy and AliceEndWCLegacy with historicalBobCompat=false rejecting a historical-witness proof, and ...
- M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling
- M060 [P2] crypto/dlnproof/proof_test.go:37-95: DLN Verify reject tests use N=23, so the 2048-bit floor rejects first and the named T/Alpha/nil checks are ...
- M062 [P3] crypto/ckd/child_key_derivation.go:108-123: ckd curve type switch returns a nil-coordinate key with nil error for non-v2 secp256k1 curve objects
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M075 [P2] ecdsa/keygen/local_party.go:71: No test shows the keygen/signing constructors enforce the explicit protocol-mode rule (the headline runtime ...
- M077 [P2] ecdsa/keygen/rounds.go:104-121: Legacy keygen transcript selection (proofSession/proofContext return nil) is not pinned by any test
- M079 [P3] ecdsa/keygen/prepare.go:129: Keygen ring-Pedersen trapdoor inverse (beta = alpha^-1 mod pq) stays variable-time, contrary to Breaking ...
- M083 [P3] ecdsa/keygen/rounds.go:125-131: The keygen getSSID doc still says "Callers must invoke this exactly once, in round 1", but legacy mode never ...
- M088 [P3] ecdsa/signing/local_party.go:~110-122: The constructor docs omit the new caller obligation, which is the main compatibility break
- M097 [P3] ecdsa/signing/signing_boundaries_test.go:32: Mixed-mode ceremonies (a legacy party among security-v2 peers, or the reverse) are not tested
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M075 [P2] ecdsa/keygen/local_party.go:71: No test shows the keygen/signing constructors enforce the explicit protocol-mode rule (the headline runtime ...
- M084 [P2] ecdsa/signing/finalize.go:82: No test inspects the delivered *common.SignatureData (breaking change 9): R/S/recovery id/M width and the ...
- M085 [P2] ecdsa/signing/round_5.go:102-111: The #36 store-before-emit ordering in signing rounds 5 and 7 has no regression test
- M086 [P3] ecdsa/signing/local_party.go:156: The constructor copies msg (lines 157-159) to stop caller mutation changing the signing context, but ...
- M088 [P3] ecdsa/signing/local_party.go:~110-122: The constructor docs omit the new caller obligation, which is the main compatibility break
- M093 [P3] ecdsa/signing/round_5.go:77: CHANGELOG, constant_time.go and round_5.go disagree on whether rx is secret; it is derived from public values ...
- M094 [P2] ecdsa/signing/round_ct_wiring_test.go:27: The signing "CT wiring" tests cannot fail if the constant-time branch is removed
- M096 [P3] ecdsa/signing/signing_boundaries_test.go:297-376: The m = q / q-1 boundaries are covered only in legacy mode and not for the SSID or full-ceremony effect
- M097 [P3] ecdsa/signing/signing_boundaries_test.go:32: Mixed-mode ceremonies (a legacy party among security-v2 peers, or the reverse) are not tested
- M104 [P3] ecdsa/signing/signing_boundaries_test.go:361-367: The m = q-1 accept subtest receives from out with select { case m := <-out: } and no timeout
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M056 [P3] crypto/mta/proofs.go:92: Review-process labels appear with no definition in production comments, CHANGELOG, READMEs and CI: "PRIOR", ...
- M122 [P2] testdata/legacy_transcript/verify.sh:22-37: The CI "bidirectional" oracle never checks that HEAD legacy provers produce proofs the historical verifier ...
- M123 [P3] testdata/legacy_transcript/README.md: Mixed-binary evidence is narrower than claimed: no live mixed keygen, stops at round 8
- M124 [P3] testdata/legacy_transcript/README.md:30-46: The README describes the proof-source digest as SHA-256 over the listed repository files, which implies a ...
- M125 [P3] testdata/legacy_transcript/oracle/main.go:225-227: The oracle vector holds no wide-witness historical Bob/BobWC proof, the shape the compat toggle exists for
- M127 [P3] testdata/legacy_transcript/README.md:93,145,160: The fixture shape is called "2-of-20" in some places and "2-of-2" in others
- M128 [P3] testdata/legacy_transcript/verify_mixed_interop.sh:16-17: The script header says the compat-on party accepts "the identical exchange"
- M129 [P2] .github/workflows/test.yml:99-103: The live mixed-binary harness (verify_mixed_interop.sh) and its own tests never run in CI
- M130 [P3] .github/workflows/test.yml:42,74,96: Documented Go 1.25.7 minimum is never built in CI
Fix the findings below from the review of PR #37 (ID, severity, review location: issue).

- M001 [P2] common/constant_time.go:14-40: CHANGELOG and PR risk text overclaim constant-time coverage: MtA proof blinds and secret EC scalar ...
- M015 [P3] common/int.go:104: CHANGELOG "Added" says IsInInterval and tss.SameCurve back the hardened range checks; IsInInterval has no ...
- M024 [P2] crypto/paillier/mod_proof.go:184-199: CHANGELOG Breaking Change 4 says ModProof/FactorProof use HashToNTagged; no production code calls it
- M056 [P3] crypto/mta/proofs.go:92: Review-process labels appear with no definition in production comments, CHANGELOG, READMEs and CI: "PRIOR", ...
- M062 [P3] crypto/ckd/child_key_derivation.go:108-123: ckd curve type switch returns a nil-coordinate key with nil error for non-v2 secp256k1 curve objects
- M067 [P3] crypto/commitments/commitment.go:55-71: CHANGELOG says canonical-generator checks were added in crypto/commitments; that package has none
- M088 [P3] ecdsa/signing/local_party.go:~110-122: The constructor docs omit the new caller obligation, which is the main compatibility break
- M093 [P3] ecdsa/signing/round_5.go:77: CHANGELOG, constant_time.go and round_5.go disagree on whether rx is secret; it is derived from public values ...
- M113 [P3] tss/params.go:160-166: SetLegacyHistoricalBobCompatibility(false) panics outside legacy mode, contrary to the documented contract
- M116 [P3] tss/params.go:226-227: SetSessionNonce godoc says keygen/signing fail closed without a nonce; true only in security-v2
- M123 [P3] testdata/legacy_transcript/README.md: Mixed-binary evidence is narrower than claimed: no live mixed keygen, stops at round 8
- M132 [P3] go.mod:1: Proposed release tag v2.0.0-threshold.1 is not consumable as a Go module version: module path has no /v2 ...
- M136 [P2] README.md:63-98,145-150: README example reuses one Parameters (and session nonce) for keygen and signing, and never says Parameters is ...
- M138 [P3] CHANGELOG.md:322-328: Breaking Changes 4 and 5 were not updated for dual-mode, and they contradict #3 (lines 335-350) and the ...
- M140 [P3] CHANGELOG.md:478-480: CHANGELOG break-ledger blockquote: wrong verification baseline and incomplete list
- M141 [P3] CHANGELOG.md:91-99: CHANGELOG claims four caller obligations are enforced at runtime; obligation 4 (and parts of 2/3) are not
- M149 [P3] README.md:83: README keygen example passes *LocalPreParams where NewLocalParty takes LocalPreParams by value
piotr-roslaniec and others added 7 commits October 5, 2026 20:13
- Samplers in common return nil instead of panicking when the limit is
  wider than the 5000-bit cap; MtA provers return an error then. The
  provers' own width pre-check and its mirrored constant are removed.
- Replace paillier HomoMultWithBitLen with HomoMultBounded(m, c1, bound),
  which takes a public bound and rejects m outside [0, bound). New
  ErrInvalidBound sentinel. BobMid/BobMidWC pass q.
- Signing round 2 checks local gamma and w are in [0, q) before the
  per-peer fan-out and blames no peer on failure.
- Drop the six redundant ceiling pre-checks; IsUsableUnknownOrderModulus
  already checks width before ProbablyPrime. Unexport the helper. Modulus
  errors print the bit length, not the full value.
- ckd decodes non-secp256k1 keys with elliptic.UnmarshalCompressed.
- ModProof test hook proves the deferred exponent wipe runs on panic.
- DLN, Bob/Alice bound, verifier-gate and ModVerify policy tests use
  equation-valid inputs, so only the named check can reject them.
- Exact T1 edges for default, compat and session bounds; S2/T2 shift
  tests; q-1 witness edges in constant-time mode.
- Signing delivery check mutates all results before comparing; errors
  before the expected round fail the test; E2E helper has a deadline;
  constant-time E2E variants become one mode x CT table; KDD delta copy
  test.
- Rename review-named test files after their behavior; replace hand-
  written panic helpers with testify; drop review labels.
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.

4 participants