Skip to content
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
8 changes: 1 addition & 7 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ import (
"bytes"
"crypto/ecdsa"
"encoding/hex"
"fmt"
"io/ioutil"
"math/big"
"os"
"testing"
"time"

"github.com/ethereum/go-ethereum/common"
)
Expand All @@ -44,13 +42,9 @@ func TestKeccak256Hash(t *testing.T) {

func BenchmarkSha3(b *testing.B) {
a := []byte("hello world")
amount := 1000000
start := time.Now()
for i := 0; i < amount; i++ {
for i := 0; i < b.N; i++ {
Keccak256(a)
}

fmt.Println(amount, ":", time.Since(start))
}

func TestSign(t *testing.T) {
Expand Down
49 changes: 49 additions & 0 deletions crypto/secp256k1/ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,55 @@ static int secp256k1_ecdsa_recover_pubkey(
return secp256k1_ec_pubkey_serialize(ctx, pubkey_out, &outputlen, &pubkey, SECP256K1_EC_UNCOMPRESSED);
}

// secp256k1_ecdsa_verify_enc verifies an encoded compact signature.
//
// Returns: 1: signature is valid
// 0: signature is invalid
// Args: ctx: pointer to a context object (cannot be NULL)
// In: sigdata: pointer to a 64-byte signature (cannot be NULL)
// msgdata: pointer to a 32-byte message (cannot be NULL)
// pubkeydata: pointer to public key data (cannot be NULL)
// pubkeylen: length of pubkeydata
static int secp256k1_ecdsa_verify_enc(
const secp256k1_context* ctx,
const unsigned char *sigdata,
const unsigned char *msgdata,
const unsigned char *pubkeydata,
size_t pubkeylen
) {
secp256k1_ecdsa_signature sig;
secp256k1_pubkey pubkey;

if (!secp256k1_ecdsa_signature_parse_compact(ctx, &sig, sigdata)) {
return 0;
}
if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, pubkeydata, pubkeylen)) {
return 0;
}
return secp256k1_ecdsa_verify(ctx, &sig, msgdata, &pubkey);
}

// secp256k1_decompress_pubkey decompresses a public key.
//
// Returns: 1: public key is valid
// 0: public key is invalid
// Args: ctx: pointer to a context object (cannot be NULL)
// Out: pubkey_out: the serialized 65-byte public key (cannot be NULL)
// In: pubkeydata: pointer to 33 bytes of compressed public key data (cannot be NULL)
static int secp256k1_decompress_pubkey(
const secp256k1_context* ctx,
unsigned char *pubkey_out,
const unsigned char *pubkeydata
) {
secp256k1_pubkey pubkey;

if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, pubkeydata, 33)) {
return 0;
}
size_t outputlen = 65;
return secp256k1_ec_pubkey_serialize(ctx, pubkey_out, &outputlen, &pubkey, SECP256K1_EC_UNCOMPRESSED);
}

// secp256k1_pubkey_scalar_mul multiplies a point by a scalar in constant time.
//
// Returns: 1: multiplication was successful
Expand Down
29 changes: 29 additions & 0 deletions crypto/secp256k1/secp256.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import "C"

import (
"errors"
"math/big"
"unsafe"
)

Expand All @@ -55,6 +56,7 @@ var (
ErrInvalidSignatureLen = errors.New("invalid signature length")
ErrInvalidRecoveryID = errors.New("invalid signature recovery id")
ErrInvalidKey = errors.New("invalid private key")
ErrInvalidPubkey = errors.New("invalid public key")
ErrSignFailed = errors.New("signing failed")
ErrRecoverFailed = errors.New("recovery failed")
)
Expand Down Expand Up @@ -119,6 +121,33 @@ func RecoverPubkey(msg []byte, sig []byte) ([]byte, error) {
return pubkey, nil
}

// VerifySignature checks that the given pubkey created signature over message.
// The signature should be in [R || S] format.
func VerifySignature(pubkey, msg, signature []byte) bool {
if len(msg) != 32 || len(signature) != 64 || len(pubkey) == 0 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't len(pubkey) be one of two values ? So either compressed or uncompressed at 65 bytes, or compressed at 33 bytes. If so, I'd prefer to check against one of those. Better to be strict whenever possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to leave it to libsecp256k1 to check for the length. If you disagree strongly we can add the check here too. The check for zero is needed to prevent a crash for the &pubkey[0] construction.

Copy link
Copy Markdown
Contributor Author

@fjl fjl Dec 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The length check is done here.

return false
}
sigdata := (*C.uchar)(unsafe.Pointer(&signature[0]))
msgdata := (*C.uchar)(unsafe.Pointer(&msg[0]))
keydata := (*C.uchar)(unsafe.Pointer(&pubkey[0]))
return C.secp256k1_ecdsa_verify_enc(context, sigdata, msgdata, keydata, C.size_t(len(pubkey))) != 0
}

