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) | ### 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 | ### 2.0.0 | ||||||
| 
 | 
 | ||||||
| * Modify how `call_info` on a transaction is conveyed. New format: | * Modify how `call_info` on a transaction is conveyed. New format: | ||||||
|  | |||||||
| @ -36,6 +36,9 @@ import ( | |||||||
| 	"github.com/ethereum/go-ethereum/rlp" | 	"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.
 | // ExternalAPI defines the external API through which signing requests are made.
 | ||||||
| type ExternalAPI interface { | type ExternalAPI interface { | ||||||
| 	// List available accounts
 | 	// 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
 | 	// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
 | ||||||
| 	// information
 | 	// information
 | ||||||
| 	OnSignerStartup(info StartupInfo) | 	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
 | // SignerAPI defines the actual implementation of ExternalAPI
 | ||||||
| @ -194,6 +200,14 @@ type ( | |||||||
| 	StartupInfo struct { | 	StartupInfo struct { | ||||||
| 		Info map[string]interface{} `json:"info"` | 		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") | 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 { | 	if len(ksLocation) > 0 { | ||||||
| 		backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) | 		backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) | ||||||
| 	} | 	} | ||||||
|  | 	if advancedMode { | ||||||
|  | 		log.Info("Clef is in advanced mode: will warn instead of reject") | ||||||
|  | 	} | ||||||
| 	if !noUSB { | 	if !noUSB { | ||||||
| 		// Start a USB hub for Ledger hardware wallets
 | 		// Start a USB hub for Ledger hardware wallets
 | ||||||
| 		if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { | 		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") | 			log.Debug("Trezor support enabled") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if advancedMode { | 	signer := &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode} | ||||||
| 		log.Info("Clef is in advanced mode: will warn instead of reject") | 	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
 | // List returns the set of wallet this signer manages. Each wallet can contain
 | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ package core | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"math/big" | 	"math/big" | ||||||
| @ -41,6 +42,10 @@ type HeadlessUI struct { | |||||||
| 	controller chan string | 	controller chan string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (ui *HeadlessUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { | ||||||
|  | 	return UserInputResponse{}, errors.New("not implemented") | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) { | func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -83,6 +83,22 @@ func (ui *CommandlineUI) readPasswordText(inputstring string) string { | |||||||
| 	return string(text) | 	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
 | // confirm returns true if user enters 'Yes', otherwise false
 | ||||||
| func (ui *CommandlineUI) confirm() bool { | func (ui *CommandlineUI) confirm() bool { | ||||||
| 	fmt.Printf("Approve? [y/N]:\n") | 	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) | 		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) | 	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) { | func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { | ||||||
| 	jsonreq, err := json.Marshal(request) | 	jsonreq, err := json.Marshal(request) | ||||||
| 	approved, err := r.checkApproval("ApproveListing", jsonreq, err) | 	approved, err := r.checkApproval("ApproveListing", jsonreq, err) | ||||||
| @ -222,6 +227,7 @@ func (r *rulesetUI) ShowInfo(message string) { | |||||||
| 	log.Info(message) | 	log.Info(message) | ||||||
| 	r.next.ShowInfo(message) | 	r.next.ShowInfo(message) | ||||||
| } | } | ||||||
|  | 
 | ||||||
| func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) { | func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) { | ||||||
| 	jsonInfo, err := json.Marshal(info) | 	jsonInfo, err := json.Marshal(info) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | |||||||
| @ -74,6 +74,10 @@ func mixAddr(a string) (*common.MixedcaseAddress, error) { | |||||||
| 
 | 
 | ||||||
| type alwaysDenyUI struct{} | type alwaysDenyUI struct{} | ||||||
| 
 | 
 | ||||||
|  | func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { | ||||||
|  | 	return core.UserInputResponse{}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) { | func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -200,6 +204,11 @@ type dummyUI struct { | |||||||
| 	calls []string | 	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) { | func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { | ||||||
| 	d.calls = append(d.calls, "ApproveTx") | 	d.calls = append(d.calls, "ApproveTx") | ||||||
| 	return core.SignTxResponse{}, core.ErrRequestDenied | 	return core.SignTxResponse{}, core.ErrRequestDenied | ||||||
| @ -509,6 +518,11 @@ type dontCallMe struct { | |||||||
| 	t *testing.T | 	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) { | func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user