Skip to content

Commit 3f677ff

Browse files
authored
Merge pull request #328 from yycen/constant-time
feat: Implement constant-time exponentiation and modular inverse in c…
2 parents ba5b734 + dca2ac4 commit 3f677ff

101 files changed

Lines changed: 1407 additions & 503 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/gofmt.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88
- name: check out
9-
uses: actions/checkout@v3
9+
uses: actions/checkout@v4
1010
- name: go fmt project
1111
uses: Jerome1337/gofmt-action@v1.0.4

.github/workflows/test.yml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@ jobs:
1414
runs-on: macOS-latest
1515
steps:
1616

17-
- name: Set up Go 1.20
18-
uses: actions/setup-go@v3
19-
with:
20-
go-version: 1.20.3
21-
id: go
22-
2317
- name: Check out code into the Go module directory
24-
uses: actions/checkout@v3
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: '1.23'
2524

2625
- name: Clean dependencies
2726
run: go clean --modcache

common/constant_time.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// Copyright © 2019-2024 Binance
2+
//
3+
// This file is part of Binance. The full Binance copyright notice, including
4+
// terms governing use, modification, and redistribution, is contained in the
5+
// file LICENSE at the root of the source code distribution tree.
6+
7+
// Package common provides constant-time big integer operations for cryptographic use.
8+
//
9+
// SECURITY NOTE: Go's math/big package is NOT constant-time and should not be used
10+
// with secret values. This module provides constant-time alternatives using
11+
// filippo.io/bigmod, which is the same library used by Go's crypto/rsa.
12+
//
13+
// Reference: https://github.com/golang/go/issues/20654
14+
15+
package common
16+
17+
import (
18+
"crypto/rand"
19+
"crypto/subtle"
20+
"math/big"
21+
"sync"
22+
"sync/atomic"
23+
"time"
24+
25+
"filippo.io/bigmod"
26+
)
27+
28+
// constantTimeEnabled controls whether constant-time operations are used.
29+
// Default is false (disabled) for performance. Enable for high-security environments.
30+
var constantTimeEnabled int32 = 0
31+
32+
// EnableConstantTimeOps enables constant-time cryptographic operations.
33+
// Call this at application startup if timing side-channel protection is required.
34+
func EnableConstantTimeOps() {
35+
atomic.StoreInt32(&constantTimeEnabled, 1)
36+
}
37+
38+
// DisableConstantTimeOps disables constant-time operations (default).
39+
func DisableConstantTimeOps() {
40+
atomic.StoreInt32(&constantTimeEnabled, 0)
41+
}
42+
43+
// IsConstantTimeEnabled returns true if constant-time operations are enabled.
44+
func IsConstantTimeEnabled() bool {
45+
return atomic.LoadInt32(&constantTimeEnabled) == 1
46+
}
47+
48+
// CTModInt provides constant-time modular arithmetic using filippo.io/bigmod.
49+
// This is the recommended implementation as bigmod is:
50+
// 1. Maintained by the Go crypto team lead (Filippo Valsorda)
51+
// 2. The same code used internally by crypto/rsa and crypto/ecdsa
52+
// 3. Highly optimized with architecture-specific assembly
53+
type CTModInt struct {
54+
mod *bigmod.Modulus
55+
modBigInt *big.Int
56+
inverseExp []byte // Exponent for modular inverse: p-2 (prime) or phi(n)-1 (composite)
57+
byteLen int
58+
bytePool sync.Pool
59+
}
60+
61+
// NewCTModInt creates a constant-time modular context using bigmod.
62+
// Note: bigmod requires odd modulus for Exp operations.
63+
func NewCTModInt(mod *big.Int) *CTModInt {
64+
modBytes := mod.Bytes()
65+
m, err := bigmod.NewModulus(modBytes)
66+
if err != nil {
67+
// Fallback: should not happen for valid modulus
68+
panic(err)
69+
}
70+
71+
// Pre-compute mod-2 for Fermat inverse: a^(-1) = a^(mod-2) mod mod
72+
modMinusTwo := new(big.Int).Sub(mod, big.NewInt(2))
73+
74+
byteLen := len(modBytes)
75+
return &CTModInt{
76+
mod: m,
77+
modBigInt: new(big.Int).Set(mod),
78+
inverseExp: modMinusTwo.Bytes(),
79+
byteLen: byteLen,
80+
bytePool: sync.Pool{
81+
New: func() interface{} {
82+
return make([]byte, byteLen)
83+
},
84+
},
85+
}
86+
}
87+
88+
// reduceToPaddedBytes reduces val mod ct.modBigInt and returns a zero-padded
89+
// byte slice of length ct.byteLen suitable for bigmod.Nat.SetBytes.
90+
// The reduction uses big.Int.Mod which is safe here because the modulus is public.
91+
func (ct *CTModInt) reduceToPaddedBytes(val *big.Int) []byte {
92+
reduced := val
93+
if val.Sign() < 0 || val.Cmp(ct.modBigInt) >= 0 {
94+
reduced = new(big.Int).Mod(val, ct.modBigInt)
95+
}
96+
97+
buf := ct.bytePool.Get().([]byte)
98+
for i := range buf {
99+
buf[i] = 0
100+
}
101+
b := reduced.Bytes()
102+
copy(buf[ct.byteLen-len(b):], b)
103+
return buf
104+
}
105+
106+
// ExpCT performs constant-time modular exponentiation using bigmod.
107+
// IMPORTANT: The modulus must be odd. Negative exponents are not supported and will panic.
108+
func (ct *CTModInt) ExpCT(base, exp *big.Int) *big.Int {
109+
if exp.Sign() == 0 {
110+
return big.NewInt(1)
111+
}
112+
if exp.Sign() < 0 {
113+
panic("ExpCT: negative exponents are not supported; use ModInverseCT explicitly")
114+
}
115+
116+
paddedBase := ct.reduceToPaddedBytes(base)
117+
defer func() {
118+
for i := range paddedBase {
119+
paddedBase[i] = 0
120+
}
121+
ct.bytePool.Put(paddedBase)
122+
}()
123+
124+
baseNat := bigmod.NewNat()
125+
baseNat.SetBytes(paddedBase, ct.mod)
126+
127+
expBytes := exp.Bytes()
128+
result := bigmod.NewNat()
129+
result.Exp(baseNat, expBytes, ct.mod)
130+
131+
return new(big.Int).SetBytes(result.Bytes(ct.mod))
132+
}
133+
134+
// ModInverseCT computes the modular inverse in constant time using Fermat's little theorem.
135+
// For a prime modulus p: a^(-1) = a^(p-2) mod p
136+
// For a non-prime modulus n with known phi(n): a^(-1) = a^(phi(n)-1) mod n
137+
// SECURITY: This uses constant-time Exp, making the entire operation constant-time.
138+
// Note: The modulus should be prime for this to work correctly. For composite moduli,
139+
// use NewCTModIntWithPhi to provide phi(n).
140+
func (ct *CTModInt) ModInverseCT(a *big.Int) *big.Int {
141+
if a.Sign() == 0 {
142+
return nil
143+
}
144+
145+
paddedA := ct.reduceToPaddedBytes(a)
146+
defer func() {
147+
for i := range paddedA {
148+
paddedA[i] = 0
149+
}
150+
ct.bytePool.Put(paddedA)
151+
}()
152+
153+
aNat := bigmod.NewNat()
154+
aNat.SetBytes(paddedA, ct.mod)
155+
156+
result := bigmod.NewNat()
157+
result.Exp(aNat, ct.inverseExp, ct.mod)
158+
159+
return new(big.Int).SetBytes(result.Bytes(ct.mod))
160+
}
161+
162+
// Mod returns the modulus as a big.Int.
163+
func (ct *CTModInt) Mod() *big.Int {
164+
return new(big.Int).Set(ct.modBigInt)
165+
}
166+
167+
// MulCT performs constant-time modular multiplication using bigmod.
168+
func (ct *CTModInt) MulCT(x, y *big.Int) *big.Int {
169+
paddedX := ct.reduceToPaddedBytes(x)
170+
paddedY := ct.reduceToPaddedBytes(y)
171+
defer func() {
172+
for i := range paddedX {
173+
paddedX[i] = 0
174+
}
175+
ct.bytePool.Put(paddedX)
176+
}()
177+
defer func() {
178+
for i := range paddedY {
179+
paddedY[i] = 0
180+
}
181+
ct.bytePool.Put(paddedY)
182+
}()
183+
184+
xNat := bigmod.NewNat()
185+
yNat := bigmod.NewNat()
186+
xNat.SetBytes(paddedX, ct.mod)
187+
yNat.SetBytes(paddedY, ct.mod)
188+
189+
xNat.Mul(yNat, ct.mod)
190+
191+
return new(big.Int).SetBytes(xNat.Bytes(ct.mod))
192+
}
193+
194+
// NewCTModIntWithPhi creates a constant-time modular context for composite moduli.
195+
// This is required for correct ModInverse on composite moduli where phi(n) is known.
196+
// For RSA-like moduli n = p*q, pass phiN = (p-1)*(q-1).
197+
func NewCTModIntWithPhi(mod, phiN *big.Int) *CTModInt {
198+
modBytes := mod.Bytes()
199+
m, err := bigmod.NewModulus(modBytes)
200+
if err != nil {
201+
panic(err)
202+
}
203+
204+
// For composite modulus: a^(-1) = a^(phi(n)-1) mod n
205+
phiMinusOne := new(big.Int).Sub(phiN, big.NewInt(1))
206+
207+
byteLen := len(modBytes)
208+
return &CTModInt{
209+
mod: m,
210+
modBigInt: new(big.Int).Set(mod),
211+
inverseExp: phiMinusOne.Bytes(), // Use phi(n)-1 instead of n-2
212+
byteLen: byteLen,
213+
bytePool: sync.Pool{
214+
New: func() interface{} {
215+
return make([]byte, byteLen)
216+
},
217+
},
218+
}
219+
}
220+
221+
// TimingProtection provides response time normalization to prevent timing attacks.
222+
type TimingProtection struct {
223+
targetDuration time.Duration
224+
jitterRange time.Duration
225+
}
226+
227+
// NewTimingProtection creates a TimingProtection with custom parameters.
228+
// targetDuration is the minimum padded duration for every operation.
229+
// jitterRange adds a random delay on top of targetDuration to prevent fingerprinting
230+
// the fixed padding boundary.
231+
func NewTimingProtection(targetDuration, jitterRange time.Duration) *TimingProtection {
232+
return &TimingProtection{
233+
targetDuration: targetDuration,
234+
jitterRange: jitterRange,
235+
}
236+
}
237+
238+
// ProtectBigInt wraps a function that returns *big.Int with timing normalization.
239+
// The total execution time is always >= targetDuration + a random jitter, regardless
240+
// of how long the actual operation takes.
241+
func (tp *TimingProtection) ProtectBigInt(fn func() (*big.Int, error)) (*big.Int, error) {
242+
startTime := time.Now()
243+
result, err := fn()
244+
elapsed := time.Since(startTime)
245+
246+
padTo := tp.targetDuration
247+
if elapsed > padTo {
248+
padTo = elapsed
249+
}
250+
if tp.jitterRange > 0 {
251+
jitterNanos, _ := rand.Int(rand.Reader, big.NewInt(int64(tp.jitterRange)))
252+
padTo += time.Duration(jitterNanos.Int64())
253+
}
254+
if remaining := padTo - elapsed; remaining > 0 {
255+
time.Sleep(remaining)
256+
}
257+
return result, err
258+
}
259+
260+
// ConstantTimeCompare compares two big.Int values in constant time.
261+
// Both values are padded to padLen bytes before comparison to avoid leaking
262+
// relative magnitude. If padLen is 0, the maximum of the two byte lengths is used.
263+
func ConstantTimeCompare(a, b *big.Int, padLen int) int {
264+
aBytes := a.Bytes()
265+
bBytes := b.Bytes()
266+
267+
if padLen <= 0 {
268+
padLen = len(aBytes)
269+
if len(bBytes) > padLen {
270+
padLen = len(bBytes)
271+
}
272+
}
273+
274+
padA := make([]byte, padLen)
275+
padB := make([]byte, padLen)
276+
if len(aBytes) <= padLen {
277+
copy(padA[padLen-len(aBytes):], aBytes)
278+
} else {
279+
copy(padA, aBytes[len(aBytes)-padLen:])
280+
}
281+
if len(bBytes) <= padLen {
282+
copy(padB[padLen-len(bBytes):], bBytes)
283+
} else {
284+
copy(padB, bBytes[len(bBytes)-padLen:])
285+
}
286+
287+
return subtle.ConstantTimeCompare(padA, padB)
288+
}

0 commit comments

Comments
 (0)