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