Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Low-s normalization for ecdsa secp256r1 signing (backport #9738) #9793

Merged
merged 1 commit into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions crypto/keys/internal/ecdsa/privkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,56 @@ import (
"math/big"
)

// GenPrivKey generates a new secp256r1 private key. It uses operating system randomness.
// p256Order returns the curve order for the secp256r1 curve
// NOTE: this is specific to the secp256r1/P256 curve,
// and not taken from the domain params for the key itself
// (which would be a more generic approach for all EC)
// In *here* we don't need to do it as a method on the key
// since this code is only called for secp256r1
// if called on a key:
// func (sk PrivKey) pCurveOrder() *.big.Int {
// return sk.Curve.Params().N
// }
var p256Order = elliptic.P256().Params().N

// p256HalfOrder returns half the curve order
// a bit shift of 1 to the right (Rsh) is equivalent
// to division by 2, only faster.
var p256HalfOrder = new(big.Int).Rsh(p256Order, 1)

// IsSNormalized returns true for the integer sigS if sigS falls in
// lower half of the curve order
func IsSNormalized(sigS *big.Int) bool {
return sigS.Cmp(p256HalfOrder) != 1
}

// NormalizeS will invert the s value if not already in the lower half
// of curve order value
func NormalizeS(sigS *big.Int) *big.Int {

if IsSNormalized(sigS) {
return sigS
}

return new(big.Int).Sub(p256Order, sigS)
}

// signatureRaw will serialize signature to R || S.
// R, S are padded to 32 bytes respectively.
// code roughly copied from secp256k1_nocgo.go
func signatureRaw(r *big.Int, s *big.Int) []byte {

rBytes := r.Bytes()
sBytes := s.Bytes()
sigBytes := make([]byte, 64)
// 0 pad the byte arrays from the left if they aren't big enough.
copy(sigBytes[32-len(rBytes):32], rBytes)
copy(sigBytes[64-len(sBytes):64], sBytes)
return sigBytes
}

// GenPrivKey generates a new secp256r1 private key. It uses operating
// system randomness.
func GenPrivKey(curve elliptic.Curve) (PrivKey, error) {
key, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
Expand Down Expand Up @@ -38,10 +87,25 @@ func (sk *PrivKey) Bytes() []byte {
return bz
}

// Sign hashes and signs the message usign ECDSA. Implements SDK PrivKey interface.
// Sign hashes and signs the message using ECDSA. Implements SDK
// PrivKey interface.
// NOTE: this now calls the ecdsa Sign function
// (not method!) directly as the s value of the signature is needed to
// low-s normalize the signature value
// See issue: https://github.com/cosmos/cosmos-sdk/issues/9723
// It then raw encodes the signature as two fixed width 32-byte values
// concatenated, reusing the code copied from secp256k1_nocgo.go
func (sk *PrivKey) Sign(msg []byte) ([]byte, error) {

digest := sha256.Sum256(msg)
return sk.PrivateKey.Sign(rand.Reader, digest[:], nil)
r, s, err := ecdsa.Sign(rand.Reader, &sk.PrivateKey, digest[:])

if err != nil {
return nil, err
}

normS := NormalizeS(s)
return signatureRaw(r, normS), nil
}

// String returns a string representation of the public key based on the curveName.
Expand Down
38 changes: 36 additions & 2 deletions crypto/keys/internal/ecdsa/privkey_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package ecdsa

import (
"testing"

"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"github.com/tendermint/tendermint/crypto"
"math/big"
"testing"

"github.com/stretchr/testify/suite"
)
Expand Down Expand Up @@ -60,6 +63,37 @@ func (suite *SKSuite) TestSign() {
require.False(suite.pk.VerifySignature(msg, sigCpy))
}

// mutate the signature by scalar neg'ing the s value
// to give a high-s signature, valid ECDSA but should
// be invalid with Cosmos signatures.
// code mostly copied from privkey/pubkey.go

// extract the r, s values from sig
r := new(big.Int).SetBytes(sig[:32])
low_s := new(big.Int).SetBytes(sig[32:64])

// test that NormalizeS simply returns an already
// normalized s
require.Equal(NormalizeS(low_s), low_s)

// flip the s value into high order of curve P256
// leave r untouched!
high_s := new(big.Int).Mod(new(big.Int).Neg(low_s), elliptic.P256().Params().N)

require.False(suite.pk.VerifySignature(msg, signatureRaw(r,high_s)))

// Valid signature using low_s, but too long
sigCpy = make([]byte, len(sig)+2)
copy(sigCpy, sig)
sigCpy[65] = byte('A')

require.False(suite.pk.VerifySignature(msg, sigCpy))

// check whether msg can be verified with same key, and high_s
// value using "regular" ecdsa signature
hash := sha256.Sum256([]byte(msg))
require.True(ecdsa.Verify(&suite.pk.PublicKey, hash[:], r, high_s))

// Mutate the message
msg[1] ^= byte(2)
require.False(suite.pk.VerifySignature(msg, sig))
Expand Down
29 changes: 26 additions & 3 deletions crypto/keys/internal/ecdsa/pubkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/asn1"
"fmt"
"math/big"

Expand All @@ -14,6 +13,16 @@ import (
"github.com/cosmos/cosmos-sdk/types/errors"
)

// signatureFromBytes function roughly copied from secp256k1_nocgo.go
// Read Signature struct from R || S. Caller needs to ensure that
// len(sigStr) == 64.
func signatureFromBytes(sigStr []byte) *signature {
return &signature{
R: new(big.Int).SetBytes(sigStr[:32]),
S: new(big.Int).SetBytes(sigStr[32:64]),
}
}

// signature holds the r and s values of an ECDSA signature.
type signature struct {
R, S *big.Int
Expand Down Expand Up @@ -46,9 +55,23 @@ func (pk *PubKey) Bytes() []byte {
}

// VerifySignature checks if sig is a valid ECDSA signature for msg.
// This includes checking for low-s normalized signatures
// where the s integer component of the signature is in the
// lower half of the curve order
// 7/21/21 - expects raw encoded signature (fixed-width 64-bytes, R || S)
func (pk *PubKey) VerifySignature(msg []byte, sig []byte) bool {
s := new(signature)
if _, err := asn1.Unmarshal(sig, s); err != nil || s == nil {

// check length for raw signature
// which is two 32-byte padded big.Ints
// concatenated
// NOT DER!

if len(sig) != 64 {
return false
}

s := signatureFromBytes(sig)
if !IsSNormalized(s.S) {
return false
}

Expand Down