mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
488 lines
17 KiB
Go
488 lines
17 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"
|
|
"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"
|
|
"google.golang.org/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")
|
|
|
|
const trezorMaxDataChunk = 1024
|
|
|
|
// 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.GetAddress(); addr != "" {
|
|
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) {
|
|
request, payload, err := w.buildSignRequest(derivationPath, tx, chainID)
|
|
if err != nil {
|
|
return common.Address{}, nil, err
|
|
}
|
|
if len(payload) > trezorMaxDataChunk {
|
|
setInitialChunk(request, payload[:trezorMaxDataChunk])
|
|
payload = payload[trezorMaxDataChunk:]
|
|
} else {
|
|
setInitialChunk(request, payload)
|
|
payload = nil
|
|
}
|
|
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(payload) {
|
|
chunk := payload[:*response.DataLength]
|
|
payload = payload[*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 {
|
|
return common.Address{}, nil, errors.New("reply lacks signature")
|
|
} else if response.GetSignatureV() == 0 && int(chainID.Int64()) <= (math.MaxUint32-36)/2 {
|
|
// for chainId >= (MaxUint32-36)/2, Trezor returns signature bit only
|
|
// https://github.com/trezor/trezor-mcu/pull/399
|
|
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
|
|
legacyTx := tx.Type() == types.LegacyTxType
|
|
if chainID == nil {
|
|
signer = new(types.HomesteadSigner)
|
|
} else {
|
|
signer = types.LatestSignerForChainID(chainID)
|
|
// Legacy (EIP-155) transactions still return the final ECDSA V value.
|
|
if legacyTx && signature[64] > 1 {
|
|
if !chainID.IsUint64() {
|
|
return common.Address{}, nil, errors.New("chain id overflows uint64")
|
|
}
|
|
sigAdj := chainID.Uint64()*2 + 35
|
|
if sigAdj > math.MaxUint8 {
|
|
return common.Address{}, nil, errors.New("chain id is too large for trezor response")
|
|
}
|
|
signature[64] -= byte(sigAdj)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (w *trezorDriver) buildSignRequest(derivationPath []uint32, tx *types.Transaction, chainID *big.Int) (proto.Message, []byte, error) {
|
|
payload := tx.Data()
|
|
length := uint32(len(payload))
|
|
switch tx.Type() {
|
|
case types.LegacyTxType:
|
|
req := &trezor.EthereumSignTx{
|
|
AddressN: derivationPath,
|
|
Nonce: encodeUint64(tx.Nonce()),
|
|
GasPrice: bigIntBytes(tx.GasPrice()),
|
|
GasLimit: encodeUint64(tx.Gas()),
|
|
Value: bigIntBytes(tx.Value()),
|
|
DataLength: &length,
|
|
}
|
|
if chainID != nil {
|
|
if !chainID.IsUint64() {
|
|
return nil, nil, errors.New("chain id overflows trezor message")
|
|
}
|
|
id := chainID.Uint64()
|
|
req.ChainId = &id
|
|
}
|
|
if to := tx.To(); to != nil {
|
|
addr := to.Hex()
|
|
req.To = &addr
|
|
}
|
|
return req, payload, nil
|
|
case types.AccessListTxType, types.DynamicFeeTxType, types.BlobTxType:
|
|
if chainID == nil {
|
|
return nil, nil, errors.New("typed transactions require a chain id")
|
|
}
|
|
if !chainID.IsUint64() {
|
|
return nil, nil, errors.New("chain id overflows trezor message")
|
|
}
|
|
id := chainID.Uint64()
|
|
req := &trezor.EthereumSignTxEIP1559{
|
|
AddressN: derivationPath,
|
|
Nonce: encodeUint64(tx.Nonce()),
|
|
GasLimit: encodeUint64(tx.Gas()),
|
|
Value: bigIntBytes(tx.Value()),
|
|
DataLength: &length,
|
|
ChainId: &id,
|
|
AccessList: convertAccessListEIP1559(tx.AccessList()),
|
|
}
|
|
if to := tx.To(); to != nil {
|
|
addr := to.Hex()
|
|
req.To = &addr
|
|
}
|
|
switch tx.Type() {
|
|
case types.AccessListTxType:
|
|
price := bigIntBytes(tx.GasPrice())
|
|
req.MaxGasFee = price
|
|
req.MaxPriorityFee = price
|
|
case types.DynamicFeeTxType:
|
|
req.MaxGasFee = bigIntBytes(tx.GasFeeCap())
|
|
req.MaxPriorityFee = bigIntBytes(tx.GasTipCap())
|
|
case types.BlobTxType:
|
|
req.MaxGasFee = bigIntBytes(tx.GasFeeCap())
|
|
req.MaxPriorityFee = bigIntBytes(tx.GasTipCap())
|
|
req.MaxFeePerBlobGas = bigIntBytes(tx.BlobGasFeeCap())
|
|
req.BlobVersionedHashes = convertBlobHashes(tx.BlobHashes())
|
|
}
|
|
return req, payload, nil
|
|
default:
|
|
return nil, nil, fmt.Errorf("unsupported transaction type %d", tx.Type())
|
|
}
|
|
}
|
|
|
|
func encodeUint64(value uint64) []byte {
|
|
if value == 0 {
|
|
return nil
|
|
}
|
|
return new(big.Int).SetUint64(value).Bytes()
|
|
}
|
|
|
|
func bigIntBytes(v *big.Int) []byte {
|
|
if v == nil || v.Sign() == 0 {
|
|
return nil
|
|
}
|
|
return new(big.Int).Set(v).Bytes()
|
|
}
|
|
|
|
func convertAccessListEIP1559(list types.AccessList) []*trezor.EthereumSignTxEIP1559_EthereumAccessList {
|
|
if len(list) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]*trezor.EthereumSignTxEIP1559_EthereumAccessList, len(list))
|
|
for i, entry := range list {
|
|
addr := entry.Address.Hex()
|
|
addrCopy := addr
|
|
keys := make([][]byte, len(entry.StorageKeys))
|
|
for j, key := range entry.StorageKeys {
|
|
keys[j] = append([]byte{}, key[:]...)
|
|
}
|
|
out[i] = &trezor.EthereumSignTxEIP1559_EthereumAccessList{
|
|
Address: &addrCopy,
|
|
StorageKeys: keys,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func convertBlobHashes(hashes []common.Hash) [][]byte {
|
|
if len(hashes) == 0 {
|
|
return nil
|
|
}
|
|
out := make([][]byte, len(hashes))
|
|
for i, h := range hashes {
|
|
out[i] = append([]byte{}, h[:]...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func setInitialChunk(msg proto.Message, chunk []byte) {
|
|
switch req := msg.(type) {
|
|
case *trezor.EthereumSignTx:
|
|
req.DataInitialChunk = chunk
|
|
case *trezor.EthereumSignTxEIP1559:
|
|
req.DataInitialChunk = chunk
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
}
|