diff --git a/pkg/chain/ethereum/ethutil/signer.go b/pkg/chain/ethereum/ethutil/signer.go new file mode 100644 index 0000000..e5ffcef --- /dev/null +++ b/pkg/chain/ethereum/ethutil/signer.go @@ -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:] +} diff --git a/pkg/chain/ethereum/ethutil/signer_test.go b/pkg/chain/ethereum/ethutil/signer_test.go new file mode 100644 index 0000000..203f2dc --- /dev/null +++ b/pkg/chain/ethereum/ethutil/signer_test.go @@ -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 +} diff --git a/pkg/chain/local/signer.go b/pkg/chain/local/signer.go new file mode 100644 index 0000000..53f7a78 --- /dev/null +++ b/pkg/chain/local/signer.go @@ -0,0 +1,112 @@ +package local + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/asn1" + "fmt" + "math/big" +) + +// Signer provides functions to sign a message and verify a signature +// using the chain-specific signature format. It also provides functions for +// convertion of a public key to address. +type Signer struct { + operatorKey *ecdsa.PrivateKey +} + +type ecdsaSignature struct { + R, S *big.Int +} + +// NewSigner creates a new Signer instance for the provided private key. +func NewSigner(privateKey *ecdsa.PrivateKey) *Signer { + return &Signer{privateKey} +} + +// PublicKey returns byte representation of a public key for the private key +// signer was created with. +func (ls *Signer) PublicKey() []byte { + publicKey := ls.operatorKey.PublicKey + return elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y) +} + +// Sign signs the provided message. +func (ls *Signer) Sign(message []byte) ([]byte, error) { + hash := sha256.Sum256(message) + + r, s, err := ecdsa.Sign(rand.Reader, ls.operatorKey, hash[:]) + if err != nil { + return nil, err + } + + return asn1.Marshal(ecdsaSignature{r, s}) +} + +// Verify verifies the provided message against a signature using the key +// Signer was created with. +func (ls *Signer) Verify(message []byte, signature []byte) (bool, error) { + return verifySignature(message, signature, &ls.operatorKey.PublicKey) +} + +// VerifyWithPublicKey verifies the provided message against a signature and +// public key. +func (ls *Signer) VerifyWithPublicKey( + message []byte, + signature []byte, + publicKey []byte, +) (bool, error) { + unmarshalledPubKey, err := unmarshalPublicKey( + publicKey, + ls.operatorKey.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) { + hash := sha256.Sum256(message) + + sig := &ecdsaSignature{} + _, err := asn1.Unmarshal(signature, sig) + if err != nil { + return false, fmt.Errorf("failed to unmarshal signature: [%v]", err) + } + + return ecdsa.Verify(publicKey, hash[:], sig.R, sig.S), nil +} + +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 chain +// address represented in bytes. +func (ls *Signer) PublicKeyToAddress(publicKey ecdsa.PublicKey) []byte { + return elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y) +} + +// PublicKeyBytesToAddress transforms the provided ECDSA public key in a bytes +// format into chain address represented in bytes. +func (ls *Signer) PublicKeyBytesToAddress(publicKey []byte) []byte { + return publicKey +} diff --git a/pkg/chain/local/signer_test.go b/pkg/chain/local/signer_test.go new file mode 100644 index 0000000..a923592 --- /dev/null +++ b/pkg/chain/local/signer_test.go @@ -0,0 +1,166 @@ +package local + +import ( + "crypto/ecdsa" + "crypto/elliptic" + crand "crypto/rand" + "testing" +) + +func TestSignAndVerify(t *testing.T) { + message := []byte("Two things only the greatest fools do: throw " + + "stones at hornets' nests and threaten a witcher.") + + signer, err := newSigner() + if err != nil { + t.Fatal(err) + } + + 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("You shall not pass") + + 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("Fly you fools"), + signature: signature, + publicKey: publicKey, + validSignatureExpected: false, + validationErrorExpected: false, + }, + "corrupted signature": { + message: message, + signature: []byte("My precious"), + 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("A Balrog"), + 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() (*Signer, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader) + if err != nil { + return nil, err + } + + return &Signer{key}, nil +}