signer, clef: implement EIP191/712 (#17789)
* Named functions and defined a basic EIP191 content type list
* Written basic content type functions
* Added ecRecover method in the clef api
* Updated the extapi changelog and addded indications in the README
* Changed the version of the external API
* Added tests for 0x45
* Implementing UnmarshalJSON() for TypedData
* Working on TypedData
* Solved the auditlog issue
* Changed method to signTypedData
* Changed mimes and implemented the 'encodeType' function for EIP-712
* Polished docstrings, ran goimports and swapped fmt.Errorf with errors.New where possible
* Drafted recursive encodeData
* Ran goimports and gofmt
* Drafted first version of EIP-712, including tests
* Temporarily switched to using common.Address in tests
* Drafted text/validator and and rewritten []byte as hexutil.Bytes
* Solved stringified address encoding issue
* Changed the property type required by signData from bytes to interface{}
* Fixed bugs in 'data/typed' signs
* Brought legal warning back after temporarily disabling it for development
* Added example RPC calls for account_signData and account_signTypedData
* Named functions and defined a basic EIP191 content type list
* Written basic content type functions
* Added ecRecover method in the clef api
* Updated the extapi changelog and addded indications in the README
* Added tests for 0x45
* Implementing UnmarshalJSON() for TypedData
* Working on TypedData
* Solved the auditlog issue
* Changed method to signTypedData
* Changed mimes and implemented the 'encodeType' function for EIP-712
* Polished docstrings, ran goimports and swapped fmt.Errorf with errors.New where possible
* Drafted recursive encodeData
* Ran goimports and gofmt
* Drafted first version of EIP-712, including tests
* Temporarily switched to using common.Address in tests
* Drafted text/validator and and rewritten []byte as hexutil.Bytes
* Solved stringified address encoding issue
* Changed the property type required by signData from bytes to interface{}
* Fixed bugs in 'data/typed' signs
* Brought legal warning back after temporarily disabling it for development
* Added example RPC calls for account_signData and account_signTypedData
* Polished and fixed PR
* Polished and fixed PR
* Solved malformed data panics and also wrote tests
* Solved malformed data panics and also wrote tests
* Added alphabetical sorting to type dependencies
* Added alphabetical sorting to type dependencies
* Added pretty print to data/typed UI
* Added pretty print to data/typed UI
* signer: more tests for typed data
* signer: more tests for typed data
* Fixed TestMalformedData4 errors and renamed IsValid to Validate
* Fixed TestMalformedData4 errors and renamed IsValid to Validate
* Fixed more new failing tests and deanonymised some functions
* Fixed more new failing tests and deanonymised some functions
* Added types to EIP712 output in cliui
* Added types to EIP712 output in cliui
* Fixed regexp issues
* Fixed regexp issues
* Added pseudo-failing test
* Added pseudo-failing test
* Fixed false positive test
* Fixed false positive test
* Added PrettyPrint method
* Added PrettyPrint method
* signer: refactor formatting and UI
* signer: make ui use new message format for signing
* Fixed breaking changes
* Fixed rules_test failing test
* Added extra regexp for reference types
* signer: more hard types
* Fixed failing test, formatted files
* signer: use golang/x keccak
* Fixed goimports error
* clef, signer: address some review concerns
* Implemented latest recommendations
* Fixed comments and uintint256 issue
* accounts, signer: fix mimetypes, add interface to sign data with passphrase
* signer, accounts: remove duplicated code, pass hash preimages to signing
* signer: prevent panic in type assertions, make cliui print rawdata as quotable-safe
* signer: linter fixes, remove deprecated crypto dependency
* accounts: fix goimport
			
			
This commit is contained in:
		
				
					committed by
					
						 Martin Holst Swende
						Martin Holst Swende
					
				
			
			
				
	
			
			
			
						parent
						
							7c60d0a6a2
						
					
				
				
					commit
					572baae10a
				
			| @@ -35,6 +35,13 @@ type Account struct { | ||||
| 	URL     URL            `json:"url"`     // Optional resource locator within a backend | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	MimetypeTextWithValidator = "text/validator" | ||||
| 	MimetypeTypedData         = "data/typed" | ||||
| 	MimetypeClique            = "application/x-clique-header" | ||||
| 	MimetypeTextPlain         = "text/plain" | ||||
| ) | ||||
|  | ||||
| // Wallet represents a software or hardware wallet that might contain one or more | ||||
| // accounts (derived from the same seed). | ||||
| type Wallet interface { | ||||
| @@ -101,6 +108,12 @@ type Wallet interface { | ||||
| 	// the account in a keystore). | ||||
| 	SignData(account Account, mimeType string, data []byte) ([]byte, error) | ||||
|  | ||||
| 	// SignDataWithPassphrase is identical to SignData, but also takes a password | ||||
| 	// NOTE: there's an chance that an erroneous call might mistake the two strings, and | ||||
| 	// supply password in the mimetype field, or vice versa. Thus, an implementation | ||||
| 	// should never echo the mimetype or return the mimetype in the error-response | ||||
| 	SignDataWithPassphrase(account Account, passphrase, mimeType string, data []byte) ([]byte, error) | ||||
|  | ||||
| 	// Signtext requests the wallet to sign the hash of a given piece of data, prefixed | ||||
| 	// by the Ethereum prefix scheme | ||||
| 	// It looks up the account specified either solely via its address contained within, | ||||
| @@ -114,6 +127,9 @@ type Wallet interface { | ||||
| 	// the account in a keystore). | ||||
| 	SignText(account Account, text []byte) ([]byte, error) | ||||
|  | ||||
| 	// SignTextWithPassphrase is identical to Signtext, but also takes a password | ||||
| 	SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) | ||||
|  | ||||
| 	// SignTx requests the wallet to sign the given transaction. | ||||
| 	// | ||||
| 	// It looks up the account specified either solely via its address contained within, | ||||
| @@ -127,18 +143,7 @@ type Wallet interface { | ||||
| 	// the account in a keystore). | ||||
| 	SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) | ||||
|  | ||||
| 	// SignTextWithPassphrase requests the wallet to sign the given text with the | ||||
| 	// given passphrase as extra authentication information. | ||||
| 	// | ||||
| 	// It looks up the account specified either solely via its address contained within, | ||||
| 	// or optionally with the aid of any location metadata from the embedded URL field. | ||||
| 	SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) | ||||
|  | ||||
| 	// SignTxWithPassphrase requests the wallet to sign the given transaction, with the | ||||
| 	// given passphrase as extra authentication information. | ||||
| 	// | ||||
| 	// It looks up the account specified either solely via its address contained within, | ||||
| 	// or optionally with the aid of any location metadata from the embedded URL field. | ||||
| 	// SignTxWithPassphrase is identical to SignTx, but also takes a password | ||||
| 	SignTxWithPassphrase(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) | ||||
| } | ||||
|  | ||||
| @@ -170,9 +175,22 @@ type Backend interface { | ||||
| // | ||||
| // This gives context to the signed message and prevents signing of transactions. | ||||
| func TextHash(data []byte) []byte { | ||||
| 	hash := sha3.NewLegacyKeccak256() | ||||
| 	fmt.Fprintf(hash, "\x19Ethereum Signed Message:\n%d%s", len(data), data) | ||||
| 	return hash.Sum(nil) | ||||
| 	hash, _ := TextAndHash(data) | ||||
| 	return hash | ||||
| } | ||||
|  | ||||
| // TextAndHash is a helper function that calculates a hash for the given message that can be | ||||
| // safely used to calculate a signature from. | ||||
| // | ||||
| // The hash is calulcated as | ||||
| //   keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). | ||||
| // | ||||
| // This gives context to the signed message and prevents signing of transactions. | ||||
| func TextAndHash(data []byte) ([]byte, string) { | ||||
| 	msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), string(data)) | ||||
| 	hasher := sha3.NewLegacyKeccak256() | ||||
| 	hasher.Write([]byte(msg)) | ||||
| 	return hasher.Sum(nil), msg | ||||
| } | ||||
|  | ||||
| // WalletEventType represents the different event types that can be fired by | ||||
|   | ||||
							
								
								
									
										9
									
								
								accounts/external/backend.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								accounts/external/backend.go
									
									
									
									
										vendored
									
									
								
							| @@ -184,11 +184,14 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio | ||||
