diff --git a/common/constant_time.go b/common/constant_time.go new file mode 100644 index 000000000..466e63161 --- /dev/null +++ b/common/constant_time.go @@ -0,0 +1,261 @@ +// 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 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 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 ( + "math/big" + "sync" + "sync/atomic" + + "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 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 + inverseExp []byte // Exponent for modular inverse: p-2 (prime) or phi(n)-1 (composite) + byteLen int + 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. +// 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 { + // 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: leftPad(modMinusTwo.Bytes(), byteLen), + byteLen: byteLen, + bytePool: sync.Pool{ + New: func() interface{} { + return make([]byte, byteLen) + }, + }, + } +} + +// 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 := 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) + + // 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) + + 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) + inv := new(big.Int).SetBytes(result.Bytes(ct.mod)) + + // 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. +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 { + if mod.Bit(0) == 0 { + panic("NewCTModIntWithPhi: modulus must be odd") + } + 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: leftPad(phiMinusOne.Bytes(), byteLen), // Use phi(n)-1 instead of n-2 + byteLen: byteLen, + bytePool: sync.Pool{ + New: func() interface{} { + return make([]byte, byteLen) + }, + }, + } +} diff --git a/common/constant_time_test.go b/common/constant_time_test.go new file mode 100644 index 000000000..0115955f2 --- /dev/null +++ b/common/constant_time_test.go @@ -0,0 +1,294 @@ +// 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 ( + "bytes" + "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) + } + } +} + +// 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) + + ctMod := NewCTModIntWithPhi(n, phiN) + + // 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) + } + 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) + } +} + +// 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) + } + + 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) + + // 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) + } + } +} + +// 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/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 new file mode 100644 index 000000000..bbf25188e --- /dev/null +++ b/crypto/paillier/constant_time_equiv_test.go @@ -0,0 +1,189 @@ +// 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") +} + +// 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) { + 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..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 } @@ -178,12 +190,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 +232,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/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") +} 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=