github.com/ConsenSys/Quorum@v20.10.0+incompatible/signer/core/api.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser 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 // The go-ethereum library 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 Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. 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 "math/big" 25 "os" 26 "reflect" 27 28 "github.com/ethereum/go-ethereum/accounts" 29 "github.com/ethereum/go-ethereum/accounts/keystore" 30 "github.com/ethereum/go-ethereum/accounts/pluggable" 31 "github.com/ethereum/go-ethereum/accounts/scwallet" 32 "github.com/ethereum/go-ethereum/accounts/usbwallet" 33 "github.com/ethereum/go-ethereum/common" 34 "github.com/ethereum/go-ethereum/common/hexutil" 35 "github.com/ethereum/go-ethereum/internal/ethapi" 36 "github.com/ethereum/go-ethereum/log" 37 "github.com/ethereum/go-ethereum/plugin" 38 "github.com/ethereum/go-ethereum/rlp" 39 "github.com/ethereum/go-ethereum/signer/storage" 40 ) 41 42 const ( 43 // numberOfAccountsToDerive For hardware wallets, the number of accounts to derive 44 numberOfAccountsToDerive = 10 45 // ExternalAPIVersion -- see extapi_changelog.md 46 ExternalAPIVersion = "6.0.0" 47 // InternalAPIVersion -- see intapi_changelog.md 48 InternalAPIVersion = "7.0.0" 49 ) 50 51 // ExternalAPI defines the external API through which signing requests are made. 52 type ExternalAPI interface { 53 // List available accounts 54 List(ctx context.Context) ([]common.Address, error) 55 // New request to create a new account 56 New(ctx context.Context) (common.Address, error) 57 // SignTransaction request to sign the specified transaction 58 SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) 59 // SignData - request to sign the given data (plus prefix) 60 SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) 61 // SignTypedData - request to sign the given structured data (plus prefix) 62 SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) 63 // EcRecover - recover public key from given message and signature 64 EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) 65 // Version info about the APIs 66 Version(ctx context.Context) (string, error) 67 } 68 69 // UIClientAPI specifies what method a UI needs to implement to be able to be used as a 70 // UI for the signer 71 type UIClientAPI interface { 72 // ApproveTx prompt the user for confirmation to request to sign Transaction 73 ApproveTx(request *SignTxRequest) (SignTxResponse, error) 74 // ApproveSignData prompt the user for confirmation to request to sign data 75 ApproveSignData(request *SignDataRequest) (SignDataResponse, error) 76 // ApproveListing prompt the user for confirmation to list accounts 77 // the list of accounts to list can be modified by the UI 78 ApproveListing(request *ListRequest) (ListResponse, error) 79 // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller 80 ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) 81 // ShowError displays error message to user 82 ShowError(message string) 83 // ShowInfo displays info message to user 84 ShowInfo(message string) 85 // OnApprovedTx notifies the UI about a transaction having been successfully signed. 86 // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. 87 OnApprovedTx(tx ethapi.SignTransactionResult) 88 // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version 89 // information 90 OnSignerStartup(info StartupInfo) 91 // OnInputRequired is invoked when clef requires user input, for example master password or 92 // pin-code for unlocking hardware wallets 93 OnInputRequired(info UserInputRequest) (UserInputResponse, error) 94 // RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication 95 RegisterUIServer(api *UIServerAPI) 96 } 97 98 // Validator defines the methods required to validate a transaction against some 99 // sanity defaults as well as any underlying 4byte method database. 100 // 101 // Use fourbyte.Database as an implementation. It is separated out of this package 102 // to allow pieces of the signer package to be used without having to load the 103 // 7MB embedded 4byte dump. 104 type Validator interface { 105 // ValidateTransaction does a number of checks on the supplied transaction, and 106 // returns either a list of warnings, or an error (indicating that the transaction 107 // should be immediately rejected). 108 ValidateTransaction(selector *string, tx *SendTxArgs) (*ValidationMessages, error) 109 } 110 111 // SignerAPI defines the actual implementation of ExternalAPI 112 type SignerAPI struct { 113 chainID *big.Int 114 am *accounts.Manager 115 UI UIClientAPI 116 validator Validator 117 rejectMode bool 118 credentials storage.Storage 119 } 120 121 // Metadata about a request 122 type Metadata struct { 123 Remote string `json:"remote"` 124 Local string `json:"local"` 125 Scheme string `json:"scheme"` 126 UserAgent string `json:"User-Agent"` 127 Origin string `json:"Origin"` 128 } 129 130 func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, plugins *plugin.Settings, scpath string) *accounts.Manager { 131 var ( 132 backends []accounts.Backend 133 n, p = keystore.StandardScryptN, keystore.StandardScryptP 134 ) 135 if lightKDF { 136 n, p = keystore.LightScryptN, keystore.LightScryptP 137 } 138 // support password based accounts 139 if len(ksLocation) > 0 { 140 backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) 141 } 142 if !nousb { 143 // Start a USB hub for Ledger hardware wallets 144 if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { 145 log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) 146 } else { 147 backends = append(backends, ledgerhub) 148 log.Debug("Ledger support enabled") 149 } 150 // Start a USB hub for Trezor hardware wallets (HID version) 151 if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil { 152 log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err)) 153 } else { 154 backends = append(backends, trezorhub) 155 log.Debug("Trezor support enabled via HID") 156 } 157 // Start a USB hub for Trezor hardware wallets (WebUSB version) 158 if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil { 159 log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err)) 160 } else { 161 backends = append(backends, trezorhub) 162 log.Debug("Trezor support enabled via WebUSB") 163 } 164 } 165 // <Quorum> 166 if plugins != nil { 167 if _, ok := plugins.Providers[plugin.AccountPluginInterfaceName]; ok { 168 pluginBackend := pluggable.NewBackend() 169 backends = append(backends, pluginBackend) 170 } 171 } 172 // </Quorum> 173 174 // Start a smart card hub 175 if len(scpath) > 0 { 176 // Sanity check that the smartcard path is valid 177 fi, err := os.Stat(scpath) 178 if err != nil { 179 log.Info("Smartcard socket file missing, disabling", "err", err) 180 } else { 181 if fi.Mode()&os.ModeType != os.ModeSocket { 182 log.Error("Invalid smartcard socket file type", "path", scpath, "type", fi.Mode().String()) 183 } else { 184 if schub, err := scwallet.NewHub(scpath, scwallet.Scheme, ksLocation); err != nil { 185 log.Warn(fmt.Sprintf("Failed to start smart card hub, disabling: %v", err)) 186 } else { 187 backends = append(backends, schub) 188 } 189 } 190 } 191 } 192 193 // Clef doesn't allow insecure http account unlock. 194 return accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: false}, backends...) 195 } 196 197 // MetadataFromContext extracts Metadata from a given context.Context 198 func MetadataFromContext(ctx context.Context) Metadata { 199 m := Metadata{"NA", "NA", "NA", "", ""} // batman 200 201 if v := ctx.Value("remote"); v != nil { 202 m.Remote = v.(string) 203 } 204 if v := ctx.Value("scheme"); v != nil { 205 m.Scheme = v.(string) 206 } 207 if v := ctx.Value("local"); v != nil { 208 m.Local = v.(string) 209 } 210 if v := ctx.Value("Origin"); v != nil { 211 m.Origin = v.(string) 212 } 213 if v := ctx.Value("User-Agent"); v != nil { 214 m.UserAgent = v.(string) 215 } 216 return m 217 } 218 219 // String implements Stringer interface 220 func (m Metadata) String() string { 221 s, err := json.Marshal(m) 222 if err == nil { 223 return string(s) 224 } 225 return err.Error() 226 } 227 228 // types for the requests/response types between signer and UI 229 type ( 230 // SignTxRequest contains info about a Transaction to sign 231 SignTxRequest struct { 232 Transaction SendTxArgs `json:"transaction"` 233 Callinfo []ValidationInfo `json:"call_info"` 234 Meta Metadata `json:"meta"` 235 } 236 // SignTxResponse result from SignTxRequest 237 SignTxResponse struct { 238 //The UI may make changes to the TX 239 Transaction SendTxArgs `json:"transaction"` 240 Approved bool `json:"approved"` 241 } 242 SignDataRequest struct { 243 ContentType string `json:"content_type"` 244 Address common.MixedcaseAddress `json:"address"` 245 Rawdata []byte `json:"raw_data"` 246 Messages []*NameValueType `json:"messages"` 247 Hash hexutil.Bytes `json:"hash"` 248 Meta Metadata `json:"meta"` 249 } 250 SignDataResponse struct { 251 Approved bool `json:"approved"` 252 } 253 NewAccountRequest struct { 254 Meta Metadata `json:"meta"` 255 } 256 NewAccountResponse struct { 257 Approved bool `json:"approved"` 258 } 259 ListRequest struct { 260 Accounts []accounts.Account `json:"accounts"` 261 Meta Metadata `json:"meta"` 262 } 263 ListResponse struct { 264 Accounts []accounts.Account `json:"accounts"` 265 } 266 Message struct { 267 Text string `json:"text"` 268 } 269 StartupInfo struct { 270 Info map[string]interface{} `json:"info"` 271 } 272 UserInputRequest struct { 273 Title string `json:"title"` 274 Prompt string `json:"prompt"` 275 IsPassword bool `json:"isPassword"` 276 } 277 UserInputResponse struct { 278 Text string `json:"text"` 279 } 280 ) 281 282 var ErrRequestDenied = errors.New("Request denied") 283 284 // NewSignerAPI creates a new API that can be used for Account management. 285 // ksLocation specifies the directory where to store the password protected private 286 // key that is generated when a new Account is created. 287 // noUSB disables USB support that is required to support hardware devices such as 288 // ledger and trezor. 289 func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI { 290 if advancedMode { 291 log.Info("Clef is in advanced mode: will warn instead of reject") 292 } 293 signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials} 294 if !noUSB { 295 signer.startUSBListener() 296 } 297 return signer 298 } 299 func (api *SignerAPI) openTrezor(url accounts.URL) { 300 resp, err := api.UI.OnInputRequired(UserInputRequest{ 301 Prompt: "Pin required to open Trezor wallet\n" + 302 "Look at the device for number positions\n\n" + 303 "7 | 8 | 9\n" + 304 "--+---+--\n" + 305 "4 | 5 | 6\n" + 306 "--+---+--\n" + 307 "1 | 2 | 3\n\n", 308 IsPassword: true, 309 Title: "Trezor unlock", 310 }) 311 if err != nil { 312 log.Warn("failed getting trezor pin", "err", err) 313 return 314 } 315 // We're using the URL instead of the pointer to the 316 // Wallet -- perhaps it is not actually present anymore 317 w, err := api.am.Wallet(url.String()) 318 if err != nil { 319 log.Warn("wallet unavailable", "url", url) 320 return 321 } 322 err = w.Open(resp.Text) 323 if err != nil { 324 log.Warn("failed to open wallet", "wallet", url, "err", err) 325 return 326 } 327 328 } 329 330 // startUSBListener starts a listener for USB events, for hardware wallet interaction 331 func (api *SignerAPI) startUSBListener() { 332 events := make(chan accounts.WalletEvent, 16) 333 am := api.am 334 am.Subscribe(events) 335 go func() { 336 337 // Open any wallets already attached 338 for _, wallet := range am.Wallets() { 339 if err := wallet.Open(""); err != nil { 340 log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err) 341 if err == usbwallet.ErrTrezorPINNeeded { 342 go api.openTrezor(wallet.URL()) 343 } 344 } 345 } 346 // Listen for wallet event till termination 347 for event := range events { 348 switch event.Kind { 349 case accounts.WalletArrived: 350 if err := event.Wallet.Open(""); err != nil { 351 log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err) 352 if err == usbwallet.ErrTrezorPINNeeded { 353 go api.openTrezor(event.Wallet.URL()) 354 } 355 } 356 case accounts.WalletOpened: 357 status, _ := event.Wallet.Status() 358 log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status) 359 360 // Derive first N accounts, hardcoded for now 361 var nextPath = make(accounts.DerivationPath, len(accounts.DefaultBaseDerivationPath)) 362 copy(nextPath[:], accounts.DefaultBaseDerivationPath[:]) 363 364 for i := 0; i < numberOfAccountsToDerive; i++ { 365 acc, err := event.Wallet.Derive(nextPath, true) 366 if err != nil { 367 log.Warn("account derivation failed", "error", err) 368 } else { 369 log.Info("derived account", "address", acc.Address) 370 } 371 nextPath[len(nextPath)-1]++ 372 } 373 case accounts.WalletDropped: 374 log.Info("Old wallet dropped", "url", event.Wallet.URL()) 375 event.Wallet.Close() 376 } 377 } 378 }() 379 } 380 381 // List returns the set of wallet this signer manages. Each wallet can contain 382 // multiple accounts. 383 func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) { 384 var accs []accounts.Account 385 for _, wallet := range api.am.Wallets() { 386 accs = append(accs, wallet.Accounts()...) 387 } 388 result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) 389 if err != nil { 390 return nil, err 391 } 392 if result.Accounts == nil { 393 return nil, ErrRequestDenied 394 395 } 396 addresses := make([]common.Address, 0) 397 for _, acc := range result.Accounts { 398 addresses = append(addresses, acc.Address) 399 } 400 401 return addresses, nil 402 } 403 404 // New creates a new password protected Account. The private key is protected with 405 // the given password. Users are responsible to backup the private key that is stored 406 // in the keystore location thas was specified when this API was created. 407 func (api *SignerAPI) New(ctx context.Context) (common.Address, error) { 408 be := api.am.Backends(keystore.KeyStoreType) 409 if len(be) == 0 { 410 return common.Address{}, errors.New("password based accounts not supported") 411 } 412 if resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}); err != nil { 413 return common.Address{}, err 414 } else if !resp.Approved { 415 return common.Address{}, ErrRequestDenied 416 } 417 418 // Three retries to get a valid password 419 for i := 0; i < 3; i++ { 420 resp, err := api.UI.OnInputRequired(UserInputRequest{ 421 "New account password", 422 fmt.Sprintf("Please enter a password for the new account to be created (attempt %d of 3)", i), 423 true}) 424 if err != nil { 425 log.Warn("error obtaining password", "attempt", i, "error", err) 426 continue 427 } 428 if pwErr := ValidatePasswordFormat(resp.Text); pwErr != nil { 429 api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", (i + 1), pwErr)) 430 } else { 431 // No error 432 acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Text) 433 log.Info("Your new key was generated", "address", acc.Address) 434 log.Warn("Please backup your key file!", "path", acc.URL.Path) 435 log.Warn("Please remember your password!") 436 return acc.Address, err 437 } 438 } 439 // Otherwise fail, with generic error message 440 return common.Address{}, errors.New("account creation failed") 441 } 442 443 // logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. 444 // it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow 445 // UI-modifications to requests 446 func logDiff(original *SignTxRequest, new *SignTxResponse) bool { 447 modified := false 448 if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { 449 log.Info("Sender-account changed by UI", "was", f0, "is", f1) 450 modified = true 451 } 452 if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { 453 log.Info("Recipient-account changed by UI", "was", t0, "is", t1) 454 modified = true 455 } 456 if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 { 457 modified = true 458 log.Info("Gas changed by UI", "was", g0, "is", g1) 459 } 460 if g0, g1 := big.Int(original.Transaction.GasPrice), big.Int(new.Transaction.GasPrice); g0.Cmp(&g1) != 0 { 461 modified = true 462 log.Info("GasPrice changed by UI", "was", g0, "is", g1) 463 } 464 if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { 465 modified = true 466 log.Info("Value changed by UI", "was", v0, "is", v1) 467 } 468 if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { 469 d0s := "" 470 d1s := "" 471 if d0 != nil { 472 d0s = hexutil.Encode(*d0) 473 } 474 if d1 != nil { 475 d1s = hexutil.Encode(*d1) 476 } 477 if d1s != d0s { 478 modified = true 479 log.Info("Data changed by UI", "was", d0s, "is", d1s) 480 } 481 } 482 if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { 483 modified = true 484 log.Info("Nonce changed by UI", "was", n0, "is", n1) 485 } 486 return modified 487 } 488 489 func (api *SignerAPI) lookupPassword(address common.Address) (string, error) { 490 return api.credentials.Get(address.Hex()) 491 } 492 493 func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) { 494 // Look up the password and return if available 495 if pw, err := api.lookupPassword(address); err == nil { 496 return pw, nil 497 } 498 // Password unavailable, request it from the user 499 pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true}) 500 if err != nil { 501 log.Warn("error obtaining password", "error", err) 502 // We'll not forward the error here, in case the error contains info about the response from the UI, 503 // which could leak the password if it was malformed json or something 504 return "", errors.New("internal error") 505 } 506 return pwResp.Text, nil 507 } 508 509 // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form 510 func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { 511 var ( 512 err error 513 result SignTxResponse 514 msgs *ValidationMessages 515 ) 516 if args.IsPrivate { 517 msgs = new(ValidationMessages) 518 } else { 519 msgs, err = api.validator.ValidateTransaction(methodSelector, &args) 520 if err != nil { 521 return nil, err 522 } 523 } 524 525 // If we are in 'rejectMode', then reject rather than show the user warnings 526 if api.rejectMode { 527 if err := msgs.getWarnings(); err != nil { 528 return nil, err 529 } 530 } 531 req := SignTxRequest{ 532 Transaction: args, 533 Meta: MetadataFromContext(ctx), 534 Callinfo: msgs.Messages, 535 } 536 // Process approval 537 result, err = api.UI.ApproveTx(&req) 538 if err != nil { 539 return nil, err 540 } 541 if !result.Approved { 542 return nil, ErrRequestDenied 543 } 544 // Log changes made by the UI to the signing-request 545 logDiff(&req, &result) 546 var ( 547 acc accounts.Account 548 wallet accounts.Wallet 549 ) 550 acc = accounts.Account{Address: result.Transaction.From.Address()} 551 wallet, err = api.am.Find(acc) 552 if err != nil { 553 return nil, err 554 } 555 // Convert fields into a real transaction 556 var unsignedTx = result.Transaction.toTransaction() 557 // Get the password for the transaction 558 pw, err := api.lookupOrQueryPassword(acc.Address, "Account password", 559 fmt.Sprintf("Please enter the password for account %s", acc.Address.String())) 560 if err != nil { 561 return nil, err 562 } 563 // The one to sign is the one that was returned from the UI 564 signedTx, err := wallet.SignTxWithPassphrase(acc, pw, unsignedTx, api.chainID) 565 if err != nil { 566 api.UI.ShowError(err.Error()) 567 return nil, err 568 } 569 570 rlpdata, err := rlp.EncodeToBytes(signedTx) 571 response := ethapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx} 572 573 // Finally, send the signed tx to the UI 574 api.UI.OnApprovedTx(response) 575 // ...and to the external caller 576 return &response, nil 577 578 } 579 580 // Returns the external api version. This method does not require user acceptance. Available methods are 581 // available via enumeration anyway, and this info does not contain user-specific data 582 func (api *SignerAPI) Version(ctx context.Context) (string, error) { 583 return ExternalAPIVersion, nil 584 }