| } | ||||
|  | ||||
| func (api *ExternalSigner) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { | ||||
| 	return []byte{}, fmt.Errorf("operation not supported on external signers") | ||||
| 	return []byte{}, fmt.Errorf("passphrase-operations not supported on external signers") | ||||
| } | ||||
|  | ||||
| func (api *ExternalSigner) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { | ||||
| 	return nil, fmt.Errorf("operation not supported on external signers") | ||||
| 	return nil, fmt.Errorf("passphrase-operations not supported on external signers") | ||||
| } | ||||
| func (api *ExternalSigner) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { | ||||
| 	return nil, fmt.Errorf("passphrase-operations not supported on external signers") | ||||
| } | ||||
|  | ||||
| func (api *ExternalSigner) listAccounts() ([]common.Address, error) { | ||||
| @@ -201,7 +204,7 @@ func (api *ExternalSigner) listAccounts() ([]common.Address, error) { | ||||
|  | ||||
| func (api *ExternalSigner) signCliqueBlock(a common.Address, rlpBlock hexutil.Bytes) (hexutil.Bytes, error) { | ||||
| 	var sig hexutil.Bytes | ||||
| 	if err := api.client.Call(&sig, "account_signData", "application/clique", a, rlpBlock); err != nil { | ||||
| 	if err := api.client.Call(&sig, "account_signData", core.ApplicationClique.Mime, a, rlpBlock); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if sig[64] != 27 && sig[64] != 28 { | ||||
|   | ||||
| @@ -97,10 +97,31 @@ func (w *keystoreWallet) SignData(account accounts.Account, mimeType string, dat | ||||
| 	return w.signHash(account, crypto.Keccak256(data)) | ||||
| } | ||||
|  | ||||
| // SignDataWithPassphrase signs keccak256(data). The mimetype parameter describes the type of data being signed | ||||
| func (w *keystoreWallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { | ||||
| 	// Make sure the requested account is contained within | ||||
| 	if !w.Contains(account) { | ||||
| 		return nil, accounts.ErrUnknownAccount | ||||
| 	} | ||||
| 	// Account seems valid, request the keystore to sign | ||||
| 	return w.keystore.SignHashWithPassphrase(account, passphrase, crypto.Keccak256(data)) | ||||
| } | ||||
|  | ||||
| func (w *keystoreWallet) SignText(account accounts.Account, text []byte) ([]byte, error) { | ||||
| 	return w.signHash(account, accounts.TextHash(text)) | ||||
| } | ||||
|  | ||||
| // SignHashWithPassphrase implements accounts.Wallet, attempting to sign the | ||||
| // given hash with the given account using passphrase as extra authentication. | ||||
| func (w *keystoreWallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { | ||||
| 	// Make sure the requested account is contained within | ||||
| 	if !w.Contains(account) { | ||||
| 		return nil, accounts.ErrUnknownAccount | ||||
| 	} | ||||
| 	// Account seems valid, request the keystore to sign | ||||
| 	return w.keystore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text)) | ||||
| } | ||||
|  | ||||
| // SignTx implements accounts.Wallet, attempting to sign the given transaction | ||||
| // with the given account. If the wallet does not wrap this particular account, | ||||
| // an error is returned to avoid account leakage (even though in theory we may | ||||
| @@ -114,17 +135,6 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, | ||||
| 	return w.keystore.SignTx(account, tx, chainID) | ||||
| } | ||||
|  | ||||
| // SignHashWithPassphrase implements accounts.Wallet, attempting to sign the | ||||
| // given hash with the given account using passphrase as extra authentication. | ||||
| func (w *keystoreWallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { | ||||
| 	// Make sure the requested account is contained within | ||||
| 	if !w.Contains(account) { | ||||
| 		return nil, accounts.ErrUnknownAccount | ||||
| 	} | ||||
| 	// Account seems valid, request the keystore to sign | ||||
| 	return w.keystore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text)) | ||||
| } | ||||
|  | ||||
| // SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given | ||||
| // transaction with the given account using passphrase as extra authentication. | ||||
| func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { | ||||
|   | ||||
| @@ -507,6 +507,13 @@ func (w *wallet) SignData(account accounts.Account, mimeType string, data []byte | ||||
| 	return w.signHash(account, crypto.Keccak256(data)) | ||||
| } | ||||
|  | ||||
| // SignDataWithPassphrase implements accounts.Wallet, attempting to sign the given | ||||
| // data with the given account using passphrase as extra authentication. | ||||
| // Since USB wallets don't rely on passphrases, these are silently ignored. | ||||
| func (w *wallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { | ||||
| 	return w.SignData(account, mimeType, data) | ||||
| } | ||||
|  | ||||
| func (w *wallet) SignText(account accounts.Account, text []byte) ([]byte, error) { | ||||
| 	return w.signHash(account, accounts.TextHash(text)) | ||||
| } | ||||
|   | ||||
| @@ -189,7 +189,9 @@ None | ||||
|   "method": "account_new", | ||||
|   "params": [] | ||||
| } | ||||
|  | ||||
| ``` | ||||
| Response | ||||
| ``` | ||||
| { | ||||
|   "id": 0, | ||||
|   "jsonrpc": "2.0", | ||||
| @@ -222,7 +224,9 @@ None | ||||
|   "jsonrpc": "2.0", | ||||
|   "method": "account_list" | ||||
| } | ||||
|  | ||||
| ``` | ||||
| Response | ||||
| ``` | ||||
| { | ||||
|   "id": 1, | ||||
|   "jsonrpc": "2.0", | ||||
| @@ -285,8 +289,8 @@ Response | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "id": 2, | ||||
|   "jsonrpc": "2.0", | ||||
|   "id": 67, | ||||
|   "error": { | ||||
|     "code": -32000, | ||||
|     "message": "Request denied" | ||||
| @@ -298,6 +302,7 @@ Response | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "id": 67, | ||||
|   "jsonrpc": "2.0", | ||||
|   "method": "account_signTransaction", | ||||
|   "params": [ | ||||
| @@ -311,8 +316,7 @@ Response | ||||
|       "data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012" | ||||
|     }, | ||||
|     "safeSend(address)" | ||||
|   ], | ||||
|   "id": 67 | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| Response | ||||
| @@ -346,15 +350,18 @@ Bash example: | ||||
| {"jsonrpc":"2.0","id":67,"result":{"raw":"0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","tx":{"nonce":"0x0","gasPrice":"0x1","gas":"0x333","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0","value":"0x0","input":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012","v":"0x26","r":"0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e","s":"0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","hash":"0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e"}}} | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### account_sign | ||||
| ### account_signData | ||||
|  | ||||
| #### Sign data | ||||
|    Signs a chunk of data and returns the calculated signature. | ||||
|  | ||||
| #### Arguments | ||||
|   - content type [string]: type of signed data | ||||
|      - `text/validator`: hex data with custom validator defined in a contract | ||||
|      - `application/clique`: [clique](https://github.com/ethereum/EIPs/issues/225) headers | ||||
|      - `text/plain`: simple hex data validated by `account_ecRecover` | ||||
|   - account [address]: account to sign with | ||||
|   - data [data]: data to sign | ||||
|   - data [object]: data to sign | ||||
|  | ||||
| #### Result | ||||
|   - calculated signature [data] | ||||
| @@ -364,8 +371,9 @@ Bash example: | ||||
| { | ||||
|   "id": 3, | ||||
|   "jsonrpc": "2.0", | ||||
|   "method": "account_sign", | ||||
|   "method": "account_signData", | ||||
|   "params": [ | ||||
|     "data/plain", | ||||
|     "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db", | ||||
|     "0xaabbccdd" | ||||
|   ] | ||||
| @@ -381,10 +389,108 @@ Response | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### account_signTypedData | ||||
|  | ||||
| #### Sign data | ||||
|    Signs a chunk of structured data conformant to [EIP712]([EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md)) and returns the calculated signature. | ||||
|  | ||||
| #### Arguments | ||||
|   - account [address]: account to sign with | ||||
|   - data [object]: data to sign | ||||
|  | ||||
| #### Result | ||||
|   - calculated signature [data] | ||||
|    | ||||
| #### Sample call | ||||
| ```json | ||||
| { | ||||
|   "id": 68, | ||||
|   "jsonrpc": "2.0", | ||||
|   "method": "account_signTypedData", | ||||
|   "params": [ | ||||
|     "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", | ||||
|     { | ||||
|       "types": { | ||||
|         "EIP712Domain": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "version", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "chainId", | ||||
|             "type": "uint256" | ||||
|           }, | ||||
|           { | ||||
|             "name": "verifyingContract", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "wallet", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Mail": [ | ||||
|           { | ||||
|             "name": "from", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "to", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "contents", | ||||
|             "type": "string" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "primaryType": "Mail", | ||||
|       "domain": { | ||||
|         "name": "Ether Mail", | ||||
|         "version": "1", | ||||
|         "chainId": 1, | ||||
|         "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
|       }, | ||||
|       "message": { | ||||
|         "from": { | ||||
|           "name": "Cow", | ||||
|           "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" | ||||
|         }, | ||||
|         "to": { | ||||
|           "name": "Bob", | ||||
|           "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" | ||||
|         }, | ||||
|         "contents": "Hello, Bob!" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
| Response | ||||
|  | ||||
| ```json | ||||
| { | ||||
|     "id": 1, | ||||
|     "jsonrpc": "2.0", | ||||
|     "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### account_ecRecover | ||||
|  | ||||
| #### Recover address | ||||
|    Derive the address from the account that was used to sign data from the data and signature. | ||||
| #### Sign data | ||||
|  | ||||
| Derive the address from the account that was used to sign data with content type `text/plain` and the signature. | ||||
|  | ||||
| #### Arguments | ||||
|   - data [data]: data that was signed | ||||
| @@ -400,6 +506,7 @@ Response | ||||
|   "jsonrpc": "2.0", | ||||
|   "method": "account_ecRecover", | ||||
|   "params": [ | ||||
|     "data/plain", | ||||
|     "0xaabbccdd", | ||||
|     "0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c" | ||||
|   ] | ||||
| @@ -413,7 +520,6 @@ Response | ||||
|   "jsonrpc": "2.0", | ||||
|   "result": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db" | ||||
| } | ||||
|  | ||||
| ``` | ||||
|  | ||||
| ### account_import | ||||
| @@ -458,7 +564,7 @@ Response | ||||
|       }, | ||||
|       "id": "09bccb61-b8d3-4e93-bf4f-205a8194f0b9", | ||||
|       "version": 3 | ||||
|     }, | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| ``` | ||||
|   | ||||
| @@ -1,5 +1,15 @@ | ||||
| ### Changelog for external API | ||||
|  | ||||
| #### 5.0.0 | ||||
|  | ||||
| * The external `account_EcRecover`-method was reimplemented. | ||||
| * The external method `account_sign(address, data)` was replaced with `account_signData(contentType, address, data)`. | ||||
| The addition of `contentType` makes it possible to use the method for different types of objects, such as: | ||||
|   * signing data with an intended validator (not yet implemented) | ||||
|   * signing clique headers, | ||||
|   * signing plain personal messages, | ||||
| * The external method `account_signTypedData` implements [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and makes it possible to sign typed data. | ||||
|    | ||||
| #### 4.0.0 | ||||
|  | ||||
| * The external `account_Ecrecover`-method was removed.  | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| ### Changelog for internal API (ui-api) | ||||
|  | ||||
| ### 3.1.0 | ||||
|  | ||||
| * Add `ContentType string` to `SignDataRequest` to accommodate the latest EIP-191 and EIP-712 implementations. | ||||
|  | ||||
| ### 3.0.0 | ||||
|  | ||||
| * Make use of `OnInputRequired(info UserInputRequest)` for obtaining master password during startup | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"math/big" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"os/user" | ||||
| @@ -39,9 +40,11 @@ import ( | ||||
| 	"github.com/ethereum/go-ethereum/cmd/utils" | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
| 	"github.com/ethereum/go-ethereum/console" | ||||
| 	"github.com/ethereum/go-ethereum/core/types" | ||||
| 	"github.com/ethereum/go-ethereum/crypto" | ||||
| 	"github.com/ethereum/go-ethereum/log" | ||||
| 	"github.com/ethereum/go-ethereum/node" | ||||
| 	"github.com/ethereum/go-ethereum/rlp" | ||||
| 	"github.com/ethereum/go-ethereum/rpc" | ||||
| 	"github.com/ethereum/go-ethereum/signer/core" | ||||
| 	"github.com/ethereum/go-ethereum/signer/rules" | ||||
| @@ -623,10 +626,40 @@ func testExternalUI(api *core.SignerAPI) { | ||||
| 	} | ||||
| 	var err error | ||||
|  | ||||
| 	cliqueHeader := types.Header{ | ||||
| 		common.HexToHash("0000H45H"), | ||||
| 		common.HexToHash("0000H45H"), | ||||
| 		common.HexToAddress("0000H45H"), | ||||
| 		common.HexToHash("0000H00H"), | ||||
| 		common.HexToHash("0000H45H"), | ||||
| 		common.HexToHash("0000H45H"), | ||||
| 		types.Bloom{}, | ||||
| 		big.NewInt(1337), | ||||
| 		big.NewInt(1337), | ||||
| 		1338, | ||||
| 		1338, | ||||
| 		big.NewInt(1338), | ||||
| 		[]byte("Extra data Extra data Extra data  Extra data  Extra data  Extra data  Extra data Extra data"), | ||||
| 		common.HexToHash("0x0000H45H"), | ||||
| 		types.BlockNonce{}, | ||||
| 	} | ||||
| 	cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) | ||||
| 	if err != nil { | ||||
| 		utils.Fatalf("Should not error: %v", err) | ||||
| 	} | ||||
| 	addr, err := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") | ||||
| 	if err != nil { | ||||
| 		utils.Fatalf("Should not error: %v", err) | ||||
| 	} | ||||
| 	_, err = api.SignData(ctx, "application/clique", *addr, cliqueRlp) | ||||
| 	checkErr("SignData", err) | ||||
|  | ||||
| 	_, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil) | ||||
| 	checkErr("SignTransaction", err) | ||||
| 	_, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304")) | ||||
| 	checkErr("Sign", err) | ||||
| 	_, err = api.SignData(ctx, "text/plain", common.MixedcaseAddress{}, common.Hex2Bytes("01020304")) | ||||
| 	checkErr("SignData", err) | ||||
| 	//_, err = api.SignTypedData(ctx, common.MixedcaseAddress{}, core.TypedData{}) | ||||
| 	//checkErr("SignTypedData", err) | ||||
| 	_, err = api.List(ctx) | ||||
| 	checkErr("List", err) | ||||
| 	_, err = api.New(ctx) | ||||
| @@ -646,7 +679,6 @@ func testExternalUI(api *core.SignerAPI) { | ||||
| 	} else { | ||||
| 		log.Info("No errors") | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| // getPassPhrase retrieves the password associated with clef, either fetched | ||||
|   | ||||
| @@ -616,7 +616,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c | ||||
| 		log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle)) | ||||
| 	} | ||||
| 	// Sign all the things! | ||||
| 	sighash, err := signFn(accounts.Account{Address: signer}, "application/x-clique-header", CliqueRLP(header)) | ||||
| 	sighash, err := signFn(accounts.Account{Address: signer}, accounts.MimetypeClique, CliqueRLP(header)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|   | ||||
| @@ -17,17 +17,16 @@ | ||||
| package core | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/ethereum/go-ethereum/accounts/abi" | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
|  | ||||
| 	"bytes" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| ) | ||||
|  | ||||
| type decodedArgument struct { | ||||
|   | ||||
| @@ -18,12 +18,11 @@ package core | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"io/ioutil" | ||||
| 	"math/big" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/ethereum/go-ethereum/accounts/abi" | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
|   | ||||
| @@ -30,7 +30,6 @@ 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/log" | ||||
| 	"github.com/ethereum/go-ethereum/rlp" | ||||
| @@ -40,9 +39,9 @@ const ( | ||||
| 	// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive | ||||
| 	numberOfAccountsToDerive = 10 | ||||
| 	// ExternalAPIVersion -- see extapi_changelog.md | ||||
| 	ExternalAPIVersion = "4.0.0" | ||||
| 	ExternalAPIVersion = "5.0.0" | ||||
| 	// InternalAPIVersion -- see intapi_changelog.md | ||||
| 	InternalAPIVersion = "3.0.0" | ||||
| 	InternalAPIVersion = "3.1.0" | ||||
| ) | ||||
|  | ||||
| // ExternalAPI defines the external API through which signing requests are made. | ||||
| @@ -53,8 +52,12 @@ type ExternalAPI interface { | ||||
| 	New(ctx context.Context) (accounts.Account, error) | ||||
| 	// SignTransaction request to sign the specified transaction | ||||
| 	SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) | ||||
| 	// Sign - request to sign the given data (plus prefix) | ||||
| 	Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) | ||||
| 	// SignData - request to sign the given data (plus prefix) | ||||
| 	SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) | ||||
| 	// SignTypedData - request to sign the given structured data (plus prefix) | ||||
| 	SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) | ||||
| 	// EcRecover - recover public key from given message and signature | ||||
| 	EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) | ||||
| 	// Export - request to export an account | ||||
| 	Export(ctx context.Context, addr common.Address) (json.RawMessage, error) | ||||
| 	// Import - request to import an account | ||||
| @@ -177,11 +180,12 @@ type ( | ||||
| 		NewPassword string `json:"new_password"` | ||||
| 	} | ||||
| 	SignDataRequest struct { | ||||
| 		Address common.MixedcaseAddress `json:"address"` | ||||
| 		Rawdata hexutil.Bytes           `json:"raw_data"` | ||||
| 		Message string                  `json:"message"` | ||||
| 		Hash    hexutil.Bytes           `json:"hash"` | ||||
| 		Meta    Metadata                `json:"meta"` | ||||
| 		ContentType string                  `json:"content_type"` | ||||
| 		Address     common.MixedcaseAddress `json:"address"` | ||||
| 		Rawdata     []byte                  `json:"raw_data"` | ||||
| 		Message     []*NameValueType        `json:"message"` | ||||
| 		Hash        hexutil.Bytes           `json:"hash"` | ||||
| 		Meta        Metadata                `json:"meta"` | ||||
| 	} | ||||
| 	SignDataResponse struct { | ||||
| 		Approved bool `json:"approved"` | ||||
| @@ -517,56 +521,6 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth | ||||
|  | ||||
| } | ||||
|  | ||||
| // Sign calculates an Ethereum ECDSA signature for: | ||||
| // keccack256("\x19Ethereum Signed Message:\n" + len(message) + message)) | ||||
| // | ||||
| // 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. | ||||
| // | ||||
| // The key used to calculate the signature is decrypted with the given password. | ||||
| // | ||||
| // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign | ||||
| func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { | ||||
| 	sighash, msg := SignHash(data) | ||||
| 	// We make the request prior to looking up if we actually have the account, to prevent | ||||
| 	// account-enumeration via the API | ||||
| 	req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: MetadataFromContext(ctx)} | ||||
| 	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 | ||||
| 	} | ||||
| 	// Assemble sign the data with the wallet | ||||
| 	signature, err := wallet.SignTextWithPassphrase(account, res.Password, data) | ||||
| 	if err != nil { | ||||
| 		api.UI.ShowError(err.Error()) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper | ||||
| 	return signature, nil | ||||
| } | ||||
|  | ||||
| // SignHash is a helper function that calculates a hash for the given message that can be | ||||
| // safely used to calculate a signature from. | ||||
| // | ||||
| // The hash is calculated as | ||||
| //   keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). | ||||
| // | ||||
| // This gives context to the signed message and prevents signing of transactions. | ||||
| func SignHash(data []byte) ([]byte, string) { | ||||
| 	msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) | ||||
| 	return crypto.Keccak256([]byte(msg)), msg | ||||
| } | ||||
|  | ||||
| // Export returns encrypted private key associated with the given address in web3 keystore format. | ||||
| func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { | ||||
| 	res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(ctx)}) | ||||
|   | ||||
| @@ -244,45 +244,6 @@ func TestNewAcc(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestSignData(t *testing.T) { | ||||
| 	api, control := setup(t) | ||||
| 	//Create two accounts | ||||
| 	createAccount(control, api, t) | ||||
| 	createAccount(control, api, t) | ||||
| 	control <- "1" | ||||
| 	list, err := api.List(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	a := common.NewMixedcaseAddress(list[0]) | ||||
|  | ||||
| 	control <- "Y" | ||||
| 	control <- "wrongpassword" | ||||
| 	h, err := api.Sign(context.Background(), a, []byte("EHLO world")) | ||||
| 	if h != nil { | ||||
| 		t.Errorf("Expected nil-data, got %x", h) | ||||
| 	} | ||||
| 	if err != keystore.ErrDecrypt { | ||||
| 		t.Errorf("Expected ErrLocked! %v", err) | ||||
| 	} | ||||
| 	control <- "No way" | ||||
| 	h, err = api.Sign(context.Background(), a, []byte("EHLO world")) | ||||
| 	if h != nil { | ||||
| 		t.Errorf("Expected nil-data, got %x", h) | ||||
| 	} | ||||
| 	if err != ErrRequestDenied { | ||||
| 		t.Errorf("Expected ErrRequestDenied! %v", err) | ||||
| 	} | ||||
| 	control <- "Y" | ||||
| 	control <- "a_long_password" | ||||
| 	h, err = api.Sign(context.Background(), a, []byte("EHLO world")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if h == nil || len(h) != 65 { | ||||
| 		t.Errorf("Expected 65 byte signature (got %d bytes)", len(h)) | ||||
| 	} | ||||
| } | ||||
| func mkTestTx(from common.MixedcaseAddress) SendTxArgs { | ||||
| 	to := common.NewMixedcaseAddress(common.HexToAddress("0x1337")) | ||||
| 	gas := hexutil.Uint64(21000) | ||||
|   | ||||
| @@ -18,7 +18,6 @@ package core | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"encoding/json" | ||||
|  | ||||
| 	"github.com/ethereum/go-ethereum/accounts" | ||||
| @@ -63,11 +62,27 @@ func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, meth | ||||
| 	return res, e | ||||
| } | ||||
|  | ||||
| func (l *AuditLogger) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { | ||||
| 	l.log.Info("Sign", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"addr", addr.String(), "data", common.Bytes2Hex(data)) | ||||
| 	b, e := l.api.Sign(ctx, addr, data) | ||||
| 	l.log.Info("Sign", "type", "response", "data", common.Bytes2Hex(b), "error", e) | ||||
| func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { | ||||
| 	l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"addr", addr.String(), "data", data, "content-type", contentType) | ||||
| 	b, e := l.api.SignData(ctx, contentType, addr, data) | ||||
| 	l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e) | ||||
| 	return b, e | ||||
| } | ||||
|  | ||||
| func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) { | ||||
| 	l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"addr", addr.String(), "data", data) | ||||
| 	b, e := l.api.SignTypedData(ctx, addr, data) | ||||
| 	l.log.Info("SignTypedData", "type", "response", "data", common.Bytes2Hex(b), "error", e) | ||||
| 	return b, e | ||||
| } | ||||
|  | ||||
| func (l *AuditLogger) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) { | ||||
| 	l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||
| 		"data", common.Bytes2Hex(data), "sig", common.Bytes2Hex(sig)) | ||||
| 	b, e := l.api.EcRecover(ctx, data, sig) | ||||
| 	l.log.Info("EcRecover", "type", "response", "address", b.String(), "error", e) | ||||
| 	return b, e | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,6 @@ import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/davecgh/go-spew/spew" | ||||
| @@ -165,8 +164,12 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp | ||||
|  | ||||
| 	fmt.Printf("-------- Sign data request--------------\n") | ||||
| 	fmt.Printf("Account:  %s\n", request.Address.String()) | ||||
| 	fmt.Printf("message:  \n%q\n", request.Message) | ||||
| 	fmt.Printf("raw data: \n%v\n", request.Rawdata) | ||||
| 	fmt.Printf("message:\n") | ||||
| 	for _, nvt := range request.Message { | ||||
| 		fmt.Printf("%v\n", nvt.Pprint(1)) | ||||
| 	} | ||||
| 	//fmt.Printf("message:  \n%v\n", request.Message) | ||||
| 	fmt.Printf("raw data:  \n%q\n", request.Rawdata) | ||||
| 	fmt.Printf("message hash:  %v\n", request.Hash) | ||||
| 	fmt.Printf("-------------------------------------------\n") | ||||
| 	showMetadata(request.Meta) | ||||
|   | ||||
							
								
								
									
										899
									
								
								signer/core/signed_data.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										899
									
								
								signer/core/signed_data.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,899 @@ | ||||
| // Copyright 2018 The go-ethereum Authors | ||||
| // This file is part of go-ethereum. | ||||
| // | ||||
| // go-ethereum is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // go-ethereum is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. | ||||
| // | ||||
| package core | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 	"mime" | ||||
| 	"regexp" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/ethereum/go-ethereum/accounts" | ||||
| 	"github.com/ethereum/go-ethereum/accounts/abi" | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||
| 	"github.com/ethereum/go-ethereum/common/math" | ||||
| 	"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 { | ||||
| 	Mime        string | ||||
| 	ByteVersion byte | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	TextValidator = SigFormat{ | ||||
| 		accounts.MimetypeTextWithValidator, | ||||
| 		0x00, | ||||
| 	} | ||||
| 	DataTyped = SigFormat{ | ||||
| 		accounts.MimetypeTypedData, | ||||
| 		0x01, | ||||
| 	} | ||||
| 	ApplicationClique = SigFormat{ | ||||
| 		accounts.MimetypeClique, | ||||
| 		0x02, | ||||
| 	} | ||||
| 	TextPlain = SigFormat{ | ||||
| 		accounts.MimetypeTextPlain, | ||||
| 		0x45, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| type ValidatorData struct { | ||||
| 	Address common.Address | ||||
| 	Message hexutil.Bytes | ||||
| } | ||||
|  | ||||
| type TypedData struct { | ||||
| 	Types       Types            `json:"types"` | ||||
| 	PrimaryType string           `json:"primaryType"` | ||||
| 	Domain      TypedDataDomain  `json:"domain"` | ||||
| 	Message     TypedDataMessage `json:"message"` | ||||
| } | ||||
|  | ||||
| type Type struct { | ||||
| 	Name string `json:"name"` | ||||
| 	Type string `json:"type"` | ||||
| } | ||||
|  | ||||
| func (t *Type) isArray() bool { | ||||
| 	return strings.HasSuffix(t.Type, "[]") | ||||
| } | ||||
|  | ||||
| // typeName returns the canonical name of the type. If the type is 'Person[]', then | ||||
| // this method returns 'Person' | ||||
| func (t *Type) typeName() string { | ||||
| 	if strings.HasSuffix(t.Type, "[]") { | ||||
| 		return strings.TrimSuffix(t.Type, "[]") | ||||
| 	} | ||||
| 	return t.Type | ||||
| } | ||||
|  | ||||
| func (t *Type) isReferenceType() bool { | ||||
| 	// Reference types must have a leading uppercase characer | ||||
| 	return unicode.IsUpper([]rune(t.Type)[0]) | ||||
| } | ||||
|  | ||||
| type Types map[string][]Type | ||||
|  | ||||
| type TypePriority struct { | ||||
| 	Type  string | ||||
| 	Value uint | ||||
| } | ||||
|  | ||||
| type TypedDataMessage = map[string]interface{} | ||||
|  | ||||
| type TypedDataDomain struct { | ||||
| 	Name              string   `json:"name"` | ||||
| 	Version           string   `json:"version"` | ||||
| 	ChainId           *big.Int `json:"chainId"` | ||||
| 	VerifyingContract string   `json:"verifyingContract"` | ||||
| 	Salt              string   `json:"salt"` | ||||
| } | ||||
|  | ||||
| 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. | ||||
| func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest) (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 | ||||
| 	} | ||||
| 	// Sign the data with the wallet | ||||
| 	signature, err := wallet.SignDataWithPassphrase(account, res.Password, req.ContentType, req.Hash) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	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, err = api.determineSignatureFormat(ctx, contentType, addr, data) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	signature, err := api.sign(addr, req) | ||||
| 	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, error) { | ||||
| 	var req *SignDataRequest | ||||
|  | ||||
| 	mediaType, _, err := mime.ParseMediaType(contentType) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	switch mediaType { | ||||
| 	case TextValidator.Mime: | ||||
| 		// Data with an intended validator | ||||
| 		validatorData, err := UnmarshalValidatorData(data) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		sighash, msg := SignTextValidator(validatorData) | ||||
| 		message := []*NameValueType{ | ||||
| 			{ | ||||
| 				Name:  "message", | ||||
| 				Typ:   "text", | ||||
| 				Value: msg, | ||||
| 			}, | ||||
| 		} | ||||
| 		req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} | ||||
| 	case ApplicationClique.Mime: | ||||
| 		// Clique is the Ethereum PoA standard | ||||
| 		stringData, ok := data.(string) | ||||
| 		if !ok { | ||||
| 			return nil, fmt.Errorf("input for %v plain must be an hex-encoded string", ApplicationClique.Mime) | ||||
| 		} | ||||
| 		cliqueData, err := hexutil.Decode(stringData) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		header := &types.Header{} | ||||
| 		if err := rlp.DecodeBytes(cliqueData, header); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// Get back the rlp data, encoded by us | ||||
| 		cliqueData = clique.CliqueRLP(header) | ||||
| 		sighash, err := SignCliqueHeader(header) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		message := []*NameValueType{ | ||||
| 			{ | ||||
| 				Name:  "Clique block", | ||||
| 				Typ:   "clique", | ||||
| 				Value: fmt.Sprintf("clique block %d [0x%x]", header.Number, header.Hash()), | ||||
| 			}, | ||||
| 		} | ||||
| 		req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueData, Message: message, 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 | ||||
| 		stringData, ok := data.(string) | ||||
| 		if !ok { | ||||
| 			return nil, fmt.Errorf("input for text/plain must be a string") | ||||
| 		} | ||||
| 		//plainData, err := hexutil.Decode(stringdata) | ||||
| 		//if err != nil { | ||||
| 		//	return nil, err | ||||
| 		//} | ||||
| 		sighash, msg := accounts.TextAndHash([]byte(stringData)) | ||||
| 		message := []*NameValueType{ | ||||
| 			{ | ||||
| 				Name:  "message", | ||||
| 				Typ:   "text/plain", | ||||
| 				Value: msg, | ||||
| 			}, | ||||
| 		} | ||||
| 		req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} | ||||
| 	} | ||||
| 	req.Address = addr | ||||
| 	req.Meta = MetadataFromContext(ctx) | ||||
| 	return req, nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // SignTextWithValidator signs the given message which can be further recovered | ||||
| // with the given validator. | ||||
| // hash = keccak256("\x19\x00"${address}${data}). | ||||
| func SignTextValidator(validatorData ValidatorData) (hexutil.Bytes, string) { | ||||
| 	msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message)) | ||||
| 	fmt.Printf("SignTextValidator:%s\n", msg) | ||||
| 	return crypto.Keccak256([]byte(msg)), msg | ||||
| } | ||||
|  | ||||
| // SignCliqueHeader returns the hash which is used as input for the proof-of-authority | ||||
| // signing. It is the hash of the entire header apart from the 65 byte signature | ||||
| // contained at the end of the extra data. | ||||
| // | ||||
| // The method requires the extra data to be at least 65 bytes -- the original implementation | ||||
| // in clique.go panics if this is the case, thus it's been reimplemented here to avoid the panic | ||||
| // and simply return an error instead | ||||
| func SignCliqueHeader(header *types.Header) (hexutil.Bytes, error) { | ||||
| 	//hash := common.Hash{} | ||||
| 	if len(header.Extra) < 65 { | ||||
| 		return nil, fmt.Errorf("clique header extradata too short, %d < 65", len(header.Extra)) | ||||
| 	} | ||||
| 	hash := clique.SealHash(header) | ||||
| 	return hash.Bytes(), 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) | ||||
| 	message := typedData.Format() | ||||
| 	req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Message: message, Hash: sighash} | ||||
| 	signature, err := api.sign(addr, req) | ||||
| 	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) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return crypto.Keccak256(encodedData), nil | ||||
| } | ||||
|  | ||||
| // Dependencies returns an array of custom types ordered by their hierarchical reference tree | ||||
| func (typedData *TypedData) Dependencies(primaryType string, found []string) []string { | ||||
| 	includes := func(arr []string, str string) bool { | ||||
| 		for _, obj := range arr { | ||||
| 			if obj == str { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if includes(found, primaryType) { | ||||
| 		return found | ||||
| 	} | ||||
| 	if typedData.Types[primaryType] == nil { | ||||
| 		return found | ||||
| 	} | ||||
| 	found = append(found, primaryType) | ||||
| 	for _, field := range typedData.Types[primaryType] { | ||||
| 		for _, dep := range typedData.Dependencies(field.Type, found) { | ||||
| 			if !includes(found, dep) { | ||||
| 				found = append(found, dep) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return found | ||||
| } | ||||
|  | ||||
| // EncodeType generates the following encoding: | ||||
| // `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"` | ||||
| // | ||||
| // each member is written as `type ‖ " " ‖ name` encodings cascade down and are sorted by name | ||||
| func (typedData *TypedData) EncodeType(primaryType string) hexutil.Bytes { | ||||
| 	// Get dependencies primary first, then alphabetical | ||||
| 	deps := typedData.Dependencies(primaryType, []string{}) | ||||
| 	slicedDeps := deps[1:] | ||||
| 	sort.Strings(slicedDeps) | ||||
| 	deps = append([]string{primaryType}, slicedDeps...) | ||||
|  | ||||
| 	// Format as a string with fields | ||||
| 	var buffer bytes.Buffer | ||||
| 	for _, dep := range deps { | ||||
| 		buffer.WriteString(dep) | ||||
| 		buffer.WriteString("(") | ||||
| 		for _, obj := range typedData.Types[dep] { | ||||
| 			buffer.WriteString(obj.Type) | ||||
| 			buffer.WriteString(" ") | ||||
| 			buffer.WriteString(obj.Name) | ||||
| 			buffer.WriteString(",") | ||||
| 		} | ||||
| 		buffer.Truncate(buffer.Len() - 1) | ||||
| 		buffer.WriteString(")") | ||||
| 	} | ||||
| 	return buffer.Bytes() | ||||
| } | ||||
|  | ||||
| // TypeHash creates the keccak256 hash  of the data | ||||
| func (typedData *TypedData) TypeHash(primaryType string) hexutil.Bytes { | ||||
| 	return crypto.Keccak256(typedData.EncodeType(primaryType)) | ||||
| } | ||||
|  | ||||
| // EncodeData generates the following encoding: | ||||
| // `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)` | ||||
| // | ||||
| // each encoded member is 32-byte long | ||||
| func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}, depth int) (hexutil.Bytes, error) { | ||||
| 	if err := typedData.validate(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	buffer := bytes.Buffer{} | ||||
|  | ||||
| 	// Verify extra data | ||||
| 	if len(typedData.Types[primaryType]) < len(data) { | ||||
| 		return nil, errors.New("there is extra data provided in the message") | ||||
| 	} | ||||
|  | ||||
| 	// Add typehash | ||||
| 	buffer.Write(typedData.TypeHash(primaryType)) | ||||
|  | ||||
| 	// Add field contents. Structs and arrays have special handlers. | ||||
| 	for _, field := range typedData.Types[primaryType] { | ||||
| 		encType := field.Type | ||||
| 		encValue := data[field.Name] | ||||
| 		if encType[len(encType)-1:] == "]" { | ||||
| 			arrayValue, ok := encValue.([]interface{}) | ||||
| 			if !ok { | ||||
| 				return nil, dataMismatchError(encType, encValue) | ||||
| 			} | ||||
|  | ||||
| 			arrayBuffer := bytes.Buffer{} | ||||
| 			parsedType := strings.Split(encType, "[")[0] | ||||
| 			for _, item := range arrayValue { | ||||
| 				if typedData.Types[parsedType] != nil { | ||||
| 					mapValue, ok := item.(map[string]interface{}) | ||||
| 					if !ok { | ||||
| 						return nil, dataMismatchError(parsedType, item) | ||||
| 					} | ||||
| 					encodedData, err := typedData.EncodeData(parsedType, mapValue, depth+1) | ||||
| 					if err != nil { | ||||
| 						return nil, err | ||||
| 					} | ||||
| 					arrayBuffer.Write(encodedData) | ||||
| 				} else { | ||||
| 					bytesValue, err := typedData.EncodePrimitiveValue(parsedType, item, depth) | ||||
| 					if err != nil { | ||||
| 						return nil, err | ||||
| 					} | ||||
| 					arrayBuffer.Write(bytesValue) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			buffer.Write(crypto.Keccak256(arrayBuffer.Bytes())) | ||||
| 		} else if typedData.Types[field.Type] != nil { | ||||
| 			mapValue, ok := encValue.(map[string]interface{}) | ||||
| 			if !ok { | ||||
| 				return nil, dataMismatchError(encType, encValue) | ||||
| 			} | ||||
| 			encodedData, err := typedData.EncodeData(field.Type, mapValue, depth+1) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			buffer.Write(crypto.Keccak256(encodedData)) | ||||
| 		} else { | ||||
| 			byteValue, err := typedData.EncodePrimitiveValue(encType, encValue, depth) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			buffer.Write(byteValue) | ||||
| 		} | ||||
| 	} | ||||
| 	return buffer.Bytes(), nil | ||||
| } | ||||
|  | ||||
| // EncodePrimitiveValue deals with the primitive values found | ||||
| // while searching through the typed data | ||||
| func (typedData *TypedData) EncodePrimitiveValue(encType string, encValue interface{}, depth int) ([]byte, error) { | ||||
|  | ||||
| 	switch encType { | ||||
| 	case "address": | ||||
| 		stringValue, ok := encValue.(string) | ||||
| 		if !ok || !common.IsHexAddress(stringValue) { | ||||
| 			return nil, dataMismatchError(encType, encValue) | ||||
| 		} | ||||
| 		retval := make([]byte, 32) | ||||
| 		copy(retval[12:], common.HexToAddress(stringValue).Bytes()) | ||||
| 		return retval, nil | ||||
| 	case "bool": | ||||
| 		boolValue, ok := encValue.(bool) | ||||
| 		if !ok { | ||||
| 			return nil, dataMismatchError(encType, encValue) | ||||
| 		} | ||||
| 		if boolValue { | ||||
| 			return math.PaddedBigBytes(common.Big1, 32), nil | ||||
| 		} | ||||
| 		return math.PaddedBigBytes(common.Big0, 32), nil | ||||
| 	case "string": | ||||
| 		strVal, ok := encValue.(string) | ||||
| 		if !ok { | ||||
| 			return nil, dataMismatchError(encType, encValue) | ||||
| 		} | ||||
| 		return crypto.Keccak256([]byte(strVal)), nil | ||||
| 	case "bytes": | ||||
| 		bytesValue, ok := encValue.([]byte) | ||||
| 		if !ok { | ||||
| 			return nil, dataMismatchError(encType, encValue) | ||||
| 		} | ||||
| 		return crypto.Keccak256(bytesValue), nil | ||||
| 	} | ||||
| 	if strings.HasPrefix(encType, "bytes") { | ||||
| 		lengthStr := strings.TrimPrefix(encType, "bytes") | ||||
| 		length, err := strconv.Atoi(lengthStr) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("invalid size on bytes: %v", lengthStr) | ||||
| 		} | ||||
| 		if length < 0 || length > 32 { | ||||
| 			return nil, fmt.Errorf("invalid size on bytes: %d", length) | ||||
| 		} | ||||
| 		if byteValue, ok := encValue.(hexutil.Bytes); !ok { | ||||
| 			return nil, dataMismatchError(encType, encValue) | ||||
| 		} else { | ||||
| 			return math.PaddedBigBytes(new(big.Int).SetBytes(byteValue), 32), nil | ||||
| 		} | ||||
| 	} | ||||
| 	if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") { | ||||
| 		length := 0 | ||||
| 		if encType == "int" || encType == "uint" { | ||||
| 			length = 256 | ||||
| 		} else { | ||||
| 			lengthStr := "" | ||||
| 			if strings.HasPrefix(encType, "uint") { | ||||
| 				lengthStr = strings.TrimPrefix(encType, "uint") | ||||
| 			} else { | ||||
| 				lengthStr = strings.TrimPrefix(encType, "int") | ||||
| 			} | ||||
| 			atoiSize, err := strconv.Atoi(lengthStr) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("invalid size on integer: %v", lengthStr) | ||||
| 			} | ||||
| 			length = atoiSize | ||||
| 		} | ||||
| 		bigIntValue, ok := encValue.(*big.Int) | ||||
| 		if bigIntValue.BitLen() > length { | ||||
| 			return nil, fmt.Errorf("integer larger than '%v'", encType) | ||||
| 		} | ||||
| 		if !ok { | ||||
| 			return nil, dataMismatchError(encType, encValue) | ||||
| 		} | ||||
| 		return abi.U256(bigIntValue), nil | ||||
| 	} | ||||
| 	return nil, fmt.Errorf("unrecognized type '%s'", encType) | ||||
|  | ||||
| } | ||||
|  | ||||
| // dataMismatchError generates an error for a mismatch between | ||||
| // the provided type and data | ||||
| 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{}) | ||||
| 	if !ok { | ||||
| 		return ValidatorData{}, errors.New("validator input is not a map[string]interface{}") | ||||
| 	} | ||||
| 	addr, ok := raw["address"].(string) | ||||
| 	if !ok { | ||||
| 		return ValidatorData{}, errors.New("validator address is not sent as a string") | ||||
| 	} | ||||
| 	addrBytes, err := hexutil.Decode(addr) | ||||
| 	if err != nil { | ||||
| 		return ValidatorData{}, err | ||||
| 	} | ||||
| 	if !ok || len(addrBytes) == 0 { | ||||
| 		return ValidatorData{}, errors.New("validator address is undefined") | ||||
| 	} | ||||
|  | ||||
| 	message, ok := raw["message"].(string) | ||||
| 	if !ok { | ||||
| 		return ValidatorData{}, errors.New("message is not sent as a string") | ||||
| 	} | ||||
| 	messageBytes, err := hexutil.Decode(message) | ||||
| 	if err != nil { | ||||
| 		return ValidatorData{}, err | ||||
| 	} | ||||
| 	if !ok || len(messageBytes) == 0 { | ||||
| 		return ValidatorData{}, errors.New("message is undefined") | ||||
| 	} | ||||
|  | ||||
| 	return ValidatorData{ | ||||
| 		Address: common.BytesToAddress(addrBytes), | ||||
| 		Message: messageBytes, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // validate makes sure the types are sound | ||||
| func (typedData *TypedData) validate() error { | ||||
| 	if err := typedData.Types.validate(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := typedData.Domain.validate(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Map generates a map version of the typed data | ||||
| func (typedData *TypedData) Map() map[string]interface{} { | ||||
| 	dataMap := map[string]interface{}{ | ||||
| 		"types":       typedData.Types, | ||||
| 		"domain":      typedData.Domain.Map(), | ||||
| 		"primaryType": typedData.PrimaryType, | ||||
| 		"message":     typedData.Message, | ||||
| 	} | ||||
| 	return dataMap | ||||
| } | ||||
|  | ||||
| // PrettyPrint generates a nice output to help the users | ||||
| // of clef present data in their apps | ||||
| func (typedData *TypedData) PrettyPrint() string { | ||||
| 	output := bytes.Buffer{} | ||||
| 	formatted := typedData.Format() | ||||
| 	for _, item := range formatted { | ||||
| 		output.WriteString(fmt.Sprintf("%v\n", item.Pprint(0))) | ||||
| 	} | ||||
| 	return output.String() | ||||
| } | ||||
|  | ||||
| // Format returns a representation of typedData, which can be easily displayed by a user-interface | ||||
| // without in-depth knowledge about 712 rules | ||||
| func (typedData *TypedData) Format() []*NameValueType { | ||||
| 	var nvts []*NameValueType | ||||
| 	nvts = append(nvts, &NameValueType{ | ||||
| 		Name:  "EIP712Domain", | ||||
| 		Value: typedData.formatData("EIP712Domain", typedData.Domain.Map()), | ||||
| 		Typ:   "domain", | ||||
| 	}) | ||||
| 	nvts = append(nvts, &NameValueType{ | ||||
| 		Name:  typedData.PrimaryType, | ||||
| 		Value: typedData.formatData(typedData.PrimaryType, typedData.Message), | ||||
| 		Typ:   "primary type", | ||||
| 	}) | ||||
| 	return nvts | ||||
| } | ||||
|  | ||||
| func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) []*NameValueType { | ||||
| 	var output []*NameValueType | ||||
|  | ||||
| 	// Add field contents. Structs and arrays have special handlers. | ||||
| 	for _, field := range typedData.Types[primaryType] { | ||||
| 		encName := field.Name | ||||
| 		encValue := data[encName] | ||||
| 		item := &NameValueType{ | ||||
| 			Name: encName, | ||||
| 			Typ:  field.Type, | ||||
| 		} | ||||
| 		if field.isArray() { | ||||
| 			arrayValue, _ := encValue.([]interface{}) | ||||
| 			parsedType := field.typeName() | ||||
| 			for _, v := range arrayValue { | ||||
| 				if typedData.Types[parsedType] != nil { | ||||
| 					mapValue, _ := v.(map[string]interface{}) | ||||
| 					mapOutput := typedData.formatData(parsedType, mapValue) | ||||
| 					item.Value = mapOutput | ||||
| 				} else { | ||||
| 					primitiveOutput := formatPrimitiveValue(field.Type, encValue) | ||||
| 					item.Value = primitiveOutput | ||||
| 				} | ||||
| 			} | ||||
| 		} else if typedData.Types[field.Type] != nil { | ||||
| 			mapValue, _ := encValue.(map[string]interface{}) | ||||
| 			mapOutput := typedData.formatData(field.Type, mapValue) | ||||
| 			item.Value = mapOutput | ||||
| 		} else { | ||||
| 			primitiveOutput := formatPrimitiveValue(field.Type, encValue) | ||||
| 			item.Value = primitiveOutput | ||||
| 		} | ||||
| 		output = append(output, item) | ||||
| 	} | ||||
| 	return output | ||||
| } | ||||
|  | ||||
| func formatPrimitiveValue(encType string, encValue interface{}) string { | ||||
| 	switch encType { | ||||
| 	case "address": | ||||
| 		stringValue, _ := encValue.(string) | ||||
| 		return common.HexToAddress(stringValue).String() | ||||
| 	case "bool": | ||||
| 		boolValue, _ := encValue.(bool) | ||||
| 		return fmt.Sprintf("%t", boolValue) | ||||
| 	case "bytes", "string": | ||||
| 		return fmt.Sprintf("%s", encValue) | ||||
| 	} | ||||
| 	if strings.HasPrefix(encType, "bytes") { | ||||
| 		return fmt.Sprintf("%s", encValue) | ||||
| 	} else if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") { | ||||
| 		bigIntValue, _ := encValue.(*big.Int) | ||||
| 		return fmt.Sprintf("%d (0x%x)", bigIntValue, bigIntValue) | ||||
| 	} | ||||
| 	return "NA" | ||||
| } | ||||
|  | ||||
| // NameValueType is a very simple struct with Name, Value and Type. It's meant for simple | ||||
| // json structures used to communicate signing-info about typed data with the UI | ||||
| type NameValueType struct { | ||||
| 	Name  string      `json:"name"` | ||||
| 	Value interface{} `json:"value"` | ||||
| 	Typ   string      `json:"type"` | ||||
| } | ||||
|  | ||||
| // Pprint returns a pretty-printed version of nvt | ||||
| func (nvt *NameValueType) Pprint(depth int) string { | ||||
| 	output := bytes.Buffer{} | ||||
| 	output.WriteString(strings.Repeat("\u00a0", depth*2)) | ||||
| 	output.WriteString(fmt.Sprintf("%s [%s]: ", nvt.Name, nvt.Typ)) | ||||
| 	if nvts, ok := nvt.Value.([]*NameValueType); ok { | ||||
| 		output.WriteString("\n") | ||||
| 		for _, next := range nvts { | ||||
| 			sublevel := next.Pprint(depth + 1) | ||||
| 			output.WriteString(sublevel) | ||||
| 		} | ||||
| 	} else { | ||||
| 		output.WriteString(fmt.Sprintf("%s\n", nvt.Value)) | ||||
| 	} | ||||
| 	return output.String() | ||||
| } | ||||
|  | ||||
| // Validate checks if the types object is conformant to the specs | ||||
| func (t Types) validate() error { | ||||
| 	for typeKey, typeArr := range t { | ||||
| 		for _, typeObj := range typeArr { | ||||
| 			if typeKey == typeObj.Type { | ||||
| 				return fmt.Errorf("type '%s' cannot reference itself", typeObj.Type) | ||||
| 			} | ||||
| 			if typeObj.isReferenceType() { | ||||
| 				if _, exist := t[typeObj.Type]; !exist { | ||||
| 					return fmt.Errorf("reference type '%s' is undefined", typeObj.Type) | ||||
| 				} | ||||
| 				if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) { | ||||
| 					return fmt.Errorf("unknown reference type '%s", typeObj.Type) | ||||
| 				} | ||||
| 			} else if !isPrimitiveTypeValid(typeObj.Type) { | ||||
| 				return fmt.Errorf("unknown type '%s'", typeObj.Type) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Checks if the primitive value is valid | ||||
| func isPrimitiveTypeValid(primitiveType string) bool { | ||||
| 	if primitiveType == "address" || | ||||
| 		primitiveType == "address[]" || | ||||
| 		primitiveType == "bool" || | ||||
| 		primitiveType == "bool[]" || | ||||
| 		primitiveType == "string" || | ||||
| 		primitiveType == "string[]" { | ||||
| 		return true | ||||
| 	} | ||||
| 	if primitiveType == "bytes" || | ||||
| 		primitiveType == "bytes[]" || | ||||
| 		primitiveType == "bytes1" || | ||||
| 		primitiveType == "bytes1[]" || | ||||
| 		primitiveType == "bytes2" || | ||||
| 		primitiveType == "bytes2[]" || | ||||
| 		primitiveType == "bytes3" || | ||||
| 		primitiveType == "bytes3[]" || | ||||
| 		primitiveType == "bytes4" || | ||||
| 		primitiveType == "bytes4[]" || | ||||
| 		primitiveType == "bytes5" || | ||||
| 		primitiveType == "bytes5[]" || | ||||
| 		primitiveType == "bytes6" || | ||||
| 		primitiveType == "bytes6[]" || | ||||
| 		primitiveType == "bytes7" || | ||||
| 		primitiveType == "bytes7[]" || | ||||
| 		primitiveType == "bytes8" || | ||||
| 		primitiveType == "bytes8[]" || | ||||
| 		primitiveType == "bytes9" || | ||||
| 		primitiveType == "bytes9[]" || | ||||
| 		primitiveType == "bytes10" || | ||||
| 		primitiveType == "bytes10[]" || | ||||
| 		primitiveType == "bytes11" || | ||||
| 		primitiveType == "bytes11[]" || | ||||
| 		primitiveType == "bytes12" || | ||||
| 		primitiveType == "bytes12[]" || | ||||
| 		primitiveType == "bytes13" || | ||||
| 		primitiveType == "bytes13[]" || | ||||
| 		primitiveType == "bytes14" || | ||||
| 		primitiveType == "bytes14[]" || | ||||
| 		primitiveType == "bytes15" || | ||||
| 		primitiveType == "bytes15[]" || | ||||
| 		primitiveType == "bytes16" || | ||||
| 		primitiveType == "bytes16[]" || | ||||
| 		primitiveType == "bytes17" || | ||||
| 		primitiveType == "bytes17[]" || | ||||
| 		primitiveType == "bytes18" || | ||||
| 		primitiveType == "bytes18[]" || | ||||
| 		primitiveType == "bytes19" || | ||||
| 		primitiveType == "bytes19[]" || | ||||
| 		primitiveType == "bytes20" || | ||||
| 		primitiveType == "bytes20[]" || | ||||
| 		primitiveType == "bytes21" || | ||||
| 		primitiveType == "bytes21[]" || | ||||
| 		primitiveType == "bytes22" || | ||||
| 		primitiveType == "bytes22[]" || | ||||
| 		primitiveType == "bytes23" || | ||||
| 		primitiveType == "bytes23[]" || | ||||
| 		primitiveType == "bytes24" || | ||||
| 		primitiveType == "bytes24[]" || | ||||
| 		primitiveType == "bytes25" || | ||||
| 		primitiveType == "bytes25[]" || | ||||
| 		primitiveType == "bytes26" || | ||||
| 		primitiveType == "bytes26[]" || | ||||
| 		primitiveType == "bytes27" || | ||||
| 		primitiveType == "bytes27[]" || | ||||
| 		primitiveType == "bytes28" || | ||||
| 		primitiveType == "bytes28[]" || | ||||
| 		primitiveType == "bytes29" || | ||||
| 		primitiveType == "bytes29[]" || | ||||
| 		primitiveType == "bytes30" || | ||||
| 		primitiveType == "bytes30[]" || | ||||
| 		primitiveType == "bytes31" || | ||||
| 		primitiveType == "bytes31[]" { | ||||
| 		return true | ||||
| 	} | ||||
| 	if primitiveType == "int" || | ||||
| 		primitiveType == "int[]" || | ||||
| 		primitiveType == "int8" || | ||||
| 		primitiveType == "int8[]" || | ||||
| 		primitiveType == "int16" || | ||||
| 		primitiveType == "int16[]" || | ||||
| 		primitiveType == "int32" || | ||||
| 		primitiveType == "int32[]" || | ||||
| 		primitiveType == "int64" || | ||||
| 		primitiveType == "int64[]" || | ||||
| 		primitiveType == "int128" || | ||||
| 		primitiveType == "int128[]" || | ||||
| 		primitiveType == "int256" || | ||||
| 		primitiveType == "int256[]" { | ||||
| 		return true | ||||
| 	} | ||||
| 	if primitiveType == "uint" || | ||||
| 		primitiveType == "uint[]" || | ||||
| 		primitiveType == "uint8" || | ||||
| 		primitiveType == "uint8[]" || | ||||
| 		primitiveType == "uint16" || | ||||
| 		primitiveType == "uint16[]" || | ||||
| 		primitiveType == "uint32" || | ||||
| 		primitiveType == "uint32[]" || | ||||
| 		primitiveType == "uint64" || | ||||
| 		primitiveType == "uint64[]" || | ||||
| 		primitiveType == "uint128" || | ||||
| 		primitiveType == "uint128[]" || | ||||
| 		primitiveType == "uint256" || | ||||
| 		primitiveType == "uint256[]" { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // validate checks if the given domain is valid, i.e. contains at least | ||||
| // the minimum viable keys and values | ||||
| func (domain *TypedDataDomain) validate() error { | ||||
| 	if domain.ChainId == big.NewInt(0) { | ||||
| 		return errors.New("chainId must be specified according to EIP-155") | ||||
| 	} | ||||
|  | ||||
| 	if len(domain.Name) == 0 && len(domain.Version) == 0 && len(domain.VerifyingContract) == 0 && len(domain.Salt) == 0 { | ||||
| 		return errors.New("domain is undefined") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Map is a helper function to generate a map version of the domain | ||||
| func (domain *TypedDataDomain) Map() map[string]interface{} { | ||||
| 	dataMap := map[string]interface{}{ | ||||
| 		"chainId": domain.ChainId, | ||||
| 	} | ||||
|  | ||||
| 	if len(domain.Name) > 0 { | ||||
| 		dataMap["name"] = domain.Name | ||||
| 	} | ||||
|  | ||||
| 	if len(domain.Version) > 0 { | ||||
| 		dataMap["version"] = domain.Version | ||||
| 	} | ||||
|  | ||||
| 	if len(domain.VerifyingContract) > 0 { | ||||
| 		dataMap["verifyingContract"] = domain.VerifyingContract | ||||
| 	} | ||||
|  | ||||
| 	if len(domain.Salt) > 0 { | ||||
| 		dataMap["salt"] = domain.Salt | ||||
| 	} | ||||
| 	return dataMap | ||||
| } | ||||
							
								
								
									
										774
									
								
								signer/core/signed_data_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										774
									
								
								signer/core/signed_data_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,774 @@ | ||||
| // Copyright 2018 The go-ethereum Authors | ||||
| // This file is part of go-ethereum. | ||||
| // | ||||
| // go-ethereum is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // go-ethereum is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. | ||||
| // | ||||
| package core | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/ethereum/go-ethereum/accounts/keystore" | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||
| ) | ||||
|  | ||||
| var typesStandard = Types{ | ||||
| 	"EIP712Domain": { | ||||
| 		{ | ||||
| 			Name: "name", | ||||
| 			Type: "string", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "version", | ||||
| 			Type: "string", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "chainId", | ||||
| 			Type: "uint256", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "verifyingContract", | ||||
| 			Type: "address", | ||||
| 		}, | ||||
| 	}, | ||||
| 	"Person": { | ||||
| 		{ | ||||
| 			Name: "name", | ||||
| 			Type: "string", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "wallet", | ||||
| 			Type: "address", | ||||
| 		}, | ||||
| 	}, | ||||
| 	"Mail": { | ||||
| 		{ | ||||
| 			Name: "from", | ||||
| 			Type: "Person", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "to", | ||||
| 			Type: "Person", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "contents", | ||||
| 			Type: "string", | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| var jsonTypedData = ` | ||||
|     { | ||||
|       "types": { | ||||
|         "EIP712Domain": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "version", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "chainId", | ||||
|             "type": "uint256" | ||||
|           }, | ||||
|           { | ||||
|             "name": "verifyingContract", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "test", | ||||
|             "type": "uint8" | ||||
|           }, | ||||
|           { | ||||
|             "name": "wallet", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Mail": [ | ||||
|           { | ||||
|             "name": "from", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "to", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "contents", | ||||
|             "type": "string" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "primaryType": "Mail", | ||||
|       "domain": { | ||||
|         "name": "Ether Mail", | ||||
|         "version": "1", | ||||
|         "chainId": 1, | ||||
|         "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
|       }, | ||||
|       "message": { | ||||
|         "from": { | ||||
|           "name": "Cow", | ||||
| 		  "test": 3, | ||||
|           "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" | ||||
|         }, | ||||
|         "to": { | ||||
|           "name": "Bob", | ||||
|           "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" | ||||
|         }, | ||||
|         "contents": "Hello, Bob!" | ||||
|       } | ||||
|     } | ||||
| ` | ||||
|  | ||||
| const primaryType = "Mail" | ||||
|  | ||||
| var domainStandard = TypedDataDomain{ | ||||
| 	"Ether Mail", | ||||
| 	"1", | ||||
| 	big.NewInt(1), | ||||
| 	"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", | ||||
| 	"", | ||||
| } | ||||
|  | ||||
| var messageStandard = map[string]interface{}{ | ||||
| 	"from": map[string]interface{}{ | ||||
| 		"name":   "Cow", | ||||
| 		"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", | ||||
| 	}, | ||||
| 	"to": map[string]interface{}{ | ||||
| 		"name":   "Bob", | ||||
| 		"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", | ||||
| 	}, | ||||
| 	"contents": "Hello, Bob!", | ||||
| } | ||||
|  | ||||
| var typedData = TypedData{ | ||||
| 	Types:       typesStandard, | ||||
| 	PrimaryType: primaryType, | ||||
| 	Domain:      domainStandard, | ||||
| 	Message:     messageStandard, | ||||
| } | ||||
|  | ||||
| func TestSignData(t *testing.T) { | ||||
| 	api, control := setup(t) | ||||
| 	//Create two accounts | ||||
| 	createAccount(control, api, t) | ||||
| 	createAccount(control, api, t) | ||||
| 	control <- "1" | ||||
| 	list, err := api.List(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	a := common.NewMixedcaseAddress(list[0]) | ||||
|  | ||||
| 	control <- "Y" | ||||
| 	control <- "wrongpassword" | ||||
| 	signature, err := api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) | ||||
| 	if signature != nil { | ||||
| 		t.Errorf("Expected nil-data, got %x", signature) | ||||
| 	} | ||||
| 	if err != keystore.ErrDecrypt { | ||||
| 		t.Errorf("Expected ErrLocked! '%v'", err) | ||||
| 	} | ||||
| 	control <- "No way" | ||||
| 	signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) | ||||
| 	if signature != nil { | ||||
| 		t.Errorf("Expected nil-data, got %x", signature) | ||||
| 	} | ||||
| 	if err != ErrRequestDenied { | ||||
| 		t.Errorf("Expected ErrRequestDenied! '%v'", err) | ||||
| 	} | ||||
| 	// text/plain | ||||
| 	control <- "Y" | ||||
| 	control <- "a_long_password" | ||||
| 	signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if signature == nil || len(signature) != 65 { | ||||
| 		t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) | ||||
| 	} | ||||
| 	// data/typed | ||||
| 	control <- "Y" | ||||
| 	control <- "a_long_password" | ||||
| 	signature, err = api.SignTypedData(context.Background(), a, typedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if signature == nil || len(signature) != 65 { | ||||
| 		t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashStruct(t *testing.T) { | ||||
| 	hash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	mainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) | ||||
| 	if mainHash != "0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e" { | ||||
| 		t.Errorf("Expected different hashStruct result (got %s)", mainHash) | ||||
| 	} | ||||
|  | ||||
| 	hash, err = typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| 	domainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) | ||||
| 	if domainHash != "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f" { | ||||
| 		t.Errorf("Expected different domain hashStruct result (got %s)", domainHash) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEncodeType(t *testing.T) { | ||||
| 	domainTypeEncoding := string(typedData.EncodeType("EIP712Domain")) | ||||
| 	if domainTypeEncoding != "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" { | ||||
| 		t.Errorf("Expected different encodeType result (got %s)", domainTypeEncoding) | ||||
| 	} | ||||
|  | ||||
| 	mailTypeEncoding := string(typedData.EncodeType(typedData.PrimaryType)) | ||||
| 	if mailTypeEncoding != "Mail(Person from,Person to,string contents)Person(string name,address wallet)" { | ||||
| 		t.Errorf("Expected different encodeType result (got %s)", mailTypeEncoding) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestTypeHash(t *testing.T) { | ||||
| 	mailTypeHash := fmt.Sprintf("0x%s", common.Bytes2Hex(typedData.TypeHash(typedData.PrimaryType))) | ||||
| 	if mailTypeHash != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2" { | ||||
| 		t.Errorf("Expected different typeHash result (got %s)", mailTypeHash) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEncodeData(t *testing.T) { | ||||
| 	hash, err := typedData.EncodeData(typedData.PrimaryType, typedData.Message, 0) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	dataEncoding := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) | ||||
| 	if dataEncoding != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" { | ||||
| 		t.Errorf("Expected different encodeData result (got %s)", dataEncoding) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMalformedDomainkeys(t *testing.T) { | ||||
| 	// Verifies that malformed domain keys are properly caught: | ||||
| 	//{ | ||||
| 	//	"name": "Ether Mail", | ||||
| 	//	"version": "1", | ||||
| 	//	"chainId": 1, | ||||
| 	//	"vxerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
| 	//} | ||||
| 	jsonTypedData := ` | ||||
|     { | ||||
|       "types": { | ||||
|         "EIP712Domain": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "version", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "chainId", | ||||
|             "type": "uint256" | ||||
|           }, | ||||
|           { | ||||
|             "name": "verifyingContract", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "wallet", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Mail": [ | ||||
|           { | ||||
|             "name": "from", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "to", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "contents", | ||||
|             "type": "string" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "primaryType": "Mail", | ||||
|       "domain": { | ||||
|         "name": "Ether Mail", | ||||
|         "version": "1", | ||||
|         "chainId": 1, | ||||
|         "vxerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
|       }, | ||||
|       "message": { | ||||
|         "from": { | ||||
|           "name": "Cow", | ||||
|           "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" | ||||
|         }, | ||||
|         "to": { | ||||
|           "name": "Bob", | ||||
|           "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" | ||||
|         }, | ||||
|         "contents": "Hello, Bob!" | ||||
|       } | ||||
|     } | ||||
| ` | ||||
| 	var malformedDomainTypedData TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &malformedDomainTypedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	_, err = malformedDomainTypedData.HashStruct("EIP712Domain", malformedDomainTypedData.Domain.Map()) | ||||
| 	if err == nil || err.Error() != "provided data '<nil>' doesn't match type 'address'" { | ||||
| 		t.Errorf("Expected `provided data '<nil>' doesn't match type 'address'`, got '%v'", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMalformedTypesAndExtradata(t *testing.T) { | ||||
| 	// Verifies several quirks | ||||
| 	// 1. Using dynamic types and only validating the prefix: | ||||
| 	//{ | ||||
| 	//	"name": "chainId", | ||||
| 	//	"type": "uint256 ... and now for something completely different" | ||||
| 	//} | ||||
| 	// 2. Extra data in message: | ||||
| 	//{ | ||||
| 	//  "blahonga": "zonk bonk" | ||||
| 	//} | ||||
| 	jsonTypedData := ` | ||||
|     { | ||||
|       "types": { | ||||
|         "EIP712Domain": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "version", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "chainId", | ||||
|             "type": "uint256 ... and now for something completely different" | ||||
|           }, | ||||
|           { | ||||
|             "name": "verifyingContract", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "wallet", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Mail": [ | ||||
|           { | ||||
|             "name": "from", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "to", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "contents", | ||||
|             "type": "string" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "primaryType": "Mail", | ||||
|       "domain": { | ||||
|         "name": "Ether Mail", | ||||
|         "version": "1", | ||||
|         "chainId": 1, | ||||
|         "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
|       }, | ||||
|       "message": { | ||||
|         "from": { | ||||
|           "name": "Cow", | ||||
|           "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" | ||||
|         }, | ||||
|         "to": { | ||||
|           "name": "Bob", | ||||
|           "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" | ||||
|         }, | ||||
|         "contents": "Hello, Bob!" | ||||
|       } | ||||
|     } | ||||
| ` | ||||
| 	var malformedTypedData TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
|  | ||||
| 	malformedTypedData.Types["EIP712Domain"][2].Type = "uint256" | ||||
| 	malformedTypedData.Message["blahonga"] = "zonk bonk" | ||||
| 	_, err = malformedTypedData.HashStruct(malformedTypedData.PrimaryType, malformedTypedData.Message) | ||||
| 	if err == nil || err.Error() != "there is extra data provided in the message" { | ||||
| 		t.Errorf("Expected `there is extra data provided in the message`, got '%v'", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestTypeMismatch(t *testing.T) { | ||||
| 	// Verifies that: | ||||
| 	// 1. Mismatches between the given type and data, i.e. `Person` and | ||||
| 	// 		the data item is a string, are properly caught: | ||||
| 	//{ | ||||
| 	//	"name": "contents", | ||||
| 	//	"type": "Person" | ||||
| 	//}, | ||||
| 	//{ | ||||
| 	//	"contents": "Hello, Bob!" <-- string not "Person" | ||||
| 	//} | ||||
| 	// 2. Nonexistent types are properly caught: | ||||
| 	//{ | ||||
| 	//	"name": "contents", | ||||
| 	//	"type": "Blahonga" | ||||
| 	//} | ||||
| 	jsonTypedData := ` | ||||
|     { | ||||
|       "types": { | ||||
|         "EIP712Domain": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "version", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "chainId", | ||||
|             "type": "uint256" | ||||
|           }, | ||||
|           { | ||||
|             "name": "verifyingContract", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "wallet", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Mail": [ | ||||
|           { | ||||
|             "name": "from", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "to", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
|             "name": "contents", | ||||
|             "type": "Person" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "primaryType": "Mail", | ||||
|       "domain": { | ||||
|         "name": "Ether Mail", | ||||
|         "version": "1", | ||||
|         "chainId": 1, | ||||
|         "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
|       }, | ||||
|       "message": { | ||||
|         "from": { | ||||
|           "name": "Cow", | ||||
|           "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" | ||||
|         }, | ||||
|         "to": { | ||||
|           "name": "Bob", | ||||
|           "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" | ||||
|         }, | ||||
|         "contents": "Hello, Bob!" | ||||
|       } | ||||
|     } | ||||
| ` | ||||
| 	var mismatchTypedData TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &mismatchTypedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	_, err = mismatchTypedData.HashStruct(mismatchTypedData.PrimaryType, mismatchTypedData.Message) | ||||
| 	if err.Error() != "provided data 'Hello, Bob!' doesn't match type 'Person'" { | ||||
| 		t.Errorf("Expected `provided data 'Hello, Bob!' doesn't match type 'Person'`, got '%v'", err) | ||||
| 	} | ||||
|  | ||||
| 	mismatchTypedData.Types["Mail"][2].Type = "Blahonga" | ||||
| 	_, err = mismatchTypedData.HashStruct(mismatchTypedData.PrimaryType, mismatchTypedData.Message) | ||||
| 	if err == nil || err.Error() != "reference type 'Blahonga' is undefined" { | ||||
| 		t.Fatalf("Expected `reference type 'Blahonga' is undefined`, got '%v'", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestTypeOverflow(t *testing.T) { | ||||
| 	// Verifies data that doesn't fit into it: | ||||
| 	//{ | ||||
| 	//	"test": 65536 <-- test defined as uint8 | ||||
| 	//} | ||||
| 	var overflowTypedData TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &overflowTypedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	// Set test to something outside uint8 | ||||
| 	(overflowTypedData.Message["from"]).(map[string]interface{})["test"] = big.NewInt(65536) | ||||
|  | ||||
| 	_, err = overflowTypedData.HashStruct(overflowTypedData.PrimaryType, overflowTypedData.Message) | ||||
| 	if err == nil || err.Error() != "integer larger than 'uint8'" { | ||||
| 		t.Fatalf("Expected `integer larger than 'uint8'`, got '%v'", err) | ||||
| 	} | ||||
|  | ||||
| 	(overflowTypedData.Message["from"]).(map[string]interface{})["test"] = big.NewInt(3) | ||||
| 	(overflowTypedData.Message["to"]).(map[string]interface{})["test"] = big.NewInt(4) | ||||
|  | ||||
| 	_, err = overflowTypedData.HashStruct(overflowTypedData.PrimaryType, overflowTypedData.Message) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no err, got '%v'", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestArray(t *testing.T) { | ||||
| 	// Makes sure that arrays work fine | ||||
| 	//{ | ||||
| 	//	"type": "address[]" | ||||
| 	//}, | ||||
| 	//{ | ||||
| 	//	"type": "string[]" | ||||
| 	//}, | ||||
| 	//{ | ||||
| 	//	"type": "uint16[]", | ||||
| 	//} | ||||
|  | ||||
| 	jsonTypedData := ` | ||||
| 		{ | ||||
| 	      "types": { | ||||
| 	        "EIP712Domain": [ | ||||
| 	          { | ||||
| 	            "name": "name", | ||||
| 	            "type": "string" | ||||
| 	          }, | ||||
| 	          { | ||||
| 	            "name": "version", | ||||
| 	            "type": "string" | ||||
| 	          }, | ||||
| 	          { | ||||
| 	            "name": "chainId", | ||||
| 	            "type": "uint256" | ||||
| 	          }, | ||||
| 	          { | ||||
| 	            "name": "verifyingContract", | ||||
| 	            "type": "address" | ||||
| 	          } | ||||
| 	        ], | ||||
| 	        "Foo": [ | ||||
| 	          { | ||||
| 	            "name": "bar", | ||||
| 	            "type": "address[]" | ||||
| 	          } | ||||
| 	        ] | ||||
| 	      }, | ||||
| 	      "primaryType": "Foo", | ||||
| 	      "domain": { | ||||
| 	        "name": "Lorem", | ||||
| 	        "version": "1", | ||||
| 	        "chainId": 1, | ||||
| 	        "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
| 	      }, | ||||
| 	      "message": { | ||||
| 	        "bar": [ | ||||
| 	        	"0x0000000000000000000000000000000000000001", | ||||
| 	        	"0x0000000000000000000000000000000000000002", | ||||
| 	        	"0x0000000000000000000000000000000000000003" | ||||
|         	] | ||||
| 	      } | ||||
| 	    } | ||||
| 	` | ||||
| 	var arrayTypedData TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &arrayTypedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	_, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no err, got '%v'", err) | ||||
| 	} | ||||
|  | ||||
| 	// Change array to string | ||||
| 	arrayTypedData.Types["Foo"][0].Type = "string[]" | ||||
| 	arrayTypedData.Message["bar"] = []interface{}{ | ||||
| 		"lorem", | ||||
| 		"ipsum", | ||||
| 		"dolores", | ||||
| 	} | ||||
| 	_, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no err, got '%v'", err) | ||||
| 	} | ||||
|  | ||||
| 	// Change array to uint | ||||
| 	arrayTypedData.Types["Foo"][0].Type = "uint[]" | ||||
| 	arrayTypedData.Message["bar"] = []interface{}{ | ||||
| 		big.NewInt(1955), | ||||
| 		big.NewInt(108), | ||||
| 		big.NewInt(44010), | ||||
| 	} | ||||
| 	_, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no err, got '%v'", err) | ||||
| 	} | ||||
|  | ||||
| 	// Should not work with fixed-size arrays | ||||
| 	arrayTypedData.Types["Foo"][0].Type = "uint[3]" | ||||
| 	_, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message) | ||||
| 	if err == nil || err.Error() != "unknown type 'uint[3]'" { | ||||
| 		t.Fatalf("Expected `unknown type 'uint[3]'`, got '%v'", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCustomTypeAsArray(t *testing.T) { | ||||
| 	var jsonTypedData = ` | ||||
|     { | ||||
|       "types": { | ||||
|         "EIP712Domain": [ | ||||
|           { | ||||
|             "name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
| 			"name": "version", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
|             "name": "chainId", | ||||
|             "type": "uint256" | ||||
|           }, | ||||
|           { | ||||
| 			"name": "verifyingContract", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person": [ | ||||
|           { | ||||
| 			"name": "name", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           { | ||||
| 			"name": "wallet", | ||||
|             "type": "address" | ||||
|           } | ||||
|         ], | ||||
|         "Person[]": [ | ||||
|           { | ||||
| 			"name": "baz", | ||||
|             "type": "string" | ||||
|           } | ||||
| 		], | ||||
|         "Mail": [ | ||||
|           { | ||||
| 			"name": "from", | ||||
|             "type": "Person" | ||||
|           }, | ||||
|           { | ||||
| 			"name": "to", | ||||
|             "type": "Person[]" | ||||
|           }, | ||||
|           { | ||||
| 			"name": "contents", | ||||
|             "type": "string" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "primaryType": "Mail", | ||||
|       "domain": { | ||||
| 		"name": "Ether Mail", | ||||
|         "version": "1", | ||||
|         "chainId": 1, | ||||
|         "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" | ||||
|       }, | ||||
|       "message": { | ||||
|         "from": { | ||||
| 			"name": "Cow", | ||||
| 			"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" | ||||
|         }, | ||||
|         "to": {"baz": "foo"}, | ||||
|         "contents": "Hello, Bob!" | ||||
|       } | ||||
|     } | ||||
|  | ||||
| ` | ||||
| 	var malformedTypedData TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	_, err = malformedTypedData.HashStruct("EIP712Domain", malformedTypedData.Domain.Map()) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error, got '%v'", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFormatter(t *testing.T) { | ||||
|  | ||||
| 	var d TypedData | ||||
| 	err := json.Unmarshal([]byte(jsonTypedData), &d) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unmarshalling failed '%v'", err) | ||||
| 	} | ||||
| 	formatted := d.Format() | ||||
| 	for _, item := range formatted { | ||||
| 		fmt.Printf("'%v'\n", item.Pprint(0)) | ||||
| 	} | ||||
|  | ||||
| 	j, _ := json.Marshal(formatted) | ||||
| 	fmt.Printf("'%v'\n", string(j)) | ||||
| } | ||||
| @@ -19,9 +19,8 @@ package core | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"math/big" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/ethereum/go-ethereum/accounts" | ||||
| 	"github.com/ethereum/go-ethereum/common" | ||||
|   | ||||
| @@ -624,7 +624,7 @@ func TestSignData(t *testing.T) { | ||||
| function ApproveSignData(r){ | ||||
|     if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") | ||||
|     { | ||||
|         if(r.message.indexOf("bazonk") >= 0){ | ||||
|         if(r.message[0].value.indexOf("bazonk") >= 0){ | ||||
|             return "Approve" | ||||
|         } | ||||
|         return "Reject" | ||||
| @@ -636,18 +636,25 @@ function ApproveSignData(r){ | ||||
| 		t.Errorf("Couldn't create evaluator %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 	message := []byte("baz bazonk foo") | ||||
| 	hash, msg := core.SignHash(message) | ||||
| 	raw := hexutil.Bytes(message) | ||||
| 	message := "baz bazonk foo" | ||||
| 	hash, rawdata := accounts.TextAndHash([]byte(message)) | ||||
| 	addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa") | ||||
|  | ||||
| 	fmt.Printf("address %v %v\n", addr.String(), addr.Original()) | ||||
|  | ||||
| 	nvt := []*core.NameValueType{ | ||||
| 		{ | ||||
| 			Name:  "message", | ||||
| 			Typ:   "text/plain", | ||||
| 			Value: message, | ||||
| 		}, | ||||
| 	} | ||||
| 	resp, err := r.ApproveSignData(&core.SignDataRequest{ | ||||
| 		Address: *addr, | ||||
| 		Message: msg, | ||||
| 		Message: nvt, | ||||
| 		Hash:    hash, | ||||
| 		Meta:    core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, | ||||
| 		Rawdata: raw, | ||||
| 		Rawdata: []byte(rawdata), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unexpected error %v", err) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user