Clef: USB hw wallet support (#17756)
* signer: implement USB interaction with hw wallets * signer: fix failing testcases
This commit is contained in:
		
							parent
							
								
									2c110c81ee
								
							
						
					
					
						commit
						dcaabfe7f6
					
				| @ -1,5 +1,21 @@ | ||||
| ### Changelog for internal API (ui-api) | ||||
| 
 | ||||
| ### 2.1.0 | ||||
| 
 | ||||
| * Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords. | ||||
| 
 | ||||
| The following structures are used: | ||||
| ```golang | ||||
|        UserInputRequest struct { | ||||
|                Prompt     string `json:"prompt"` | ||||
|                Title      string `json:"title"` | ||||
|                IsPassword bool   `json:"isPassword"` | ||||
|        } | ||||
|        UserInputResponse struct { | ||||
|                Text string `json:"text"` | ||||
|        } | ||||
| ``` | ||||
| 
 | ||||
| ### 2.0.0 | ||||
| 
 | ||||
| * Modify how `call_info` on a transaction is conveyed. New format: | ||||
|  | ||||
| @ -36,6 +36,9 @@ import ( | ||||
| 	"github.com/ethereum/go-ethereum/rlp" | ||||
| ) | ||||
| 
 | ||||
| // numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
 | ||||
| const numberOfAccountsToDerive = 10 | ||||
| 
 | ||||
| // ExternalAPI defines the external API through which signing requests are made.
 | ||||
| type ExternalAPI interface { | ||||
| 	// List available accounts
 | ||||
| @ -79,6 +82,9 @@ type SignerUI interface { | ||||
| 	// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
 | ||||
| 	// information
 | ||||
| 	OnSignerStartup(info StartupInfo) | ||||
| 	// OnInputRequried is invoked when clef requires user input, for example master password or
 | ||||
| 	// pin-code for unlocking hardware wallets
 | ||||
| 	OnInputRequired(info UserInputRequest) (UserInputResponse, error) | ||||
| } | ||||
| 
 | ||||
| // SignerAPI defines the actual implementation of ExternalAPI
 | ||||
| @ -194,6 +200,14 @@ type ( | ||||
| 	StartupInfo struct { | ||||
| 		Info map[string]interface{} `json:"info"` | ||||
| 	} | ||||
| 	UserInputRequest struct { | ||||
| 		Prompt     string `json:"prompt"` | ||||
| 		Title      string `json:"title"` | ||||
| 		IsPassword bool   `json:"isPassword"` | ||||
| 	} | ||||
| 	UserInputResponse struct { | ||||
| 		Text string `json:"text"` | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| var ErrRequestDenied = errors.New("Request denied") | ||||
| @ -215,6 +229,9 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abi | ||||
| 	if len(ksLocation) > 0 { | ||||
| 		backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) | ||||
| 	} | ||||
| 	if advancedMode { | ||||
| 		log.Info("Clef is in advanced mode: will warn instead of reject") | ||||
| 	} | ||||
| 	if !noUSB { | ||||
| 		// Start a USB hub for Ledger hardware wallets
 | ||||
| 		if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { | ||||
| @ -231,10 +248,94 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abi | ||||
| 			log.Debug("Trezor support enabled") | ||||
| 		} | ||||
| 	} | ||||
| 	if advancedMode { | ||||
| 		log.Info("Clef is in advanced mode: will warn instead of reject") | ||||
| 	signer := &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode} | ||||
| 	if !noUSB { | ||||
| 		signer.startUSBListener() | ||||
| 	} | ||||
| 	return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode} | ||||
| 	return signer | ||||
| } | ||||
| func (api *SignerAPI) openTrezor(url accounts.URL) { | ||||
| 	resp, err := api.UI.OnInputRequired(UserInputRequest{ | ||||
| 		Prompt: "Pin required to open Trezor wallet\n" + | ||||
| 			"Look at the device for number positions\n\n" + | ||||
| 			"7 | 8 | 9\n" + | ||||
| 			"--+---+--\n" + | ||||
| 			"4 | 5 | 6\n" + | ||||
| 			"--+---+--\n" + | ||||
| 			"1 | 2 | 3\n\n", | ||||
| 		IsPassword: true, | ||||
| 		Title:      "Trezor unlock", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Warn("failed getting trezor pin", "err", err) | ||||
| 		return | ||||
| 	} | ||||
| 	// We're using the URL instead of the pointer to the
 | ||||
| 	// Wallet -- perhaps it is not actually present anymore
 | ||||
| 	w, err := api.am.Wallet(url.String()) | ||||
| 	if err != nil { | ||||
| 		log.Warn("wallet unavailable", "url", url) | ||||
| 		return | ||||
| 	} | ||||
| 	err = w.Open(resp.Text) | ||||
| 	if err != nil { | ||||
| 		log.Warn("failed to open wallet", "wallet", url, "err", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // startUSBListener starts a listener for USB events, for hardware wallet interaction
 | ||||
| func (api *SignerAPI) startUSBListener() { | ||||
| 	events := make(chan accounts.WalletEvent, 16) | ||||
| 	am := api.am | ||||
| 	am.Subscribe(events) | ||||
| 	go func() { | ||||
| 
 | ||||
| 		// Open any wallets already attached
 | ||||
| 		for _, wallet := range am.Wallets() { | ||||
| 			if err := wallet.Open(""); err != nil { | ||||
| 				log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err) | ||||
| 				if err == usbwallet.ErrTrezorPINNeeded { | ||||
| 					go api.openTrezor(wallet.URL()) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		// Listen for wallet event till termination
 | ||||
| 		for event := range events { | ||||
| 			switch event.Kind { | ||||
| 			case accounts.WalletArrived: | ||||
| 				if err := event.Wallet.Open(""); err != nil { | ||||
| 					log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err) | ||||
| 					if err == usbwallet.ErrTrezorPINNeeded { | ||||
| 						go api.openTrezor(event.Wallet.URL()) | ||||
| 					} | ||||
| 				} | ||||
| 			case accounts.WalletOpened: | ||||
| 				status, _ := event.Wallet.Status() | ||||
| 				log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status) | ||||
| 
 | ||||
| 				derivationPath := accounts.DefaultBaseDerivationPath | ||||
| 				if event.Wallet.URL().Scheme == "ledger" { | ||||
| 					derivationPath = accounts.DefaultLedgerBaseDerivationPath | ||||
| 				} | ||||
| 				var nextPath = derivationPath | ||||
| 				// Derive first N accounts, hardcoded for now
 | ||||
| 				for i := 0; i < numberOfAccountsToDerive; i++ { | ||||
| 					acc, err := event.Wallet.Derive(nextPath, true) | ||||
| 					if err != nil { | ||||
| 						log.Warn("account derivation failed", "error", err) | ||||
| 					} else { | ||||
| 						log.Info("derived account", "address", acc.Address) | ||||
| 					} | ||||
| 					nextPath[len(nextPath)-1]++ | ||||
| 				} | ||||
| 			case accounts.WalletDropped: | ||||
| 				log.Info("Old wallet dropped", "url", event.Wallet.URL()) | ||||
| 				event.Wallet.Close() | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| // List returns the set of wallet this signer manages. Each wallet can contain
 | ||||
|  | ||||
| @ -19,6 +19,7 @@ package core | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"math/big" | ||||
| @ -41,6 +42,10 @@ type HeadlessUI struct { | ||||
| 	controller chan string | ||||
| } | ||||
| 
 | ||||
| func (ui *HeadlessUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { | ||||
| 	return UserInputResponse{}, errors.New("not implemented") | ||||
| } | ||||
| 
 | ||||
| func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) { | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -83,6 +83,22 @@ func (ui *CommandlineUI) readPasswordText(inputstring string) string { | ||||
| 	return string(text) | ||||
| } | ||||
| 
 | ||||
| func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { | ||||
| 	fmt.Println(info.Title) | ||||
| 	fmt.Println(info.Prompt) | ||||
| 	if info.IsPassword { | ||||
| 		text, err := terminal.ReadPassword(int(os.Stdin.Fd())) | ||||
| 		if err != nil { | ||||
| 			log.Error("Failed to read password", "err", err) | ||||
| 		} | ||||
| 		fmt.Println("-----------------------") | ||||
| 		return UserInputResponse{string(text)}, err | ||||
| 	} | ||||
| 	text := ui.readString() | ||||
| 	fmt.Println("-----------------------") | ||||
| 	return UserInputResponse{text}, nil | ||||
| } | ||||
| 
 | ||||
| // confirm returns true if user enters 'Yes', otherwise false
 | ||||
| func (ui *CommandlineUI) confirm() bool { | ||||
| 	fmt.Printf("Approve? [y/N]:\n") | ||||
|  | ||||
| @ -111,3 +111,11 @@ func (ui *StdIOUI) OnSignerStartup(info StartupInfo) { | ||||
| 		log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info) | ||||
| 	} | ||||
| } | ||||
| func (ui *StdIOUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { | ||||
| 	var result UserInputResponse | ||||
| 	err := ui.dispatch("OnInputRequired", info, &result) | ||||
| 	if err != nil { | ||||
| 		log.Info("Error calling 'OnInputRequired'", "exc", err.Error(), "info", info) | ||||
| 	} | ||||
| 	return result, err | ||||
| } | ||||
|  | ||||
| @ -194,6 +194,11 @@ func (r *rulesetUI) ApproveImport(request *core.ImportRequest) (core.ImportRespo | ||||
| 	return r.next.ApproveImport(request) | ||||
| } | ||||
| 
 | ||||
| // OnInputRequired not handled by rules
 | ||||
| func (r *rulesetUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { | ||||
| 	return r.next.OnInputRequired(info) | ||||
| } | ||||
| 
 | ||||
| func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { | ||||
| 	jsonreq, err := json.Marshal(request) | ||||
| 	approved, err := r.checkApproval("ApproveListing", jsonreq, err) | ||||
| @ -222,6 +227,7 @@ func (r *rulesetUI) ShowInfo(message string) { | ||||
| 	log.Info(message) | ||||
| 	r.next.ShowInfo(message) | ||||
| } | ||||
| 
 | ||||
| func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) { | ||||
| 	jsonInfo, err := json.Marshal(info) | ||||
| 	if err != nil { | ||||
|  | ||||
| @ -74,6 +74,10 @@ func mixAddr(a string) (*common.MixedcaseAddress, error) { | ||||
| 
 | ||||
| type alwaysDenyUI struct{} | ||||
| 
 | ||||
| func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { | ||||
| 	return core.UserInputResponse{}, nil | ||||
| } | ||||
| 
 | ||||
| func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) { | ||||
| } | ||||
| 
 | ||||
| @ -200,6 +204,11 @@ type dummyUI struct { | ||||
| 	calls []string | ||||
| } | ||||
| 
 | ||||
| func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { | ||||
| 	d.calls = append(d.calls, "OnInputRequired") | ||||
| 	return core.UserInputResponse{}, nil | ||||
| } | ||||
| 
 | ||||
| func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { | ||||
| 	d.calls = append(d.calls, "ApproveTx") | ||||
| 	return core.SignTxResponse{}, core.ErrRequestDenied | ||||
| @ -509,6 +518,11 @@ type dontCallMe struct { | ||||
| 	t *testing.T | ||||
| } | ||||
| 
 | ||||
| func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { | ||||
| 	d.t.Fatalf("Did not expect next-handler to be called") | ||||
| 	return core.UserInputResponse{}, nil | ||||
| } | ||||
| 
 | ||||
| func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user