Skip to content

feat(crypto): opt-in constant-time path for secret-exponent modexps [DO NOT MERGE] - #8

Closed
piotr-roslaniec wants to merge 2 commits into
devfrom
constant-time-hardening
Closed

piotr-roslaniec wants to merge 2 commits into
devfrom
constant-time-hardening

Conversation

@piotr-roslaniec

Copy link
Copy Markdown

⛔ DO NOT MERGE

This PR is not ready to merge. It is opened for review and discussion only.
Merging carries an external coordination cost (a Go toolchain bump that ripples
into keep-core — see "Blocking considerations" below) that must be agreed first.

Summary

Closes the timing side-channel / constant-time gap (CVE-2023-26557 class) identified in the
threshold-ECDSA variant analysis. Ports the upstream BNB tss-lib constant-time framework
(common/constant_time.go, built on filippo.io/bigmod —
the same constant-time bignum library used inside Go's crypto/rsa/crypto/ecdsa) and wires it,
behind a default-off runtime toggle, into the secret-exponent modular exponentiations.

math/big is variable-time and leaks information about secret exponents through execution time;
this provides a constant-time alternative for the operations that touch long-lived secrets.

What is wired (all behind common.IsConstantTimeEnabled())

File Operation CT primitive Secret
crypto/paillier/paillier.go Decrypt c^λ, Γ^λ mod N²; ModInverse(Lg, N) ExpCT; NewCTModIntWithPhi.ModInverseCT LambdaN, PhiN
crypto/paillier/paillier.go Proof xs[i]^M mod N ExpCT M = N⁻¹ mod φ
crypto/dlnproof/proof.go NewDLNProof h1^a mod N; c·x mod pq ExpCT; MulCT DLN witness x
crypto/paillier/factor_proof.go FactorProof s^p, s^q mod NTilde ExpCT primes p, q
crypto/paillier/mod_proof.go ModProof + QR helpers y^invN, x^e, x^((p-1)/2) ExpCT φ, p, q

Every modulus fed to bigmod is odd; the two φ(N)-even modular inverses (M, invN) stay on
math/big exactly as upstream documents. Each else branch preserves the original code verbatim.

Default-off / opt-in

constantTimeEnabled defaults to 0. With it off (the default), behavior and performance are
identical to before. A consumer enables it once at startup:

common.EnableConstantTimeOps()

The gap only closes once the consumer opts in. keep-core integration note: call
EnableConstantTimeOps() at startup, and note the Go-version requirement below.

Verification

  • Default (CT-off), no regression: go build ./..., go vet (changed pkgs), full
    common/crypto/tss suites, ecdsa/keygen, ecdsa/signing all pass.
  • CT-on, functionally equivalent: framework suite; per-proof equivalence — byte-identical for
    the deterministic ops (Decrypt, Proof, QR helpers), verifies for the randomized proofs;
    full CT-on keygen E2E (all parties agree on the public key) and CT-on signing E2E
    (valid ECDSA signature). Every CT test asserts IsConstantTimeEnabled() so it cannot pass
    vacuously, and the QR helper test covers an explicit non-residue.

Blocking considerations (why "do not merge" yet)

  1. Go toolchain bump 1.16 → 1.23 (required by filippo.io/bigmod). keep-core pins this fork
    and would need to build with Go ≥1.23. The bump also crosses Go 1.22 loop-variable
    semantics
    — broader than just the bigmod floor. This coordination must be agreed before merge.
  2. New dependency: filippo.io/bigmod v0.1.0.
  3. Scope honesty:
    • isQuadResidueModPrime CT-wrapping goes beyond upstream (no analog) and is partial —
      only the exponentiation is constant-time; NewCTModInt(p)'s reduction of the secret prime is
      not. Consistent with upstream's own MulCT mod pMulQ, but a future upstream re-sync shows a
      delta here. Conscious choice, flagged for reviewer agreement.
    • Mixed CT / non-CT party interop is sound by construction (CT outputs are byte-identical;
      Verify is always non-CT over public data) but is not directly tested.

Out of scope (separate follow-up)

The secp256k1 EC scalar-mult side channel (2019 btcec NAF → btcec/v2) — large dependency
migration, rated low-exploitability in the variant analysis, and not part of "the constant_time.go
issue."

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.
@mswilkison

mswilkison commented Sep 9, 2026 •

Copy link
Copy Markdown

Update on blocking consideration 1: the Go >=1.23 compiler requirement is already satisfied on keep-core's dev branch. Its current go.mod declares go 1.24.0 and toolchain go1.24.1, and its Docker builders already use Go 1.24.

keep-core PR #4312 goes further: it raises the module minimum to Go 1.25.7 and aligns the toolchain, CI, and Docker builders on Go 1.26.8. That PR is currently open and unmerged, but the minimum-version requirement here does not depend on it landing.

The remaining consumer work is integrating the updated tss-lib revision, explicitly enabling constant-time operations where intended, and validating that integration. The keep-core PR retains the historical tss-lib pin, so its passing CI does not exercise this PR's constant-time implementation. The loop-variable semantics change from tss-lib's own go.mod bump also remains a library/integration-validation consideration: Go selects those semantics per module.

The minimum keep-core compiler-version requirement in this PR's blocker list can be marked satisfied for dev; consumer adoption and integration validation are separate follow-up work.

@piotr-roslaniec
piotr-roslaniec changed the base branch from master to dev September 14, 2026 11:49
piotr-roslaniec added a commit that referenced this pull request Sep 21, 2026
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)
piotr-roslaniec added a commit that referenced this pull request Sep 22, 2026
Addresses the project go-rand-v2 rule on new code. crypto/schnorr/
constant_time_equiv_test.go used math/rand.NewSource for its deterministic
rand.Reader override; the sibling paillier/mta/dlnproof files pre-date
the rule and stay on legacy math/rand, but new code on this fork uses
math/rand/v2 (see PR #4243 for the established v2+PCG+fillRandom pattern).

math/rand/v2 dropped the Read method on *Rand, so the v2 *Rand does not
implement io.Reader and cannot substitute for crypto/rand.Reader directly.
This commit adds a small seededReader type that adapts a v2 PCG to
io.Reader (8-byte Little-Endian chunks, matching the byte granularity of
the legacy generator the test was written against) and uses it via
newSeededReader(1, 1). The two proof generations in each test reseed
the same (1, 1) tuple so both CT and non-CT runs draw identical bytes
from offset zero, preserving the bit-exact equivalence asserts.

Behaviour, determinism, and the test's catching-power property
(bit-exact Alpha/T/U identity between CT and non-CT under matched
entropy) are unchanged. CHANGELOG notes the refactor under Breaking
Change #8.
@piotr-roslaniec

Copy link
Copy Markdown
Author

Closing as superseded by #11 (merged into dev).

Hunk-by-hunk check against dev: every functional change here is in dev through #11. common/constant_time.go, the MtA/Paillier equivalence tests, and keygen prepare plus its tests are byte-identical. The DLN, MtA, and Paillier proof files carry the same constant-time statements inside the stronger versions already on dev (session binding, 2048-bit verifier floor). The Go toolchain blocker in the description is already met on keep-core dev (see the earlier comment).

The branch constant-time-hardening is left in place for reference.

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.

2 participants