Skip to content

Commit

Permalink
internal/ethapi: add personal_sign and fix eth_sign to hash message (e…
Browse files Browse the repository at this point in the history
…thereum#2940)

This commit includes several API changes:

- The behavior of eth_sign is changed. It now accepts an arbitrary
  message, prepends the well-known string

        \x19Ethereum Signed Message:\n<length of message>

  hashes the result using keccak256 and calculates the signature of
  the hash. This breaks backwards compatability!
  
- personal_sign(hash, address [, password]) is added. It has the same
  semantics as eth_sign but also accepts a password. The private key
  used to sign the hash is temporarily unlocked in the scope of the
  request.
  
- personal_recover(message, signature) is added and returns the
  address for the account that created a signature.
  • Loading branch information
bas-vk authored and fjl committed Oct 28, 2016
1 parent 289b307 commit b59c839
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 34 deletions.
2 changes: 1 addition & 1 deletion accounts/abi/bind/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func NewKeyedTransactor(key *ecdsa.PrivateKey) *TransactOpts {
if address != keyAddr {
return nil, errors.New("not authorized to sign this account")
}
signature, err := crypto.Sign(tx.SigHash().Bytes(), key)
signature, err := crypto.SignEthereum(tx.SigHash().Bytes(), key)
if err != nil {
return nil, err
}
Expand Down
25 changes: 20 additions & 5 deletions accounts/account_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,11 @@ func (am *Manager) DeleteAccount(a Account, passphrase string) error {
return err
}

// Sign signs hash with an unlocked private key matching the given address.
func (am *Manager) Sign(addr common.Address, hash []byte) (signature []byte, err error) {
// Sign calculates a ECDSA signature for the given hash.
// Note, Ethereum signatures have a particular format as described in the
// yellow paper. Use the SignEthereum function to calculate a signature
// in Ethereum format.
func (am *Manager) Sign(addr common.Address, hash []byte) ([]byte, error) {
am.mu.RLock()
defer am.mu.RUnlock()
unlockedKey, found := am.unlocked[addr]
Expand All @@ -147,16 +150,28 @@ func (am *Manager) Sign(addr common.Address, hash []byte) (signature []byte, err
return crypto.Sign(hash, unlockedKey.PrivateKey)
}

// SignWithPassphrase signs hash if the private key matching the given address can be
// decrypted with the given passphrase.
// SignEthereum calculates a ECDSA signature for the given hash.
// The signature has the format as described in the Ethereum yellow paper.
func (am *Manager) SignEthereum(addr common.Address, hash []byte) ([]byte, error) {
am.mu.RLock()
defer am.mu.RUnlock()
unlockedKey, found := am.unlocked[addr]
if !found {
return nil, ErrLocked
}
return crypto.SignEthereum(hash, unlockedKey.PrivateKey)
}

// SignWithPassphrase signs hash if the private key matching the given
// address can be decrypted with the given passphrase.
func (am *Manager) SignWithPassphrase(addr common.Address, passphrase string, hash []byte) (signature []byte, err error) {
_, key, err := am.getDecryptedKey(Account{Address: addr}, passphrase)
if err != nil {
return nil, err
}

defer zeroKey(key.PrivateKey)
return crypto.Sign(hash, key.PrivateKey)
return crypto.SignEthereum(hash, key.PrivateKey)
}

// Unlock unlocks the given account indefinitely.
Expand Down
2 changes: 1 addition & 1 deletion common/bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func ToHex(b []byte) string {

func FromHex(s string) []byte {
if len(s) > 1 {
if s[0:2] == "0x" {
if s[0:2] == "0x" || s[0:2] == "0X" {
s = s[2:]
}
if len(s)%2 == 1 {
Expand Down
2 changes: 1 addition & 1 deletion common/registrar/ethreg/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ func (be *registryAPIBackend) Transact(fromStr, toStr, nonceStr, valueStr, gasSt
tx = types.NewTransaction(nonce, to, value, gas, price, data)
}

signature, err := be.am.Sign(from, tx.SigHash().Bytes())
signature, err := be.am.SignEthereum(from, tx.SigHash().Bytes())
if err != nil {
return "", err
}
Expand Down
38 changes: 38 additions & 0 deletions console/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,44 @@ func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) {
return val
}

// Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password
// prompt to aquire the passphrase and executes the original RPC method (saved in
// jeth.sign) with it to actually execute the RPC call.
func (b *bridge) Sign(call otto.FunctionCall) (response otto.Value) {
var (
message = call.Argument(0)
account = call.Argument(1)
passwd = call.Argument(2)
)

if !message.IsString() {
throwJSException("first argument must be the message to sign")
}
if !account.IsString() {
throwJSException("second argument must be the account to sign with")
}

// if the password is not given or null ask the user and ensure password is a string
if passwd.IsUndefined() || passwd.IsNull() {
fmt.Fprintf(b.printer, "Give password for account %s\n", account)
if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil {
throwJSException(err.Error())
} else {
passwd, _ = otto.ToValue(input)
}
}
if !passwd.IsString() {
throwJSException("third argument must be the password to unlock the account")
}

// Send the request to the backend and return
val, err := call.Otto.Call("jeth.sign", nil, message, account, passwd)
if err != nil {
throwJSException(err.Error())
}
return val
}

// Sleep will block the console for the specified number of seconds.
func (b *bridge) Sleep(call otto.FunctionCall) (response otto.Value) {
if call.Argument(0).IsNumber() {
Expand Down
11 changes: 7 additions & 4 deletions console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,19 +156,22 @@ func (c *Console) init(preload []string) error {
if err != nil {
return err
}
// Override the unlockAccount and newAccount methods since these require user interaction.
// Assign the jeth.unlockAccount and jeth.newAccount in the Console the original web3 callbacks.
// These will be called by the jeth.* methods after they got the password from the user and send
// the original web3 request to the backend.
// Override the unlockAccount, newAccount and sign methods since these require user interaction.
// Assign these method in the Console the original web3 callbacks. These will be called by the jeth.*
// methods after they got the password from the user and send the original web3 request to the backend.
if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface
if _, err = c.jsre.Run(`jeth.unlockAccount = personal.unlockAccount;`); err != nil {
return fmt.Errorf("personal.unlockAccount: %v", err)
}
if _, err = c.jsre.Run(`jeth.newAccount = personal.newAccount;`); err != nil {
return fmt.Errorf("personal.newAccount: %v", err)
}
if _, err = c.jsre.Run(`jeth.sign = personal.sign;`); err != nil {
return fmt.Errorf("personal.sign: %v", err)
}
obj.Set("unlockAccount", bridge.UnlockAccount)
obj.Set("newAccount", bridge.NewAccount)
obj.Set("sign", bridge.Sign)
}
}
// The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer.
Expand Down
2 changes: 1 addition & 1 deletion core/types/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestBlockEncoding(t *testing.T) {
check("Size", block.Size(), common.StorageSize(len(blockEnc)))

tx1 := NewTransaction(0, common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), big.NewInt(10), big.NewInt(50000), big.NewInt(10), nil)
tx1, _ = tx1.WithSignature(common.Hex2Bytes("9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b100"))
tx1, _ = tx1.WithSignature(common.Hex2Bytes("9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b11b"))
check("len(Transactions)", len(block.Transactions()), 1)
check("Transactions[0].Hash", block.Transactions()[0].Hash(), tx1.Hash())

Expand Down
6 changes: 4 additions & 2 deletions core/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,20 +318,22 @@ func (tx *Transaction) publicKey(homestead bool) ([]byte, error) {
return pub, nil
}

// WithSignature returns a new transaction with the given signature.
// This signature needs to be formatted as described in the yellow paper (v+27).
func (tx *Transaction) WithSignature(sig []byte) (*Transaction, error) {
if len(sig) != 65 {
panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
}
cpy := &Transaction{data: tx.data}
cpy.data.R = new(big.Int).SetBytes(sig[:32])
cpy.data.S = new(big.Int).SetBytes(sig[32:64])
cpy.data.V = sig[64] + 27
cpy.data.V = sig[64]
return cpy, nil
}

func (tx *Transaction) SignECDSA(prv *ecdsa.PrivateKey) (*Transaction, error) {
h := tx.SigHash()
sig, err := crypto.Sign(h[:], prv)
sig, err := crypto.SignEthereum(h[:], prv)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion core/types/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var (
big.NewInt(1),
common.FromHex("5544"),
).WithSignature(
common.Hex2Bytes("98ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4a8887321be575c8095f789dd4c743dfe42c1820f9231f98a962b210e3ac2452a301"),
common.Hex2Bytes("98ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4a8887321be575c8095f789dd4c743dfe42c1820f9231f98a962b210e3ac2452a31c"),
)
)

Expand Down
37 changes: 33 additions & 4 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ func Ripemd160(data []byte) []byte {
return ripemd.Sum(nil)
}

// Ecrecover returns the public key for the private key that was used to
// calculate the signature.
//
// Note: secp256k1 expects the recover id to be either 0, 1. Ethereum
// signatures have a recover id with an offset of 27. Callers must take
// this into account and if "recovering" from an Ethereum signature adjust.
func Ecrecover(hash, sig []byte) ([]byte, error) {
return secp256k1.RecoverPubkey(hash, sig)
}
Expand Down Expand Up @@ -192,17 +198,40 @@ func SigToPub(hash, sig []byte) (*ecdsa.PublicKey, error) {
return &ecdsa.PublicKey{Curve: secp256k1.S256(), X: x, Y: y}, nil
}

func Sign(hash []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) {
if len(hash) != 32 {
return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(hash))
// Sign calculates an ECDSA signature.
// This function is susceptible to choosen plaintext attacks that can leak
// information about the private key that is used for signing. Callers must
// be aware that the given hash cannot be choosen by an adversery. Common
// solution is to hash any input before calculating the signature.
//
// Note: the calculated signature is not Ethereum compliant. The yellow paper
// dictates Ethereum singature to have a V value with and offset of 27 v in [27,28].
// Use SignEthereum to get an Ethereum compliant signature.
func Sign(data []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) {
if len(data) != 32 {
return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(data))
}

seckey := common.LeftPadBytes(prv.D.Bytes(), prv.Params().BitSize/8)
defer zeroBytes(seckey)
sig, err = secp256k1.Sign(hash, seckey)
sig, err = secp256k1.Sign(data, seckey)
return
}

// SignEthereum calculates an Ethereum ECDSA signature.
// This function is susceptible to choosen plaintext attacks that can leak
// information about the private key that is used for signing. Callers must
// be aware that the given hash cannot be freely choosen by an adversery.
// Common solution is to hash the message before calculating the signature.
func SignEthereum(data []byte, prv *ecdsa.PrivateKey) ([]byte, error) {
sig, err := Sign(data, prv)
if err != nil {
return nil, err
}
sig[64] += 27 // as described in the yellow paper
return sig, err
}

func Encrypt(pub *ecdsa.PublicKey, message []byte) ([]byte, error) {
return ecies.Encrypt(rand.Reader, ecies.ImportECDSAPublic(pub), message, nil, nil)
}
Expand Down
35 changes: 29 additions & 6 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,28 @@ func Test0Key(t *testing.T) {
}
}

func TestSign(t *testing.T) {
func testSign(signfn func([]byte, *ecdsa.PrivateKey) ([]byte, error), t *testing.T) {
key, _ := HexToECDSA(testPrivHex)
addr := common.HexToAddress(testAddrHex)

msg := Keccak256([]byte("foo"))
sig, err := Sign(msg, key)
sig, err := signfn(msg, key)
if err != nil {
t.Errorf("Sign error: %s", err)
}

// signfn can return a recover id of either [0,1] or [27,28].
// In the latter case its an Ethereum signature, adjust recover id.
if sig[64] == 27 || sig[64] == 28 {
sig[64] -= 27
}

recoveredPub, err := Ecrecover(msg, sig)
if err != nil {
t.Errorf("ECRecover error: %s", err)
}
recoveredAddr := PubkeyToAddress(*ToECDSAPub(recoveredPub))
pubKey := ToECDSAPub(recoveredPub)
recoveredAddr := PubkeyToAddress(*pubKey)
if addr != recoveredAddr {
t.Errorf("Address mismatch: want: %x have: %x", addr, recoveredAddr)
}
Expand All @@ -107,21 +115,36 @@ func TestSign(t *testing.T) {
if addr != recoveredAddr2 {
t.Errorf("Address mismatch: want: %x have: %x", addr, recoveredAddr2)
}
}

func TestSign(t *testing.T) {
testSign(Sign, t)
}

func TestInvalidSign(t *testing.T) {
_, err := Sign(make([]byte, 1), nil)
func TestSignEthereum(t *testing.T) {
testSign(SignEthereum, t)
}

func testInvalidSign(signfn func([]byte, *ecdsa.PrivateKey) ([]byte, error), t *testing.T) {
_, err := signfn(make([]byte, 1), nil)
if err == nil {
t.Errorf("expected sign with hash 1 byte to error")
}

_, err = Sign(make([]byte, 33), nil)
_, err = signfn(make([]byte, 33), nil)
if err == nil {
t.Errorf("expected sign with hash 33 byte to error")
}
}

func TestInvalidSign(t *testing.T) {
testInvalidSign(Sign, t)
}

func TestInvalidSignEthereum(t *testing.T) {
testInvalidSign(SignEthereum, t)
}

func TestNewContractAddress(t *testing.T) {
key, _ := HexToECDSA(testPrivHex)
addr := common.HexToAddress(testAddrHex)
Expand Down
Loading

0 comments on commit b59c839

Please sign in to comment.