Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/ethapi: add personal_sign method #2940

Merged
merged 5 commits into from
Oct 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accounts/abi/bind/auth.go Asset Amount Asset Type Date Acquired Date Sold Proceeds (USD) Cost Basis (USD) Gain (USD) Type
0.00000229 BTC 12/12/2014 08/26/2017 0.01 0.00 0.01 Long Term
1.00010001 ZEC 12/20/2014 09/19/2017 0.39 0.03 0.36 Long Term
0.00082408 BTC 12/12/2014 09/19/2017 3.22 0.29 2.93 Long Term
4.12723423 XMR 08/20/2017 10/19/2017 538.57 565.08 -26.51 Short Term
0.00000002 BTC 01/28/2015 09/19/2017 0.00 0.00 0.00 Long Term
0.00000001 BTC 01/10/2015 09/19/2017 0.00 0.00 0.00 Long Term
0.10001200 LTC 12/21/2014 09/19/2017 0.04 0.00 0.04 Long Term
0.00004001 BTC 12/23/2014 09/19/2017 0.12 0.01 0.11 Long Term
0.00000100 ETH 12/20/2014 09/19/2017 0.00 0.00 0.00 Long Term

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import priv-keys in confirmation order

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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

32rzbnjmJfRa4MH8TcpdNtcdzSwkMRzRr9

}

// 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 @@ -244,20 +244,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"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤙

)
)

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