-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #61 from keep-network/signer
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
Showing
4 changed files
with
587 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.