forked from cerc-io/plugeth
b628d72766
This changes the CI / release builds to use the latest Go version. It also upgrades golangci-lint to a newer version compatible with Go 1.19. In Go 1.19, godoc has gained official support for links and lists. The syntax for code blocks in doc comments has changed and now requires a leading tab character. gofmt adapts comments to the new syntax automatically, so there are a lot of comment re-formatting changes in this PR. We need to apply the new format in order to pass the CI lint stage with Go 1.19. With the linter upgrade, I have decided to disable 'gosec' - it produces too many false-positive warnings. The 'deadcode' and 'varcheck' linters have also been removed because golangci-lint warns about them being unmaintained. 'unused' provides similar coverage and we already have it enabled, so we don't lose much with this change.
372 lines
14 KiB
Go
372 lines
14 KiB
Go
// 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 Trezor hardware
|
|
// wallets. The wire protocol spec can be found on the SatoshiLabs website:
|
|
// https://doc.satoshilabs.com/trezor-tech/api-protobuf.html
|
|
|
|
package usbwallet
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/accounts/usbwallet/trezor"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/golang/protobuf/proto"
|
|
)
|
|
|
|
// ErrTrezorPINNeeded is returned if opening the trezor requires a PIN code. In
|
|
// this case, the calling application should display a pinpad and send back the
|
|
// encoded passphrase.
|
|
var ErrTrezorPINNeeded = errors.New("trezor: pin needed")
|
|
|
|
// ErrTrezorPassphraseNeeded is returned if opening the trezor requires a passphrase
|
|
var ErrTrezorPassphraseNeeded = errors.New("trezor: passphrase needed")
|
|
|
|
// errTrezorReplyInvalidHeader is the error message returned by a Trezor data exchange
|
|
// if the device replies with a mismatching header. This usually means the device
|
|
// is in browser mode.
|
|
var errTrezorReplyInvalidHeader = errors.New("trezor: invalid reply header")
|
|
|
|
// trezorDriver implements the communication with a Trezor hardware wallet.
|
|
type trezorDriver struct {
|
|
device io.ReadWriter // USB device connection to communicate through
|
|
version [3]uint32 // Current version of the Trezor firmware
|
|
label string // Current textual label of the Trezor device
|
|
pinwait bool // Flags whether the device is waiting for PIN entry
|
|
passphrasewait bool // Flags whether the device is waiting for passphrase entry
|
|
failure error // Any failure that would make the device unusable
|
|
log log.Logger // Contextual logger to tag the trezor with its id
|
|
}
|
|
|
|
// newTrezorDriver creates a new instance of a Trezor USB protocol driver.
|
|
func newTrezorDriver(logger log.Logger) driver {
|
|
return &trezorDriver{
|
|
log: logger,
|
|
}
|
|
}
|
|
|
|
// Status implements accounts.Wallet, always whether the Trezor is opened, closed
|
|
// or whether the Ethereum app was not started on it.
|
|
func (w *trezorDriver) Status() (string, error) {
|
|
if w.failure != nil {
|
|
return fmt.Sprintf("Failed: %v", w.failure), w.failure
|
|
}
|
|
if w.device == nil {
|
|
return "Closed", w.failure
|
|
}
|
|
if w.pinwait {
|
|
return fmt.Sprintf("Trezor v%d.%d.%d '%s' waiting for PIN", w.version[0], w.version[1], w.version[2], w.label), w.failure
|
|
}
|
|
return fmt.Sprintf("Trezor v%d.%d.%d '%s' online", w.version[0], w.version[1], w.version[2], w.label), w.failure
|
|
}
|
|
|
|
// Open implements usbwallet.driver, attempting to initialize the connection to
|
|
// the Trezor hardware wallet. Initializing the Trezor is a two or three phase operation:
|
|
// - The first phase is to initialize the connection and read the wallet's
|
|
// features. This phase is invoked if the provided passphrase is empty. The
|
|
// device will display the pinpad as a result and will return an appropriate
|
|
// error to notify the user that a second open phase is needed.
|
|
// - The second phase is to unlock access to the Trezor, which is done by the
|
|
// user actually providing a passphrase mapping a keyboard keypad to the pin
|
|
// number of the user (shuffled according to the pinpad displayed).
|
|
// - If needed the device will ask for passphrase which will require calling
|
|
// open again with the actual passphrase (3rd phase)
|
|
func (w *trezorDriver) Open(device io.ReadWriter, passphrase string) error {
|
|
w.device, w.failure = device, nil
|
|
|
|
// If phase 1 is requested, init the connection and wait for user callback
|
|
if passphrase == "" && !w.passphrasewait {
|
|
// If we're already waiting for a PIN entry, insta-return
|
|
if w.pinwait {
|
|
return ErrTrezorPINNeeded
|
|
}
|
|
// Initialize a connection to the device
|
|
features := new(trezor.Features)
|
|
if _, err := w.trezorExchange(&trezor.Initialize{}, features); err != nil {
|
|
return err
|
|
}
|
|
w.version = [3]uint32{features.GetMajorVersion(), features.GetMinorVersion(), features.GetPatchVersion()}
|
|
w.label = features.GetLabel()
|
|
|
|
// Do a manual ping, forcing the device to ask for its PIN and Passphrase
|
|
askPin := true
|
|
askPassphrase := true
|
|
res, err := w.trezorExchange(&trezor.Ping{PinProtection: &askPin, PassphraseProtection: &askPassphrase}, new(trezor.PinMatrixRequest), new(trezor.PassphraseRequest), new(trezor.Success))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Only return the PIN request if the device wasn't unlocked until now
|
|
switch res {
|
|
case 0:
|
|
w.pinwait = true
|
|
return ErrTrezorPINNeeded
|
|
case 1:
|
|
w.pinwait = false
|
|
w.passphrasewait = true
|
|
return ErrTrezorPassphraseNeeded
|
|
case 2:
|
|
return nil // responded with trezor.Success
|
|
}
|
|
}
|
|
// Phase 2 requested with actual PIN entry
|
|
if w.pinwait {
|
|
w.pinwait = false
|
|
res, err := w.trezorExchange(&trezor.PinMatrixAck{Pin: &passphrase}, new(trezor.Success), new(trezor.PassphraseRequest))
|
|
if err != nil {
|
|
w.failure = err
|
|
return err
|
|
}
|
|
if res == 1 {
|
|
w.passphrasewait = true
|
|
return ErrTrezorPassphraseNeeded
|
|
}
|
|
} else if w.passphrasewait {
|
|
w.passphrasewait = false
|
|
if _, err := w.trezorExchange(&trezor.PassphraseAck{Passphrase: &passphrase}, new(trezor.Success)); err != nil {
|
|
w.failure = err
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close implements usbwallet.driver, cleaning up and metadata maintained within
|
|
// the Trezor driver.
|
|
func (w *trezorDriver) Close() error {
|
|
w.version, w.label, w.pinwait = [3]uint32{}, "", false
|
|
return nil
|
|
}
|
|
|
|
// Heartbeat implements usbwallet.driver, performing a sanity check against the
|
|
// Trezor to see if it's still online.
|
|
func (w *trezorDriver) Heartbeat() error {
|
|
if _, err := w.trezorExchange(&trezor.Ping{}, new(trezor.Success)); err != nil {
|
|
w.failure = err
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Derive implements usbwallet.driver, sending a derivation request to the Trezor
|
|
// and returning the Ethereum address located on that derivation path.
|
|
func (w *trezorDriver) Derive(path accounts.DerivationPath) (common.Address, error) {
|
|
return w.trezorDerive(path)
|
|
}
|
|
|
|
// SignTx implements usbwallet.driver, sending the transaction to the Trezor and
|
|
// waiting for the user to confirm or deny the transaction.
|
|
func (w *trezorDriver) SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) {
|
|
if w.device == nil {
|
|
return common.Address{}, nil, accounts.ErrWalletClosed
|
|
}
|
|
return w.trezorSign(path, tx, chainID)
|
|
}
|
|
|
|
func (w *trezorDriver) SignTypedMessage(path accounts.DerivationPath, domainHash []byte, messageHash []byte) ([]byte, error) {
|
|
return nil, accounts.ErrNotSupported
|
|
}
|
|
|
|
// trezorDerive sends a derivation request to the Trezor device and returns the
|
|
// Ethereum address located on that path.
|
|
func (w *trezorDriver) trezorDerive(derivationPath []uint32) (common.Address, error) {
|
|
address := new(trezor.EthereumAddress)
|
|
if _, err := w.trezorExchange(&trezor.EthereumGetAddress{AddressN: derivationPath}, address); err != nil {
|
|
return common.Address{}, err
|
|
}
|
|
if addr := address.GetAddressBin(); len(addr) > 0 { // Older firmwares use binary formats
|
|
return common.BytesToAddress(addr), nil
|
|
}
|
|
if addr := address.GetAddressHex(); len(addr) > 0 { // Newer firmwares use hexadecimal formats
|
|
return common.HexToAddress(addr), nil
|
|
}
|
|
return common.Address{}, errors.New("missing derived address")
|
|
}
|
|
|
|
// trezorSign sends the transaction to the Trezor wallet, and waits for the user
|
|
// to confirm or deny the transaction.
|
|
func (w *trezorDriver) trezorSign(derivationPath []uint32, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) {
|
|
// Create the transaction initiation message
|
|
data := tx.Data()
|
|
length := uint32(len(data))
|
|
|
|
request := &trezor.EthereumSignTx{
|
|
AddressN: derivationPath,
|
|
Nonce: new(big.Int).SetUint64(tx.Nonce()).Bytes(),
|
|
GasPrice: tx.GasPrice().Bytes(),
|
|
GasLimit: new(big.Int).SetUint64(tx.Gas()).Bytes(),
|
|
Value: tx.Value().Bytes(),
|
|
DataLength: &length,
|
|
}
|
|
if to := tx.To(); to != nil {
|
|
// Non contract deploy, set recipient explicitly
|
|
hex := to.Hex()
|
|
request.ToHex = &hex // Newer firmwares (old will ignore)
|
|
request.ToBin = (*to)[:] // Older firmwares (new will ignore)
|
|
}
|
|
if length > 1024 { // Send the data chunked if that was requested
|
|
request.DataInitialChunk, data = data[:1024], data[1024:]
|
|
} else {
|
|
request.DataInitialChunk, data = data, nil
|
|
}
|
|
if chainID != nil { // EIP-155 transaction, set chain ID explicitly (only 32 bit is supported!?)
|
|
id := uint32(chainID.Int64())
|
|
request.ChainId = &id
|
|
}
|
|
// Send the initiation message and stream content until a signature is returned
|
|
response := new(trezor.EthereumTxRequest)
|
|
if _, err := w.trezorExchange(request, response); err != nil {
|
|
return common.Address{}, nil, err
|
|
}
|
|
for response.DataLength != nil && int(*response.DataLength) <= len(data) {
|
|
chunk := data[:*response.DataLength]
|
|
data = data[*response.DataLength:]
|
|
|
|
if _, err := w.trezorExchange(&trezor.EthereumTxAck{DataChunk: chunk}, response); err != nil {
|
|
return common.Address{}, nil, err
|
|
}
|
|
}
|
|
// Extract the Ethereum signature and do a sanity validation
|
|
if len(response.GetSignatureR()) == 0 || len(response.GetSignatureS()) == 0 || response.GetSignatureV() == 0 {
|
|
return common.Address{}, nil, errors.New("reply lacks signature")
|
|
}
|
|
signature := append(append(response.GetSignatureR(), response.GetSignatureS()...), byte(response.GetSignatureV()))
|
|
|
|
// Create the correct signer and signature transform based on the chain ID
|
|
var signer types.Signer
|
|
if chainID == nil {
|
|
signer = new(types.HomesteadSigner)
|
|
} else {
|
|
// Trezor backend does not support typed transactions yet.
|
|
signer = types.NewEIP155Signer(chainID)
|
|
signature[64] -= byte(chainID.Uint64()*2 + 35)
|
|
}
|
|
|
|
// Inject the final signature into the transaction and sanity check the sender
|
|
signed, err := tx.WithSignature(signer, signature)
|
|
if err != nil {
|
|
return common.Address{}, nil, err
|
|
}
|
|
sender, err := types.Sender(signer, signed)
|
|
if err != nil {
|
|
return common.Address{}, nil, err
|
|
}
|
|
return sender, signed, nil
|
|
}
|
|
|
|
// trezorExchange performs a data exchange with the Trezor wallet, sending it a
|
|
// message and retrieving the response. If multiple responses are possible, the
|
|
// method will also return the index of the destination object used.
|
|
func (w *trezorDriver) trezorExchange(req proto.Message, results ...proto.Message) (int, error) {
|
|
// Construct the original message payload to chunk up
|
|
data, err := proto.Marshal(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
payload := make([]byte, 8+len(data))
|
|
copy(payload, []byte{0x23, 0x23})
|
|
binary.BigEndian.PutUint16(payload[2:], trezor.Type(req))
|
|
binary.BigEndian.PutUint32(payload[4:], uint32(len(data)))
|
|
copy(payload[8:], data)
|
|
|
|
// Stream all the chunks to the device
|
|
chunk := make([]byte, 64)
|
|
chunk[0] = 0x3f // Report ID magic number
|
|
|
|
for len(payload) > 0 {
|
|
// Construct the new message to stream, padding with zeroes if needed
|
|
if len(payload) > 63 {
|
|
copy(chunk[1:], payload[:63])
|
|
payload = payload[63:]
|
|
} else {
|
|
copy(chunk[1:], payload)
|
|
copy(chunk[1+len(payload):], make([]byte, 63-len(payload)))
|
|
payload = nil
|
|
}
|
|
// Send over to the device
|
|
w.log.Trace("Data chunk sent to the Trezor", "chunk", hexutil.Bytes(chunk))
|
|
if _, err := w.device.Write(chunk); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
// Stream the reply back from the wallet in 64 byte chunks
|
|
var (
|
|
kind uint16
|
|
reply []byte
|
|
)
|
|
for {
|
|
// Read the next chunk from the Trezor wallet
|
|
if _, err := io.ReadFull(w.device, chunk); err != nil {
|
|
return 0, err
|
|
}
|
|
w.log.Trace("Data chunk received from the Trezor", "chunk", hexutil.Bytes(chunk))
|
|
|
|
// Make sure the transport header matches
|
|
if chunk[0] != 0x3f || (len(reply) == 0 && (chunk[1] != 0x23 || chunk[2] != 0x23)) {
|
|
return 0, errTrezorReplyInvalidHeader
|
|
}
|
|
// If it's the first chunk, retrieve the reply message type and total message length
|
|
var payload []byte
|
|
|
|
if len(reply) == 0 {
|
|
kind = binary.BigEndian.Uint16(chunk[3:5])
|
|
reply = make([]byte, 0, int(binary.BigEndian.Uint32(chunk[5:9])))
|
|
payload = chunk[9:]
|
|
} else {
|
|
payload = chunk[1:]
|
|
}
|
|
// Append to the reply and stop when filled up
|
|
if left := cap(reply) - len(reply); left > len(payload) {
|
|
reply = append(reply, payload...)
|
|
} else {
|
|
reply = append(reply, payload[:left]...)
|
|
break
|
|
}
|
|
}
|
|
// Try to parse the reply into the requested reply message
|
|
if kind == uint16(trezor.MessageType_MessageType_Failure) {
|
|
// Trezor returned a failure, extract and return the message
|
|
failure := new(trezor.Failure)
|
|
if err := proto.Unmarshal(reply, failure); err != nil {
|
|
return 0, err
|
|
}
|
|
return 0, errors.New("trezor: " + failure.GetMessage())
|
|
}
|
|
if kind == uint16(trezor.MessageType_MessageType_ButtonRequest) {
|
|
// Trezor is waiting for user confirmation, ack and wait for the next message
|
|
return w.trezorExchange(&trezor.ButtonAck{}, results...)
|
|
}
|
|
for i, res := range results {
|
|
if trezor.Type(res) == kind {
|
|
return i, proto.Unmarshal(reply, res)
|
|
}
|
|
}
|
|
expected := make([]string, len(results))
|
|
for i, res := range results {
|
|
expected[i] = trezor.Name(trezor.Type(res))
|
|
}
|
|
return 0, fmt.Errorf("trezor: expected reply types %s, got %s", expected, trezor.Name(kind))
|
|
}
|