github.com/luckypickle/go-ethereum-vet@v1.14.2/signer/core/api.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package core 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io/ioutil" 25 "math/big" 26 "reflect" 27 28 "github.com/luckypickle/go-ethereum-vet/accounts" 29 "github.com/luckypickle/go-ethereum-vet/accounts/keystore" 30 "github.com/luckypickle/go-ethereum-vet/accounts/usbwallet" 31 "github.com/luckypickle/go-ethereum-vet/common" 32 "github.com/luckypickle/go-ethereum-vet/common/hexutil" 33 "github.com/luckypickle/go-ethereum-vet/crypto" 34 "github.com/luckypickle/go-ethereum-vet/internal/ethapi" 35 "github.com/luckypickle/go-ethereum-vet/log" 36 "github.com/luckypickle/go-ethereum-vet/rlp" 37 ) 38 39 // ExternalAPI defines the external API through which signing requests are made. 40 type ExternalAPI interface { 41 // List available accounts 42 List(ctx context.Context) (Accounts, error) 43 // New request to create a new account 44 New(ctx context.Context) (accounts.Account, error) 45 // SignTransaction request to sign the specified transaction 46 SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) 47 // Sign - request to sign the given data (plus prefix) 48 Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) 49 // EcRecover - request to perform ecrecover 50 EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) 51 // Export - request to export an account 52 Export(ctx context.Context, addr common.Address) (json.RawMessage, error) 53 // Import - request to import an account 54 Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) 55 } 56 57 // SignerUI specifies what method a UI needs to implement to be able to be used as a UI for the signer 58 type SignerUI interface { 59 // ApproveTx prompt the user for confirmation to request to sign Transaction 60 ApproveTx(request *SignTxRequest) (SignTxResponse, error) 61 // ApproveSignData prompt the user for confirmation to request to sign data 62 ApproveSignData(request *SignDataRequest) (SignDataResponse, error) 63 // ApproveExport prompt the user for confirmation to export encrypted Account json 64 ApproveExport(request *ExportRequest) (ExportResponse, error) 65 // ApproveImport prompt the user for confirmation to import Account json 66 ApproveImport(request *ImportRequest) (ImportResponse, error) 67 // ApproveListing prompt the user for confirmation to list accounts 68 // the list of accounts to list can be modified by the UI 69 ApproveListing(request *ListRequest) (ListResponse, error) 70 // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller 71 ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) 72 // ShowError displays error message to user 73 ShowError(message string) 74 // ShowInfo displays info message to user 75 ShowInfo(message string) 76 // OnApprovedTx notifies the UI about a transaction having been successfully signed. 77 // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. 78 OnApprovedTx(tx ethapi.SignTransactionResult) 79 // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version 80 // information 81 OnSignerStartup(info StartupInfo) 82 } 83 84 // SignerAPI defines the actual implementation of ExternalAPI 85 type SignerAPI struct { 86 chainID *big.Int 87 am *accounts.Manager 88 UI SignerUI 89 validator *Validator 90 } 91 92 // Metadata about a request 93 type Metadata struct { 94 Remote string `json:"remote"` 95 Local string `json:"local"` 96 Scheme string `json:"scheme"` 97 } 98 99 // MetadataFromContext extracts Metadata from a given context.Context 100 func MetadataFromContext(ctx context.Context) Metadata { 101 m := Metadata{"NA", "NA", "NA"} // batman 102 103 if v := ctx.Value("remote"); v != nil { 104 m.Remote = v.(string) 105 } 106 if v := ctx.Value("scheme"); v != nil { 107 m.Scheme = v.(string) 108 } 109 if v := ctx.Value("local"); v != nil { 110 m.Local = v.(string) 111 } 112 return m 113 } 114 115 // String implements Stringer interface 116 func (m Metadata) String() string { 117 s, err := json.Marshal(m) 118 if err == nil { 119 return string(s) 120 } 121 return err.Error() 122 } 123 124 // types for the requests/response types between signer and UI 125 type ( 126 // SignTxRequest contains info about a Transaction to sign 127 SignTxRequest struct { 128 Transaction SendTxArgs `json:"transaction"` 129 Callinfo []ValidationInfo `json:"call_info"` 130 Meta Metadata `json:"meta"` 131 } 132 // SignTxResponse result from SignTxRequest 133 SignTxResponse struct { 134 //The UI may make changes to the TX 135 Transaction SendTxArgs `json:"transaction"` 136 Approved bool `json:"approved"` 137 Password string `json:"password"` 138 } 139 // ExportRequest info about query to export accounts 140 ExportRequest struct { 141 Address common.Address `json:"address"` 142 Meta Metadata `json:"meta"` 143 } 144 // ExportResponse response to export-request 145 ExportResponse struct { 146 Approved bool `json:"approved"` 147 } 148 // ImportRequest info about request to import an Account 149 ImportRequest struct { 150 Meta Metadata `json:"meta"` 151 } 152 ImportResponse struct { 153 Approved bool `json:"approved"` 154 OldPassword string `json:"old_password"` 155 NewPassword string `json:"new_password"` 156 } 157 SignDataRequest struct { 158 Address common.MixedcaseAddress `json:"address"` 159 Rawdata hexutil.Bytes `json:"raw_data"` 160 Message string `json:"message"` 161 Hash hexutil.Bytes `json:"hash"` 162 Meta Metadata `json:"meta"` 163 } 164 SignDataResponse struct { 165 Approved bool `json:"approved"` 166 Password string 167 } 168 NewAccountRequest struct { 169 Meta Metadata `json:"meta"` 170 } 171 NewAccountResponse struct { 172 Approved bool `json:"approved"` 173 Password string `json:"password"` 174 } 175 ListRequest struct { 176 Accounts []Account `json:"accounts"` 177 Meta Metadata `json:"meta"` 178 } 179 ListResponse struct { 180 Accounts []Account `json:"accounts"` 181 } 182 Message struct { 183 Text string `json:"text"` 184 } 185 StartupInfo struct { 186 Info map[string]interface{} `json:"info"` 187 } 188 ) 189 190 var ErrRequestDenied = errors.New("Request denied") 191 192 // NewSignerAPI creates a new API that can be used for Account management. 193 // ksLocation specifies the directory where to store the password protected private 194 // key that is generated when a new Account is created. 195 // noUSB disables USB support that is required to support hardware devices such as 196 // ledger and trezor. 197 func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abidb *AbiDb, lightKDF bool) *SignerAPI { 198 var ( 199 backends []accounts.Backend 200 n, p = keystore.StandardScryptN, keystore.StandardScryptP 201 ) 202 if lightKDF { 203 n, p = keystore.LightScryptN, keystore.LightScryptP 204 } 205 // support password based accounts 206 if len(ksLocation) > 0 { 207 backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) 208 } 209 if !noUSB { 210 // Start a USB hub for Ledger hardware wallets 211 if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { 212 log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) 213 } else { 214 backends = append(backends, ledgerhub) 215 log.Debug("Ledger support enabled") 216 } 217 // Start a USB hub for Trezor hardware wallets 218 if trezorhub, err := usbwallet.NewTrezorHub(); err != nil { 219 log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err)) 220 } else { 221 backends = append(backends, trezorhub) 222 log.Debug("Trezor support enabled") 223 } 224 } 225 return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb)} 226 } 227 228 // List returns the set of wallet this signer manages. Each wallet can contain 229 // multiple accounts. 230 func (api *SignerAPI) List(ctx context.Context) (Accounts, error) { 231 var accs []Account 232 for _, wallet := range api.am.Wallets() { 233 for _, acc := range wallet.Accounts() { 234 acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address} 235 accs = append(accs, acc) 236 } 237 } 238 result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) 239 if err != nil { 240 return nil, err 241 } 242 if result.Accounts == nil { 243 return nil, ErrRequestDenied 244 245 } 246 return result.Accounts, nil 247 } 248 249 // New creates a new password protected Account. The private key is protected with 250 // the given password. Users are responsible to backup the private key that is stored 251 // in the keystore location thas was specified when this API was created. 252 func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) { 253 be := api.am.Backends(keystore.KeyStoreType) 254 if len(be) == 0 { 255 return accounts.Account{}, errors.New("password based accounts not supported") 256 } 257 resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}) 258 259 if err != nil { 260 return accounts.Account{}, err 261 } 262 if !resp.Approved { 263 return accounts.Account{}, ErrRequestDenied 264 } 265 return be[0].(*keystore.KeyStore).NewAccount(resp.Password) 266 } 267 268 // logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. 269 // it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow 270 // UI-modifications to requests 271 func logDiff(original *SignTxRequest, new *SignTxResponse) bool { 272 modified := false 273 if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { 274 log.Info("Sender-account changed by UI", "was", f0, "is", f1) 275 modified = true 276 } 277 if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { 278 log.Info("Recipient-account changed by UI", "was", t0, "is", t1) 279 modified = true 280 } 281 if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 { 282 modified = true 283 log.Info("Gas changed by UI", "was", g0, "is", g1) 284 } 285 if g0, g1 := big.Int(original.Transaction.GasPrice), big.Int(new.Transaction.GasPrice); g0.Cmp(&g1) != 0 { 286 modified = true 287 log.Info("GasPrice changed by UI", "was", g0, "is", g1) 288 } 289 if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { 290 modified = true 291 log.Info("Value changed by UI", "was", v0, "is", v1) 292 } 293 if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { 294 d0s := "" 295 d1s := "" 296 if d0 != nil { 297 d0s = common.ToHex(*d0) 298 } 299 if d1 != nil { 300 d1s = common.ToHex(*d1) 301 } 302 if d1s != d0s { 303 modified = true 304 log.Info("Data changed by UI", "was", d0s, "is", d1s) 305 } 306 } 307 if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { 308 modified = true 309 log.Info("Nonce changed by UI", "was", n0, "is", n1) 310 } 311 return modified 312 } 313 314 // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form 315 func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { 316 var ( 317 err error 318 result SignTxResponse 319 ) 320 msgs, err := api.validator.ValidateTransaction(&args, methodSelector) 321 if err != nil { 322 return nil, err 323 } 324 325 req := SignTxRequest{ 326 Transaction: args, 327 Meta: MetadataFromContext(ctx), 328 Callinfo: msgs.Messages, 329 } 330 // Process approval 331 result, err = api.UI.ApproveTx(&req) 332 if err != nil { 333 return nil, err 334 } 335 if !result.Approved { 336 return nil, ErrRequestDenied 337 } 338 // Log changes made by the UI to the signing-request 339 logDiff(&req, &result) 340 var ( 341 acc accounts.Account 342 wallet accounts.Wallet 343 ) 344 acc = accounts.Account{Address: result.Transaction.From.Address()} 345 wallet, err = api.am.Find(acc) 346 if err != nil { 347 return nil, err 348 } 349 // Convert fields into a real transaction 350 var unsignedTx = result.Transaction.toTransaction() 351 352 // The one to sign is the one that was returned from the UI 353 signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, unsignedTx, api.chainID) 354 if err != nil { 355 api.UI.ShowError(err.Error()) 356 return nil, err 357 } 358 359 rlpdata, err := rlp.EncodeToBytes(signedTx) 360 response := ethapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx} 361 362 // Finally, send the signed tx to the UI 363 api.UI.OnApprovedTx(response) 364 // ...and to the external caller 365 return &response, nil 366 367 } 368 369 // Sign calculates an Ethereum ECDSA signature for: 370 // keccack256("\x19Ethereum Signed Message:\n" + len(message) + message)) 371 // 372 // Note, the produced signature conforms to the secp256k1 curve R, S and V values, 373 // where the V value will be 27 or 28 for legacy reasons. 374 // 375 // The key used to calculate the signature is decrypted with the given password. 376 // 377 // https://github.com/luckypickle/go-ethereum-vet/wiki/Management-APIs#personal_sign 378 func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { 379 sighash, msg := SignHash(data) 380 // We make the request prior to looking up if we actually have the account, to prevent 381 // account-enumeration via the API 382 req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: MetadataFromContext(ctx)} 383 res, err := api.UI.ApproveSignData(req) 384 385 if err != nil { 386 return nil, err 387 } 388 if !res.Approved { 389 return nil, ErrRequestDenied 390 } 391 // Look up the wallet containing the requested signer 392 account := accounts.Account{Address: addr.Address()} 393 wallet, err := api.am.Find(account) 394 if err != nil { 395 return nil, err 396 } 397 // Assemble sign the data with the wallet 398 signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash) 399 if err != nil { 400 api.UI.ShowError(err.Error()) 401 return nil, err 402 } 403 signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper 404 return signature, nil 405 } 406 407 // EcRecover returns the address for the Account that was used to create the signature. 408 // Note, this function is compatible with eth_sign and personal_sign. As such it recovers 409 // the address of: 410 // hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message}) 411 // addr = ecrecover(hash, signature) 412 // 413 // Note, the signature must conform to the secp256k1 curve R, S and V values, where 414 // the V value must be be 27 or 28 for legacy reasons. 415 // 416 // https://github.com/luckypickle/go-ethereum-vet/wiki/Management-APIs#personal_ecRecover 417 func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) { 418 if len(sig) != 65 { 419 return common.Address{}, fmt.Errorf("signature must be 65 bytes long") 420 } 421 if sig[64] != 27 && sig[64] != 28 { 422 return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)") 423 } 424 sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 425 hash, _ := SignHash(data) 426 rpk, err := crypto.SigToPub(hash, sig) 427 if err != nil { 428 return common.Address{}, err 429 } 430 return crypto.PubkeyToAddress(*rpk), nil 431 } 432 433 // SignHash is a helper function that calculates a hash for the given message that can be 434 // safely used to calculate a signature from. 435 // 436 // The hash is calculated as 437 // 438 // keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). 439 // 440 // This gives context to the signed message and prevents signing of transactions. 441 func SignHash(data []byte) ([]byte, string) { 442 msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) 443 return crypto.Keccak256([]byte(msg)), msg 444 } 445 446 // Export returns encrypted private key associated with the given address in web3 keystore format. 447 func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { 448 res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(ctx)}) 449 450 if err != nil { 451 return nil, err 452 } 453 if !res.Approved { 454 return nil, ErrRequestDenied 455 } 456 // Look up the wallet containing the requested signer 457 wallet, err := api.am.Find(accounts.Account{Address: addr}) 458 if err != nil { 459 return nil, err 460 } 461 if wallet.URL().Scheme != keystore.KeyStoreScheme { 462 return nil, fmt.Errorf("Account is not a keystore-account") 463 } 464 return ioutil.ReadFile(wallet.URL().Path) 465 } 466 467 // Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be 468 // in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful 469 // decryption it will encrypt the key with the given newPassphrase and store it in the keystore. 470 func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) { 471 be := api.am.Backends(keystore.KeyStoreType) 472 473 if len(be) == 0 { 474 return Account{}, errors.New("password based accounts not supported") 475 } 476 res, err := api.UI.ApproveImport(&ImportRequest{Meta: MetadataFromContext(ctx)}) 477 478 if err != nil { 479 return Account{}, err 480 } 481 if !res.Approved { 482 return Account{}, ErrRequestDenied 483 } 484 acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword) 485 if err != nil { 486 api.UI.ShowError(err.Error()) 487 return Account{}, err 488 } 489 return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil 490 }