This commit is contained in:
Lucia 2026-02-25 21:57:09 -08:00 committed by GitHub
commit 16f9b45af5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 903 additions and 598 deletions

View file

@ -50,6 +50,8 @@ var ErrTrezorPassphraseNeeded = errors.New("trezor: passphrase needed")
// 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
@ -197,10 +199,7 @@ func (w *trezorDriver) trezorDerive(derivationPath []uint32) (common.Address, er
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
if addr := address.GetAddress(); addr != "" {
return common.HexToAddress(addr), nil
}
return common.Address{}, errors.New("missing derived address")
@ -209,41 +208,24 @@ func (w *trezorDriver) trezorDerive(derivationPath []uint32) (common.Address, er
// 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,
request, payload, err := w.buildSignRequest(derivationPath, tx, chainID)
if err != nil {
return common.Address{}, nil, err
}
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:]
if len(payload) > trezorMaxDataChunk {
setInitialChunk(request, payload[:trezorMaxDataChunk])
payload = payload[trezorMaxDataChunk:]
} else {
request.DataInitialChunk, data = data, nil
setInitialChunk(request, payload)
payload = 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:]
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
@ -261,15 +243,21 @@ func (w *trezorDriver) trezorSign(derivationPath []uint32, tx *types.Transaction
// 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 {
// Trezor backend does not support typed transactions yet.
signer = types.NewEIP155Signer(chainID)
// if chainId is above (MaxUint32 - 36) / 2 then the final v values is returned
// directly. Otherwise, the returned value is 35 + chainid * 2.
if signature[64] > 1 && int(chainID.Int64()) <= (math.MaxUint32-36)/2 {
signature[64] -= byte(chainID.Uint64()*2 + 35)
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)
}
}
@ -285,6 +273,126 @@ func (w *trezorDriver) trezorSign(derivationPath []uint32, tx *types.Transaction
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.

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,12 @@
// This file originates from the SatoshiLabs Trezor `common` repository at:
// https://github.com/trezor/trezor-common/blob/master/protob/messages-ethereum.proto
// dated 28.05.2019, commit 893fd219d4a01bcffa0cd9cfa631856371ec5aa9.
syntax = "proto2";
package hw.trezor.messages.ethereum;
option go_package = "github.com/ethereum/go-ethereum/accounts/usbwallet/trezor";
// Sugar for easier handling in Java
option java_package = "com.satoshilabs.trezor.lib.protobuf";
option java_outer_classname = "TrezorMessageEthereum";
import "messages-common.proto";
import "options.proto";
/**
* Request: Ask device for public key corresponding to address_n path
@ -21,8 +15,8 @@ import "messages-common.proto";
* @next Failure
*/
message EthereumGetPublicKey {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // optionally show on display before sending the result
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // optionally show on display before sending the result
}
/**
@ -30,8 +24,8 @@ message EthereumGetPublicKey {
* @end
*/
message EthereumPublicKey {
optional hw.trezor.messages.common.HDNodeType node = 1; // BIP32 public node
optional string xpub = 2; // serialized form of public node
required hw.trezor.messages.common.HDNodeType node = 1; // BIP32 public node
required string xpub = 2; // serialized form of public node
}
/**
@ -41,8 +35,10 @@ message EthereumPublicKey {
* @next Failure
*/
message EthereumGetAddress {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // optionally show on display before sending the result
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // optionally show on display before sending the result
optional bytes encoded_network = 3; // encoded Ethereum network, see external-definitions.md for details
optional bool chunkify = 4; // display the address in chunks of 4 characters
}
/**
@ -50,30 +46,63 @@ message EthereumGetAddress {
* @end
*/
message EthereumAddress {
optional bytes addressBin = 1; // Ethereum address as 20 bytes (legacy firmwares)
optional string addressHex = 2; // Ethereum address as hex string (newer firmwares)
optional bytes _old_address = 1 [deprecated=true]; // trezor <1.8.0, <2.1.0 - raw bytes of Ethereum address
optional string address = 2; // Ethereum address as hex-encoded string
optional bytes mac = 3; // Address authentication code
}
/**
* Request: Ask device to sign transaction
* All fields are optional from the protocol's point of view. Each field defaults to value `0` if missing.
* gas_price, gas_limit and chain_id must be provided and non-zero.
* All other fields are optional and default to value `0` if missing.
* Note: the first at most 1024 bytes of data MUST be transmitted as part of this message.
* @start
* @next EthereumTxRequest
* @next Failure
*/
message EthereumSignTx {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bytes nonce = 2; // <=256 bit unsigned big endian
optional bytes gas_price = 3; // <=256 bit unsigned big endian (in wei)
optional bytes gas_limit = 4; // <=256 bit unsigned big endian
optional bytes toBin = 5; // recipient address (20 bytes, legacy firmware)
optional string toHex = 11; // recipient address (hex string, newer firmware)
optional bytes value = 6; // <=256 bit unsigned big endian (in wei)
optional bytes data_initial_chunk = 7; // The initial data chunk (<= 1024 bytes)
optional uint32 data_length = 8; // Length of transaction payload
optional uint32 chain_id = 9; // Chain Id for EIP 155
optional uint32 tx_type = 10; // (only for Wanchain)
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bytes nonce = 2 [default='']; // <=256 bit unsigned big endian
required bytes gas_price = 3; // <=256 bit unsigned big endian (in wei)
required bytes gas_limit = 4; // <=256 bit unsigned big endian
optional string to = 11 [default='']; // recipient address
optional bytes value = 6 [default='']; // <=256 bit unsigned big endian (in wei)
optional bytes data_initial_chunk = 7 [default='']; // The initial data chunk (<= 1024 bytes)
optional uint32 data_length = 8 [default=0]; // Length of transaction payload
required uint64 chain_id = 9; // Chain Id for EIP 155
optional uint32 tx_type = 10; // Used for Wanchain
optional EthereumDefinitions definitions = 12; // network and/or token definitions for tx
optional bool chunkify = 13; // display the address in chunks of 4 characters
optional common.PaymentRequest payment_req = 14 [(experimental_field)=true]; // SLIP-24 payment request
}
/**
* Request: Ask device to sign EIP1559 transaction
* Note: the first at most 1024 bytes of data MUST be transmitted as part of this message.
* @start
* @next EthereumTxRequest
* @next Failure
*/
message EthereumSignTxEIP1559 {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
required bytes nonce = 2; // <=256 bit unsigned big endian
required bytes max_gas_fee = 3; // <=256 bit unsigned big endian (in wei)
required bytes max_priority_fee = 4; // <=256 bit unsigned big endian (in wei)
required bytes gas_limit = 5; // <=256 bit unsigned big endian
optional string to = 6 [default='']; // recipient address
required bytes value = 7; // <=256 bit unsigned big endian (in wei)
optional bytes data_initial_chunk = 8 [default='']; // The initial data chunk (<= 1024 bytes)
required uint32 data_length = 9; // Length of transaction payload
required uint64 chain_id = 10; // Chain Id for EIP 155
repeated EthereumAccessList access_list = 11; // Access List
optional EthereumDefinitions definitions = 12; // network and/or token definitions for tx
optional bool chunkify = 13; // display the address in chunks of 4 characters
optional common.PaymentRequest payment_req = 14 [(experimental_field)=true]; // SLIP-24 payment request
message EthereumAccessList {
required string address = 1;
repeated bytes storage_keys = 2;
}
}
/**
@ -95,7 +124,7 @@ message EthereumTxRequest {
* @next EthereumTxRequest
*/
message EthereumTxAck {
optional bytes data_chunk = 1; // Bytes from transaction payload (<= 1024 bytes)
required bytes data_chunk = 1; // Bytes from transaction payload (<= 1024 bytes)
}
/**
@ -105,8 +134,10 @@ message EthereumTxAck {
* @next Failure
*/
message EthereumSignMessage {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bytes message = 2; // message to be signed
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
required bytes message = 2; // message to be signed
optional bytes encoded_network = 3; // encoded Ethereum network, see external-definitions.md for details
optional bool chunkify = 4; // display the address in chunks of 4 characters
}
/**
@ -114,9 +145,8 @@ message EthereumSignMessage {
* @end
*/
message EthereumMessageSignature {
optional bytes addressBin = 1; // address used to sign the message (20 bytes, legacy firmware)
optional bytes signature = 2; // signature of the message
optional string addressHex = 3; // address used to sign the message (hex string, newer firmware)
required bytes signature = 2; // signature of the message
required string address = 3; // address used to sign the message
}
/**
@ -126,8 +156,39 @@ message EthereumMessageSignature {
* @next Failure
*/
message EthereumVerifyMessage {
optional bytes addressBin = 1; // address to verify (20 bytes, legacy firmware)
optional bytes signature = 2; // signature to verify
optional bytes message = 3; // message to verify
optional string addressHex = 4; // address to verify (hex string, newer firmware)
required bytes signature = 2; // signature to verify
required bytes message = 3; // message to verify
required string address = 4; // address to verify
optional bool chunkify = 5; // display the address in chunks of 4 characters
}
/**
* Request: Ask device to sign hash of typed data
* @start
* @next EthereumTypedDataSignature
* @next Failure
*/
message EthereumSignTypedHash {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
required bytes domain_separator_hash = 2; // Hash of domainSeparator of typed data to be signed
optional bytes message_hash = 3; // Hash of the data of typed data to be signed (empty if domain-only data)
optional bytes encoded_network = 4; // encoded Ethereum network, see external-definitions.md for details
}
/**
* Response: Signed typed data
* @end
*/
message EthereumTypedDataSignature {
required bytes signature = 1; // signature of the typed data
required string address = 2; // address used to sign the typed data
}
/**
* Contains an encoded network and/or token definition. See external-definitions.md for details.
* @embed
*/
message EthereumDefinitions {
optional bytes encoded_network = 1; // encoded ethereum network
optional bytes encoded_token = 2; // encoded ethereum token
}