diff options
author | Péter Szilágyi <peterke@gmail.com> | 2017-02-07 18:47:34 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2017-02-13 20:00:07 +0800 |
commit | fad5eb0a87abfc12812647344a26de8a43830182 (patch) | |
tree | a679f37933a85edade15ee69b385b6705cc94043 /accounts/usbwallet | |
parent | b3c0e9d3ccb0bb326646aea47dda391a9552b122 (diff) | |
download | dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.gz dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.zst dexon-fad5eb0a87abfc12812647344a26de8a43830182.zip |
accounts, cmd, eth, internal, miner, node: wallets and HD APIs
Diffstat (limited to 'accounts/usbwallet')
-rw-r--r-- | accounts/usbwallet/ledger_hub.go | 205 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_wallet.go (renamed from accounts/usbwallet/ledger.go) | 547 |
2 files changed, 487 insertions, 265 deletions
diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go new file mode 100644 index 000000000..ebd6fddb4 --- /dev/null +++ b/accounts/usbwallet/ledger_hub.go @@ -0,0 +1,205 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +// This file contains the implementation for interacting with the Ledger hardware +// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: +// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc + +// +build !ios + +package usbwallet + +import ( + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/event" + "github.com/karalabe/gousb/usb" +) + +// ledgerDeviceIDs are the known device IDs that Ledger wallets use. +var ledgerDeviceIDs = []deviceID{ + {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue + {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S +} + +// Maximum time between wallet refreshes (if USB hotplug notifications don't work). +const ledgerRefreshCycle = time.Second + +// Minimum time between wallet refreshes to avoid USB trashing. +const ledgerRefreshThrottling = 500 * time.Millisecond + +// LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets. +type LedgerHub struct { + ctx *usb.Context // Context interfacing with a libusb instance + + refreshed time.Time // Time instance when the list of wallets was last refreshed + wallets []accounts.Wallet // List of Ledger devices currently tracking + updateFeed event.Feed // Event feed to notify wallet additions/removals + updateScope event.SubscriptionScope // Subscription scope tracking current live listeners + updating bool // Whether the event notification loop is running + + quit chan chan error + lock sync.RWMutex +} + +// NewLedgerHub creates a new hardware wallet manager for Ledger devices. +func NewLedgerHub() (*LedgerHub, error) { + // Initialize the USB library to access Ledgers through + ctx, err := usb.NewContext() + if err != nil { + return nil, err + } + // Create the USB hub, start and return it + hub := &LedgerHub{ + ctx: ctx, + quit: make(chan chan error), + } + hub.refreshWallets() + + return hub, nil +} + +// Wallets implements accounts.Backend, returning all the currently tracked USB +// devices that appear to be Ledger hardware wallets. +func (hub *LedgerHub) Wallets() []accounts.Wallet { + // Make sure the list of wallets is up to date + hub.refreshWallets() + + hub.lock.RLock() + defer hub.lock.RUnlock() + + cpy := make([]accounts.Wallet, len(hub.wallets)) + copy(cpy, hub.wallets) + return cpy +} + +// refreshWallets scans the USB devices attached to the machine and updates the +// list of wallets based on the found devices. +func (hub *LedgerHub) refreshWallets() { + // Don't scan the USB like crazy it the user fetches wallets in a loop + hub.lock.RLock() + elapsed := time.Since(hub.refreshed) + hub.lock.RUnlock() + + if elapsed < ledgerRefreshThrottling { + return + } + // Retrieve the current list of Ledger devices + var devIDs []deviceID + var busIDs []uint16 + + hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { + // Gather Ledger devices, don't connect any just yet + for _, id := range ledgerDeviceIDs { + if desc.Vendor == id.Vendor && desc.Product == id.Product { + devIDs = append(devIDs, deviceID{Vendor: desc.Vendor, Product: desc.Product}) + busIDs = append(busIDs, uint16(desc.Bus)<<8+uint16(desc.Address)) + return false + } + } + // Not ledger, ignore and don't connect either + return false + }) + // Transform the current list of wallets into the new one + hub.lock.Lock() + + wallets := make([]accounts.Wallet, 0, len(devIDs)) + events := []accounts.WalletEvent{} + + for i := 0; i < len(devIDs); i++ { + devID, busID := devIDs[i], busIDs[i] + url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff) + + // Drop wallets while they were in front of the next account + for len(hub.wallets) > 0 && hub.wallets[0].URL() < url { + events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) + hub.wallets = hub.wallets[1:] + } + // If there are no more wallets or the account is before the next, wrap new wallet + if len(hub.wallets) == 0 || hub.wallets[0].URL() > url { + wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url} + + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) + wallets = append(wallets, wallet) + continue + } + // If the account is the same as the first wallet, keep it + if hub.wallets[0].URL() == url { + wallets = append(wallets, hub.wallets[0]) + hub.wallets = hub.wallets[1:] + continue + } + } + // Drop any leftover wallets and set the new batch + for _, wallet := range hub.wallets { + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) + } + hub.refreshed = time.Now() + hub.wallets = wallets + hub.lock.Unlock() + + // Fire all wallet events and return + for _, event := range events { + hub.updateFeed.Send(event) + } +} + +// Subscribe implements accounts.Backend, creating an async subscription to +// receive notifications on the addition or removal of Ledger wallets. +func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + // We need the mutex to reliably start/stop the update loop + hub.lock.Lock() + defer hub.lock.Unlock() + + // Subscribe the caller and track the subscriber count + sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) + + // Subscribers require an active notification loop, start it + if !hub.updating { + hub.updating = true + go hub.updater() + } + return sub +} + +// updater is responsible for maintaining an up-to-date list of wallets stored in +// the keystore, and for firing wallet addition/removal events. It listens for +// account change events from the underlying account cache, and also periodically +// forces a manual refresh (only triggers for systems where the filesystem notifier +// is not running). +func (hub *LedgerHub) updater() { + for { + // Wait for a USB hotplug event (not supported yet) or a refresh timeout + select { + //case <-hub.changes: // reenable on hutplug implementation + case <-time.After(ledgerRefreshCycle): + } + // Run the wallet refresher + hub.refreshWallets() + + // If all our subscribers left, stop the updater + hub.lock.Lock() + if hub.updateScope.Count() == 0 { + hub.updating = false + hub.lock.Unlock() + return + } + hub.lock.Unlock() + } +} diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger_wallet.go index 9917d0d70..35e671c46 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -43,14 +43,8 @@ import ( "github.com/karalabe/gousb/usb" ) -// ledgerDeviceIDs are the known device IDs that Ledger wallets use. -var ledgerDeviceIDs = []deviceID{ - {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue - {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S -} - -// ledgerDerivationPath is the key derivation parameters used by the wallet. -var ledgerDerivationPath = [4]uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} +// ledgerDerivationPath is the base derivation parameters used by the wallet. +var ledgerDerivationPath = []uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} // ledgerOpcode is an enumeration encoding the supported Ledger opcodes. type ledgerOpcode byte @@ -78,274 +72,297 @@ const ( // ledgerWallet represents a live USB Ledger hardware wallet. type ledgerWallet struct { - device *usb.Device // USB device advertising itself as a Ledger wallet - input usb.Endpoint // Input endpoint to send data to this device - output usb.Endpoint // Output endpoint to receive data from this device - - address common.Address // Current address of the wallet (may be zero if Ethereum app offline) - url string // Textual URL uniquely identifying this wallet - version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) -} + context *usb.Context // USB context to interface libusb through + hardwareID deviceID // USB identifiers to identify this device type + locationID uint16 // USB bus and address to identify this device instance + url string // Textual URL uniquely identifying this wallet -// LedgerHub is a USB hardware wallet interface that can find and handle Ledger -// wallets. -type LedgerHub struct { - ctx *usb.Context // Context interfacing with a libusb instance + device *usb.Device // USB device advertising itself as a Ledger wallet + input usb.Endpoint // Input endpoint to send data to this device + output usb.Endpoint // Output endpoint to receive data from this device + failure error // Any failure that would make the device unusable - wallets map[uint16]*ledgerWallet // Apparent Ledger wallets (some may be inactive) - accounts []accounts.Account // List of active Ledger accounts - index map[common.Address]uint16 // Set of addresses with active wallets + version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) + accounts []accounts.Account // List of derive accounts pinned on the Ledger + paths map[common.Address][]uint32 // Known derivation paths for signing operations quit chan chan error lock sync.RWMutex } -// NewLedgerHub creates a new hardware wallet manager for Ledger devices. -func NewLedgerHub() (*LedgerHub, error) { - // Initialize the USB library to access Ledgers through - ctx, err := usb.NewContext() - if err != nil { - return nil, err - } - // Create the USB hub, start and return it - hub := &LedgerHub{ - ctx: ctx, - wallets: make(map[uint16]*ledgerWallet), - index: make(map[common.Address]uint16), - quit: make(chan chan error), - } - go hub.watch() - return hub, nil +// Type implements accounts.Wallet, returning the textual type of the wallet. +func (w *ledgerWallet) Type() string { + return "ledger" } -// Accounts retrieves the live of accounts currently known by the Ledger hub. -func (hub *LedgerHub) Accounts() []accounts.Account { - hub.lock.RLock() - defer hub.lock.RUnlock() - - cpy := make([]accounts.Account, len(hub.accounts)) - copy(cpy, hub.accounts) - return cpy +// URL implements accounts.Wallet, returning the URL of the Ledger device. +func (w *ledgerWallet) URL() string { + return w.url } -// HasAddress reports whether an account with the given address is present. -func (hub *LedgerHub) HasAddress(addr common.Address) bool { - hub.lock.RLock() - defer hub.lock.RUnlock() +// Status implements accounts.Wallet, always whether the Ledger is opened, closed +// or whether the Ethereum app was not started on it. +func (w *ledgerWallet) Status() string { + w.lock.RLock() + defer w.lock.RUnlock() - _, known := hub.index[addr] - return known + if w.failure != nil { + return fmt.Sprintf("Failed: %v", w.failure) + } + if w.device == nil { + return "Closed" + } + if w.version == [3]byte{0, 0, 0} { + return "Ethereum app not started" + } + return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2]) } -// SignHash is not supported for Ledger wallets, so this method will always -// return an error. -func (hub *LedgerHub) SignHash(acc accounts.Account, hash []byte) ([]byte, error) { - return nil, accounts.ErrNotSupported -} +// Open implements accounts.Wallet, attempting to open a USB connection to the +// Ledger hardware wallet. The Ledger does not require a user passphrase so that +// is silently discarded. +func (w *ledgerWallet) Open(passphrase string) error { + w.lock.Lock() + defer w.lock.Unlock() -// SignTx sends the transaction over to the Ledger wallet to request a confirmation -// from the user. It returns either the signed transaction or a failure if the user -// denied the transaction. -// -// Note, if the version of the Ethereum application running on the Ledger wallet is -// too old to sign EIP-155 transactions, but such is requested nonetheless, an error -// will be returned opposed to silently signing in Homestead mode. -func (hub *LedgerHub) SignTx(acc accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - hub.lock.RLock() - defer hub.lock.RUnlock() - - // If the account contains the device URL, flatten it to make sure - var id uint16 - if acc.URL != "" { - if parts := strings.Split(acc.URL, "."); len(parts) == 2 { - bus, busErr := strconv.Atoi(parts[0]) - addr, addrErr := strconv.Atoi(parts[1]) - - if busErr == nil && addrErr == nil { - id = uint16(bus)<<8 + uint16(addr) - } - } + // If the wallet was already opened, don't try to open again + if w.device != nil { + return accounts.ErrWalletAlreadyOpen } - // If the id is still zero, URL is either missing or bad, resolve - if id == 0 { - var ok bool - if id, ok = hub.index[acc.Address]; !ok { - return nil, accounts.ErrUnknownAccount - } + // Otherwise iterate over all USB devices and find this again (no way to directly do this) + // Iterate over all attached devices and fetch those seemingly Ledger + devices, err := w.context.ListDevices(func(desc *usb.Descriptor) bool { + // Only open this single specific device + return desc.Vendor == w.hardwareID.Vendor && desc.Product == w.hardwareID.Product && + uint16(desc.Bus)<<8+uint16(desc.Address) == w.locationID + }) + if err != nil { + return err } - // Retrieve the wallet associated with the URL - wallet, ok := hub.wallets[id] - if !ok { - return nil, accounts.ErrUnknownAccount + // Device opened, attach to the input and output endpoints + device := devices[0] + + var invalid string + switch { + case len(device.Descriptor.Configs) == 0: + invalid = "no endpoint config available" + case len(device.Descriptor.Configs[0].Interfaces) == 0: + invalid = "no endpoint interface available" + case len(device.Descriptor.Configs[0].Interfaces[0].Setups) == 0: + invalid = "no endpoint setup available" + case len(device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints) < 2: + invalid = "not enough IO endpoints available" } - // Ensure the wallet is capable of signing the given transaction - if chainID != nil && wallet.version[0] <= 1 && wallet.version[1] <= 0 && wallet.version[2] <= 2 { - return nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", - wallet.version[0], wallet.version[1], wallet.version[2]) + if invalid != "" { + device.Close() + return fmt.Errorf("ledger wallet [%s] invalid: %s", w.url, invalid) } - return wallet.sign(tx, chainID) + // Open the input and output endpoints to the device + input, err := device.OpenEndpoint( + device.Descriptor.Configs[0].Config, + device.Descriptor.Configs[0].Interfaces[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[1].Address, + ) + if err != nil { + device.Close() + return fmt.Errorf("ledger wallet [%s] input open failed: %v", w.url, err) + } + output, err := device.OpenEndpoint( + device.Descriptor.Configs[0].Config, + device.Descriptor.Configs[0].Interfaces[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[0].Address, + ) + if err != nil { + device.Close() + return fmt.Errorf("ledger wallet [%s] output open failed: %v", w.url, err) + } + // Wallet seems to be successfully opened, guess if the Ethereum app is running + w.device, w.input, w.output = device, input, output + + w.paths = make(map[common.Address][]uint32) + w.quit = make(chan chan error) + defer func() { + go w.heartbeat() + }() + + if _, err := w.deriveAddress(ledgerDerivationPath); err != nil { + // Ethereum app is not running, nothing more to do, return + return nil + } + // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 + if w.resolveVersion() != nil { + w.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 + } + return nil } -// SignHashWithPassphrase is not supported for Ledger wallets, so this method -// will always return an error. -func (hub *LedgerHub) SignHashWithPassphrase(acc accounts.Account, passphrase string, hash []byte) ([]byte, error) { - return nil, accounts.ErrNotSupported +// heartbeat is a health check loop for the Ledger wallets to periodically verify +// whether they are still present or if they malfunctioned. It is needed because: +// - libusb on Windows doesn't support hotplug, so we can't detect USB unplugs +// - communication timeout on the Ledger requires a device power cycle to fix +func (w *ledgerWallet) heartbeat() { + // Execute heartbeat checks until termination or error + var ( + errc chan error + fail error + ) + for errc == nil && fail == nil { + // Wait until termination is requested or the heartbeat cycle arrives + select { + case errc = <-w.quit: + // Termination requested + continue + case <-time.After(time.Second): + // Heartbeat time + } + // Execute a tiny data exchange to see responsiveness + w.lock.Lock() + if err := w.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { + w.failure = err + fail = err + } + w.lock.Unlock() + } + // In case of error, wait for termination + if fail != nil { + errc = <-w.quit + } + errc <- fail } -// SignTxWithPassphrase requests the backend to sign the given transaction, with the -// given passphrase as extra authentication information. Since the Ledger does not -// support this feature, it will just silently ignore the passphrase. -func (hub *LedgerHub) SignTxWithPassphrase(acc accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - return hub.SignTx(acc, tx, chainID) +// Close implements accounts.Wallet, closing the USB connection to the Ledger. +func (w *ledgerWallet) Close() error { + // Terminate the health checks + errc := make(chan error) + w.quit <- errc + herr := <-errc // Save for later, we *must* close the USB + + // Terminate the device connection + w.lock.Lock() + defer w.lock.Unlock() + + if err := w.device.Close(); err != nil { + return err + } + w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil + + return herr // If all went well, return any health-check errors } -// Close terminates the usb watching for Ledger wallets and returns when it -// successfully terminated. -func (hub *LedgerHub) Close() error { - // Terminate the USB scanner - errc := make(chan error) - hub.quit <- errc - err := <-errc +// Accounts implements accounts.Wallet, returning the list of accounts pinned to +// the Ledger hardware wallet. +func (w *ledgerWallet) Accounts() []accounts.Account { + w.lock.RLock() + defer w.lock.RUnlock() - // Release the USB interface and return - hub.ctx.Close() - return err + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy } -// watch starts watching the local machine's USB ports for the connection or -// disconnection of Ledger devices. -func (hub *LedgerHub) watch() { - for { - // Rescan the USB ports for devices newly added or removed - hub.rescan() +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not pinned into this Ledger instance. Although we could attempt to resolve +// unpinned accounts, that would be an non-negligible hardware operation. +func (w *ledgerWallet) Contains(account accounts.Account) bool { + w.lock.RLock() + defer w.lock.RUnlock() - // Sleep for a certain amount of time or until terminated - select { - case errc := <-hub.quit: - errc <- nil - return - case <-time.After(time.Second): - } - } + _, exists := w.paths[account.Address] + return exists } -// rescan searches the USB ports for attached Ledger hardware wallets. -func (hub *LedgerHub) rescan() { - hub.lock.Lock() - defer hub.lock.Unlock() - - // Iterate over all connected Ledger devices and do a heartbeat test - for id, wallet := range hub.wallets { - // If the device doesn't respond (io error on Windows, no device on Linux), drop - if err := wallet.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { - // Wallet disconnected or at least in a useless state - if wallet.address == (common.Address{}) { - glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] disconnected", wallet.device.Bus, wallet.device.Address) - } else { - // A live account disconnected, remove it from the tracked accounts - for i, account := range hub.accounts { - if account.Address == wallet.address && account.URL == wallet.url { - hub.accounts = append(hub.accounts[:i], hub.accounts[i+1:]...) - break - } - } - delete(hub.index, wallet.address) - - glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] v%d.%d.%d disconnected: %s", wallet.device.Bus, wallet.device.Address, - wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) - } - delete(hub.wallets, id) - wallet.device.Close() - } +// Derive implements accounts.Wallet, deriving a new account at the specific +// derivation path. If pin is set to true, the account will be added to the list +// of tracked accounts. +func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) { + w.lock.Lock() + defer w.lock.Unlock() + + // If the wallet is closed, or the Ethereum app doesn't run, abort + if w.device == nil || w.version == [3]byte{0, 0, 0} { + return accounts.Account{}, accounts.ErrWalletClosed } - // Iterate over all attached devices and fetch those seemingly Ledger - devices, _ := hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { - // Discard all devices not advertizing as Ledger - ledger := false - for _, id := range ledgerDeviceIDs { - if desc.Vendor == id.Vendor && desc.Product == id.Product { - ledger = true - } + // All seems fine, convert the user derivation path to Ledger representation + path = strings.TrimPrefix(path, "/") + + parts := strings.Split(path, "/") + lpath := make([]uint32, len(parts)) + for i, part := range parts { + // Handle hardened paths + if strings.HasSuffix(part, "'") { + lpath[i] = 0x80000000 + part = strings.TrimSuffix(part, "'") } - if !ledger { - return false - } - // If we have an already known Ledger, skip opening it - id := uint16(desc.Bus)<<8 + uint16(desc.Address) - if _, known := hub.wallets[id]; known { - return false - } - // New Ledger device, open it for communication - return true - }) - // Start tracking all wallets which newly appeared - var err error - for _, device := range devices { - // Make sure the alleged device has the correct IO endpoints - wallet := &ledgerWallet{ - device: device, - url: fmt.Sprintf("%03d.%03d", device.Bus, device.Address), - } - var invalid string - switch { - case len(device.Descriptor.Configs) == 0: - invalid = "no endpoint config available" - case len(device.Descriptor.Configs[0].Interfaces) == 0: - invalid = "no endpoint interface available" - case len(device.Descriptor.Configs[0].Interfaces[0].Setups) == 0: - invalid = "no endpoint setup available" - case len(device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints) < 2: - invalid = "not enough IO endpoints available" - } - if invalid != "" { - glog.V(logger.Debug).Infof("ledger wallet [%s] deemed invalid: %s", wallet.url, invalid) - device.Close() - continue - } - // Open the input and output endpoints to the device - wallet.input, err = device.OpenEndpoint( - device.Descriptor.Configs[0].Config, - device.Descriptor.Configs[0].Interfaces[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[1].Address, - ) + // Handle the non hardened component + val, err := strconv.Atoi(part) if err != nil { - glog.V(logger.Debug).Infof("ledger wallet [%s] input open failed: %v", wallet.url, err) - device.Close() - continue + return accounts.Account{}, fmt.Errorf("path element %d: %v", i, err) } - wallet.output, err = device.OpenEndpoint( - device.Descriptor.Configs[0].Config, - device.Descriptor.Configs[0].Interfaces[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[0].Address, - ) - if err != nil { - glog.V(logger.Debug).Infof("ledger wallet [%s] output open failed: %v", wallet.url, err) - device.Close() - continue + lpath[i] += uint32(val) + } + // Try to derive the actual account and update it's URL if succeeful + address, err := w.deriveAddress(lpath) + if err != nil { + return accounts.Account{}, err + } + account := accounts.Account{ + Address: address, + URL: fmt.Sprintf("%s/%s", w.url, path), + } + // If pinning was requested, track the account + if pin { + if _, ok := w.paths[address]; !ok { + w.accounts = append(w.accounts, account) + w.paths[address] = lpath } - // Start tracking the device as a probably Ledger wallet - id := uint16(device.Bus)<<8 + uint16(device.Address) - hub.wallets[id] = wallet + } + return account, nil +} - if wallet.resolveAddress() != nil { - glog.V(logger.Info).Infof("ledger wallet [%s] connected, Ethereum app not started", wallet.url) - } else { - // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 - if wallet.resolveVersion() != nil { - wallet.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 - } - hub.accounts = append(hub.accounts, accounts.Account{ - Address: wallet.address, - URL: wallet.url, - }) - hub.index[wallet.address] = id - - glog.V(logger.Info).Infof("ledger wallet [%s] v%d.%d.%d connected: %s", wallet.url, - wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) - } +// SignHash implements accounts.Wallet, however signing arbitrary data is not +// supported for Ledger wallets, so this method will always return an error. +func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger +// wallet to request a confirmation from the user. It returns either the signed +// transaction or a failure if the user denied the transaction. +// +// Note, if the version of the Ethereum application running on the Ledger wallet is +// too old to sign EIP-155 transactions, but such is requested nonetheless, an error +// will be returned opposed to silently signing in Homestead mode. +func (w *ledgerWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + w.lock.Lock() + defer w.lock.Unlock() + + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] + if !ok { + return nil, accounts.ErrUnknownAccount + } + // Ensure the wallet is capable of signing the given transaction + if chainID != nil && w.version[0] <= 1 && w.version[1] <= 0 && w.version[2] <= 2 { + return nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", + w.version[0], w.version[1], w.version[2]) } + return w.sign(path, account.Address, tx, chainID) +} + +// SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary +// data is not supported for Ledger wallets, so this method will always return +// an error. +func (w *ledgerWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +// Since the Ledger does not support extra passphrases, it is silently ignored. +func (w *ledgerWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return w.SignTx(account, tx, chainID) } // resolveVersion retrieves the current version of the Ethereum wallet app running @@ -379,8 +396,8 @@ func (wallet *ledgerWallet) resolveVersion() error { return nil } -// resolveAddress retrieves the currently active Ethereum address from a Ledger -// wallet and caches it for future reference. +// deriveAddress retrieves the currently active Ethereum address from a Ledger +// wallet at the specified derivation path. // // The address derivation protocol is defined as follows: // @@ -410,33 +427,34 @@ func (wallet *ledgerWallet) resolveVersion() error { // Ethereum address length | 1 byte // Ethereum address | 40 bytes hex ascii // Chain code if requested | 32 bytes -func (wallet *ledgerWallet) resolveAddress() error { +func (w *ledgerWallet) deriveAddress(derivationPath []uint32) (common.Address, error) { // Flatten the derivation path into the Ledger request - path := make([]byte, 1+4*len(ledgerDerivationPath)) - path[0] = byte(len(ledgerDerivationPath)) - for i, component := range ledgerDerivationPath { + path := make([]byte, 1+4*len(derivationPath)) + path[0] = byte(len(derivationPath)) + for i, component := range derivationPath { binary.BigEndian.PutUint32(path[1+4*i:], component) } // Send the request and wait for the response - reply, err := wallet.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) + reply, err := w.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) if err != nil { - return err + return common.Address{}, err } // Discard the public key, we don't need that for now if len(reply) < 1 || len(reply) < 1+int(reply[0]) { - return errors.New("reply lacks public key entry") + return common.Address{}, errors.New("reply lacks public key entry") } reply = reply[1+int(reply[0]):] // Extract the Ethereum hex address string if len(reply) < 1 || len(reply) < 1+int(reply[0]) { - return errors.New("reply lacks address entry") + return common.Address{}, errors.New("reply lacks address entry") } hexstr := reply[1 : 1+int(reply[0])] // Decode the hex sting into an Ethereum address and return - hex.Decode(wallet.address[:], hexstr) - return nil + var address common.Address + hex.Decode(address[:], hexstr) + return address, nil } // sign sends the transaction to the Ledger wallet, and waits for the user to @@ -473,15 +491,15 @@ func (wallet *ledgerWallet) resolveAddress() error { // signature V | 1 byte // signature R | 32 bytes // signature S | 32 bytes -func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +func (w *ledgerWallet) sign(derivationPath []uint32, address common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { // We need to modify the timeouts to account for user feedback - defer func(old time.Duration) { wallet.device.ReadTimeout = old }(wallet.device.ReadTimeout) - wallet.device.ReadTimeout = time.Minute + defer func(old time.Duration) { w.device.ReadTimeout = old }(w.device.ReadTimeout) + w.device.ReadTimeout = time.Minute // Flatten the derivation path into the Ledger request - path := make([]byte, 1+4*len(ledgerDerivationPath)) - path[0] = byte(len(ledgerDerivationPath)) - for i, component := range ledgerDerivationPath { + path := make([]byte, 1+4*len(derivationPath)) + path[0] = byte(len(derivationPath)) + for i, component := range derivationPath { binary.BigEndian.PutUint32(path[1+4*i:], component) } // Create the transaction RLP based on whether legacy or EIP155 signing was requeste @@ -512,7 +530,7 @@ func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*type chunk = len(payload) } // Send the chunk over, ensuring it's processed correctly - reply, err = wallet.exchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) + reply, err = w.exchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) if err != nil { return nil, err } @@ -543,9 +561,8 @@ func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*type if err != nil { return nil, err } - if sender != wallet.address { - glog.V(logger.Error).Infof("Ledger signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) - return nil, fmt.Errorf("signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) + if sender != address { + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", address.Hex(), sender.Hex()) } return signed, nil } @@ -583,7 +600,7 @@ func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*type // APDU P2 | 1 byte // APDU length | 1 byte // Optional APDU data | arbitrary -func (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { +func (w *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { // Construct the message payload, possibly split into multiple chunks var chunks [][]byte for left := data; len(left) > 0 || len(chunks) == 0; { @@ -613,9 +630,9 @@ func (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 le // Send over to the device if glog.V(logger.Core) { - glog.Infof("-> %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, msg) + glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, msg) } - if _, err := wallet.input.Write(msg); err != nil { + if _, err := w.input.Write(msg); err != nil { return nil, err } } @@ -624,11 +641,11 @@ func (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 le for { // Read the next chunk from the Ledger wallet chunk := make([]byte, 64) - if _, err := io.ReadFull(wallet.output, chunk); err != nil { + if _, err := io.ReadFull(w.output, chunk); err != nil { return nil, err } if glog.V(logger.Core) { - glog.Infof("<- %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, chunk) + glog.Infof("<- %03d.%03d: %x", w.device.Bus, w.device.Address, chunk) } // Make sure the transport header matches if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { |