diff --git a/signer/core/api.go b/signer/core/api.go index 244767acaf..c0ed6b6a15 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -14,14 +14,16 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . +// +build !js + package core import ( "context" - "encoding/json" "errors" "fmt" "math/big" + "mime" "os" "reflect" @@ -31,7 +33,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/signer/storage" @@ -116,15 +120,6 @@ type SignerAPI struct { credentials storage.Storage } -// Metadata about a request -type Metadata struct { - Remote string `json:"remote"` - Local string `json:"local"` - Scheme string `json:"scheme"` - UserAgent string `json:"User-Agent"` - Origin string `json:"Origin"` -} - func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, scpath string) *accounts.Manager { var ( backends []accounts.Backend @@ -206,15 +201,6 @@ func MetadataFromContext(ctx context.Context) Metadata { return m } -// String implements Stringer interface -func (m Metadata) String() string { - s, err := json.Marshal(m) - if err == nil { - return string(s) - } - return err.Error() -} - // types for the requests/response types between signer and UI type ( // SignTxRequest contains info about a Transaction to sign @@ -229,14 +215,6 @@ type ( Transaction SendTxArgs `json:"transaction"` Approved bool `json:"approved"` } - SignDataRequest struct { - ContentType string `json:"content_type"` - Address common.MixedcaseAddress `json:"address"` - Rawdata []byte `json:"raw_data"` - Messages []*NameValueType `json:"messages"` - Hash hexutil.Bytes `json:"hash"` - Meta Metadata `json:"meta"` - } SignDataResponse struct { Approved bool `json:"approved"` } @@ -286,6 +264,226 @@ func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAP } return signer } + +// sign receives a request and produces a signature +// +// Note, the produced signature conforms to the secp256k1 curve R, S and V values, +// where the V value will be 27 or 28 for legacy reasons, if legacyV==true. +func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { + // We make the request prior to looking up if we actually have the account, to prevent + // account-enumeration via the API + res, err := api.UI.ApproveSignData(req) + if err != nil { + return nil, err + } + if !res.Approved { + return nil, ErrRequestDenied + } + // Look up the wallet containing the requested signer + account := accounts.Account{Address: addr.Address()} + wallet, err := api.am.Find(account) + if err != nil { + return nil, err + } + pw, err := api.lookupOrQueryPassword(account.Address, + "Password for signing", + fmt.Sprintf("Please enter password for signing data with account %s", account.Address.Hex())) + if err != nil { + return nil, err + } + // Sign the data with the wallet + signature, err := wallet.SignDataWithPassphrase(account, pw, req.ContentType, req.Rawdata) + if err != nil { + return nil, err + } + if legacyV { + signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + } + return signature, nil +} + +// SignData signs the hash of the provided data, but does so differently +// depending on the content-type specified. +// +// Different types of validation occur. +func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { + var req, transformV, err = api.determineSignatureFormat(ctx, contentType, addr, data) + if err != nil { + return nil, err + } + signature, err := api.sign(addr, req, transformV) + if err != nil { + api.UI.ShowError(err.Error()) + return nil, err + } + return signature, nil +} + +// determineSignatureFormat determines which signature method should be used based upon the mime type +// In the cases where it matters ensure that the charset is handled. The charset +// resides in the 'params' returned as the second returnvalue from mime.ParseMediaType +// charset, ok := params["charset"] +// As it is now, we accept any charset and just treat it as 'raw'. +// This method returns the mimetype for signing along with the request +func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (*SignDataRequest, bool, error) { + var ( + req *SignDataRequest + useEthereumV = true // Default to use V = 27 or 28, the legacy Ethereum format + ) + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + return nil, useEthereumV, err + } + + switch mediaType { + case IntendedValidator.Mime: + // Data with an intended validator + validatorData, err := UnmarshalValidatorData(data) + if err != nil { + return nil, useEthereumV, err + } + sighash, msg := SignTextValidator(validatorData) + messages := []*NameValueType{ + { + Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", + Typ: "description", + Value: "", + }, + { + Name: "Intended validator address", + Typ: "address", + Value: validatorData.Address.String(), + }, + { + Name: "Application-specific data", + Typ: "hexdata", + Value: validatorData.Message, + }, + { + Name: "Full message for signing", + Typ: "hexdata", + Value: fmt.Sprintf("0x%x", msg), + }, + } + req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} + case ApplicationClique.Mime: + // Clique is the Ethereum PoA standard + stringData, ok := data.(string) + if !ok { + return nil, useEthereumV, fmt.Errorf("input for %v must be an hex-encoded string", ApplicationClique.Mime) + } + cliqueData, err := hexutil.Decode(stringData) + if err != nil { + return nil, useEthereumV, err + } + header := &types.Header{} + if err := rlp.DecodeBytes(cliqueData, header); err != nil { + return nil, useEthereumV, err + } + // The incoming clique header is already truncated, sent to us with a extradata already shortened + if len(header.Extra) < 65 { + // Need to add it back, to get a suitable length for hashing + newExtra := make([]byte, len(header.Extra)+65) + copy(newExtra, header.Extra) + header.Extra = newExtra + } + // Get back the rlp data, encoded by us + sighash, cliqueRlp, err := cliqueHeaderHashAndRlp(header) + if err != nil { + return nil, useEthereumV, err + } + messages := []*NameValueType{ + { + Name: "Clique header", + Typ: "clique", + Value: fmt.Sprintf("clique header %d [0x%x]", header.Number, header.Hash()), + }, + } + // Clique uses V on the form 0 or 1 + useEthereumV = false + req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash} + default: // also case TextPlain.Mime: + // Calculates an Ethereum ECDSA signature for: + // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}") + // We expect it to be a string + if stringData, ok := data.(string); !ok { + return nil, useEthereumV, fmt.Errorf("input for text/plain must be an hex-encoded string") + } else { + if textData, err := hexutil.Decode(stringData); err != nil { + return nil, useEthereumV, err + } else { + sighash, msg := accounts.TextAndHash(textData) + messages := []*NameValueType{ + { + Name: "message", + Typ: accounts.MimetypeTextPlain, + Value: msg, + }, + } + req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} + } + } + } + req.Address = addr + req.Meta = MetadataFromContext(ctx) + return req, useEthereumV, nil +} + +// SignTypedData signs EIP-712 conformant typed data +// hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}") +func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return nil, err + } + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return nil, err + } + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) + sighash := crypto.Keccak256(rawData) + messages, err := typedData.Format() + if err != nil { + return nil, err + } + req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash} + signature, err := api.sign(addr, req, true) + if err != nil { + api.UI.ShowError(err.Error()) + return nil, err + } + return signature, nil +} + +// EcRecover recovers the address associated with the given sig. +// Only compatible with `text/plain` +func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) { + // Returns the address for the Account that was used to create the signature. + // + // Note, this function is compatible with eth_sign and personal_sign. As such it recovers + // the address of: + // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}") + // addr = ecrecover(hash, signature) + // + // Note, the signature must conform to the secp256k1 curve R, S and V values, where + // the V value must be be 27 or 28 for legacy reasons. + // + // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover + if len(sig) != 65 { + return common.Address{}, fmt.Errorf("signature must be 65 bytes long") + } + if sig[64] != 27 && sig[64] != 28 { + return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)") + } + sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 + hash := accounts.TextHash(data) + rpk, err := crypto.SigToPub(hash, sig) + if err != nil { + return common.Address{}, err + } + return crypto.PubkeyToAddress(*rpk), nil +} + func (api *SignerAPI) openTrezor(url accounts.URL) { resp, err := api.UI.OnInputRequired(UserInputRequest{ Prompt: "Pin required to open Trezor wallet\n" + diff --git a/signer/core/auditlog.go b/signer/core/auditlog.go index 1092e7a923..1e90576096 100644 --- a/signer/core/auditlog.go +++ b/signer/core/auditlog.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . +// +build !js + package core import ( diff --git a/signer/core/cliui.go b/signer/core/cliui.go index 1502238bf7..5333e013cb 100644 --- a/signer/core/cliui.go +++ b/signer/core/cliui.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . +// +build !js + package core import ( diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index f512be7cea..8639bed4f0 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -18,11 +18,10 @@ package core import ( "bytes" - "context" + "encoding/json" "errors" "fmt" "math/big" - "mime" "reflect" "regexp" "sort" @@ -38,7 +37,6 @@ import ( "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rlp" ) type SigFormat struct { @@ -122,168 +120,31 @@ type TypedDataDomain struct { var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`) -// sign receives a request and produces a signature -// -// Note, the produced signature conforms to the secp256k1 curve R, S and V values, -// where the V value will be 27 or 28 for legacy reasons, if legacyV==true. -func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { - // We make the request prior to looking up if we actually have the account, to prevent - // account-enumeration via the API - res, err := api.UI.ApproveSignData(req) - if err != nil { - return nil, err - } - if !res.Approved { - return nil, ErrRequestDenied - } - // Look up the wallet containing the requested signer - account := accounts.Account{Address: addr.Address()} - wallet, err := api.am.Find(account) - if err != nil { - return nil, err - } - pw, err := api.lookupOrQueryPassword(account.Address, - "Password for signing", - fmt.Sprintf("Please enter password for signing data with account %s", account.Address.Hex())) - if err != nil { - return nil, err - } - // Sign the data with the wallet - signature, err := wallet.SignDataWithPassphrase(account, pw, req.ContentType, req.Rawdata) - if err != nil { - return nil, err - } - if legacyV { - signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - } - return signature, nil +// Metadata about a request +type Metadata struct { + Remote string `json:"remote"` + Local string `json:"local"` + Scheme string `json:"scheme"` + UserAgent string `json:"User-Agent"` + Origin string `json:"Origin"` } -// SignData signs the hash of the provided data, but does so differently -// depending on the content-type specified. -// -// Different types of validation occur. -func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { - var req, transformV, err = api.determineSignatureFormat(ctx, contentType, addr, data) - if err != nil { - return nil, err +// String implements Stringer interface +func (m Metadata) String() string { + s, err := json.Marshal(m) + if err == nil { + return string(s) } - signature, err := api.sign(addr, req, transformV) - if err != nil { - api.UI.ShowError(err.Error()) - return nil, err - } - return signature, nil + return err.Error() } -// determineSignatureFormat determines which signature method should be used based upon the mime type -// In the cases where it matters ensure that the charset is handled. The charset -// resides in the 'params' returned as the second returnvalue from mime.ParseMediaType -// charset, ok := params["charset"] -// As it is now, we accept any charset and just treat it as 'raw'. -// This method returns the mimetype for signing along with the request -func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (*SignDataRequest, bool, error) { - var ( - req *SignDataRequest - useEthereumV = true // Default to use V = 27 or 28, the legacy Ethereum format - ) - mediaType, _, err := mime.ParseMediaType(contentType) - if err != nil { - return nil, useEthereumV, err - } - - switch mediaType { - case IntendedValidator.Mime: - // Data with an intended validator - validatorData, err := UnmarshalValidatorData(data) - if err != nil { - return nil, useEthereumV, err - } - sighash, msg := SignTextValidator(validatorData) - messages := []*NameValueType{ - { - Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", - Typ: "description", - Value: "", - }, - { - Name: "Intended validator address", - Typ: "address", - Value: validatorData.Address.String(), - }, - { - Name: "Application-specific data", - Typ: "hexdata", - Value: validatorData.Message, - }, - { - Name: "Full message for signing", - Typ: "hexdata", - Value: fmt.Sprintf("0x%x", msg), - }, - } - req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} - case ApplicationClique.Mime: - // Clique is the Ethereum PoA standard - stringData, ok := data.(string) - if !ok { - return nil, useEthereumV, fmt.Errorf("input for %v must be an hex-encoded string", ApplicationClique.Mime) - } - cliqueData, err := hexutil.Decode(stringData) - if err != nil { - return nil, useEthereumV, err - } - header := &types.Header{} - if err := rlp.DecodeBytes(cliqueData, header); err != nil { - return nil, useEthereumV, err - } - // The incoming clique header is already truncated, sent to us with a extradata already shortened - if len(header.Extra) < 65 { - // Need to add it back, to get a suitable length for hashing - newExtra := make([]byte, len(header.Extra)+65) - copy(newExtra, header.Extra) - header.Extra = newExtra - } - // Get back the rlp data, encoded by us - sighash, cliqueRlp, err := cliqueHeaderHashAndRlp(header) - if err != nil { - return nil, useEthereumV, err - } - messages := []*NameValueType{ - { - Name: "Clique header", - Typ: "clique", - Value: fmt.Sprintf("clique header %d [0x%x]", header.Number, header.Hash()), - }, - } - // Clique uses V on the form 0 or 1 - useEthereumV = false - req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash} - default: // also case TextPlain.Mime: - // Calculates an Ethereum ECDSA signature for: - // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}") - // We expect it to be a string - if stringData, ok := data.(string); !ok { - return nil, useEthereumV, fmt.Errorf("input for text/plain must be an hex-encoded string") - } else { - if textData, err := hexutil.Decode(stringData); err != nil { - return nil, useEthereumV, err - } else { - sighash, msg := accounts.TextAndHash(textData) - messages := []*NameValueType{ - { - Name: "message", - Typ: accounts.MimetypeTextPlain, - Value: msg, - }, - } - req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} - } - } - } - req.Address = addr - req.Meta = MetadataFromContext(ctx) - return req, useEthereumV, nil +type SignDataRequest struct { + ContentType string `json:"content_type"` + Address common.MixedcaseAddress `json:"address"` + Rawdata []byte `json:"raw_data"` + Messages []*NameValueType `json:"messages"` + Hash hexutil.Bytes `json:"hash"` + Meta Metadata `json:"meta"` } // SignTextWithValidator signs the given message which can be further recovered @@ -311,32 +172,6 @@ func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error) return hash, rlp, err } -// SignTypedData signs EIP-712 conformant typed data -// hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}") -func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { - domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - if err != nil { - return nil, err - } - typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) - if err != nil { - return nil, err - } - rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) - sighash := crypto.Keccak256(rawData) - messages, err := typedData.Format() - if err != nil { - return nil, err - } - req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash} - signature, err := api.sign(addr, req, true) - if err != nil { - api.UI.ShowError(err.Error()) - return nil, err - } - return signature, nil -} - // HashStruct generates a keccak256 hash of the encoding of the provided data func (typedData *TypedData) HashStruct(primaryType string, data TypedDataMessage) (hexutil.Bytes, error) { encodedData, err := typedData.EncodeData(primaryType, data, 1) @@ -599,35 +434,6 @@ func dataMismatchError(encType string, encValue interface{}) error { return fmt.Errorf("provided data '%v' doesn't match type '%s'", encValue, encType) } -// EcRecover recovers the address associated with the given sig. -// Only compatible with `text/plain` -func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) { - // Returns the address for the Account that was used to create the signature. - // - // Note, this function is compatible with eth_sign and personal_sign. As such it recovers - // the address of: - // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}") - // addr = ecrecover(hash, signature) - // - // Note, the signature must conform to the secp256k1 curve R, S and V values, where - // the V value must be be 27 or 28 for legacy reasons. - // - // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover - if len(sig) != 65 { - return common.Address{}, fmt.Errorf("signature must be 65 bytes long") - } - if sig[64] != 27 && sig[64] != 28 { - return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)") - } - sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 - hash := accounts.TextHash(data) - rpk, err := crypto.SigToPub(hash, sig) - if err != nil { - return common.Address{}, err - } - return crypto.PubkeyToAddress(*rpk), nil -} - // UnmarshalValidatorData converts the bytes input to typed data func UnmarshalValidatorData(data interface{}) (ValidatorData, error) { raw, ok := data.(map[string]interface{}) diff --git a/signer/core/stdioui.go b/signer/core/stdioui.go index 9ffe1398d4..f5952daf22 100644 --- a/signer/core/stdioui.go +++ b/signer/core/stdioui.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . +// +build !js + package core import ( diff --git a/signer/core/uiapi.go b/signer/core/uiapi.go index 7c2d233f89..3f3473efa9 100644 --- a/signer/core/uiapi.go +++ b/signer/core/uiapi.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . +// +build !js + package core import (