From d1dc77fd753ed676389b373e52d6301de0c31e9e Mon Sep 17 00:00:00 2001 From: yaiba <4yaiba@gmail.com> Date: Wed, 16 Aug 2023 14:10:07 -0500 Subject: [PATCH] extract msg manipulate and hash algo to Signer and Signature.Verify --- pkg/crypto/keys.go | 4 +- pkg/crypto/secp256k1.go | 29 +++++--- pkg/crypto/secp256k1_test.go | 77 ++++++++++++++++++-- pkg/crypto/signature.go | 41 +++++++++-- pkg/crypto/signature_test.go | 133 +++++++++++++++++++++++++++++++++++ pkg/crypto/signer.go | 29 ++++++++ pkg/crypto/signer_test.go | 30 ++++++++ 7 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 pkg/crypto/signature_test.go create mode 100644 pkg/crypto/signer.go create mode 100644 pkg/crypto/signer_test.go diff --git a/pkg/crypto/keys.go b/pkg/crypto/keys.go index 4ebc8492d..1ad44b811 100644 --- a/pkg/crypto/keys.go +++ b/pkg/crypto/keys.go @@ -5,7 +5,7 @@ type KeyType string type PrivateKey interface { Bytes() []byte Type() KeyType - Sign(msg []byte) (*Signature, error) + Sign(msg []byte) ([]byte, error) PubKey() PublicKey Hex() string } @@ -13,7 +13,7 @@ type PrivateKey interface { type PublicKey interface { Bytes() []byte Type() KeyType - Verify(sig *Signature, msg []byte) error + Verify(sig []byte, hash []byte) error Address() Address } diff --git a/pkg/crypto/secp256k1.go b/pkg/crypto/secp256k1.go index 2f1e564af..5ccaec795 100644 --- a/pkg/crypto/secp256k1.go +++ b/pkg/crypto/secp256k1.go @@ -3,7 +3,6 @@ package crypto import ( "crypto/ecdsa" "encoding/hex" - "fmt" ethAccount "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" @@ -29,19 +28,25 @@ func (s *Secp256k1PrivateKey) PubKey() PublicKey { } } -// Sign signs the given message using EIP-191 personal sign. -func (s *Secp256k1PrivateKey) Sign(msg []byte) (*Signature, error) { +// SignMsg signs the given message(not hashed) according to EIP-191 personal_sign. +// This is default signature type for sec256k1. +func (s *Secp256k1PrivateKey) SignMsg(msg []byte) (*Signature, error) { hash := ethAccount.TextHash(msg) - sig, err := ethCrypto.Sign(hash, s.privateKey) + sig, err := s.Sign(hash) if err != nil { return nil, err } return &Signature{ Signature: sig, - Type: SIGNATURE_TYPE_SECP256K1, + Type: SIGNATURE_TYPE_SECP256K1_PERSONAL, }, nil } +// Sign signs the given hash utilizing go-ethereum's Sign function. +func (s *Secp256k1PrivateKey) Sign(hash []byte) ([]byte, error) { + return ethCrypto.Sign(hash, s.privateKey) +} + func (s *Secp256k1PrivateKey) Type() KeyType { return Secp256k1 } @@ -66,12 +71,14 @@ func (s *Secp256k1PublicKey) Type() KeyType { // Verify verifies the given signature against the given message according to EIP-191 // personal sign. -func (s *Secp256k1PublicKey) Verify(sig *Signature, msg []byte) error { - hash := ethAccount.TextHash(msg) - // Remove recovery ID - signature := sig.Signature[:len(sig.Signature)-1] - if !ethCrypto.VerifySignature(s.Bytes(), hash, signature) { - return fmt.Errorf("invalid signature") +func (s *Secp256k1PublicKey) Verify(sig []byte, hash []byte) error { + if len(sig) != 64 { + return errInvalidSignature + } + + // signature should have the 64 byte [R || S] format + if !ethCrypto.VerifySignature(s.Bytes(), hash, sig) { + return errVerifySignatureFailed } return nil diff --git a/pkg/crypto/secp256k1_test.go b/pkg/crypto/secp256k1_test.go index bf99ca6e2..df713566b 100644 --- a/pkg/crypto/secp256k1_test.go +++ b/pkg/crypto/secp256k1_test.go @@ -2,6 +2,7 @@ package crypto import ( "encoding/hex" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" ) @@ -12,12 +13,80 @@ func TestSecp256k1PrivateKey_Sign(t *testing.T) { require.NoError(t, err, "error parse private key") msg := []byte("foo") + hash := Sha256(msg) - sig, err := pk.Sign(msg) + sig, err := pk.Sign(hash) require.NoError(t, err, "error sign") - err = pk.PubKey().Verify(sig, msg) - require.NoError(t, err, "error verify") + expectSig := "19a4aced02d5b9142b4f622b06442b1904445e16bd25409e6b0ff357ccc021d001d0e7824654b695b4b6e0991cb7507f487b82be4b2ed713d1e3e2cbc3d2518d01" + require.Equal(t, SIGNATURE_SECP256K1_PERSONAL_LENGTH, len(sig), "invalid signature length") + require.EqualValues(t, hex.EncodeToString(sig), expectSig, "invalid signature") +} + +func TestSecp256k1PrivateKey_SignMsg(t *testing.T) { + key := "f1aa5a7966c3863ccde3047f6a1e266cdc0c76b399e256b8fede92b1c69e4f4e" + pk, err := loadSecp256k1PrivateKeyFromHex(key) + require.NoError(t, err, "error parse private key") + + msg := []byte("foo") + + sig, err := pk.SignMsg(msg) + require.NoError(t, err, "error sign msg") + + expectSignature := "cb3fed7f6ff36e59054c04a831b215e514052753ee353e6fe31d4b4ef736acd6155127db555d3006ba14fcb4c79bbad56c8e63b81a9896319bb053a9e253475800" + expectSignatureBytes, _ := hex.DecodeString(expectSignature) + + expectSig := &Signature{ + Signature: expectSignatureBytes, + Type: SIGNATURE_TYPE_SECP256K1_PERSONAL, + } + + assert.EqualValues(t, expectSig, sig, "unexpect signature") +} + +func TestSecp256k1PublicKey_Verify(t *testing.T) { + key := "04812bef44f6e7b2a19c0b01c2dca5e54ba1935a1890ffdcb93abd0c534b209c21e4f6176823fef493f7b5afaa456f31d5293363d8f801c540ebcc061812890cba" + keyBytes, err := hex.DecodeString(key) + require.NoError(t, err, "error decode key") + + pubKey, err := loadSecp256k1PublicKeyFromByte(keyBytes) + require.NoError(t, err, "error parse public key") + + msg := []byte("foo") + hash := Sha256(msg) + + sig := "19a4aced02d5b9142b4f622b06442b1904445e16bd25409e6b0ff357ccc021d001d0e7824654b695b4b6e0991cb7507f487b82be4b2ed713d1e3e2cbc3d2518d01" + sigBytes, _ := hex.DecodeString(sig) + require.Equal(t, SIGNATURE_SECP256K1_PERSONAL_LENGTH, len(sigBytes), "invalid signature length") + + tests := []struct { + name string + sigBytes []byte + wantErr error + }{ + { + name: "verify success", + sigBytes: sigBytes[:len(sigBytes)-1], + wantErr: nil, + }, + { + name: "invalid signature length", + sigBytes: sigBytes, + wantErr: errInvalidSignature, + }, + { + name: "invalid signature", + sigBytes: sigBytes[1:], + wantErr: errVerifySignatureFailed, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := pubKey.Verify(tt.sigBytes, hash) + assert.ErrorIs(t, err, tt.wantErr) + }) + } } func TestSecp256k1PublicKey_Address(t *testing.T) { @@ -26,7 +95,7 @@ func TestSecp256k1PublicKey_Address(t *testing.T) { require.NoError(t, err, "error decode key") pubKey, err := loadSecp256k1PublicKeyFromByte(keyBytes) - require.NoError(t, err, "error parse private key") + require.NoError(t, err, "error parse public key") eq := pubKey.Address().String() == "0xc89D42189f0450C2b2c3c61f58Ec5d628176A1E7" require.True(t, eq, "mismatch address") diff --git a/pkg/crypto/signature.go b/pkg/crypto/signature.go index 3983bdd95..911dfa7ba 100644 --- a/pkg/crypto/signature.go +++ b/pkg/crypto/signature.go @@ -2,6 +2,7 @@ package crypto import ( "fmt" + ethAccount "github.com/ethereum/go-ethereum/accounts" ) type SignatureType int32 @@ -9,15 +10,27 @@ type SignatureType int32 const ( SIGNATURE_TYPE_INVALID SignatureType = iota SIGNATURE_TYPE_EMPTY - SIGNATURE_TYPE_SECP256K1 + SIGNATURE_TYPE_SECP256K1_COMETBFT + SIGNATURE_TYPE_SECP256K1_PERSONAL // ethereum EIP-191 personal_sign SIGNATURE_TYPE_ED25519 END_SIGNATURE_TYPE ) +const ( + SIGNATURE_SECP256K1_COMETBFT_LENGTH = 64 + SIGNATURE_SECP256K1_PERSONAL_LENGTH = 65 +) + +var ( + errInvalidSignature = fmt.Errorf("invalid signature") + errVerifySignatureFailed = fmt.Errorf("verify signature failed") + errNotSupportedSignatureType = fmt.Errorf("not supported signature type") +) + // IsValid returns an error if the signature type is invalid. func (s *SignatureType) IsValid() error { if *s < SIGNATURE_TYPE_INVALID || *s >= END_SIGNATURE_TYPE { - return fmt.Errorf("invalid signature type '%d'", *s) + return fmt.Errorf("%w: %d", errNotSupportedSignatureType, *s) } return nil } @@ -34,6 +47,26 @@ type Signature struct { } // Verify verifies the signature against the given public key and data. -func (s *Signature) Verify(publicKey PublicKey, data []byte) error { - return publicKey.Verify(s, data) +func (s *Signature) Verify(publicKey PublicKey, msg []byte) error { + switch s.Type { + case SIGNATURE_TYPE_SECP256K1_PERSONAL: + if len(s.Signature) != SIGNATURE_SECP256K1_PERSONAL_LENGTH { + return errInvalidSignature + } + hash := ethAccount.TextHash(msg) + // Remove recovery ID + sig := s.Signature[:len(s.Signature)-1] + return publicKey.Verify(sig, hash) + case SIGNATURE_TYPE_SECP256K1_COMETBFT: + if len(s.Signature) != SIGNATURE_SECP256K1_COMETBFT_LENGTH { + return errInvalidSignature + } + // cometbft using sha256 and 64 bytes signature(no recovery ID 'v') + hash := Sha256(msg) + return publicKey.Verify(s.Signature, hash) + case SIGNATURE_TYPE_ED25519: + panic("not implemented") + default: + return fmt.Errorf("%w: %d", errNotSupportedSignatureType, s.Type) + } } diff --git a/pkg/crypto/signature_test.go b/pkg/crypto/signature_test.go new file mode 100644 index 000000000..977fd74a0 --- /dev/null +++ b/pkg/crypto/signature_test.go @@ -0,0 +1,133 @@ +package crypto + +import ( + "encoding/hex" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSignature_Verify(t *testing.T) { + msg := []byte("foo") + anotherMsg := []byte("bar") + + secp256k1PubKeyHex := "04812bef44f6e7b2a19c0b01c2dca5e54ba1935a1890ffdcb93abd0c534b209c21e4f6176823fef493f7b5afaa456f31d5293363d8f801c540ebcc061812890cba" + secp256k1PubKeyBytes, _ := hex.DecodeString(secp256k1PubKeyHex) + secp256k1PublicKey, _ := loadSecp256k1PublicKeyFromByte(secp256k1PubKeyBytes) + + personalSignSig := "cb3fed7f6ff36e59054c04a831b215e514052753ee353e6fe31d4b4ef736acd6155127db555d3006ba14fcb4c79bbad56c8e63b81a9896319bb053a9e253475800" + personalSignSigBytes, _ := hex.DecodeString(personalSignSig) + + cometbftSecp256k1Sig := "19a4aced02d5b9142b4f622b06442b1904445e16bd25409e6b0ff357ccc021d001d0e7824654b695b4b6e0991cb7507f487b82be4b2ed713d1e3e2cbc3d2518d" + cometbftSecp256k1SigBytes, _ := hex.DecodeString(cometbftSecp256k1Sig) + + type fields struct { + Signature []byte + Type SignatureType + } + type args struct { + publicKey PublicKey + msg []byte + } + + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "test secp256k1 personal_sign", + fields: fields{ + Signature: personalSignSigBytes, + Type: SIGNATURE_TYPE_SECP256K1_PERSONAL, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: msg, + }, + wantErr: nil, + }, + { + name: "test secp256k1 personal_sign invalid signature", + fields: fields{ + Signature: personalSignSigBytes[1:], + Type: SIGNATURE_TYPE_SECP256K1_PERSONAL, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: msg, + }, + wantErr: errInvalidSignature, + }, + { + name: "test secp256k1 personal_sign wrong signature", + fields: fields{ + Signature: personalSignSigBytes, + Type: SIGNATURE_TYPE_SECP256K1_PERSONAL, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: anotherMsg, + }, + wantErr: errVerifySignatureFailed, + }, + { + name: "test secp256k1 cometbft", + fields: fields{ + Signature: cometbftSecp256k1SigBytes, + Type: SIGNATURE_TYPE_SECP256K1_COMETBFT, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: msg, + }, + wantErr: nil, + }, + { + name: "test secp256k1 cometbft invalid signature", + fields: fields{ + Signature: cometbftSecp256k1SigBytes[1:], + Type: SIGNATURE_TYPE_SECP256K1_COMETBFT, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: msg, + }, + wantErr: errInvalidSignature, + }, + { + name: "test secp256k1 cometbft wrong signature", + fields: fields{ + Signature: cometbftSecp256k1SigBytes, + Type: SIGNATURE_TYPE_SECP256K1_COMETBFT, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: anotherMsg, + }, + wantErr: errVerifySignatureFailed, + }, + { + name: "unsupported signature type", + fields: fields{ + Signature: nil, + Type: SIGNATURE_TYPE_INVALID, + }, + args: args{ + publicKey: secp256k1PublicKey, + msg: msg, + }, + wantErr: errNotSupportedSignatureType, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signature{ + Signature: tt.fields.Signature, + Type: tt.fields.Type, + } + err := s.Verify(tt.args.publicKey, tt.args.msg) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/crypto/signer.go b/pkg/crypto/signer.go new file mode 100644 index 000000000..bc168244c --- /dev/null +++ b/pkg/crypto/signer.go @@ -0,0 +1,29 @@ +package crypto + +type Signer interface { + SignMsg(msg []byte) (*Signature, error) +} + +type Eip712Signer struct { + key PrivateKey +} + +func (e *Eip712Signer) SignMsg(msg []byte) (*Signature, error) { + panic("not implemented") +} + +type ComebftSecp256k1Signer struct { + key PrivateKey +} + +func (c *ComebftSecp256k1Signer) SignMsg(msg []byte) (*Signature, error) { + hash := Sha256(msg) + sig, err := c.key.Sign(hash) + if err != nil { + return nil, err + } + return &Signature{ + Signature: sig[:len(sig)-1], + Type: SIGNATURE_TYPE_SECP256K1_COMETBFT, + }, nil +} diff --git a/pkg/crypto/signer_test.go b/pkg/crypto/signer_test.go new file mode 100644 index 000000000..4093dddeb --- /dev/null +++ b/pkg/crypto/signer_test.go @@ -0,0 +1,30 @@ +package crypto + +import ( + "encoding/hex" + "github.com/cometbft/cometbft/crypto/secp256k1" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestComebftSecp256k1Signer_SignMsg(t *testing.T) { + msg := []byte("foo") + + pvKeyHex := "f1aa5a7966c3863ccde3047f6a1e266cdc0c76b399e256b8fede92b1c69e4f4e" + sigHex := "19a4aced02d5b9142b4f622b06442b1904445e16bd25409e6b0ff357ccc021d001d0e7824654b695b4b6e0991cb7507f487b82be4b2ed713d1e3e2cbc3d2518d" + + pvKeyBytes, _ := hex.DecodeString(pvKeyHex) + cometBftSecp256k1Key := secp256k1.PrivKey(pvKeyBytes) + cometBfgSecp256k1Sig, err := cometBftSecp256k1Key.Sign(msg) + assert.NoError(t, err, "error signing message") + assert.Equal(t, sigHex, hex.EncodeToString(cometBfgSecp256k1Sig), "signature mismatch") + + // use the cometbft signer to sign the message + kwilCometBftKey, _ := loadSecp256k1PrivateKeyFromHex(pvKeyHex) + cometBfgSigner := &ComebftSecp256k1Signer{ + key: kwilCometBftKey, + } + kwilCometBftKeySig, err := cometBfgSigner.SignMsg(msg) + assert.NoError(t, err, "error signing message") + assert.Equal(t, sigHex, hex.EncodeToString(kwilCometBftKeySig.Signature), "signature mismatch") +}