Skip to content

Commit 3a72308

Browse files
Merge pull request #25 from BitHighlander/test/eth-signing-guards-master
test(eth): EIP-1559 + contract clear-sign signing-guard regression tests
2 parents a79ce7b + 79ff6b1 commit 3a72308

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

‎tests/test_msg_ethereum_erc20_0x_signtx.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ def test_sign_longdata_swap(self):
164164
# test transformERC20
165165
def test__sign_transformERC20(self):
166166
self.requires_fullFeature()
167+
# transformERC20 is pinned to the 0x ExchangeProxy and bounded by its
168+
# displayed input/min-output amounts, so it clear-signs WITHOUT
169+
# AdvancedMode at any calldata size (the transformations[] tail exceeds
170+
# one chunk). No AdvancedMode policy is set here on purpose.
167171
self.requires_firmware("7.1.5")
168172
self.setup_mnemonic_nopin_nopassphrase()
169173
# transformERC20 to the 0x Exchange Proxy is blind contract data (no
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# This file is part of the KeepKey project.
2+
#
3+
# Regression tests for Ethereum signing pre-image / clear-sign correctness:
4+
# - EIP-1559 transaction-type vs fee-field / chain_id consistency, and
5+
# - contract clear-sign handlers must not confirm a prefix while later
6+
# streamed calldata is signed unshown, nor classify a contract CREATE.
7+
#
8+
# These exercise the guards added in the firmware ethereum signing path.
9+
10+
import unittest
11+
import common
12+
import binascii
13+
14+
import keepkeylib.messages_ethereum_pb2 as eth_proto
15+
from keepkeylib.client import CallException
16+
from keepkeylib.tools import int_to_big_endian
17+
18+
# Sablier proxy address — the withdrawFromSalary clear-sign handler target.
19+
SABLIER_PROXY = binascii.unhexlify("bd6a40bb904aea5a49c59050b5395f7484a4203d")
20+
RECIPIENT = binascii.unhexlify("1d1c328764a41bda0492b66baa30c4a339ff85ef")
21+
22+
23+
class TestMsgEthereumSigningGuards(common.KeepKeyTest):
24+
# ---- EIP-1559 type / fee / chain_id pre-image consistency ----
25+
26+
def test_eip1559_requires_chain_id(self):
27+
"""type=2 with no chain_id: Stage 1 counts chain_id as 1 byte but
28+
hash_rlp_number(0) hashes nothing -> over-declared list header ->
29+
wrong/garbage signer. The device must reject rather than sign it."""
30+
self.requires_firmware("7.15.1")
31+
self.requires_fullFeature()
32+
self.setup_mnemonic_nopin_nopassphrase()
33+
self.client.apply_policy("AdvancedMode", 1)
34+
self.assertRaises(
35+
CallException,
36+
self.client.ethereum_sign_tx,
37+
n=[0, 0],
38+
nonce=0,
39+
gas_limit=21000,
40+
max_fee_per_gas=20,
41+
max_priority_fee_per_gas=1,
42+
to=RECIPIENT,
43+
value=10,
44+
# chain_id intentionally omitted -> chain_id == 0
45+
)
46+
47+
def test_eip1559_no_priority_fee_signs(self):
48+
"""max_priority_fee_per_gas is a mandatory EIP-1559 RLP field; when
49+
absent it must encode as the empty integer (0x80). Stage 1 always
50+
counts it, so Stage 2 must always hash it -- the device must still
51+
produce a valid signature (not desync the list header)."""
52+
self.requires_firmware("7.15.1")
53+
self.requires_fullFeature()
54+
self.setup_mnemonic_nopin_nopassphrase()
55+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
56+
n=[0, 0],
57+
nonce=0,
58+
gas_limit=21000,
59+
max_fee_per_gas=20, # no max_priority_fee_per_gas
60+
to=RECIPIENT,
61+
value=10,
62+
chain_id=1,
63+
)
64+
self.assertIn(sig_v, (0, 1)) # EIP-1559 recovery-id parity
65+
self.assertEqual(len(sig_r), 32)
66+
self.assertEqual(len(sig_s), 32)
67+
68+
def test_type2_without_max_fee_rejected(self):
69+
"""Typed prefix (0x02) is chosen from msg.type but the fee fields from
70+
has_max_fee_per_gas. A type=2 tx carrying only gas_price would sign a
71+
malformed (legacy-fee-in-1559-envelope) field list -> reject."""
72+
self.requires_firmware("7.15.1")
73+
self.requires_fullFeature()
74+
self.setup_mnemonic_nopin_nopassphrase()
75+
self.client.apply_policy("AdvancedMode", 1)
76+
msg = eth_proto.EthereumSignTx(
77+
address_n=[0, 0],
78+
nonce=int_to_big_endian(0),
79+
gas_price=int_to_big_endian(20), # legacy fee field ...
80+
gas_limit=int_to_big_endian(21000),
81+
value=int_to_big_endian(10),
82+
chain_id=1,
83+
type=2, # ... but typed as EIP-1559
84+
)
85+
msg.to = RECIPIENT
86+
self.assertRaises(CallException, self.client.call, msg)
87+
88+
def test_legacy_with_max_fee_rejected(self):
89+
"""A legacy tx (type omitted) carrying max_fee_per_gas would hash two
90+
fee fields into a legacy structure -> reject the mismatch."""
91+
self.requires_firmware("7.15.1")
92+
self.requires_fullFeature()
93+
self.setup_mnemonic_nopin_nopassphrase()
94+
self.client.apply_policy("AdvancedMode", 1)
95+
msg = eth_proto.EthereumSignTx(
96+
address_n=[0, 0],
97+
nonce=int_to_big_endian(0),
98+
max_fee_per_gas=int_to_big_endian(20),
99+
max_priority_fee_per_gas=int_to_big_endian(1),
100+
gas_limit=int_to_big_endian(21000),
101+
value=int_to_big_endian(10),
102+
chain_id=1,
103+
# type omitted -> legacy
104+
)
105+
msg.to = RECIPIENT
106+
self.assertRaises(CallException, self.client.call, msg)
107+
108+
# ---- Contract clear-sign handler gate ----
109+
110+
def test_contract_handler_streamed_calldata_signs_full_data(self):
111+
"""A handler selector (sablier withdrawFromSalary) whose calldata is
112+
larger than the initial chunk must NOT be clear-signed from the prefix.
113+
The device falls back to generic raw-data confirmation and signs the
114+
full streamed calldata.
115+
116+
Asserts here that signing completes over the full (streamed) calldata;
117+
the screen-level assertion (no 'Sablier' clear-sign summary appears for
118+
streamed calldata) is verified on-device / on the emulator via
119+
DebugLink layout."""
120+
self.requires_firmware("7.15.1")
121+
self.requires_fullFeature()
122+
self.setup_mnemonic_nopin_nopassphrase()
123+
self.client.apply_policy("AdvancedMode", 1)
124+
# withdrawFromSalary selector + 2 words, then padded past 1024 bytes so
125+
# data_total != data_initial_chunk.size (forces the streaming path).
126+
data = binascii.unhexlify(
127+
"fea7c53f"
128+
+ "0000000000000000000000000000000000000000000000000000000000001210"
129+
+ "0000000000000000000000000000000000000000000000000000000000000001"
130+
) + b"\x00" * 1100
131+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
132+
n=[2147483692, 2147483708, 2147483648, 0, 0],
133+
nonce=0xAB,
134+
gas_price=0x24C988AC00,
135+
gas_limit=0x26249,
136+
value=0,
137+
to=SABLIER_PROXY,
138+
address_type=0,
139+
chain_id=1,
140+
data=data,
141+
)
142+
self.assertEqual(len(sig_r), 32)
143+
self.assertEqual(len(sig_s), 32)
144+
145+
146+
if __name__ == "__main__":
147+
unittest.main()

0 commit comments

Comments
 (0)