From 50ccd47856ae8d15c4b7635e2df187ae50aa6c28 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Tue, 16 Jun 2026 04:45:25 +0000
Subject: [PATCH 1/2] feat(crypto): add opt-in constant-time path for
secret-exponent modexps
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.
---
common/constant_time.go | 288 +++++++++++++++++++
common/constant_time_test.go | 303 ++++++++++++++++++++
crypto/dlnproof/constant_time_equiv_test.go | 54 ++++
crypto/dlnproof/proof.go | 34 ++-
crypto/paillier/constant_time_equiv_test.go | 142 +++++++++
crypto/paillier/factor_proof.go | 14 +-
crypto/paillier/mod_proof.go | 29 +-
crypto/paillier/paillier.go | 43 ++-
ecdsa/keygen/constant_time_e2e_test.go | 102 +++++++
ecdsa/signing/constant_time_e2e_test.go | 113 ++++++++
go.mod | 21 +-
go.sum | 6 +-
12 files changed, 1124 insertions(+), 25 deletions(-)
create mode 100644 common/constant_time.go
create mode 100644 common/constant_time_test.go
create mode 100644 crypto/dlnproof/constant_time_equiv_test.go
create mode 100644 crypto/paillier/constant_time_equiv_test.go
create mode 100644 ecdsa/keygen/constant_time_e2e_test.go
create mode 100644 ecdsa/signing/constant_time_e2e_test.go
diff --git a/common/constant_time.go b/common/constant_time.go
new file mode 100644
index 000000000..a2e50998b
--- /dev/null
+++ b/common/constant_time.go
@@ -0,0 +1,288 @@
+// Copyright © 2019-2024 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+// Package common provides constant-time big integer operations for cryptographic use.
+//
+// SECURITY NOTE: Go's math/big package is NOT constant-time and should not be used
+// with secret values. This module provides constant-time alternatives using
+// filippo.io/bigmod, which is the same library used by Go's crypto/rsa.
+//
+// Reference: https://github.com/golang/go/issues/20654
+
+package common
+
+import (
+ "crypto/rand"
+ "crypto/subtle"
+ "math/big"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "filippo.io/bigmod"
+)
+
+// constantTimeEnabled controls whether constant-time operations are used.
+// Default is false (disabled) for performance. Enable for high-security environments.
+var constantTimeEnabled int32 = 0
+
+// EnableConstantTimeOps enables constant-time cryptographic operations.
+// Call this at application startup if timing side-channel protection is required.
+func EnableConstantTimeOps() {
+ atomic.StoreInt32(&constantTimeEnabled, 1)
+}
+
+// DisableConstantTimeOps disables constant-time operations (default).
+func DisableConstantTimeOps() {
+ atomic.StoreInt32(&constantTimeEnabled, 0)
+}
+
+// IsConstantTimeEnabled returns true if constant-time operations are enabled.
+func IsConstantTimeEnabled() bool {
+ return atomic.LoadInt32(&constantTimeEnabled) == 1
+}
+
+// CTModInt provides constant-time modular arithmetic using filippo.io/bigmod.
+// This is the recommended implementation as bigmod is:
+// 1. Maintained by the Go crypto team lead (Filippo Valsorda)
+// 2. The same code used internally by crypto/rsa and crypto/ecdsa
+// 3. Highly optimized with architecture-specific assembly
+type CTModInt struct {
+ mod *bigmod.Modulus
+ modBigInt *big.Int
+ inverseExp []byte // Exponent for modular inverse: p-2 (prime) or phi(n)-1 (composite)
+ byteLen int
+ bytePool sync.Pool
+}
+
+// NewCTModInt creates a constant-time modular context using bigmod.
+// Note: bigmod requires odd modulus for Exp operations.
+func NewCTModInt(mod *big.Int) *CTModInt {
+ modBytes := mod.Bytes()
+ m, err := bigmod.NewModulus(modBytes)
+ if err != nil {
+ // Fallback: should not happen for valid modulus
+ panic(err)
+ }
+
+ // Pre-compute mod-2 for Fermat inverse: a^(-1) = a^(mod-2) mod mod
+ modMinusTwo := new(big.Int).Sub(mod, big.NewInt(2))
+
+ byteLen := len(modBytes)
+ return &CTModInt{
+ mod: m,
+ modBigInt: new(big.Int).Set(mod),
+ inverseExp: modMinusTwo.Bytes(),
+ byteLen: byteLen,
+ bytePool: sync.Pool{
+ New: func() interface{} {
+ return make([]byte, byteLen)
+ },
+ },
+ }
+}
+
+// reduceToPaddedBytes reduces val mod ct.modBigInt and returns a zero-padded
+// byte slice of length ct.byteLen suitable for bigmod.Nat.SetBytes.
+// The reduction uses big.Int.Mod which is safe here because the modulus is public.
+func (ct *CTModInt) reduceToPaddedBytes(val *big.Int) []byte {
+ reduced := val
+ if val.Sign() < 0 || val.Cmp(ct.modBigInt) >= 0 {
+ reduced = new(big.Int).Mod(val, ct.modBigInt)
+ }
+
+ buf := ct.bytePool.Get().([]byte)
+ for i := range buf {
+ buf[i] = 0
+ }
+ b := reduced.Bytes()
+ copy(buf[ct.byteLen-len(b):], b)
+ return buf
+}
+
+// ExpCT performs constant-time modular exponentiation using bigmod.
+// IMPORTANT: The modulus must be odd. Negative exponents are not supported and will panic.
+func (ct *CTModInt) ExpCT(base, exp *big.Int) *big.Int {
+ if exp.Sign() == 0 {
+ return big.NewInt(1)
+ }
+ if exp.Sign() < 0 {
+ panic("ExpCT: negative exponents are not supported; use ModInverseCT explicitly")
+ }
+
+ paddedBase := ct.reduceToPaddedBytes(base)
+ defer func() {
+ for i := range paddedBase {
+ paddedBase[i] = 0
+ }
+ ct.bytePool.Put(paddedBase)
+ }()
+
+ baseNat := bigmod.NewNat()
+ baseNat.SetBytes(paddedBase, ct.mod)
+
+ expBytes := exp.Bytes()
+ result := bigmod.NewNat()
+ result.Exp(baseNat, expBytes, ct.mod)
+
+ return new(big.Int).SetBytes(result.Bytes(ct.mod))
+}
+
+// ModInverseCT computes the modular inverse in constant time using Fermat's little theorem.
+// For a prime modulus p: a^(-1) = a^(p-2) mod p
+// For a non-prime modulus n with known phi(n): a^(-1) = a^(phi(n)-1) mod n
+// SECURITY: This uses constant-time Exp, making the entire operation constant-time.
+// Note: The modulus should be prime for this to work correctly. For composite moduli,
+// use NewCTModIntWithPhi to provide phi(n).
+func (ct *CTModInt) ModInverseCT(a *big.Int) *big.Int {
+ if a.Sign() == 0 {
+ return nil
+ }
+
+ paddedA := ct.reduceToPaddedBytes(a)
+ defer func() {
+ for i := range paddedA {
+ paddedA[i] = 0
+ }
+ ct.bytePool.Put(paddedA)
+ }()
+
+ aNat := bigmod.NewNat()
+ aNat.SetBytes(paddedA, ct.mod)
+
+ result := bigmod.NewNat()
+ result.Exp(aNat, ct.inverseExp, ct.mod)
+
+ return new(big.Int).SetBytes(result.Bytes(ct.mod))
+}
+
+// Mod returns the modulus as a big.Int.
+func (ct *CTModInt) Mod() *big.Int {
+ return new(big.Int).Set(ct.modBigInt)
+}
+
+// MulCT performs constant-time modular multiplication using bigmod.
+func (ct *CTModInt) MulCT(x, y *big.Int) *big.Int {
+ paddedX := ct.reduceToPaddedBytes(x)
+ paddedY := ct.reduceToPaddedBytes(y)
+ defer func() {
+ for i := range paddedX {
+ paddedX[i] = 0
+ }
+ ct.bytePool.Put(paddedX)
+ }()
+ defer func() {
+ for i := range paddedY {
+ paddedY[i] = 0
+ }
+ ct.bytePool.Put(paddedY)
+ }()
+
+ xNat := bigmod.NewNat()
+ yNat := bigmod.NewNat()
+ xNat.SetBytes(paddedX, ct.mod)
+ yNat.SetBytes(paddedY, ct.mod)
+
+ xNat.Mul(yNat, ct.mod)
+
+ return new(big.Int).SetBytes(xNat.Bytes(ct.mod))
+}
+
+// NewCTModIntWithPhi creates a constant-time modular context for composite moduli.
+// This is required for correct ModInverse on composite moduli where phi(n) is known.
+// For RSA-like moduli n = p*q, pass phiN = (p-1)*(q-1).
+func NewCTModIntWithPhi(mod, phiN *big.Int) *CTModInt {
+ modBytes := mod.Bytes()
+ m, err := bigmod.NewModulus(modBytes)
+ if err != nil {
+ panic(err)
+ }
+
+ // For composite modulus: a^(-1) = a^(phi(n)-1) mod n
+ phiMinusOne := new(big.Int).Sub(phiN, big.NewInt(1))
+
+ byteLen := len(modBytes)
+ return &CTModInt{
+ mod: m,
+ modBigInt: new(big.Int).Set(mod),
+ inverseExp: phiMinusOne.Bytes(), // Use phi(n)-1 instead of n-2
+ byteLen: byteLen,
+ bytePool: sync.Pool{
+ New: func() interface{} {
+ return make([]byte, byteLen)
+ },
+ },
+ }
+}
+
+// TimingProtection provides response time normalization to prevent timing attacks.
+type TimingProtection struct {
+ targetDuration time.Duration
+ jitterRange time.Duration
+}
+
+// NewTimingProtection creates a TimingProtection with custom parameters.
+// targetDuration is the minimum padded duration for every operation.
+// jitterRange adds a random delay on top of targetDuration to prevent fingerprinting
+// the fixed padding boundary.
+func NewTimingProtection(targetDuration, jitterRange time.Duration) *TimingProtection {
+ return &TimingProtection{
+ targetDuration: targetDuration,
+ jitterRange: jitterRange,
+ }
+}
+
+// ProtectBigInt wraps a function that returns *big.Int with timing normalization.
+// The total execution time is always >= targetDuration + a random jitter, regardless
+// of how long the actual operation takes.
+func (tp *TimingProtection) ProtectBigInt(fn func() (*big.Int, error)) (*big.Int, error) {
+ startTime := time.Now()
+ result, err := fn()
+ elapsed := time.Since(startTime)
+
+ padTo := tp.targetDuration
+ if elapsed > padTo {
+ padTo = elapsed
+ }
+ if tp.jitterRange > 0 {
+ jitterNanos, _ := rand.Int(rand.Reader, big.NewInt(int64(tp.jitterRange)))
+ padTo += time.Duration(jitterNanos.Int64())
+ }
+ if remaining := padTo - elapsed; remaining > 0 {
+ time.Sleep(remaining)
+ }
+ return result, err
+}
+
+// ConstantTimeCompare compares two big.Int values in constant time.
+// Both values are padded to padLen bytes before comparison to avoid leaking
+// relative magnitude. If padLen is 0, the maximum of the two byte lengths is used.
+func ConstantTimeCompare(a, b *big.Int, padLen int) int {
+ aBytes := a.Bytes()
+ bBytes := b.Bytes()
+
+ if padLen <= 0 {
+ padLen = len(aBytes)
+ if len(bBytes) > padLen {
+ padLen = len(bBytes)
+ }
+ }
+
+ padA := make([]byte, padLen)
+ padB := make([]byte, padLen)
+ if len(aBytes) <= padLen {
+ copy(padA[padLen-len(aBytes):], aBytes)
+ } else {
+ copy(padA, aBytes[len(aBytes)-padLen:])
+ }
+ if len(bBytes) <= padLen {
+ copy(padB[padLen-len(bBytes):], bBytes)
+ } else {
+ copy(padB, bBytes[len(bBytes)-padLen:])
+ }
+
+ return subtle.ConstantTimeCompare(padA, padB)
+}
diff --git a/common/constant_time_test.go b/common/constant_time_test.go
new file mode 100644
index 000000000..5595babc1
--- /dev/null
+++ b/common/constant_time_test.go
@@ -0,0 +1,303 @@
+// Copyright © 2019-2024 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package common
+
+import (
+ "crypto/rand"
+ "math/big"
+ "testing"
+ "time"
+)
+
+// TestExpCTCorrectness verifies that constant-time exponentiation produces
+// correct results by comparing with math/big.Exp
+func TestExpCTCorrectness(t *testing.T) {
+ // Generate test parameters - use odd modulus for bigmod
+ p, _ := rand.Prime(rand.Reader, 512)
+ q, _ := rand.Prime(rand.Reader, 512)
+ N := new(big.Int).Mul(p, q)
+
+ ctMod := NewCTModInt(N)
+
+ testCases := []struct {
+ name string
+ expBits int
+ }{
+ {"small_exp", 32},
+ {"medium_exp", 256},
+ {"large_exp", 1024},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ base, _ := rand.Int(rand.Reader, N)
+ exp, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), uint(tc.expBits)))
+
+ ctResult := ctMod.ExpCT(base, exp)
+ expectedResult := new(big.Int).Exp(base, exp, N)
+
+ if ctResult.Cmp(expectedResult) != 0 {
+ t.Errorf("ExpCT result mismatch for %s: got %v, expected %v", tc.name, ctResult, expectedResult)
+ }
+ })
+ }
+}
+
+// TestExpCTEdgeCases tests edge cases for constant-time exponentiation
+func TestExpCTEdgeCases(t *testing.T) {
+ p, _ := rand.Prime(rand.Reader, 512)
+ q, _ := rand.Prime(rand.Reader, 512)
+ N := new(big.Int).Mul(p, q)
+ ctMod := NewCTModInt(N)
+
+ base, _ := rand.Int(rand.Reader, N)
+
+ // Test exp = 0
+ result := ctMod.ExpCT(base, big.NewInt(0))
+ if result.Cmp(big.NewInt(1)) != 0 {
+ t.Errorf("ExpCT(base, 0) should be 1, got %v", result)
+ }
+
+ // Test exp = 1
+ result = ctMod.ExpCT(base, big.NewInt(1))
+ expected := new(big.Int).Mod(base, N)
+ if result.Cmp(expected) != 0 {
+ t.Errorf("ExpCT(base, 1) should be base mod N, got %v, expected %v", result, expected)
+ }
+
+ // Test base = 0
+ result = ctMod.ExpCT(big.NewInt(0), big.NewInt(5))
+ if result.Cmp(big.NewInt(0)) != 0 {
+ t.Errorf("ExpCT(0, exp) should be 0, got %v", result)
+ }
+
+ // Test base = 1
+ exp, _ := rand.Int(rand.Reader, big.NewInt(1000))
+ result = ctMod.ExpCT(big.NewInt(1), exp)
+ if result.Cmp(big.NewInt(1)) != 0 {
+ t.Errorf("ExpCT(1, exp) should be 1, got %v", result)
+ }
+}
+
+// TestModInverseCTCorrectness verifies constant-time ModInverse correctness
+func TestModInverseCTCorrectness(t *testing.T) {
+ p, _ := rand.Prime(rand.Reader, 1024)
+ ctMod := NewCTModInt(p)
+
+ for i := 0; i < 10; i++ {
+ a, _ := rand.Int(rand.Reader, p)
+ if a.Cmp(big.NewInt(0)) == 0 {
+ a = big.NewInt(1)
+ }
+
+ ctInv := ctMod.ModInverseCT(a)
+ stdInv := new(big.Int).ModInverse(a, p)
+
+ if ctInv.Cmp(stdInv) != 0 {
+ t.Errorf("ModInverseCT mismatch: got %v, expected %v", ctInv, stdInv)
+ }
+
+ product := new(big.Int).Mul(a, ctInv)
+ product.Mod(product, p)
+ if product.Cmp(big.NewInt(1)) != 0 {
+ t.Errorf("Inverse verification failed: a * a^(-1) = %v, expected 1", product)
+ }
+ }
+}
+
+// TestMulCTCorrectness verifies constant-time multiplication correctness
+func TestMulCTCorrectness(t *testing.T) {
+ p, _ := rand.Prime(rand.Reader, 1024)
+ ctMod := NewCTModInt(p)
+
+ for i := 0; i < 10; i++ {
+ x, _ := rand.Int(rand.Reader, p)
+ y, _ := rand.Int(rand.Reader, p)
+
+ ctResult := ctMod.MulCT(x, y)
+ expected := new(big.Int).Mul(x, y)
+ expected.Mod(expected, p)
+
+ if ctResult.Cmp(expected) != 0 {
+ t.Errorf("MulCT mismatch: got %v, expected %v", ctResult, expected)
+ }
+ }
+}
+
+// TestCTModIntWithPhi verifies ModInverse for composite moduli with known phi
+func TestCTModIntWithPhi(t *testing.T) {
+ // Generate RSA-like modulus n = p * q
+ p, _ := rand.Prime(rand.Reader, 512)
+ q, _ := rand.Prime(rand.Reader, 512)
+ n := new(big.Int).Mul(p, q)
+
+ // phi(n) = (p-1) * (q-1)
+ pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
+ qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
+ phiN := new(big.Int).Mul(pMinus1, qMinus1)
+
+ ctMod := NewCTModIntWithPhi(n, phiN)
+
+ for i := 0; i < 5; i++ {
+ // Generate a coprime to n
+ a, _ := rand.Int(rand.Reader, n)
+ gcd := new(big.Int).GCD(nil, nil, a, n)
+ for gcd.Cmp(big.NewInt(1)) != 0 {
+ a, _ = rand.Int(rand.Reader, n)
+ gcd = new(big.Int).GCD(nil, nil, a, n)
+ }
+
+ ctInv := ctMod.ModInverseCT(a)
+ stdInv := new(big.Int).ModInverse(a, n)
+
+ if ctInv.Cmp(stdInv) != 0 {
+ t.Errorf("ModInverseCT with phi mismatch: got %v, expected %v", ctInv, stdInv)
+ }
+
+ // Verify: a * a^(-1) = 1 mod n
+ product := new(big.Int).Mul(a, ctInv)
+ product.Mod(product, n)
+ if product.Cmp(big.NewInt(1)) != 0 {
+ t.Errorf("Inverse verification with phi failed: a * a^(-1) = %v, expected 1", product)
+ }
+ }
+}
+
+// TestConstantTimeCompare verifies constant-time comparison
+func TestConstantTimeCompare(t *testing.T) {
+ a := big.NewInt(12345)
+ b := big.NewInt(12345)
+ c := big.NewInt(54321)
+
+ // Use explicit padLen to avoid leaking relative magnitude
+ if ConstantTimeCompare(a, b, 128) != 1 {
+ t.Error("ConstantTimeCompare should return 1 for equal values")
+ }
+
+ if ConstantTimeCompare(a, c, 128) != 0 {
+ t.Error("ConstantTimeCompare should return 0 for different values")
+ }
+
+ large := new(big.Int).Lsh(big.NewInt(1), 1024)
+ small := big.NewInt(1)
+ if ConstantTimeCompare(large, small, 128) != 0 {
+ t.Error("ConstantTimeCompare should return 0 for values with different magnitudes")
+ }
+
+ // Test with padLen=0 (fallback to maxLen)
+ if ConstantTimeCompare(a, b, 0) != 1 {
+ t.Error("ConstantTimeCompare with padLen=0 should return 1 for equal values")
+ }
+}
+
+// TestTimingProtectionBigInt verifies timing protection for BigInt operations
+func TestTimingProtectionBigInt(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping timing test in short mode")
+ }
+
+ tp := NewTimingProtection(50*time.Millisecond, 0)
+
+ p, _ := rand.Prime(rand.Reader, 512)
+ q, _ := rand.Prime(rand.Reader, 512)
+ N := new(big.Int).Mul(p, q)
+
+ base, _ := rand.Int(rand.Reader, N)
+ exp, _ := rand.Int(rand.Reader, big.NewInt(1000))
+
+ start := time.Now()
+ result, err := tp.ProtectBigInt(func() (*big.Int, error) {
+ return new(big.Int).Exp(base, exp, N), nil
+ })
+ elapsed := time.Since(start)
+
+ if err != nil {
+ t.Errorf("ProtectBigInt returned error: %v", err)
+ }
+ if result == nil {
+ t.Error("ProtectBigInt returned nil result")
+ }
+
+ if elapsed < 20*time.Millisecond {
+ t.Errorf("TimingProtection should normalize to ~50ms, got %v", elapsed)
+ }
+}
+
+// BenchmarkExpCT benchmarks constant-time exponentiation
+func BenchmarkExpCT(b *testing.B) {
+ p, _ := rand.Prime(rand.Reader, 1024)
+ q, _ := rand.Prime(rand.Reader, 1024)
+ N := new(big.Int).Mul(p, q)
+ ctMod := NewCTModInt(N)
+
+ base, _ := rand.Int(rand.Reader, N)
+ exp, _ := rand.Int(rand.Reader, N)
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ ctMod.ExpCT(base, exp)
+ }
+}
+
+// BenchmarkExpStandard benchmarks standard math/big exponentiation for comparison
+func BenchmarkExpStandard(b *testing.B) {
+ p, _ := rand.Prime(rand.Reader, 1024)
+ q, _ := rand.Prime(rand.Reader, 1024)
+ N := new(big.Int).Mul(p, q)
+
+ base, _ := rand.Int(rand.Reader, N)
+ exp, _ := rand.Int(rand.Reader, N)
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ new(big.Int).Exp(base, exp, N)
+ }
+}
+
+// TestExpCTTimingConsistency checks timing consistency
+func TestExpCTTimingConsistency(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping timing test in short mode")
+ }
+
+ p, _ := rand.Prime(rand.Reader, 512)
+ q, _ := rand.Prime(rand.Reader, 512)
+ N := new(big.Int).Mul(p, q)
+ ctMod := NewCTModInt(N)
+
+ base, _ := rand.Int(rand.Reader, N)
+
+ expLowHamming := new(big.Int).Lsh(big.NewInt(1), 511)
+ expHighHamming := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 512), big.NewInt(1))
+
+ const iterations = 100
+ var timesLow, timesHigh []time.Duration
+
+ for i := 0; i < iterations; i++ {
+ start := time.Now()
+ ctMod.ExpCT(base, expLowHamming)
+ timesLow = append(timesLow, time.Since(start))
+
+ start = time.Now()
+ ctMod.ExpCT(base, expHighHamming)
+ timesHigh = append(timesHigh, time.Since(start))
+ }
+
+ var sumLow, sumHigh time.Duration
+ for i := 0; i < iterations; i++ {
+ sumLow += timesLow[i]
+ sumHigh += timesHigh[i]
+ }
+ meanLow := sumLow / time.Duration(iterations)
+ meanHigh := sumHigh / time.Duration(iterations)
+
+ ratio := float64(meanHigh) / float64(meanLow)
+ if ratio < 0.5 || ratio > 2.0 {
+ t.Logf("Warning: Timing ratio between high and low Hamming weight exponents: %.2f", ratio)
+ t.Logf("Low Hamming mean: %v, High Hamming mean: %v", meanLow, meanHigh)
+ }
+}
diff --git a/crypto/dlnproof/constant_time_equiv_test.go b/crypto/dlnproof/constant_time_equiv_test.go
new file mode 100644
index 000000000..39f4472fb
--- /dev/null
+++ b/crypto/dlnproof/constant_time_equiv_test.go
@@ -0,0 +1,54 @@
+// Copyright © 2019-2020 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package dlnproof
+
+import (
+ "context"
+ "math/big"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/bnb-chain/tss-lib/common"
+)
+
+// TestDLNProofCTVerifies builds a valid ring-Pedersen instance (h2 = h1^alpha mod NTilde)
+// exactly as keygen does, then checks that a proof generated with constant-time ops enabled
+// still verifies, alongside the standard (non-CT) baseline. The proof is randomised, so this
+// asserts correctness of the CT prover; the primitive-level ExpCT==Exp / MulCT==Mul
+// equivalence is covered in common/constant_time_test.go.
+func TestDLNProofCTVerifies(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+ defer cancel()
+
+ sgps, err := common.GetRandomSafePrimesConcurrent(ctx, 512, 2, runtime.NumCPU())
+ assert.NoError(t, err)
+ assert.NotNil(t, sgps)
+
+ P, Q := sgps[0].SafePrime(), sgps[1].SafePrime()
+ NTilde := new(big.Int).Mul(P, Q)
+ p, q := sgps[0].Prime(), sgps[1].Prime()
+ modNTilde := common.ModInt(NTilde)
+
+ f1 := common.GetRandomPositiveRelativelyPrimeInt(NTilde)
+ alpha := common.GetRandomPositiveRelativelyPrimeInt(NTilde)
+ h1 := modNTilde.Mul(f1, f1)
+ h2 := modNTilde.Exp(h1, alpha)
+
+ // Baseline: non-CT proof verifies.
+ proofOff := NewDLNProof(h1, h2, alpha, p, q, NTilde)
+ assert.True(t, proofOff.Verify(h1, h2, NTilde), "non-CT DLN proof must verify")
+
+ // CT proof must also verify.
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ proofOn := NewDLNProof(h1, h2, alpha, p, q, NTilde)
+ assert.True(t, proofOn.Verify(h1, h2, NTilde), "CT DLN proof must verify")
+}
diff --git a/crypto/dlnproof/proof.go b/crypto/dlnproof/proof.go
index b5d5f7749..e2a748041 100644
--- a/crypto/dlnproof/proof.go
+++ b/crypto/dlnproof/proof.go
@@ -36,18 +36,38 @@ func NewDLNProof(h1, h2, x, p, q, N *big.Int) *Proof {
modN, modPQ := common.ModInt(N), common.ModInt(pMulQ)
a := make([]*big.Int, Iterations)
alpha := [Iterations]*big.Int{}
- for i := range alpha {
- a[i] = common.GetRandomPositiveInt(pMulQ)
- alpha[i] = modN.Exp(h1, a[i])
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: h1^a[i] mod N uses the constant-time path (N is odd).
+ ctModN := common.NewCTModInt(N)
+ for i := range alpha {
+ a[i] = common.GetRandomPositiveInt(pMulQ)
+ alpha[i] = ctModN.ExpCT(h1, a[i])
+ }
+ } else {
+ for i := range alpha {
+ a[i] = common.GetRandomPositiveInt(pMulQ)
+ alpha[i] = modN.Exp(h1, a[i])
+ }
}
msg := append([]*big.Int{h1, h2, N}, alpha[:]...)
c := common.SHA512_256i(msg...)
t := [Iterations]*big.Int{}
cIBI := new(big.Int)
- for i := range t {
- cI := c.Bit(i)
- cIBI = cIBI.SetInt64(int64(cI))
- t[i] = modPQ.Add(a[i], modPQ.Mul(cIBI, x))
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: x is the secret discrete-log witness; multiply it in constant time
+ // (the modulus p*q is odd).
+ ctModPQ := common.NewCTModInt(pMulQ)
+ for i := range t {
+ cI := c.Bit(i)
+ cIBI = cIBI.SetInt64(int64(cI))
+ t[i] = modPQ.Add(a[i], ctModPQ.MulCT(cIBI, x))
+ }
+ } else {
+ for i := range t {
+ cI := c.Bit(i)
+ cIBI = cIBI.SetInt64(int64(cI))
+ t[i] = modPQ.Add(a[i], modPQ.Mul(cIBI, x))
+ }
}
return &Proof{alpha, t}
}
diff --git a/crypto/paillier/constant_time_equiv_test.go b/crypto/paillier/constant_time_equiv_test.go
new file mode 100644
index 000000000..6a391c37f
--- /dev/null
+++ b/crypto/paillier/constant_time_equiv_test.go
@@ -0,0 +1,142 @@
+// Copyright © 2019 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package paillier
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/bnb-chain/tss-lib/common"
+ "github.com/bnb-chain/tss-lib/crypto"
+ "github.com/bnb-chain/tss-lib/tss"
+)
+
+// These tests verify the invariant that the constant-time path computes the SAME
+// function as the standard path: for deterministic operations the outputs are
+// byte-identical, and randomised proofs produced with constant-time ops enabled
+// still verify. (The primitive-level ExpCT==Exp / MulCT==Mul equivalence is covered
+// in common/constant_time_test.go.)
+
+// TestDecryptCTEquivalence: Decrypt is deterministic; CT and non-CT must agree
+// byte-for-byte and both must recover the plaintext.
+func TestDecryptCTEquivalence(t *testing.T) {
+ facSetUp(t)
+
+ pt := big.NewInt(424242)
+ cipher, err := publicKey.Encrypt(pt)
+ assert.NoError(t, err)
+
+ mOff, err := privateKey.Decrypt(cipher)
+ assert.NoError(t, err)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ mOn, err := privateKey.Decrypt(cipher)
+ assert.NoError(t, err)
+
+ assert.Zero(t, mOff.Cmp(mOn), "CT and non-CT Decrypt must be byte-identical")
+ assert.Zero(t, pt.Cmp(mOn), "CT Decrypt must recover the plaintext")
+}
+
+// TestPaillierProofCTEquivalence: the Paillier square-free Proof is deterministic
+// given (k, key, ecdsaPub); CT and non-CT must agree byte-for-byte and both verify.
+func TestPaillierProofCTEquivalence(t *testing.T) {
+ facSetUp(t)
+
+ ki := common.MustGetRandomInt(256)
+ ui := common.GetRandomPositiveInt(tss.EC().Params().N)
+ yX, yY := tss.EC().ScalarBaseMult(ui.Bytes())
+ pub := crypto.NewECPointNoCurveCheck(tss.EC(), yX, yY)
+
+ proofOff := privateKey.Proof(ki, pub)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ proofOn := privateKey.Proof(ki, pub)
+
+ for i := range proofOff {
+ assert.Zero(t, proofOff[i].Cmp(proofOn[i]), "Proof element %d must be byte-identical", i)
+ }
+
+ okOff, err := proofOff.Verify(publicKey.N, ki, pub)
+ assert.NoError(t, err)
+ assert.True(t, okOff, "non-CT proof must verify")
+ okOn, err := proofOn.Verify(publicKey.N, ki, pub)
+ assert.NoError(t, err)
+ assert.True(t, okOn, "CT proof must verify")
+}
+
+// TestFactorProofCTVerifies: FactorProof is randomised; a CT-generated proof must verify.
+func TestFactorProofCTVerifies(t *testing.T) {
+ facSetUp(t)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ proof := privateKey.FactorProof(auxPrime.N, s, tt)
+ res, err := proof.FactorVerify(publicKey.N, auxPrime.N, s, tt)
+ assert.NoError(t, err)
+ assert.True(t, res, "CT FactorProof must verify")
+}
+
+// TestModProofCTVerifies: ModProof is randomised; a CT-generated proof must verify.
+func TestModProofCTVerifies(t *testing.T) {
+ facSetUp(t)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ proof := privateKey.ModProof()
+ res, err := proof.ModVerify(publicKey.N)
+ assert.NoError(t, err)
+ assert.True(t, res, "CT ModProof must verify")
+}
+
+// TestModProofHelpersCTEquivalence: the QR helpers are deterministic; CT and non-CT
+// must agree (boolean predicates and the byte-identical fourth root) on real-key inputs.
+func TestModProofHelpersCTEquivalence(t *testing.T) {
+ facSetUp(t)
+
+ p, q := privateKey.GetPQ()
+ N := publicKey.N
+ phiN := privateKey.PhiN
+
+ // x = r^2 mod N is a quadratic residue mod N (exercises the true branch).
+ r := common.GetRandomPositiveRelativelyPrimeInt(N)
+ x := new(big.Int).Mod(new(big.Int).Mul(r, r), N)
+
+ // nr is a known quadratic NON-residue mod p (exercises the false branch of
+ // isQuadResidueModPrime); located using the standard (non-CT) predicate.
+ nr := big.NewInt(2)
+ for isQuadResidueModPrime(nr, p) {
+ nr.Add(nr, big.NewInt(1))
+ }
+
+ qrPOff := isQuadResidueModPrime(x, p)
+ nrPOff := isQuadResidueModPrime(nr, p)
+ qrCompOff := isQuadResidueModComposite(x, p, q)
+ rootOff := quadResidueModComposite(x, p, q, N, phiN)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ qrPOn := isQuadResidueModPrime(x, p)
+ nrPOn := isQuadResidueModPrime(nr, p)
+ qrCompOn := isQuadResidueModComposite(x, p, q)
+ rootOn := quadResidueModComposite(x, p, q, N, phiN)
+
+ assert.True(t, qrPOff, "x=r^2 must be a residue mod p")
+ assert.False(t, nrPOff, "nr must be a non-residue mod p")
+ assert.Equal(t, qrPOff, qrPOn, "isQuadResidueModPrime must agree (residue)")
+ assert.Equal(t, nrPOff, nrPOn, "isQuadResidueModPrime must agree (non-residue)")
+ assert.Equal(t, qrCompOff, qrCompOn, "isQuadResidueModComposite must agree")
+ assert.Zero(t, rootOff.Cmp(rootOn), "quadResidueModComposite must be byte-identical")
+}
diff --git a/crypto/paillier/factor_proof.go b/crypto/paillier/factor_proof.go
index b376b7388..b59241bb5 100644
--- a/crypto/paillier/factor_proof.go
+++ b/crypto/paillier/factor_proof.go
@@ -52,8 +52,18 @@ func (privateKey *PrivateKey) FactorProof(N, s, t *big.Int) *FactorProof {
modN := common.ModInt(N)
- P := modN.ExpMulExp(s, p, t, mu)
- Q := modN.ExpMulExp(s, q, t, v)
+ var P, Q *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: p and q are the secret prime factors; their exponentiations get the
+ // constant-time path (N is the verifier's odd ring-Pedersen modulus). The t^mu /
+ // t^v blinds use random exponents and stay on math/big.
+ ctModN := common.NewCTModInt(N)
+ P = modN.Mul(ctModN.ExpCT(s, p), modN.Exp(t, mu))
+ Q = modN.Mul(ctModN.ExpCT(s, q), modN.Exp(t, v))
+ } else {
+ P = modN.ExpMulExp(s, p, t, mu)
+ Q = modN.ExpMulExp(s, q, t, v)
+ }
A := modN.ExpMulExp(s, a, t, x)
B := modN.ExpMulExp(s, b, t, y)
T := modN.ExpMulExp(Q, a, t, r)
diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go
index 4c6822a58..ffa4a1d85 100644
--- a/crypto/paillier/mod_proof.go
+++ b/crypto/paillier/mod_proof.go
@@ -42,14 +42,27 @@ func (privateKey *PrivateKey) ModProof() *ModProof {
var b [PARAM_M]bool
var z [PARAM_M]*big.Int
+ // invN = N^(-1) mod phiN. phiN is even, so this inverse stays on math/big (bigmod
+ // requires an odd modulus); it is a prover-side value, never transmitted. Only the
+ // Exp mod N (odd) below carries the secret exponent and gets the constant-time path.
+ invN := new(big.Int).ModInverse(N, phiN)
+ var ctModN *common.CTModInt
+ if common.IsConstantTimeEnabled() {
+ ctModN = common.NewCTModInt(N)
+ }
+
for i, y_i := range y {
a_i, b_i, x_i := defineXi(w, y_i, p, q, N, phiN)
x[i] = x_i
a[i] = a_i
b[i] = b_i
- z_i := new(big.Int).ModInverse(N, phiN)
- z_i.Exp(y_i, z_i, N)
+ var z_i *big.Int
+ if common.IsConstantTimeEnabled() {
+ z_i = ctModN.ExpCT(y_i, invN)
+ } else {
+ z_i = new(big.Int).Exp(y_i, invN, N)
+ }
z[i] = z_i
}
@@ -173,6 +186,11 @@ func isQuadResidueModPrime(x, p *big.Int) bool {
ps := new(big.Int).Sub(p, big.NewInt(1))
ps = ps.Div(ps, big.NewInt(2))
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: p is a secret prime (odd) and the exponent (p-1)/2 is secret-derived;
+ // use the constant-time path.
+ return common.Eq(common.NewCTModInt(p).ExpCT(x, ps), big.NewInt(1))
+ }
return common.Eq(new(big.Int).Exp(x, ps, p), big.NewInt(1))
}
@@ -182,6 +200,13 @@ func quadResidueModComposite(x, p, q, n, phiN *big.Int) *big.Int {
e := new(big.Int).Add(phiN, big.NewInt(4))
e = e.Div(e, big.NewInt(8))
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: the fourth-root exponent e derives from secret phiN; the modulus
+ // n = N is odd, so use the constant-time path for both square-root steps.
+ ctModN := common.NewCTModInt(n)
+ res := ctModN.ExpCT(x, e)
+ return ctModN.ExpCT(res, e)
+ }
res := new(big.Int).Exp(x, e, n)
res = res.Exp(res, e, n)
diff --git a/crypto/paillier/paillier.go b/crypto/paillier/paillier.go
index d5cdb2582..51e51cd20 100644
--- a/crypto/paillier/paillier.go
+++ b/crypto/paillier/paillier.go
@@ -178,12 +178,33 @@ func (privateKey *PrivateKey) Decrypt(c *big.Int) (m *big.Int, err error) {
if cg.Cmp(one) == 1 {
return nil, ErrMessageMalFormed
}
+
+ var cExpLambda, gammaExpLambda *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: constant-time exponentiation prevents leaking the secret
+ // exponent LambdaN through execution-time variation. N2 is odd.
+ ctModN2 := common.NewCTModInt(N2)
+ cExpLambda = ctModN2.ExpCT(c, privateKey.LambdaN)
+ gammaExpLambda = ctModN2.ExpCT(privateKey.Gamma(), privateKey.LambdaN)
+ } else {
+ cExpLambda = new(big.Int).Exp(c, privateKey.LambdaN, N2)
+ gammaExpLambda = new(big.Int).Exp(privateKey.Gamma(), privateKey.LambdaN, N2)
+ }
+
// 1. L(u) = (c^LambdaN-1 mod N2) / N
- Lc := L(new(big.Int).Exp(c, privateKey.LambdaN, N2), privateKey.N)
+ Lc := L(cExpLambda, privateKey.N)
// 2. L(u) = (Gamma^LambdaN-1 mod N2) / N
- Lg := L(new(big.Int).Exp(privateKey.Gamma(), privateKey.LambdaN, N2), privateKey.N)
+ Lg := L(gammaExpLambda, privateKey.N)
// 3. (1) * modInv(2) mod N
- inv := new(big.Int).ModInverse(Lg, privateKey.N)
+ var inv *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: Lg derives from the secret LambdaN exponentiation; N = P*Q is
+ // composite, so provide phi(N) for the Euler inverse (the bigmod modulus N is odd).
+ ctModN := common.NewCTModIntWithPhi(privateKey.N, privateKey.PhiN)
+ inv = ctModN.ModInverseCT(Lg)
+ } else {
+ inv = new(big.Int).ModInverse(Lg, privateKey.N)
+ }
m = common.ModInt(privateKey.N).Mul(Lc, inv)
return
}
@@ -199,9 +220,19 @@ func (privateKey *PrivateKey) Proof(k *big.Int, ecdsaPub *crypto2.ECPoint) Proof
var pi Proof
iters := ProofIters
xs := GenerateXs(iters, k, privateKey.N, ecdsaPub)
- for i := 0; i < iters; i++ {
- M := new(big.Int).ModInverse(privateKey.N, privateKey.PhiN)
- pi[i] = new(big.Int).Exp(xs[i], M, privateKey.N)
+ // M = N^(-1) mod PhiN. PhiN is even, so this inverse stays on math/big (bigmod
+ // requires an odd modulus); only the subsequent Exp mod N (odd) carries the secret
+ // exponent M and gets the constant-time path when enabled.
+ M := new(big.Int).ModInverse(privateKey.N, privateKey.PhiN)
+ if common.IsConstantTimeEnabled() {
+ ctModN := common.NewCTModInt(privateKey.N)
+ for i := 0; i < iters; i++ {
+ pi[i] = ctModN.ExpCT(xs[i], M)
+ }
+ } else {
+ for i := 0; i < iters; i++ {
+ pi[i] = new(big.Int).Exp(xs[i], M, privateKey.N)
+ }
}
return pi
}
diff --git a/ecdsa/keygen/constant_time_e2e_test.go b/ecdsa/keygen/constant_time_e2e_test.go
new file mode 100644
index 000000000..5c850c11d
--- /dev/null
+++ b/ecdsa/keygen/constant_time_e2e_test.go
@@ -0,0 +1,102 @@
+// Copyright © 2019 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package keygen
+
+import (
+ "sync/atomic"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/bnb-chain/tss-lib/common"
+ "github.com/bnb-chain/tss-lib/test"
+ "github.com/bnb-chain/tss-lib/tss"
+)
+
+// TestE2EConcurrentConstantTime runs the full distributed key generation protocol with
+// constant-time cryptographic operations enabled, using pre-generated preparams from the
+// test fixtures. This exercises the CT-wired keygen-path provers (DLN proof, Paillier
+// FactorProof / ModProof / square-free Proof) in-protocol and asserts every party converges
+// on the same ECDSA public key, confirming the CT path is functionally equivalent in the
+// integrated keygen flow that keep-core runs.
+func TestE2EConcurrentConstantTime(t *testing.T) {
+ setUp("info")
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "constant-time ops must be enabled for this test")
+
+ threshold := testThreshold
+
+ fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
+ if err != nil {
+ t.Skip("keygen test fixtures are required for the constant-time keygen E2E (avoids safe-prime generation)")
+ }
+ assert.Equal(t, testParticipants, len(fixtures))
+
+ p2pCtx := tss.NewPeerContext(pIDs)
+ parties := make([]*LocalParty, 0, len(pIDs))
+
+ errCh := make(chan *tss.Error, len(pIDs))
+ outCh := make(chan tss.Message, len(pIDs))
+ endCh := make(chan LocalPartySaveData, len(pIDs))
+
+ updater := test.SharedPartyUpdater
+
+ for i := 0; i < len(pIDs); i++ {
+ params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold)
+ P := NewLocalParty(params, outCh, endCh, fixtures[i].LocalPreParams).(*LocalParty)
+ parties = append(parties, P)
+ go func(P *LocalParty) {
+ if err := P.Start(); err != nil {
+ errCh <- err
+ }
+ }(P)
+ }
+
+ var ended int32
+ saves := make([]LocalPartySaveData, 0, len(pIDs))
+keygen:
+ for {
+ select {
+ case err := <-errCh:
+ common.Logger.Errorf("Error: %s", err)
+ assert.FailNow(t, err.Error())
+ break keygen
+
+ case msg := <-outCh:
+ dest := msg.GetTo()
+ if dest == nil {
+ for _, P := range parties {
+ if P.PartyID().Index == msg.GetFrom().Index {
+ continue
+ }
+ go updater(P, msg, errCh)
+ }
+ } else {
+ if dest[0].Index == msg.GetFrom().Index {
+ t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
+ }
+ go updater(parties[dest[0].Index], msg, errCh)
+ }
+
+ case save := <-endCh:
+ saves = append(saves, save)
+ atomic.AddInt32(&ended, 1)
+ if atomic.LoadInt32(&ended) == int32(len(pIDs)) {
+ // Every party must converge on the same group ECDSA public key.
+ pk0 := saves[0].ECDSAPub
+ for i, s := range saves {
+ assert.True(t, pk0.Equals(s.ECDSAPub),
+ "party %d ECDSA public key must match party 0 (constant-time keygen)", i)
+ }
+ t.Log("Constant-time keygen E2E done; all parties agree on the public key.")
+ break keygen
+ }
+ }
+ }
+}
diff --git a/ecdsa/signing/constant_time_e2e_test.go b/ecdsa/signing/constant_time_e2e_test.go
new file mode 100644
index 000000000..e89effdf6
--- /dev/null
+++ b/ecdsa/signing/constant_time_e2e_test.go
@@ -0,0 +1,113 @@
+// Copyright © 2019 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package signing
+
+import (
+ "crypto/ecdsa"
+ "math/big"
+ "sync/atomic"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/bnb-chain/tss-lib/common"
+ "github.com/bnb-chain/tss-lib/ecdsa/keygen"
+ "github.com/bnb-chain/tss-lib/test"
+ "github.com/bnb-chain/tss-lib/tss"
+)
+
+// TestE2EConcurrentConstantTime runs the full threshold-signing protocol with
+// constant-time cryptographic operations enabled. This exercises the CT-wired
+// Paillier Decrypt path (used in MtA) end-to-end across all parties and asserts
+// the produced ECDSA signature verifies, confirming the CT path is functionally
+// equivalent to the standard path in the integrated protocol.
+func TestE2EConcurrentConstantTime(t *testing.T) {
+ setUp("info")
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "constant-time ops must be enabled for this test")
+
+ threshold := testThreshold
+
+ keys, signPIDs, err := keygen.LoadKeygenTestFixturesRandomSet(testThreshold+1, testParticipants)
+ assert.NoError(t, err, "should load keygen fixtures")
+ assert.Equal(t, testThreshold+1, len(keys))
+ assert.Equal(t, testThreshold+1, len(signPIDs))
+
+ p2pCtx := tss.NewPeerContext(signPIDs)
+ parties := make([]*LocalParty, 0, len(signPIDs))
+
+ errCh := make(chan *tss.Error, len(signPIDs))
+ outCh := make(chan tss.Message, len(signPIDs))
+ endCh := make(chan common.SignatureData, len(signPIDs))
+
+ updater := test.SharedPartyUpdater
+
+ for i := 0; i < len(signPIDs); i++ {
+ params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[i], len(signPIDs), threshold)
+
+ P := NewLocalParty(big.NewInt(42), params, keys[i], outCh, endCh).(*LocalParty)
+ parties = append(parties, P)
+ go func(P *LocalParty) {
+ if err := P.Start(); err != nil {
+ errCh <- err
+ }
+ }(P)
+ }
+
+ var ended int32
+signing:
+ for {
+ select {
+ case err := <-errCh:
+ common.Logger.Errorf("Error: %s", err)
+ assert.FailNow(t, err.Error())
+ break signing
+
+ case msg := <-outCh:
+ dest := msg.GetTo()
+ if dest == nil {
+ for _, P := range parties {
+ if P.PartyID().Index == msg.GetFrom().Index {
+ continue
+ }
+ go updater(P, msg, errCh)
+ }
+ } else {
+ if dest[0].Index == msg.GetFrom().Index {
+ t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
+ }
+ go updater(parties[dest[0].Index], msg, errCh)
+ }
+
+ case <-endCh:
+ atomic.AddInt32(&ended, 1)
+ if atomic.LoadInt32(&ended) == int32(len(signPIDs)) {
+ R := parties[0].temp.bigR
+
+ modN := common.ModInt(tss.S256().Params().N)
+ sumS := big.NewInt(0)
+ for _, p := range parties {
+ sumS = modN.Add(sumS, p.temp.si)
+ }
+
+ pkX, pkY := keys[0].ECDSAPub.X(), keys[0].ECDSAPub.Y()
+ pk := ecdsa.PublicKey{
+ Curve: tss.EC(),
+ X: pkX,
+ Y: pkY,
+ }
+ ok := ecdsa.Verify(&pk, big.NewInt(42).Bytes(), R.X(), sumS)
+ assert.True(t, ok, "ecdsa verify must pass with constant-time ops enabled")
+ t.Log("ECDSA constant-time signing test done.")
+
+ break signing
+ }
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index c5d5426f8..3edbc5c7f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,24 +1,33 @@
module github.com/bnb-chain/tss-lib
-go 1.16
+go 1.23
require (
+ filippo.io/bigmod v0.1.0
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412
github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
- github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0
github.com/hashicorp/go-multierror v1.0.0
github.com/ipfs/go-log v0.0.1
- github.com/mattn/go-colorable v0.1.2 // indirect
- github.com/opentracing/opentracing-go v1.1.0 // indirect
- github.com/otiai10/mint v1.2.4 // indirect
github.com/otiai10/primes v0.0.0-20180210170552-f6d2a1ba97c4
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44
- golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
google.golang.org/protobuf v1.27.1
)
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/gogo/protobuf v1.2.1 // indirect
+ github.com/hashicorp/errwrap v1.0.0 // indirect
+ github.com/mattn/go-colorable v0.1.2 // indirect
+ github.com/mattn/go-isatty v0.0.8 // indirect
+ github.com/opentracing/opentracing-go v1.1.0 // indirect
+ github.com/otiai10/mint v1.2.4 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc // indirect
+ golang.org/x/sys v0.11.0 // indirect
+)
+
replace github.com/agl/ed25519 => github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43
diff --git a/go.sum b/go.sum
index ab3fee3c8..e9d33335c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U=
bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg=
+filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
+filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 h1:Vkf7rtHx8uHx8gDfkQaCdVfc+gfrF9v6sR6xJy7RXNg=
github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43/go.mod h1:TnVqVdGEK8b6erOMkcyYGWzCQMw7HEMCOw3BgFYCFWs=
@@ -71,8 +73,8 @@ golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
-golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
From 4aee35704f20721dd792ec00d6997b62ef19ee8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Tue, 16 Jun 2026 06:07:18 +0000
Subject: [PATCH 2/2] feat(crypto): extend constant-time coverage and harden CT
primitives
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.
---
common/constant_time.go | 167 ++++++++------------
common/constant_time_test.go | 79 ++++-----
crypto/mta/constant_time_equiv_test.go | 75 +++++++++
crypto/mta/proofs.go | 22 ++-
crypto/mta/range_proof.go | 12 +-
crypto/paillier/constant_time_equiv_test.go | 47 ++++++
crypto/paillier/paillier.go | 16 +-
ecdsa/keygen/prepare.go | 10 +-
ecdsa/keygen/prepare_constant_time_test.go | 39 +++++
9 files changed, 313 insertions(+), 154 deletions(-)
create mode 100644 crypto/mta/constant_time_equiv_test.go
create mode 100644 ecdsa/keygen/prepare_constant_time_test.go
diff --git a/common/constant_time.go b/common/constant_time.go
index a2e50998b..466e63161 100644
--- a/common/constant_time.go
+++ b/common/constant_time.go
@@ -4,23 +4,37 @@
// terms governing use, modification, and redistribution, is contained in the
// file LICENSE at the root of the source code distribution tree.
-// Package common provides constant-time big integer operations for cryptographic use.
+// Package common provides constant-time big integer helpers for cryptographic use.
//
// SECURITY NOTE: Go's math/big package is NOT constant-time and should not be used
-// with secret values. This module provides constant-time alternatives using
-// filippo.io/bigmod, which is the same library used by Go's crypto/rsa.
+// with secret values. This module provides constant-time helpers for the
+// secret-exponent operations enumerated below, using filippo.io/bigmod (the same
+// constant-time core used by Go's crypto/rsa).
+//
+// COVERAGE: When enabled via EnableConstantTimeOps, the constant-time path is applied
+// to modular exponentiations whose EXPONENT is a long-term secret, witness, trapdoor,
+// or secret plaintext/scalar: Paillier Decrypt / Encrypt (gamma^m) / HomoMult, the
+// Paillier mod- and factor-proofs, the DLN proof, the ring-Pedersen trapdoor setup in
+// keygen, and the MtA range and regular proofs.
+//
+// Deliberately NOT hardened (left on math/big), and the reasons:
+// - Public-exponent operations, where the timing reveals only public data: x^N in
+// Encrypt, r^e, and every verifier-side exponentiation (challenges and proof
+// responses are public).
+// - One-time random per-proof blinds (e.g. h2^rho, h1^alpha in the MtA proofs). These
+// are the same class as the secrets but are fresh, single-use auxiliary randomness;
+// leaving them on math/big is a pragmatic deferral, NOT a safety guarantee.
+// - Exponentiations modulo an even value (e.g. inverses mod phi(N)): bigmod requires
+// an odd modulus, so these stay on math/big.
//
// Reference: https://github.com/golang/go/issues/20654
package common
import (
- "crypto/rand"
- "crypto/subtle"
"math/big"
"sync"
"sync/atomic"
- "time"
"filippo.io/bigmod"
)
@@ -45,11 +59,8 @@ func IsConstantTimeEnabled() bool {
return atomic.LoadInt32(&constantTimeEnabled) == 1
}
-// CTModInt provides constant-time modular arithmetic using filippo.io/bigmod.
-// This is the recommended implementation as bigmod is:
-// 1. Maintained by the Go crypto team lead (Filippo Valsorda)
-// 2. The same code used internally by crypto/rsa and crypto/ecdsa
-// 3. Highly optimized with architecture-specific assembly
+// CTModInt provides constant-time modular arithmetic backed by filippo.io/bigmod
+// (the same constant-time core used by Go's crypto/rsa and crypto/ecdsa).
type CTModInt struct {
mod *bigmod.Modulus
modBigInt *big.Int
@@ -58,9 +69,27 @@ type CTModInt struct {
bytePool sync.Pool
}
+// leftPad returns b left-padded with zero bytes to width; if b is already at least
+// width bytes it is returned unchanged. Padding a secret exponent to a fixed width
+// keeps bigmod.Nat.Exp's running time independent of the exponent's magnitude (its
+// work is proportional to len(e)); leading zero bytes are no-op squarings and do not
+// change the result.
+func leftPad(b []byte, width int) []byte {
+ if len(b) >= width {
+ return b
+ }
+ padded := make([]byte, width)
+ copy(padded[width-len(b):], b)
+ return padded
+}
+
// NewCTModInt creates a constant-time modular context using bigmod.
-// Note: bigmod requires odd modulus for Exp operations.
+// The modulus must be odd (a requirement of bigmod's Exp); this is asserted here so
+// the failure surfaces at construction rather than at the first ExpCT call.
func NewCTModInt(mod *big.Int) *CTModInt {
+ if mod.Bit(0) == 0 {
+ panic("NewCTModInt: modulus must be odd")
+ }
modBytes := mod.Bytes()
m, err := bigmod.NewModulus(modBytes)
if err != nil {
@@ -75,7 +104,7 @@ func NewCTModInt(mod *big.Int) *CTModInt {
return &CTModInt{
mod: m,
modBigInt: new(big.Int).Set(mod),
- inverseExp: modMinusTwo.Bytes(),
+ inverseExp: leftPad(modMinusTwo.Bytes(), byteLen),
byteLen: byteLen,
bytePool: sync.Pool{
New: func() interface{} {
@@ -85,14 +114,14 @@ func NewCTModInt(mod *big.Int) *CTModInt {
}
}
-// reduceToPaddedBytes reduces val mod ct.modBigInt and returns a zero-padded
-// byte slice of length ct.byteLen suitable for bigmod.Nat.SetBytes.
-// The reduction uses big.Int.Mod which is safe here because the modulus is public.
+// reduceToPaddedBytes reduces val into [0, modulus) and returns a zero-padded
+// big-endian byte slice of length ct.byteLen suitable for bigmod.Nat.SetBytes.
+// NOTE: big.Int.Mod is not constant-time, but it is applied unconditionally (no
+// secret-dependent branch) and the bases reduced here are public or already in range
+// at every call site. A caller passing a secret base near the modulus should be aware
+// the reduction's timing depends on the value.
func (ct *CTModInt) reduceToPaddedBytes(val *big.Int) []byte {
- reduced := val
- if val.Sign() < 0 || val.Cmp(ct.modBigInt) >= 0 {
- reduced = new(big.Int).Mod(val, ct.modBigInt)
- }
+ reduced := new(big.Int).Mod(val, ct.modBigInt)
buf := ct.bytePool.Get().([]byte)
for i := range buf {
@@ -124,7 +153,14 @@ func (ct *CTModInt) ExpCT(base, exp *big.Int) *big.Int {
baseNat := bigmod.NewNat()
baseNat.SetBytes(paddedBase, ct.mod)
- expBytes := exp.Bytes()
+ // Pad the exponent to a fixed width so the exponentiation's running time does not
+ // leak the secret exponent's magnitude (see leftPad).
+ expBytes := leftPad(exp.Bytes(), ct.byteLen)
+ defer func() {
+ for i := range expBytes {
+ expBytes[i] = 0
+ }
+ }()
result := bigmod.NewNat()
result.Exp(baseNat, expBytes, ct.mod)
@@ -155,13 +191,16 @@ func (ct *CTModInt) ModInverseCT(a *big.Int) *big.Int {
result := bigmod.NewNat()
result.Exp(aNat, ct.inverseExp, ct.mod)
+ inv := new(big.Int).SetBytes(result.Bytes(ct.mod))
- return new(big.Int).SetBytes(result.Bytes(ct.mod))
-}
-
-// Mod returns the modulus as a big.Int.
-func (ct *CTModInt) Mod() *big.Int {
- return new(big.Int).Set(ct.modBigInt)
+ // The Fermat/Euler inverse a^(mod-2) (or a^(phi-1)) is only the true inverse when
+ // gcd(a, mod) == 1; for non-coprime a it returns a well-defined but WRONG value
+ // rather than failing. Verify a*inv == 1 and return nil otherwise, so this matches
+ // math/big.ModInverse's nil-on-no-inverse contract and both code paths agree.
+ if ct.MulCT(a, inv).Cmp(big.NewInt(1)) != 0 {
+ return nil
+ }
+ return inv
}
// MulCT performs constant-time modular multiplication using bigmod.
@@ -195,6 +234,9 @@ func (ct *CTModInt) MulCT(x, y *big.Int) *big.Int {
// This is required for correct ModInverse on composite moduli where phi(n) is known.
// For RSA-like moduli n = p*q, pass phiN = (p-1)*(q-1).
func NewCTModIntWithPhi(mod, phiN *big.Int) *CTModInt {
+ if mod.Bit(0) == 0 {
+ panic("NewCTModIntWithPhi: modulus must be odd")
+ }
modBytes := mod.Bytes()
m, err := bigmod.NewModulus(modBytes)
if err != nil {
@@ -208,7 +250,7 @@ func NewCTModIntWithPhi(mod, phiN *big.Int) *CTModInt {
return &CTModInt{
mod: m,
modBigInt: new(big.Int).Set(mod),
- inverseExp: phiMinusOne.Bytes(), // Use phi(n)-1 instead of n-2
+ inverseExp: leftPad(phiMinusOne.Bytes(), byteLen), // Use phi(n)-1 instead of n-2
byteLen: byteLen,
bytePool: sync.Pool{
New: func() interface{} {
@@ -217,72 +259,3 @@ func NewCTModIntWithPhi(mod, phiN *big.Int) *CTModInt {
},
}
}
-
-// TimingProtection provides response time normalization to prevent timing attacks.
-type TimingProtection struct {
- targetDuration time.Duration
- jitterRange time.Duration
-}
-
-// NewTimingProtection creates a TimingProtection with custom parameters.
-// targetDuration is the minimum padded duration for every operation.
-// jitterRange adds a random delay on top of targetDuration to prevent fingerprinting
-// the fixed padding boundary.
-func NewTimingProtection(targetDuration, jitterRange time.Duration) *TimingProtection {
- return &TimingProtection{
- targetDuration: targetDuration,
- jitterRange: jitterRange,
- }
-}
-
-// ProtectBigInt wraps a function that returns *big.Int with timing normalization.
-// The total execution time is always >= targetDuration + a random jitter, regardless
-// of how long the actual operation takes.
-func (tp *TimingProtection) ProtectBigInt(fn func() (*big.Int, error)) (*big.Int, error) {
- startTime := time.Now()
- result, err := fn()
- elapsed := time.Since(startTime)
-
- padTo := tp.targetDuration
- if elapsed > padTo {
- padTo = elapsed
- }
- if tp.jitterRange > 0 {
- jitterNanos, _ := rand.Int(rand.Reader, big.NewInt(int64(tp.jitterRange)))
- padTo += time.Duration(jitterNanos.Int64())
- }
- if remaining := padTo - elapsed; remaining > 0 {
- time.Sleep(remaining)
- }
- return result, err
-}
-
-// ConstantTimeCompare compares two big.Int values in constant time.
-// Both values are padded to padLen bytes before comparison to avoid leaking
-// relative magnitude. If padLen is 0, the maximum of the two byte lengths is used.
-func ConstantTimeCompare(a, b *big.Int, padLen int) int {
- aBytes := a.Bytes()
- bBytes := b.Bytes()
-
- if padLen <= 0 {
- padLen = len(aBytes)
- if len(bBytes) > padLen {
- padLen = len(bBytes)
- }
- }
-
- padA := make([]byte, padLen)
- padB := make([]byte, padLen)
- if len(aBytes) <= padLen {
- copy(padA[padLen-len(aBytes):], aBytes)
- } else {
- copy(padA, aBytes[len(aBytes)-padLen:])
- }
- if len(bBytes) <= padLen {
- copy(padB[padLen-len(bBytes):], bBytes)
- } else {
- copy(padB, bBytes[len(bBytes)-padLen:])
- }
-
- return subtle.ConstantTimeCompare(padA, padB)
-}
diff --git a/common/constant_time_test.go b/common/constant_time_test.go
index 5595babc1..0115955f2 100644
--- a/common/constant_time_test.go
+++ b/common/constant_time_test.go
@@ -7,6 +7,7 @@
package common
import (
+ "bytes"
"crypto/rand"
"math/big"
"testing"
@@ -167,63 +168,53 @@ func TestCTModIntWithPhi(t *testing.T) {
}
}
-// TestConstantTimeCompare verifies constant-time comparison
-func TestConstantTimeCompare(t *testing.T) {
- a := big.NewInt(12345)
- b := big.NewInt(12345)
- c := big.NewInt(54321)
-
- // Use explicit padLen to avoid leaking relative magnitude
- if ConstantTimeCompare(a, b, 128) != 1 {
- t.Error("ConstantTimeCompare should return 1 for equal values")
- }
+// TestModInverseCTNonCoprime: for a base that shares a factor with the modulus there
+// is no inverse; ModInverseCT must return nil, matching math/big.ModInverse. Regression
+// for the Fermat/Euler-inverse silent-wrong-answer issue.
+func TestModInverseCTNonCoprime(t *testing.T) {
+ p, _ := rand.Prime(rand.Reader, 256)
+ q, _ := rand.Prime(rand.Reader, 256)
+ n := new(big.Int).Mul(p, q)
+ pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
+ qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
+ phiN := new(big.Int).Mul(pMinus1, qMinus1)
- if ConstantTimeCompare(a, c, 128) != 0 {
- t.Error("ConstantTimeCompare should return 0 for different values")
- }
+ ctMod := NewCTModIntWithPhi(n, phiN)
- large := new(big.Int).Lsh(big.NewInt(1), 1024)
- small := big.NewInt(1)
- if ConstantTimeCompare(large, small, 128) != 0 {
- t.Error("ConstantTimeCompare should return 0 for values with different magnitudes")
+ // a = p shares the factor p with n, so it is not invertible mod n.
+ if got := ctMod.ModInverseCT(p); got != nil {
+ t.Errorf("ModInverseCT(p) for non-coprime input must be nil, got %v", got)
}
-
- // Test with padLen=0 (fallback to maxLen)
- if ConstantTimeCompare(a, b, 0) != 1 {
- t.Error("ConstantTimeCompare with padLen=0 should return 1 for equal values")
+ if std := new(big.Int).ModInverse(p, n); std != nil {
+ t.Errorf("sanity: math/big.ModInverse(p, n) should also be nil, got %v", std)
}
}
-// TestTimingProtectionBigInt verifies timing protection for BigInt operations
-func TestTimingProtectionBigInt(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping timing test in short mode")
+// TestExpCTExponentPadding verifies that padding the exponent to a fixed width (the
+// fix that hides the secret exponent's magnitude) does not change the result: leftPad
+// zero-extends correctly, and a short exponent still produces the same value as
+// math/big.Exp. Regression for the fixed-width exponent padding.
+func TestExpCTExponentPadding(t *testing.T) {
+ if got := leftPad([]byte{0x12, 0x34}, 5); !bytes.Equal(got, []byte{0, 0, 0, 0x12, 0x34}) {
+ t.Errorf("leftPad zero-extension = %v, want [0 0 0 18 52]", got)
+ }
+ if got := leftPad([]byte{0x12, 0x34}, 1); !bytes.Equal(got, []byte{0x12, 0x34}) {
+ t.Errorf("leftPad with width <= len must return input unchanged, got %v", got)
}
-
- tp := NewTimingProtection(50*time.Millisecond, 0)
p, _ := rand.Prime(rand.Reader, 512)
q, _ := rand.Prime(rand.Reader, 512)
N := new(big.Int).Mul(p, q)
-
+ ctMod := NewCTModInt(N)
base, _ := rand.Int(rand.Reader, N)
- exp, _ := rand.Int(rand.Reader, big.NewInt(1000))
- start := time.Now()
- result, err := tp.ProtectBigInt(func() (*big.Int, error) {
- return new(big.Int).Exp(base, exp, N), nil
- })
- elapsed := time.Since(start)
-
- if err != nil {
- t.Errorf("ProtectBigInt returned error: %v", err)
- }
- if result == nil {
- t.Error("ProtectBigInt returned nil result")
- }
-
- if elapsed < 20*time.Millisecond {
- t.Errorf("TimingProtection should normalize to ~50ms, got %v", elapsed)
+ // A short exponent is padded to the full modulus width internally; the result must
+ // still match math/big.Exp.
+ for _, exp := range []*big.Int{big.NewInt(1), big.NewInt(0x010203), big.NewInt(255)} {
+ want := new(big.Int).Exp(base, exp, N)
+ if got := ctMod.ExpCT(base, exp); got.Cmp(want) != 0 {
+ t.Errorf("ExpCT(base, %v) = %v, want %v", exp, got, want)
+ }
}
}
diff --git a/crypto/mta/constant_time_equiv_test.go b/crypto/mta/constant_time_equiv_test.go
new file mode 100644
index 000000000..5fd41a792
--- /dev/null
+++ b/crypto/mta/constant_time_equiv_test.go
@@ -0,0 +1,75 @@
+// Copyright © 2019 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package mta
+
+import (
+ "context"
+ "math/big"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/bnb-chain/tss-lib/common"
+ "github.com/bnb-chain/tss-lib/crypto"
+ "github.com/bnb-chain/tss-lib/crypto/paillier"
+ "github.com/bnb-chain/tss-lib/ecdsa/keygen"
+ "github.com/bnb-chain/tss-lib/tss"
+)
+
+// These tests verify that the MtA flow run with constant-time ops enabled — which
+// hardens the secret-witness exponentiations h1^x, h1^y (proofs.go) and h1^m
+// (range_proof.go), plus the secret-exponent Paillier Encrypt (gamma^m) and HomoMult
+// (c1^m) — still produces verifying proofs and the correct homomorphic result. The
+// proofs are randomised, so outputs are not byte-identical across paths; the protocol
+// completing and the result matching is the invariant. Primitive ExpCT==Exp / MulCT==Mul
+// equivalence is covered in common/constant_time_test.go.
+
+// TestShareProtocolWCConstantTime runs the full MtA "with check" share protocol with
+// constant-time ops enabled. It exercises ProveRangeAlice (h1^m via AliceInit),
+// ProveBobWC (h1^x, h1^y with x=b, y=betaPrm via BobMidWC), and the CT Paillier
+// Encrypt/HomoMult paths, with proof verification happening inside BobMidWC/AliceEndWC.
+func TestShareProtocolWCConstantTime(t *testing.T) {
+ q := tss.EC().Params().N
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+ defer cancel()
+
+ sk, pk, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength)
+ assert.NoError(t, err)
+
+ a := common.GetRandomPositiveInt(q)
+ b := common.GetRandomPositiveInt(q)
+ gBX, gBY := tss.EC().ScalarBaseMult(b.Bytes())
+
+ NTildei, h1i, h2i, err := keygen.LoadNTildeH1H2FromTestFixture(0)
+ assert.NoError(t, err)
+ NTildej, h1j, h2j, err := keygen.LoadNTildeH1H2FromTestFixture(1)
+ assert.NoError(t, err)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+
+ cA, pf, err := AliceInit(tss.EC(), pk, a, NTildej, h1j, h2j)
+ assert.NoError(t, err)
+
+ gBPoint, err := crypto.NewECPoint(tss.EC(), gBX, gBY)
+ assert.NoError(t, err)
+ _, cB, betaPrm, pfB, err := BobMidWC(tss.EC(), pk, pf, b, cA, NTildei, h1i, h2i, NTildej, h1j, h2j, gBPoint)
+ assert.NoError(t, err)
+
+ alpha, err := AliceEndWC(tss.EC(), pk, pfB, gBPoint, cA, cB, NTildei, h1i, h2i, sk)
+ assert.NoError(t, err)
+
+ // expect: alpha = ab + betaPrm (mod q) — proves the CT-hardened proofs verified and
+ // the CT Encrypt/HomoMult produced the correct homomorphic ciphertext.
+ aTimesB := new(big.Int).Mul(a, b)
+ aTimesBPlusBeta := new(big.Int).Add(aTimesB, betaPrm)
+ aTimesBPlusBetaModQ := new(big.Int).Mod(aTimesBPlusBeta, q)
+ assert.Equal(t, 0, alpha.Cmp(aTimesBPlusBetaModQ), "constant-time MtA must yield alpha = ab + betaPrm")
+}
diff --git a/crypto/mta/proofs.go b/crypto/mta/proofs.go
index 4e9baefab..5c57db6b5 100644
--- a/crypto/mta/proofs.go
+++ b/crypto/mta/proofs.go
@@ -70,18 +70,24 @@ func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c
u = crypto.ScalarBaseMult(ec, alpha)
}
- // 6.
+ // 6, 7, 8: z and t carry the secret MtA witnesses x and y as exponents; zPrm and the
+ // h2^* terms use one-time random blinds. Harden only the secret-exponent terms.
modNTilde := common.ModInt(NTilde)
- z := modNTilde.Exp(h1, x)
- z = modNTilde.Mul(z, modNTilde.Exp(h2, rho))
-
- // 7.
zPrm := modNTilde.Exp(h1, alpha)
zPrm = modNTilde.Mul(zPrm, modNTilde.Exp(h2, rhoPrm))
- // 8.
- t := modNTilde.Exp(h1, y)
- t = modNTilde.Mul(t, modNTilde.Exp(h2, sigma))
+ var z, t *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: x and y are Bob's secret MtA inputs; exponentiate them in constant
+ // time (NTilde is odd). The h2^rho / h2^sigma blinds use one-time randomness and
+ // stay on math/big (see the coverage note in common/constant_time.go).
+ ctModNTilde := common.NewCTModInt(NTilde)
+ z = modNTilde.Mul(ctModNTilde.ExpCT(h1, x), modNTilde.Exp(h2, rho))
+ t = modNTilde.Mul(ctModNTilde.ExpCT(h1, y), modNTilde.Exp(h2, sigma))
+ } else {
+ z = modNTilde.Mul(modNTilde.Exp(h1, x), modNTilde.Exp(h2, rho))
+ t = modNTilde.Mul(modNTilde.Exp(h1, y), modNTilde.Exp(h2, sigma))
+ }
// 9.
modNSquared := common.ModInt(NSquared)
diff --git a/crypto/mta/range_proof.go b/crypto/mta/range_proof.go
index 0cff6703d..5917e4f13 100644
--- a/crypto/mta/range_proof.go
+++ b/crypto/mta/range_proof.go
@@ -56,8 +56,16 @@ func ProveRangeAlice(ec elliptic.Curve, pk *paillier.PublicKey, c, NTilde, h1, h
// 5.
modNTilde := common.ModInt(NTilde)
- z := modNTilde.Exp(h1, m)
- z = modNTilde.Mul(z, modNTilde.Exp(h2, rho))
+ var z *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: m is Alice's secret value used as the exponent; exponentiate in
+ // constant time (NTilde is odd). The h2^rho blind and the u/w terms use one-time
+ // randomness and stay on math/big (see common/constant_time.go).
+ z = modNTilde.Mul(common.NewCTModInt(NTilde).ExpCT(h1, m), modNTilde.Exp(h2, rho))
+ } else {
+ z = modNTilde.Exp(h1, m)
+ z = modNTilde.Mul(z, modNTilde.Exp(h2, rho))
+ }
// 6.
modNSquared := common.ModInt(pk.NSquare())
diff --git a/crypto/paillier/constant_time_equiv_test.go b/crypto/paillier/constant_time_equiv_test.go
index 6a391c37f..bbf25188e 100644
--- a/crypto/paillier/constant_time_equiv_test.go
+++ b/crypto/paillier/constant_time_equiv_test.go
@@ -45,6 +45,53 @@ func TestDecryptCTEquivalence(t *testing.T) {
assert.Zero(t, pt.Cmp(mOn), "CT Decrypt must recover the plaintext")
}
+// TestHomoMultCTEquivalence: HomoMult(m, c1) = c1^m mod N2 is deterministic; CT and
+// non-CT must agree byte-for-byte, and the homomorphic multiplication property must
+// hold under CT. m is the secret scalar exponent hardened by the CT path.
+func TestHomoMultCTEquivalence(t *testing.T) {
+ facSetUp(t)
+
+ a := big.NewInt(111111)
+ b := big.NewInt(222222)
+ cA, err := publicKey.Encrypt(a)
+ assert.NoError(t, err)
+
+ cbOff, err := publicKey.HomoMult(b, cA)
+ assert.NoError(t, err)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ cbOn, err := publicKey.HomoMult(b, cA)
+ assert.NoError(t, err)
+
+ assert.Zero(t, cbOff.Cmp(cbOn), "CT and non-CT HomoMult must be byte-identical")
+
+ // Dec(HomoMult(b, Enc(a))) must equal a*b mod N.
+ dec, err := privateKey.Decrypt(cbOn)
+ assert.NoError(t, err)
+ want := new(big.Int).Mod(new(big.Int).Mul(a, b), publicKey.N)
+ assert.Zero(t, want.Cmp(dec), "CT HomoMult must satisfy the homomorphic multiplication property")
+}
+
+// TestEncryptCTRoundTrip: Encrypt is randomised (fresh nonce x), so CT and non-CT
+// ciphertexts differ; instead verify that a CT-produced ciphertext decrypts back to the
+// plaintext, exercising the constant-time gamma^m path (m is the secret exponent).
+func TestEncryptCTRoundTrip(t *testing.T) {
+ facSetUp(t)
+
+ pt := big.NewInt(987654321)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ cipher, err := publicKey.Encrypt(pt)
+ assert.NoError(t, err)
+ dec, err := privateKey.Decrypt(cipher)
+ assert.NoError(t, err)
+ assert.Zero(t, pt.Cmp(dec), "CT Encrypt must round-trip through Decrypt")
+}
+
// TestPaillierProofCTEquivalence: the Paillier square-free Proof is deterministic
// given (k, key, ecdsaPub); CT and non-CT must agree byte-for-byte and both verify.
func TestPaillierProofCTEquivalence(t *testing.T) {
diff --git a/crypto/paillier/paillier.go b/crypto/paillier/paillier.go
index 51e51cd20..6de1f85b3 100644
--- a/crypto/paillier/paillier.go
+++ b/crypto/paillier/paillier.go
@@ -116,8 +116,15 @@ func (publicKey *PublicKey) EncryptAndReturnRandomness(m *big.Int) (c *big.Int,
x = common.GetRandomPositiveRelativelyPrimeInt(publicKey.N)
N2 := publicKey.NSquare()
// 1. gamma^m mod N2
- Gm := new(big.Int).Exp(publicKey.Gamma(), m, N2)
- // 2. x^N mod N2
+ var Gm *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: m is the (often secret) plaintext used as the exponent; exponentiate
+ // in constant time (N2 is odd).
+ Gm = common.NewCTModInt(N2).ExpCT(publicKey.Gamma(), m)
+ } else {
+ Gm = new(big.Int).Exp(publicKey.Gamma(), m, N2)
+ }
+ // 2. x^N mod N2 (exponent N is public; the secret base x stays on math/big)
xN := new(big.Int).Exp(x, publicKey.N, N2)
// 3. (1) * (2) mod N2
c = common.ModInt(N2).Mul(Gm, xN)
@@ -138,6 +145,11 @@ func (publicKey *PublicKey) HomoMult(m, c1 *big.Int) (*big.Int, error) {
return nil, ErrMessageTooLong
}
// cipher^m mod N2
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: m is the secret scalar multiplier used as the exponent; exponentiate
+ // in constant time (N2 is odd).
+ return common.NewCTModInt(N2).ExpCT(c1, m), nil
+ }
return common.ModInt(N2).Exp(c1, m), nil
}
diff --git a/ecdsa/keygen/prepare.go b/ecdsa/keygen/prepare.go
index 6076e58ee..b011f0af2 100644
--- a/ecdsa/keygen/prepare.go
+++ b/ecdsa/keygen/prepare.go
@@ -128,7 +128,15 @@ consumer:
alpha := common.GetRandomPositiveRelativelyPrimeInt(NTildei)
beta := modPQ.ModInverse(alpha)
h1i := modNTildeI.Mul(f1, f1)
- h2i := modNTildeI.Exp(h1i, alpha)
+ var h2i *big.Int
+ if common.IsConstantTimeEnabled() {
+ // SECURITY: alpha is the secret ring-Pedersen trapdoor (stored long-term in
+ // LocalPreParams.Alpha and the discrete-log witness for the DLN proofs);
+ // exponentiate in constant time (NTildei is odd).
+ h2i = common.NewCTModInt(NTildei).ExpCT(h1i, alpha)
+ } else {
+ h2i = modNTildeI.Exp(h1i, alpha)
+ }
preParams := &LocalPreParams{
PaillierSK: paiSK,
diff --git a/ecdsa/keygen/prepare_constant_time_test.go b/ecdsa/keygen/prepare_constant_time_test.go
new file mode 100644
index 000000000..78c82343f
--- /dev/null
+++ b/ecdsa/keygen/prepare_constant_time_test.go
@@ -0,0 +1,39 @@
+// Copyright © 2019 Binance
+//
+// This file is part of Binance. The full Binance copyright notice, including
+// terms governing use, modification, and redistribution, is contained in the
+// file LICENSE at the root of the source code distribution tree.
+
+package keygen
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/bnb-chain/tss-lib/common"
+)
+
+// TestPrepareTrapdoorCTEquivalence verifies that the constant-time path GeneratePreParams
+// uses to derive the ring-Pedersen element h2i = h1i^alpha mod NTildei (alpha is the
+// secret trapdoor stored long-term in LocalPreParams) computes the same value as
+// math/big, on real fixture key material. It mirrors the exact operation hardened in
+// prepare.go; fixtures are used to avoid multi-minute safe-prime generation.
+func TestPrepareTrapdoorCTEquivalence(t *testing.T) {
+ fixtures, _, err := LoadKeygenTestFixtures(1)
+ if err != nil {
+ t.Skip("keygen test fixtures are required (avoids safe-prime generation)")
+ }
+ pp := fixtures[0].LocalPreParams
+
+ modNTildeI := common.ModInt(pp.NTildei)
+ want := modNTildeI.Exp(pp.H1i, pp.Alpha)
+
+ common.EnableConstantTimeOps()
+ defer common.DisableConstantTimeOps()
+ assert.True(t, common.IsConstantTimeEnabled(), "CT must be engaged (else this test is vacuous)")
+ got := common.NewCTModInt(pp.NTildei).ExpCT(pp.H1i, pp.Alpha)
+
+ assert.Zero(t, want.Cmp(got), "CT trapdoor exponentiation must match math/big")
+ assert.Zero(t, pp.H2i.Cmp(got), "recomputed h2i must equal the stored trapdoor element")
+}