Skip to content

Integrate dev hardening set into master - #37

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

piotr-roslaniec wants to merge 152 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 and others added 16 commits September 30, 2026 08:23
fix(proofs): bound unknown-order verification
docs(changelog): record integration follow-ups
fix(signing): publish round state before messages
Add ExpCTWithBytes/ExpCTCanonicalWithBytes/ExpCTCanonicalWithBitLen/
MulCTCanonical to CTModInt so a caller that has already proven an
operand is canonical (0 <= operand < modulus) can skip the generic
big.Int.Mod reduction, and a proof that reuses one exponent across many
iterations can encode it once instead of re-encoding per call.

ModProof pre-encodes its invariant secret exponents (psP, psQ, rootExp,
invN) once per proof and reuses them across all 80 iterations, wiping
the owned encodings on completion. Existing reducing APIs, proof
equations, and transcript bytes are unchanged.
ProveBobWC and ProveRangeAlice now reject a negative or over-width
witness with an error, before any constant-time exponentiation, instead
of panicking inside the fixed-width exponent encoder. The guard sits
ahead of randomness sampling so an unusable key (N<=1) is rejected the
same way in both modes.

VerifyLegacy's compatibility-enabled bound is now derived inside the
shared verifier core, after its existing nil-input guard, so a nil
curve, key, or modulus returns false instead of panicking.

Move the shared Bob/BobWC verifier core into proof_bob_verifier.go
(proofs.go was 656 lines) with equations unchanged. Replace the
wording-pinned randomness-domain assertion in sampling_test.go with a
behavior-based rejection check.
Encrypt, HomoMult, and Decrypt now validate the modulus (and, for
Decrypt, LambdaN) before constructing a constant-time context, in both
timing modes, instead of panicking on a malformed caller-constructed
key such as an even N.

Add an owner-scoped reuse cache keyed by weak.Pointer to the PublicKey/
PrivateKey, so the N^2 constant-time context, the fixed-width LambdaN
encoding, and the ciphertext-independent decryption coefficient are
built once per unchanged key value and reused across calls. Entries are
removed via runtime.AddCleanup when the owning key becomes unreachable,
and value snapshots detect sequential mutation of the exported N/
LambdaN fields and rebuild. Exported struct layouts, JSON/Gob encoding,
and by-value copy semantics are unchanged.

Drop the wording-pinned empty-randomness-domain test in sampling_test.go
now that an N=1 modulus is rejected earlier by the new key validation.
Replace the single ReachedRound8 flag with separate
AliceReachedRound8/BobReachedRound8 actor flags and a round8BothReached
termination predicate. The message pump now captures and drops a round-
8-or-later message instead of forwarding it, and keeps delivering
pending lower-round messages until both actors have emitted round 8,
so a successful exchange can no longer qualify on only one direction's
evidence.

The accept scenario now requires each captured Bob and BobWC proof to
independently reject at the tight bound (compat off) and accept at the
widened historical bound (compat on), pairing the accept run's own
proof evidence with the same discrimination the reject scenario already
performs, instead of relying on an identical-seed replay claim. Update
the README and scenario comments to describe a fresh exchange with
paired per-proof verification.
NewZKVProof and NewZKVProofWithSession reject V and R points from
different curve groups, but no existing test exercised that boundary;
every constructor test used same-curve fixtures. Add a regression with
individually valid secp256k1/P256 points in both role assignments, plus
a same-curve control whose proof verifies, so the negative cases are
attributable to the curve mismatch rather than an invalid point.
README.md and the PR intent state Go 1.25.7 or newer is required, but
the go directive declared 1.25.6, so the module's enforced minimum
disagreed with the documented contract. The preferred development and
CI toolchain remains 1.26.8.

Record the PR #37 hardening fixes in the changelog: exported arithmetic
boundary errors, owner-scoped Paillier CT reuse, proof-local exponent
reuse, bidirectional live qualification, and the new boundary regression
coverage.
A per-key cache of LambdaN, its byte encoding, and the decryption coefficient measured no Decrypt speedup (70.7 vs 70.6 ms) while keeping unzeroed secret-derived copies in package-global state. Decrypt rebuilds its state per call again; key validation and error returns are kept, as is the public-key N^2 cache.
@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.
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