From 7480a8ef388110c8bfdef5549dbddb0c22648357 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 24 Jun 2026 05:09:30 -0500 Subject: [PATCH 1/3] secp256k1: Separate const time compare funcs. The various functions for comparing in constant time are no longer limited to only being used in mod n scalars, so move them into their own file. --- dcrec/secp256k1/common.go | 46 +++++++++++++++++++++++++++++++++++ dcrec/secp256k1/modnscalar.go | 41 ------------------------------- 2 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 dcrec/secp256k1/common.go diff --git a/dcrec/secp256k1/common.go b/dcrec/secp256k1/common.go new file mode 100644 index 0000000000..02513a817b --- /dev/null +++ b/dcrec/secp256k1/common.go @@ -0,0 +1,46 @@ +// Copyright (c) 2020-2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +// constantTimeEq returns 1 if a == b or 0 otherwise in constant time. +func constantTimeEq(a, b uint32) uint32 { + return uint32((uint64(a^b) - 1) >> 63) +} + +// constantTimeEq64 returns 1 if a == b or 0 otherwise in constant time. +func constantTimeEq64(a, b uint64) uint32 { + t := a ^ b + return uint32(((t | -t) >> 63) ^ 1) +} + +// constantTimeNotEq returns 1 if a != b or 0 otherwise in constant time. +func constantTimeNotEq(a, b uint32) uint32 { + return ^uint32((uint64(a^b)-1)>>63) & 1 +} + +// constantTimeLess returns 1 if a < b or 0 otherwise in constant time. +func constantTimeLess(a, b uint32) uint32 { + return uint32((uint64(a) - uint64(b)) >> 63) +} + +// constantTimeLessOrEq returns 1 if a <= b or 0 otherwise in constant time. +func constantTimeLessOrEq(a, b uint32) uint32 { + return uint32((uint64(a) - uint64(b) - 1) >> 63) +} + +// constantTimeGreater returns 1 if a > b or 0 otherwise in constant time. +func constantTimeGreater(a, b uint32) uint32 { + return constantTimeLess(b, a) +} + +// constantTimeGreaterOrEq returns 1 if a >= b or 0 otherwise in constant time. +func constantTimeGreaterOrEq(a, b uint32) uint32 { + return constantTimeLessOrEq(b, a) +} + +// constantTimeMin returns min(a,b) in constant time. +func constantTimeMin(a, b uint32) uint32 { + return b ^ ((a ^ b) & -constantTimeLess(a, b)) +} diff --git a/dcrec/secp256k1/modnscalar.go b/dcrec/secp256k1/modnscalar.go index ec6443f543..30c11f9d7c 100644 --- a/dcrec/secp256k1/modnscalar.go +++ b/dcrec/secp256k1/modnscalar.go @@ -210,47 +210,6 @@ func (s *ModNScalar) SetInt(ui uint32) *ModNScalar { return s } -// constantTimeEq returns 1 if a == b or 0 otherwise in constant time. -func constantTimeEq(a, b uint32) uint32 { - return uint32((uint64(a^b) - 1) >> 63) -} - -// constantTimeEq64 returns 1 if a == b or 0 otherwise in constant time. -func constantTimeEq64(a, b uint64) uint32 { - t := a ^ b - return uint32(((t | -t) >> 63) ^ 1) -} - -// constantTimeNotEq returns 1 if a != b or 0 otherwise in constant time. -func constantTimeNotEq(a, b uint32) uint32 { - return ^uint32((uint64(a^b)-1)>>63) & 1 -} - -// constantTimeLess returns 1 if a < b or 0 otherwise in constant time. -func constantTimeLess(a, b uint32) uint32 { - return uint32((uint64(a) - uint64(b)) >> 63) -} - -// constantTimeLessOrEq returns 1 if a <= b or 0 otherwise in constant time. -func constantTimeLessOrEq(a, b uint32) uint32 { - return uint32((uint64(a) - uint64(b) - 1) >> 63) -} - -// constantTimeGreater returns 1 if a > b or 0 otherwise in constant time. -func constantTimeGreater(a, b uint32) uint32 { - return constantTimeLess(b, a) -} - -// constantTimeGreaterOrEq returns 1 if a >= b or 0 otherwise in constant time. -func constantTimeGreaterOrEq(a, b uint32) uint32 { - return constantTimeLessOrEq(b, a) -} - -// constantTimeMin returns min(a,b) in constant time. -func constantTimeMin(a, b uint32) uint32 { - return b ^ ((a ^ b) & -constantTimeLess(a, b)) -} - // overflows determines if the current scalar is greater than or equal to the // group order in constant time and returns 1 if it is or 0 otherwise. func (s *ModNScalar) overflows() uint32 { From a200c57c4d60417599bd4449aa1402d2a832443d Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 24 Jun 2026 05:09:37 -0500 Subject: [PATCH 2/3] secp256k1: Refactor constant time select code. This introduces a new function for selecting between two uint64s in constant time and modifies code that manually performs the selection to make use of the function to improve readability. The function gets inlined, so there is no loss of performance. The order of the condition is also reversed to match the typical hardware intrinsic which also resembles the ternary operator. --- dcrec/secp256k1/common.go | 9 +++++++++ dcrec/secp256k1/field64.go | 30 ++++++++++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/dcrec/secp256k1/common.go b/dcrec/secp256k1/common.go index 02513a817b..b45e013bd5 100644 --- a/dcrec/secp256k1/common.go +++ b/dcrec/secp256k1/common.go @@ -44,3 +44,12 @@ func constantTimeGreaterOrEq(a, b uint32) uint32 { func constantTimeMin(a, b uint32) uint32 { return b ^ ((a ^ b) & -constantTimeLess(a, b)) } + +// constantTimeSelect64 returns a if cond == 1 or b if cond == 0 in constant +// time. +// +// WARNING: The behavior is undefined if cond is anything other than 0 or 1. +func constantTimeSelect64(cond, a, b uint64) uint64 { + mask := -cond + return b ^ (a^b)&mask +} diff --git a/dcrec/secp256k1/field64.go b/dcrec/secp256k1/field64.go index c751f2b07a..99e7ea7133 100644 --- a/dcrec/secp256k1/field64.go +++ b/dcrec/secp256k1/field64.go @@ -84,11 +84,10 @@ func (f *FieldVal64) SetBytes(b *[32]byte) uint32 { s2, borrow = bits.Sub64(f.n[2], field64Prime2, borrow) s3, borrow = bits.Sub64(f.n[3], field64Prime3, borrow) - mask := -(1 - borrow) - f.n[0] ^= (s0 ^ f.n[0]) & mask - f.n[1] ^= (s1 ^ f.n[1]) & mask - f.n[2] ^= (s2 ^ f.n[2]) & mask - f.n[3] ^= (s3 ^ f.n[3]) & mask + f.n[0] = constantTimeSelect64(borrow, f.n[0], s0) + f.n[1] = constantTimeSelect64(borrow, f.n[1], s1) + f.n[2] = constantTimeSelect64(borrow, f.n[2], s2) + f.n[3] = constantTimeSelect64(borrow, f.n[3], s3) return uint32(1 - borrow) } @@ -301,11 +300,11 @@ func (f *FieldVal64) Add2(a, b *FieldVal64) *FieldVal64 { // Pass 3: constant-time select. Keep t only when there was no overflow and // t < p (borrow set); otherwise use s - mask := -((1 - overflow) & borrow) - f.n[0] = s0 ^ ((t0 ^ s0) & mask) - f.n[1] = s1 ^ ((t1 ^ s1) & mask) - f.n[2] = s2 ^ ((t2 ^ s2) & mask) - f.n[3] = s3 ^ ((t3 ^ s3) & mask) + cond := (1 - overflow) & borrow + f.n[0] = constantTimeSelect64(cond, t0, s0) + f.n[1] = constantTimeSelect64(cond, t1, s1) + f.n[2] = constantTimeSelect64(cond, t2, s2) + f.n[3] = constantTimeSelect64(cond, t3, s3) return f } @@ -553,17 +552,16 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { // subtract of p fully reduces it. t4 = carry - var s0, s1, s2, s3, mask, borrow uint64 + var s0, s1, s2, s3, borrow uint64 s0, borrow = bits.Sub64(t0, field64Prime0, 0) s1, borrow = bits.Sub64(t1, field64Prime1, borrow) s2, borrow = bits.Sub64(t2, field64Prime2, borrow) s3, borrow = bits.Sub64(t3, field64Prime3, borrow) _, borrow = bits.Sub64(t4, 0, borrow) - mask = -borrow - r[0] = s0 ^ ((t0 ^ s0) & mask) - r[1] = s1 ^ ((t1 ^ s1) & mask) - r[2] = s2 ^ ((t2 ^ s2) & mask) - r[3] = s3 ^ ((t3 ^ s3) & mask) + r[0] = constantTimeSelect64(borrow, t0, s0) + r[1] = constantTimeSelect64(borrow, t1, s1) + r[2] = constantTimeSelect64(borrow, t2, s2) + r[3] = constantTimeSelect64(borrow, t3, s3) } // field64Mul sets r = a * b (mod p). From 5b993e048a1315cabf65f2789047666bea587fee Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 24 Jun 2026 05:25:36 -0500 Subject: [PATCH 3/3] secp256k1: Add constant time sel test. This adds a test for the new constantTimeSelect64 function to help ensure proper behavior. --- dcrec/secp256k1/common_test.go | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 dcrec/secp256k1/common_test.go diff --git a/dcrec/secp256k1/common_test.go b/dcrec/secp256k1/common_test.go new file mode 100644 index 0000000000..cde06019ff --- /dev/null +++ b/dcrec/secp256k1/common_test.go @@ -0,0 +1,37 @@ +// Copyright (c) 2026 The Decred developers +// Copyright (c) 2013-2026 Dave Collins +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import "testing" + +// TestConstantTimeSelect64 ensures that the 64-bit constant time selection +// function works as expected in terms of behavior. +func TestConstantTimeSelect64(t *testing.T) { + tests := []struct { + name string // test description + cond uint64 // condition value + a uint64 // first value + b uint64 // second value + want uint64 // expected selected value + }{ + {"sel(0,1,2)==2", 0, 1, 2, 2}, + {"sel(1,1,2)==1", 1, 1, 2, 1}, + {"sel(0,1<<64-1,1)==1", 0, 1<<64 - 1, 1, 1}, + {"sel(1,1<<64-1,1)==1<<64-1", 1, 1<<64 - 1, 1, 1<<64 - 1}, + {"sel(0,1,1<<64-1)==1<<64-1", 0, 1, 1<<64 - 1, 1<<64 - 1}, + {"sel(1,1,1<<64-1)==1", 1, 1, 1<<64 - 1, 1}, + {"sel(0,1<<64-1,1<<64-2)==1<<64-2", 0, 1<<64 - 1, 1<<64 - 2, 1<<64 - 2}, + {"sel(1,1<<64-1,1<<64-2)==1<<64-1", 1, 1<<64 - 1, 1<<64 - 2, 1<<64 - 1}, + } + + for _, test := range tests { + got := constantTimeSelect64(test.cond, test.a, test.b) + if got != test.want { + t.Errorf("%q: unexpected result -- got %d, want %d", test.name, + got, test.want) + } + } +}