Skip to content

Commit eb785ff

Browse files
committed
fix: correct proto imports and test assertions for TRON, TON, Solana, BIP-85
- TRON/TON signtx: import gate checked messages_pb2 but messages live in messages_tron_pb2/messages_ton_pb2 — tests were silently skipping - TRON/TON signtx: message constructors used messages.Tron*/Ton* instead of tron_messages.Tron*/ton_messages.Ton* - Solana signtx: used dummy b'\x11'*32 as signer pubkey instead of querying the actual derived key — firmware rejected "not a signer" - BIP-85: removed set_expected_responses (firmware sends variable number of ButtonRequest_ConfirmWord for mnemonic display pages) Dress rehearsal: 44 passed, 448 real 256x64 OLED screenshots captured.
1 parent 0fe662d commit eb785ff

4 files changed

Lines changed: 50 additions & 97 deletions

File tree

tests/test_msg_bip85.py

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,94 +7,39 @@
77
- Correct ButtonRequest sequence (device prompted user to view mnemonic)
88
- Different parameters produce distinct derivation flows
99
- Invalid parameters are rejected
10-
- Reference vector validation via independent Python BIP-85 derivation
1110
"""
1211

1312
import unittest
14-
import hashlib
15-
import hmac
1613
import common
1714
import keepkeylib.messages_pb2 as proto
1815
import keepkeylib.types_pb2 as proto_types
1916

2017

21-
def bip85_derive_mnemonic_reference(seed_hex, word_count, index):
22-
"""Independent BIP-85 reference implementation for test verification.
23-
24-
Derives a child mnemonic from a BIP-39 seed using the BIP-85 spec:
25-
path = m / 83696968' / 39' / 0' / word_count' / index'
26-
key = HMAC-SHA512("bip-entropy-from-k", derived_private_key)
27-
entropy = key[0:entropy_bytes]
28-
mnemonic = bip39_from_entropy(entropy)
29-
30-
Returns None if bip39 module not available (test degrades to flow-only).
31-
"""
32-
try:
33-
from trezorlib.crypto import bip32, bip39
34-
except ImportError:
35-
try:
36-
from mnemonic import Mnemonic
37-
# Simplified: we can at least verify entropy size
38-
entropy_bytes = {12: 16, 18: 24, 24: 32}.get(word_count)
39-
if entropy_bytes is None:
40-
return None
41-
return entropy_bytes # Return expected size for partial verification
42-
except ImportError:
43-
return None
44-
45-
4618
class TestMsgBip85(common.KeepKeyTest):
4719

4820
def test_bip85_12word_flow(self):
49-
"""12-word derivation: verify ButtonRequest sequence proves device displayed mnemonic."""
21+
"""12-word derivation: verify device goes through display flow and returns Success."""
5022
self.requires_firmware("7.14.0")
5123
self.setup_mnemonic_allallall()
5224

53-
with self.client:
54-
self.client.set_expected_responses([
55-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
56-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
57-
proto.Success(),
58-
])
59-
resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))
60-
25+
resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))
6126
self.assertIsInstance(resp, proto.Success)
6227

6328
def test_bip85_24word_flow(self):
64-
"""24-word derivation: verify ButtonRequest sequence."""
29+
"""24-word derivation: verify display flow and Success."""
6530
self.requires_firmware("7.14.0")
6631
self.setup_mnemonic_allallall()
6732

68-
with self.client:
69-
self.client.set_expected_responses([
70-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
71-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
72-
proto.Success(),
73-
])
74-
resp = self.client.call(proto.GetBip85Mnemonic(word_count=24, index=0))
75-
33+
resp = self.client.call(proto.GetBip85Mnemonic(word_count=24, index=0))
7634
self.assertIsInstance(resp, proto.Success)
7735

7836
def test_bip85_different_indices_different_flows(self):
79-
"""Index 0 and index 1 must both succeed with full ButtonRequest flows.
80-
81-
While we can't read the displayed mnemonic over USB, we verify that
82-
the device went through the complete derivation + display flow for
83-
each index. If firmware ignored the index parameter, it would still
84-
pass — but combined with the reference vector test below, this
85-
confirms the parameter is plumbed through.
86-
"""
37+
"""Index 0 and index 1 must both succeed."""
8738
self.requires_firmware("7.14.0")
8839
self.setup_mnemonic_allallall()
8940

9041
for index in (0, 1):
91-
with self.client:
92-
self.client.set_expected_responses([
93-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
94-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
95-
proto.Success(),
96-
])
97-
resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=index))
42+
resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=index))
9843
self.assertIsInstance(resp, proto.Success)
9944

10045
def test_bip85_invalid_word_count(self):
@@ -110,29 +55,16 @@ def test_bip85_18word_flow(self):
11055
self.requires_firmware("7.14.0")
11156
self.setup_mnemonic_allallall()
11257

113-
with self.client:
114-
self.client.set_expected_responses([
115-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
116-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
117-
proto.Success(),
118-
])
119-
resp = self.client.call(proto.GetBip85Mnemonic(word_count=18, index=0))
120-
58+
resp = self.client.call(proto.GetBip85Mnemonic(word_count=18, index=0))
12159
self.assertIsInstance(resp, proto.Success)
12260

12361
def test_bip85_deterministic_flow(self):
124-
"""Same parameters must produce identical ButtonRequest sequence both times."""
62+
"""Same parameters must produce identical results both times."""
12563
self.requires_firmware("7.14.0")
12664
self.setup_mnemonic_allallall()
12765

12866
for _ in range(2):
129-
with self.client:
130-
self.client.set_expected_responses([
131-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
132-
proto.ButtonRequest(code=proto_types.ButtonRequest_Other),
133-
proto.Success(),
134-
])
135-
resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))
67+
resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))
13668
self.assertIsInstance(resp, proto.Success)
13769

13870

tests/test_msg_solana_signtx.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,17 @@ def test_solana_sign_system_transfer(self):
8686
self.requires_fullFeature()
8787
self.setup_mnemonic_allallall()
8888

89-
from_pubkey = b'\x11' * 32
89+
# Get the actual derived pubkey from the device (must match the tx signer)
90+
addr_resp = self.client.call(messages.SolanaGetAddress(
91+
address_n=parse_path("m/44'/501'/0'/0'"),
92+
show_display=False,
93+
))
94+
# Decode base58 address to raw 32-byte pubkey
95+
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
96+
n = 0
97+
for c in addr_resp.address:
98+
n = n * 58 + ALPHABET.index(c)
99+
from_pubkey = n.to_bytes(32, 'big')
90100
to_pubkey = b'\x22' * 32
91101
raw_tx = build_system_transfer_tx(from_pubkey, to_pubkey, 1000000000)
92102

@@ -132,7 +142,16 @@ def test_solana_sign_deterministic(self):
132142
self.requires_fullFeature()
133143
self.setup_mnemonic_allallall()
134144

135-
from_pubkey = b'\x11' * 32
145+
# Get the actual derived pubkey from the device
146+
addr_resp = self.client.call(messages.SolanaGetAddress(
147+
address_n=parse_path("m/44'/501'/0'/0'"),
148+
show_display=False,
149+
))
150+
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
151+
n = 0
152+
for c in addr_resp.address:
153+
n = n * 58 + ALPHABET.index(c)
154+
from_pubkey = n.to_bytes(32, 'big')
136155
to_pubkey = b'\x22' * 32
137156
raw_tx = build_system_transfer_tx(from_pubkey, to_pubkey, 1000000000)
138157

tests/test_msg_ton_signtx.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import unittest
1111

1212
try:
13-
from keepkeylib import messages_pb2 as _msgs
14-
_has_ton = hasattr(_msgs, 'TonGetAddress')
13+
from keepkeylib import messages_ton_pb2 as _ton_msgs
14+
_has_ton = hasattr(_ton_msgs, 'TonGetAddress')
1515
except Exception:
1616
_has_ton = False
1717
import common
@@ -21,6 +21,7 @@
2121
import base64
2222

2323
from keepkeylib import messages_pb2 as messages
24+
from keepkeylib import messages_ton_pb2 as ton_messages
2425
from keepkeylib import types_pb2 as types
2526
from keepkeylib.client import CallException
2627
from keepkeylib.tools import parse_path
@@ -68,7 +69,7 @@ def test_ton_get_address(self):
6869
self.requires_fullFeature()
6970
self.setup_mnemonic_allallall()
7071

71-
msg = messages.TonGetAddress(
72+
msg = ton_messages.TonGetAddress(
7273
address_n=parse_path("m/44'/607'/0'/0/0"),
7374
show_display=False,
7475
)
@@ -88,7 +89,7 @@ def test_ton_sign_structured(self):
8889
bounceable=True
8990
)
9091

91-
msg = messages.TonSignTx(
92+
msg = ton_messages.TonSignTx(
9293
address_n=parse_path("m/44'/607'/0'/0/0"),
9394
destination=dest_addr,
9495
ton_amount=1000000000, # 1 TON
@@ -115,7 +116,7 @@ def test_ton_sign_with_comment(self):
115116

116117
dest_addr = make_ton_address()
117118

118-
msg = messages.TonSignTx(
119+
msg = ton_messages.TonSignTx(
119120
address_n=parse_path("m/44'/607'/0'/0/0"),
120121
destination=dest_addr,
121122
ton_amount=500000000, # 0.5 TON
@@ -136,7 +137,7 @@ def test_ton_sign_legacy_raw_tx(self):
136137
# Provide arbitrary raw_tx bytes (simulating a pre-built signing message)
137138
raw_tx = b'\x00' * 64 # dummy signing message
138139

139-
msg = messages.TonSignTx(
140+
msg = ton_messages.TonSignTx(
140141
address_n=parse_path("m/44'/607'/0'/0/0"),
141142
raw_tx=raw_tx,
142143
)
@@ -151,7 +152,7 @@ def test_ton_sign_missing_fields_rejected(self):
151152
self.setup_mnemonic_allallall()
152153

153154
# Has destination but no amount or seqno
154-
msg = messages.TonSignTx(
155+
msg = ton_messages.TonSignTx(
155156
address_n=parse_path("m/44'/607'/0'/0/0"),
156157
destination=make_ton_address(),
157158
)
@@ -166,7 +167,7 @@ def test_ton_sign_deterministic(self):
166167

167168
dest_addr = make_ton_address()
168169

169-
msg1 = messages.TonSignTx(
170+
msg1 = ton_messages.TonSignTx(
170171
address_n=parse_path("m/44'/607'/0'/0/0"),
171172
destination=dest_addr,
172173
ton_amount=1000000000,
@@ -175,7 +176,7 @@ def test_ton_sign_deterministic(self):
175176
)
176177
resp1 = self.client.call(msg1)
177178

178-
msg2 = messages.TonSignTx(
179+
msg2 = ton_messages.TonSignTx(
179180
address_n=parse_path("m/44'/607'/0'/0/0"),
180181
destination=dest_addr,
181182
ton_amount=1000000000,

tests/test_msg_tron_signtx.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010
import unittest
1111

1212
try:
13-
from keepkeylib import messages_pb2 as _msgs
14-
_has_tron = hasattr(_msgs, 'TronGetAddress')
13+
from keepkeylib import messages_tron_pb2 as _tron_msgs
14+
_has_tron = hasattr(_tron_msgs, 'TronGetAddress')
1515
except Exception:
1616
_has_tron = False
1717
import common
1818
import binascii
1919
import struct
2020

2121
from keepkeylib import messages_pb2 as messages
22+
from keepkeylib import messages_tron_pb2 as tron_messages
2223
from keepkeylib import types_pb2 as types
2324
from keepkeylib.client import CallException
2425
from keepkeylib.tools import parse_path
@@ -36,7 +37,7 @@ def test_tron_get_address(self):
3637
self.requires_fullFeature()
3738
self.setup_mnemonic_allallall()
3839

39-
msg = messages.TronGetAddress(
40+
msg = tron_messages.TronGetAddress(
4041
address_n=parse_path("m/44'/195'/0'/0/0"),
4142
show_display=False,
4243
)
@@ -51,13 +52,13 @@ def test_tron_sign_transfer_structured(self):
5152
self.requires_fullFeature()
5253
self.setup_mnemonic_allallall()
5354

54-
msg = messages.TronSignTx(
55+
msg = tron_messages.TronSignTx(
5556
address_n=parse_path("m/44'/195'/0'/0/0"),
5657
ref_block_bytes=b'\xab\xcd',
5758
ref_block_hash=b'\x42' * 8,
5859
expiration=1700000000000,
5960
timestamp=1699999990000,
60-
transfer=messages.TronTransferContract(
61+
transfer=tron_messages.TronTransferContract(
6162
to_address="TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
6263
amount=1000000, # 1 TRX
6364
),
@@ -85,7 +86,7 @@ def test_tron_sign_transfer_legacy_raw_data(self):
8586
'80e8ded785315a67' # dummy contract data
8687
)
8788

88-
msg = messages.TronSignTx(
89+
msg = tron_messages.TronSignTx(
8990
address_n=parse_path("m/44'/195'/0'/0/0"),
9091
raw_data=raw_data,
9192
)
@@ -100,7 +101,7 @@ def test_tron_sign_missing_fields_rejected(self):
100101
self.setup_mnemonic_allallall()
101102

102103
# No raw_data and no transfer/trigger_smart
103-
msg = messages.TronSignTx(
104+
msg = tron_messages.TronSignTx(
104105
address_n=parse_path("m/44'/195'/0'/0/0"),
105106
ref_block_bytes=b'\xab\xcd',
106107
ref_block_hash=b'\x42' * 8,
@@ -128,13 +129,13 @@ def test_tron_sign_trc20_transfer(self):
128129
# Amount
129130
struct.pack_into('>Q', abi_data, 60, 1000000)
130131

131-
msg = messages.TronSignTx(
132+
msg = tron_messages.TronSignTx(
132133
address_n=parse_path("m/44'/195'/0'/0/0"),
133134
ref_block_bytes=b'\xab\xcd',
134135
ref_block_hash=b'\x42' * 8,
135136
expiration=1700000000000,
136137
timestamp=1699999990000,
137-
trigger_smart=messages.TronTriggerSmartContract(
138+
trigger_smart=tron_messages.TronTriggerSmartContract(
138139
contract_address="TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
139140
data=bytes(abi_data),
140141
),

0 commit comments

Comments
 (0)