Skip to content

Commit

Permalink
Merge pull request #61 from keep-network/signer
Browse files Browse the repository at this point in the history
EthereumSigner and local Signer

EthereumSigner provides functions to sign a message and verify a
signature using Ethereum-specific signature format. It also provides
functions for the conversion of a public key to address.

The code has been extracted from `keep-core` with slight modifications
that will allow it to be imported from `keep-core` and `keep-ecdsa`.

The code is being extracted to allow us decouple ethereum-specific
implementation functions we use in generic code of `keep-ecdsa` client,
especially in `node.go`.

Also extracted `localSigning` from `keep-core` as a `local.Signer`.
Having `local` `Signer` implementation in `keep-common` will let us not
duplicate the code between local chain implementations in `keep-core`
and `keep-ecdsa`.
  • Loading branch information
nkuba committed Dec 29, 2020
2 parents e39a681 + 3de8696 commit c7eca9a
Show file tree
Hide file tree
Showing 4 changed files with 587 additions and 0 deletions.
144 changes: 144 additions & 0 deletions pkg/chain/ethereum/ethutil/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package ethutil

import (
"crypto/ecdsa"
"crypto/elliptic"
"fmt"

"github.com/ethereum/go-ethereum/crypto"
)

// SignatureSize is a byte size of a signature calculated by Ethereum with
// recovery-id, V, included. The signature consists of three values (R,S,V)
// in the following order:
// R = [0:31]
// S = [32:63]
// V = [64]
const SignatureSize = 65

// EthereumSigner provides functions to sign a message and verify a signature
// using the Ethereum-specific signature format. It also provides functions for
// conversion of a public key to an address.
type EthereumSigner struct {
privateKey *ecdsa.PrivateKey
}

// NewSigner creates a new EthereumSigner instance for the provided ECDSA
// private key.
func NewSigner(privateKey *ecdsa.PrivateKey) *EthereumSigner {
return &EthereumSigner{privateKey}
}

// PublicKey returns byte representation of a public key for the private key
// signer was created with.
func (es *EthereumSigner) PublicKey() []byte {
publicKey := es.privateKey.PublicKey
return elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y)
}

// Sign signs the provided message using Ethereum-specific format.
func (es *EthereumSigner) Sign(message []byte) ([]byte, error) {
signature, err := crypto.Sign(ethereumPrefixedHash(message), es.privateKey)
if err != nil {
return nil, err
}

if len(signature) == SignatureSize {
// go-ethereum/crypto produces signature with v={0, 1} and we need to add
// 27 to v-part (signature[64]) to conform with the on-chain signature
// validation code that accepts v={27, 28} as specified in the
// Appendix F of the Ethereum Yellow Paper
// https://ethereum.github.io/yellowpaper/paper.pdf
signature[len(signature)-1] = signature[len(signature)-1] + 27
}

return signature, nil
}

// Verify verifies the provided message against a signature using the key
// EthereumSigner was created with. The signature has to be provided in
// Ethereum-specific format.
func (es *EthereumSigner) Verify(message []byte, signature []byte) (bool, error) {
return verifySignature(message, signature, &es.privateKey.PublicKey)
}

// VerifyWithPublicKey verifies the provided message against a signature and
// public key. The signature has to be provided in Ethereum-specific format.
func (es *EthereumSigner) VerifyWithPublicKey(
message []byte,
signature []byte,
publicKey []byte,
) (bool, error) {
unmarshalledPubKey, err := unmarshalPublicKey(
publicKey,
es.privateKey.Curve,
)
if err != nil {
return false, fmt.Errorf("failed to unmarshal public key: [%v]", err)
}

return verifySignature(message, signature, unmarshalledPubKey)
}

func verifySignature(
message []byte,
signature []byte,
publicKey *ecdsa.PublicKey,
) (bool, error) {
// Convert the operator's static key into an uncompressed public key
// which should be 65 bytes in length.
uncompressedPubKey := crypto.FromECDSAPub(publicKey)
// If our signature is in the [R || S || V] format, ensure we strip out
// the Ethereum-specific recovery-id, V, if it already hasn't been done.
if len(signature) == SignatureSize {
signature = signature[:len(signature)-1]
}

// The signature should be now 64 bytes long.
if len(signature) != 64 {
return false, fmt.Errorf(
"signature should have 64 bytes; has: [%d]",
len(signature),
)
}

return crypto.VerifySignature(
uncompressedPubKey,
ethereumPrefixedHash(message),
signature,
), nil
}

func ethereumPrefixedHash(message []byte) []byte {
return crypto.Keccak256(
[]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%v", len(message))),
message,
)
}