// DecompressPubkey parses a public key in the 33-byte compressed format.
// It returns non-nil coordinates if the public key is valid.
func DecompressPubkey(pubkey []byte) (X, Y *big.Int) {
if len(pubkey) != 33 {
return nil, nil
}
buf := make([]byte, 65)
bufdata := (*C.uchar)(unsafe.Pointer(&buf[0]))
pubkeydata := (*C.uchar)(unsafe.Pointer(&pubkey[0]))
if C.secp256k1_decompress_pubkey(context, bufdata, pubkeydata) == 0 {
return nil, nil
}
return new(big.Int).SetBytes(buf[1:33]), new(big.Int).SetBytes(buf[33:])
}

func checkSignature(sig []byte) error {
if len(sig) != 65 {
return ErrInvalidSignatureLen
Expand Down
18 changes: 18 additions & 0 deletions crypto/signature_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ import (
"github.com/ethereum/go-ethereum/crypto/secp256k1"
)

// Ecrecover returns the uncompressed public key that created the given signature.
func Ecrecover(hash, sig []byte) ([]byte, error) {
return secp256k1.RecoverPubkey(hash, sig)
}

// SigToPub returns the public key that created the given signature.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"SigToPub returns the public key" -> "SigToPub returns the uncompressed public key"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compressed/uncompressed doesn't matter for SigToPub because the return value isn't encoded.

func SigToPub(hash, sig []byte) (*ecdsa.PublicKey, error) {
s, err := Ecrecover(hash, sig)
if err != nil {
Expand Down Expand Up @@ -58,6 +60,22 @@ func Sign(hash []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) {
return secp256k1.Sign(hash, seckey)
}

// VerifySignature checks that the given public key created signature over hash.
// The public key should be in compressed (33 bytes) or uncompressed (65 bytes) format.
// The signature should have the 64 byte [R || S] format.
func VerifySignature(pubkey, hash, signature []byte) bool {
return secp256k1.VerifySignature(pubkey, hash, signature)
}

// DecompressPubkey parses a public key in the 33-byte compressed format.
func DecompressPubkey(pubkey []byte) (*ecdsa.PublicKey, error) {
x, y := secp256k1.DecompressPubkey(pubkey)
if x == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design question: Why does the underlying method return nil, and we create an error here, instead of returning error from the lower layer?

Copy link
Copy Markdown
Contributor Author

@fjl fjl Dec 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided this way to simplify the interface. There is only one possible error anyway: "invalid". elliptic.Unmarshal uses the same convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right though, maybe this should be changed.

return nil, fmt.Errorf("invalid public key")
}
return &ecdsa.PublicKey{X: x, Y: y, Curve: S256()}, nil
}

// S256 returns an instance of the secp256k1 curve.
func S256() elliptic.Curve {
return secp256k1.S256()
Expand Down
31 changes: 31 additions & 0 deletions crypto/signature_nocgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ package crypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
"math/big"

"github.com/btcsuite/btcd/btcec"
)

// Ecrecover returns the uncompressed public key that created the given signature.
func Ecrecover(hash, sig []byte) ([]byte, error) {
pub, err := SigToPub(hash, sig)
if err != nil {
Expand All @@ -35,6 +38,7 @@ func Ecrecover(hash, sig []byte) ([]byte, error) {
return bytes, err
}

// SigToPub returns the public key that created the given signature.
func SigToPub(hash, sig []byte) (*ecdsa.PublicKey, error) {
// Convert to btcec input format with 'recovery id' v at the beginning.
btcsig := make([]byte, 65)
Expand Down Expand Up @@ -71,6 +75,33 @@ func Sign(hash []byte, prv *ecdsa.PrivateKey) ([]byte, error) {
return sig, nil
}

// VerifySignature checks that the given public key created signature over hash.
// The public key should be in compressed (33 bytes) or uncompressed (65 bytes) format.
// The signature should have the 64 byte [R || S] format.
func VerifySignature(pubkey, hash, signature []byte) bool {
if len(signature) != 64 {
return false
}
sig := &btcec.Signature{R: new(big.Int).SetBytes(signature[:32]), S: new(big.Int).SetBytes(signature[32:])}
key, err := btcec.ParsePubKey(pubkey, btcec.S256())
if err != nil {
return false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another design question: we're throwing away error-info here, is that really needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btcec is the fallback for platforms that can't link C code. It provides better errors because it's Go code. There is no good way to get those errors from the C version. IMHO it doesn't matter much: the signature is invalid if any of the inputs are.

Copy link
Copy Markdown
Contributor Author

@fjl fjl Dec 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe more context: I added this because I want a function that does decompression and verification in one call without any pubkey/signature decoding. We get the compressed pubkey in ENR and need to verify that it created the signature. The fastest and simplest way is to just pass these values into libsecp256k1.

We could go ahead and make it so secp256k1_ecdsa_verify_enc returns different ints for 'invalid signature encoding', 'invalid pubkey encoding' and 'signature mismatch', but it'll just make the code more complicated. Callers won't care: all they want to know is whether the signature checks out.

}
return sig.Verify(hash, key)
}

// DecompressPubkey parses a public key in the 33-byte compressed format.
func DecompressPubkey(pubkey []byte) (*ecdsa.PublicKey, error) {
if len(pubkey) != 33 {
return nil, errors.New("invalid compressed public key length")
}
key, err := btcec.ParsePubKey(pubkey, btcec.S256())
if err != nil {
return nil, err
}
return key.ToECDSA(), nil
}

// S256 returns an instance of the secp256k1 curve.
func S256() elliptic.Curve {
return btcec.S256()
Expand Down
92 changes: 84 additions & 8 deletions crypto/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,95 @@ package crypto

import (
"bytes"
"encoding/hex"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)

var (
testmsg = hexutil.MustDecode("0xce0677bb30baa8cf067c88db9811f4333d131bf8bcf12fe7065d211dce971008")
testsig = hexutil.MustDecode("0x90f27b8b488db00b00606796d2987f6a5f59ae62ea05effe84fef5b8b0e549984a691139ad57a3f0b906637673aa2f63d1f55cb1a69199d4009eea23ceaddc9301")
testpubkey = hexutil.MustDecode("0x04e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a0a2b2667f7e725ceea70c673093bf67663e0312623c8e091b13cf2c0f11ef652")
testpubkeyc = hexutil.MustDecode("0x02e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a")
)

func TestRecoverSanity(t *testing.T) {
msg, _ := hex.DecodeString("ce0677bb30baa8cf067c88db9811f4333d131bf8bcf12fe7065d211dce971008")
sig, _ := hex.DecodeString("90f27b8b488db00b00606796d2987f6a5f59ae62ea05effe84fef5b8b0e549984a691139ad57a3f0b906637673aa2f63d1f55cb1a69199d4009eea23ceaddc9301")
pubkey1, _ := hex.DecodeString("04e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a0a2b2667f7e725ceea70c673093bf67663e0312623c8e091b13cf2c0f11ef652")
pubkey2, err := Ecrecover(msg, sig)
func TestEcrecover(t *testing.T) {
pubkey, err := Ecrecover(testmsg, testsig)
if err != nil {
t.Fatalf("recover error: %s", err)
}
if !bytes.Equal(pubkey1, pubkey2) {
t.Errorf("pubkey mismatch: want: %x have: %x", pubkey1, pubkey2)
if !bytes.Equal(pubkey, testpubkey) {
t.Errorf("pubkey mismatch: want: %x have: %x", testpubkey, pubkey)
}
}

func TestVerifySignature(t *testing.T) {
sig := testsig[:len(testsig)-1] // remove recovery id
if !VerifySignature(testpubkey, testmsg, sig) {
t.Errorf("can't verify signature with uncompressed key")
}
if !VerifySignature(testpubkeyc, testmsg, sig) {
t.Errorf("can't verify signature with compressed key")
}

if VerifySignature(nil, testmsg, sig) {
t.Errorf("signature valid with no key")
}
if VerifySignature(testpubkey, nil, sig) {
t.Errorf("signature valid with no message")
}
if VerifySignature(testpubkey, testmsg, nil) {
t.Errorf("nil signature valid")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More potential testcases:

  • VerifySignature(testpubkey, testmsg, sig+extra_bytes)
  • VerifySignature(testpubkey, testmsg, testsig[:len(testsig)-2])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

if VerifySignature(testpubkey, testmsg, append(common.CopyBytes(sig), 1, 2, 3)) {
t.Errorf("signature valid with extra bytes at the end")
}
if VerifySignature(testpubkey, testmsg, sig[:len(sig)-2]) {
t.Errorf("signature valid even though it's incomplete")
}
}

func TestDecompressPubkey(t *testing.T) {
key, err := DecompressPubkey(testpubkeyc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More potential testcases:

  • DecompressPubkey with nil, slice with < 32 bytes, slice with > 32 bytes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

if err != nil {
t.Fatal(err)
}
if uncompressed := FromECDSAPub(key); !bytes.Equal(uncompressed, testpubkey) {
t.Errorf("wrong public key result: got %x, want %x", uncompressed, testpubkey)
}
if _, err := DecompressPubkey(nil); err == nil {
t.Errorf("no error for nil pubkey")
}
if _, err := DecompressPubkey(testpubkeyc[:5]); err == nil {
t.Errorf("no error for incomplete pubkey")
}
if _, err := DecompressPubkey(append(common.CopyBytes(testpubkeyc), 1, 2, 3)); err == nil {
t.Errorf("no error for pubkey with extra bytes at the end")
}
}

func BenchmarkEcrecoverSignature(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := Ecrecover(testmsg, testsig); err != nil {
b.Fatal("ecrecover error", err)
}
}
}

func BenchmarkVerifySignature(b *testing.B) {
sig := testsig[:len(testsig)-1] // remove recovery id
for i := 0; i < b.N; i++ {
if !VerifySignature(testpubkey, testmsg, sig) {
b.Fatal("verify error")
}
}
}

func BenchmarkDecompressPubkey(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := DecompressPubkey(testpubkeyc); err != nil {
b.Fatal(err)
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading