Skip to content

Commit dfa332c

Browse files
authored
Merge pull request #7 from ecadlabs/364-intermittent-invalid-signature-with-tz2-in-aws-kms
implement low-S canonization for ECDSA signatures
2 parents 225e0ac + ba46ff7 commit dfa332c

File tree

3 files changed

+150
-2
lines changed

3 files changed

+150
-2
lines changed

crypt/common.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,12 @@ func NewSignatureFromBytes(sig []byte, pub PublicKey) (Signature, error) {
224224
!inner.Empty() {
225225
return nil, errors.New("crypt: invalid ASN.1")
226226
}
227-
return &ECDSASignature{
227+
// Canonize to low-S form for secp256k1 compatibility with Tezos/libsecp256k1
228+
return canonizeSignature(&ECDSASignature{
228229
R: &r,
229230
S: &s,
230231
Curve: pub.Curve,
231-
}, nil
232+
}), nil
232233

233234
case Ed25519PublicKey:
234235
return Ed25519Signature(sig), nil

crypt/crypt_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/ed25519"
66
"crypto/elliptic"
77
"crypto/rand"
8+
"math/big"
89
"sync"
910
"testing"
1011

@@ -165,3 +166,137 @@ func TestMinpkGenkey1kParallel(t *testing.T) {
165166
}
166167
wg.Wait()
167168
}
169+
170+
// TestLowSCanonization verifies that ECDSA signatures are normalized to low-S form.
171+
// This is required for secp256k1 (tz2) compatibility with Tezos/libsecp256k1.
172+
// See issue #364: https://github.com/ecadlabs/signatory/issues/364
173+
func TestLowSCanonization(t *testing.T) {
174+
curves := []struct {
175+
name string
176+
curve elliptic.Curve
177+
}{
178+
{"secp256k1", secp256k1.S256()},
179+
{"P256", elliptic.P256()},
180+
}
181+
182+
for _, c := range curves {
183+
t.Run(c.name, func(t *testing.T) {
184+
order := c.curve.Params().N
185+
halfOrder := new(big.Int).Quo(order, big.NewInt(2))
186+
187+
// Create a high-S value (S > N/2)
188+
highS := new(big.Int).Add(halfOrder, big.NewInt(1000))
189+
r := big.NewInt(12345)
190+
191+
t.Run("NewECDSASignature_canonizes_highS", func(t *testing.T) {
192+
sig := NewECDSASignature(r, highS, c.curve)
193+
// S should now be <= N/2
194+
require.LessOrEqual(t, sig.S.Cmp(halfOrder), 0,
195+
"NewECDSASignature should produce low-S signature")
196+
// Verify S was actually changed
197+
require.NotEqual(t, 0, highS.Cmp(sig.S),
198+
"high-S should be different from canonized S")
199+
})
200+
201+
t.Run("NewECDSASignature_preserves_lowS", func(t *testing.T) {
202+
lowS := big.NewInt(1000) // definitely < N/2
203+
sig := NewECDSASignature(r, lowS, c.curve)
204+
require.Equal(t, 0, lowS.Cmp(sig.S),
205+
"low-S should remain unchanged")
206+
})
207+
208+
t.Run("canonizeSignature_math_correctness", func(t *testing.T) {
209+
// When S > N/2, canonical S should be N - S
210+
sig := &ECDSASignature{R: r, S: new(big.Int).Set(highS), Curve: c.curve}
211+
canonical := canonizeSignature(sig)
212+
213+
expectedS := new(big.Int).Sub(order, highS)
214+
require.Equal(t, 0, expectedS.Cmp(canonical.S),
215+
"canonical S should equal N - S for high-S values")
216+
})
217+
})
218+
}
219+
}
220+
221+
// TestNewSignatureFromBytesCanonization verifies that signatures parsed from
222+
// ASN.1 (as received from cloud KMS providers) are canonized to low-S form.
223+
func TestNewSignatureFromBytesCanonization(t *testing.T) {
224+
curves := []struct {
225+
name string
226+
curve elliptic.Curve
227+
}{
228+
{"secp256k1", secp256k1.S256()},
229+
{"P256", elliptic.P256()},
230+
}
231+
232+
for _, c := range curves {
233+
t.Run(c.name, func(t *testing.T) {
234+
// Generate a real key and sign to get valid signature bytes
235+
priv, err := ecdsa.GenerateKey(c.curve, rand.Reader)
236+
require.NoError(t, err)
237+
238+
message := []byte("test message for signature canonization")
239+
digest := DigestFunc(message)
240+
241+
// Sign multiple times - statistically ~50% should have high-S originally
242+
// All should be low-S after canonization
243+
halfOrder := new(big.Int).Quo(c.curve.Params().N, big.NewInt(2))
244+
245+
for i := 0; i < 20; i++ {
246+
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
247+
require.NoError(t, err)
248+
249+
// Create ASN.1 DER encoded signature (as cloud KMS would return)
250+
asn1Sig := encodeASN1Signature(r, s)
251+
252+
pub := (*ECDSAPublicKey)(&priv.PublicKey)
253+
sig, err := NewSignatureFromBytes(asn1Sig, pub)
254+
require.NoError(t, err)
255+
256+
ecdsaSig, ok := sig.(*ECDSASignature)
257+
require.True(t, ok)
258+
259+
// Verify S is in low-S form
260+
require.LessOrEqual(t, ecdsaSig.S.Cmp(halfOrder), 0,
261+
"NewSignatureFromBytes must produce low-S signature (iteration %d)", i)
262+
263+
// Verify signature still validates
264+
require.True(t, sig.Verify(pub, message),
265+
"canonized signature must still verify")
266+
}
267+
})
268+
}
269+
}
270+
271+
// encodeASN1Signature creates ASN.1 DER encoded signature from R, S values
272+
// (simulating what cloud KMS providers return)
273+
func encodeASN1Signature(r, s *big.Int) []byte {
274+
rBytes := r.Bytes()
275+
sBytes := s.Bytes()
276+
277+
// Add leading zero if high bit is set (ASN.1 integer is signed)
278+
if len(rBytes) > 0 && rBytes[0]&0x80 != 0 {
279+
rBytes = append([]byte{0}, rBytes...)
280+
}
281+
if len(sBytes) > 0 && sBytes[0]&0x80 != 0 {
282+
sBytes = append([]byte{0}, sBytes...)
283+
}
284+
285+
// Build ASN.1 SEQUENCE
286+
innerLen := 2 + len(rBytes) + 2 + len(sBytes)
287+
result := make([]byte, 0, 2+innerLen)
288+
289+
// SEQUENCE tag and length
290+
result = append(result, 0x30)
291+
result = append(result, byte(innerLen))
292+
293+
// INTEGER for R
294+
result = append(result, 0x02, byte(len(rBytes)))
295+
result = append(result, rBytes...)
296+
297+
// INTEGER for S
298+
result = append(result, 0x02, byte(len(sBytes)))
299+
result = append(result, sBytes...)
300+
301+
return result
302+
}

crypt/ecdsa.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,18 @@ func canonizeSignature(sig *ECDSASignature) *ECDSASignature {
200200
}
201201
}
202202

203+
// NewECDSASignature creates an ECDSA signature from R, S values and curve,
204+
// normalizing to low-S form for compatibility with Tezos/libsecp256k1.
205+
// Use this instead of directly constructing ECDSASignature when receiving
206+
// signatures from external sources (e.g., cloud KMS providers).
207+
func NewECDSASignature(r, s *big.Int, curve elliptic.Curve) *ECDSASignature {
208+
return canonizeSignature(&ECDSASignature{
209+
R: r,
210+
S: s,
211+
Curve: curve,
212+
})
213+
}
214+
203215
// See https://github.com/golang/go/blob/master/src/crypto/elliptic/elliptic.go
204216
func unmarshalCompressed(data []byte, curve elliptic.Curve) (x, y *big.Int, err error) {
205217
byteLen := (curve.Params().BitSize + 7) / 8

0 commit comments

Comments
 (0)