func unmarshalPublicKey(
bytes []byte,
curve elliptic.Curve,
) (*ecdsa.PublicKey, error) {
x, y := elliptic.Unmarshal(curve, bytes)
if x == nil {
return nil, fmt.Errorf(
"invalid public key bytes",
)
}
ecdsaPublicKey := &ecdsa.PublicKey{Curve: curve, X: x, Y: y}
return (*ecdsa.PublicKey)(ecdsaPublicKey), nil
}

// PublicKeyToAddress transforms the provided ECDSA public key into Ethereum
// address represented in bytes.
func (es *EthereumSigner) PublicKeyToAddress(publicKey ecdsa.PublicKey) []byte {
return crypto.PubkeyToAddress(publicKey).Bytes()
}

// PublicKeyBytesToAddress transforms the provided ECDSA public key in a bytes
// format into Ethereum address represented in bytes.
func (es *EthereumSigner) PublicKeyBytesToAddress(publicKey []byte) []byte {
// Does the same as crypto.PubkeyToAddress but directly on public key bytes.
return crypto.Keccak256(publicKey[1:])[12:]
}
165 changes: 165 additions & 0 deletions pkg/chain/ethereum/ethutil/signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package ethutil

import (
"testing"

"github.com/ethereum/go-ethereum/crypto"
)

func TestSignAndVerify(t *testing.T) {
signer, err := newSigner()
if err != nil {
t.Fatal(err)
}

message := []byte("He that breaks a thing to find out what it is, has " +
"left the path of wisdom.")

signature, err := signer.Sign(message)
if err != nil {
t.Fatal(err)
}

var tests = map[string]struct {
message []byte
signature []byte
validSignatureExpected bool
validationErrorExpected bool
}{
"valid signature for message": {
message: message,
signature: signature,
validSignatureExpected: true,
validationErrorExpected: false,
},
"invalid signature for message": {
message: []byte("I am sorry"),
signature: signature,
validSignatureExpected: false,
validationErrorExpected: false,
},
"corrupted signature": {
message: message,
signature: []byte("I am so sorry"),
validSignatureExpected: false,
validationErrorExpected: true,
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
ok, err := signer.Verify(test.message, test.signature)

if !ok && test.validSignatureExpected {
t.Errorf("expected valid signature but verification failed")
}
if ok && !test.validSignatureExpected {
t.Errorf("expected invalid signature but verification succeeded")
}

if err == nil && test.validationErrorExpected {
t.Errorf("expected signature validation error; none happened")
}
if err != nil && !test.validationErrorExpected {
t.Errorf("unexpected signature validation error [%v]", err)
}
})
}
}

func TestSignAndVerifyWithProvidedPublicKey(t *testing.T) {
message := []byte("I am looking for someone to share in an adventure")

signer1, err := newSigner()
if err != nil {
t.Fatal(err)
}

signer2, err := newSigner()
if err != nil {
t.Fatal(err)
}

publicKey := signer1.PublicKey()
signature, err := signer1.Sign(message)
if err != nil {
t.Fatal(err)
}

var tests = map[string]struct {
message []byte
signature []byte
publicKey []byte
validSignatureExpected bool
validationErrorExpected bool
}{
"valid signature for message": {
message: message,
signature: signature,
publicKey: publicKey,
validSignatureExpected: true,
validationErrorExpected: false,
},
"invalid signature for message": {
message: []byte("And here..."),
signature: signature,
publicKey: publicKey,
validSignatureExpected: false,
validationErrorExpected: false,
},
"corrupted signature": {
message: message,
signature: []byte("we..."),
publicKey: publicKey,
validSignatureExpected: false,
validationErrorExpected: true,
},
"invalid remote public key": {
message: message,
signature: signature,
publicKey: signer2.PublicKey(),
validSignatureExpected: false,
validationErrorExpected: false,
},
"corrupted remote public key": {
message: message,
signature: signature,
publicKey: []byte("go..."),
validSignatureExpected: false,
validationErrorExpected: true,
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
ok, err := signer2.VerifyWithPublicKey(
test.message,
test.signature,
test.publicKey,
)

if !ok && test.validSignatureExpected {
t.Errorf("expected valid signature but verification failed")
}
if ok && !test.validSignatureExpected {
t.Errorf("expected invalid signature but verification succeeded")
}

if err == nil && test.validationErrorExpected {
t.Errorf("expected signature validation error; none happened")
}
if err != nil && !test.validationErrorExpected {
t.Errorf("unexpected signature validation error [%v]", err)
}
})
}
}

func newSigner() (*EthereumSigner, error) {
key, err := crypto.GenerateKey()
if err != nil {
return nil, err
}

return &EthereumSigner{key}, nil
}
Loading

0 comments on commit c7eca9a

Please sign in to comment.