Skip to content

Commit ca1063e

Browse files
committed
test(tron): add TIP-191 SignMessage / VerifyMessage test suite
Coverage: - Round-trip sign-then-verify on printable text + binary bytes - Empty-message TIP-191 (ASCII '0' length, valid) - Address returned by sign() matches tron_get_address() - Corrupted signature rejection (flipped r||s byte) - Wrong-message rejection (valid sig, different message) - Non-TRON path rejection (firmware guard against m/44'/60'/...) Gated on requires_message('TronSignMessage') so it skips on firmwares that don't yet have the feature.
1 parent 2be1b34 commit ca1063e

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

tests/test_msg_tron_signmessage.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# This file is part of the KeepKey project.
2+
#
3+
# Copyright (C) 2026 KeepKey
4+
#
5+
# This library is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Lesser General Public License version 3
7+
# as published by the Free Software Foundation.
8+
9+
import binascii
10+
import unittest
11+
12+
import pytest
13+
14+
try:
15+
from keepkeylib import messages_tron_pb2 as _tron_msgs
16+
_has_tron_signmessage = hasattr(_tron_msgs, 'TronSignMessage')
17+
except Exception:
18+
_has_tron_signmessage = False
19+
20+
import common
21+
from keepkeylib.client import CallException
22+
from keepkeylib.tools import parse_path
23+
24+
25+
TRON_DEFAULT_PATH = "m/44'/195'/0'/0/0"
26+
27+
28+
@unittest.skipUnless(
29+
_has_tron_signmessage,
30+
"TronSignMessage protobuf not available in this build",
31+
)
32+
class TestMsgTronSignMessage(common.KeepKeyTest):
33+
34+
def setUp(self):
35+
super().setUp()
36+
self.requires_firmware("7.14.0")
37+
self.requires_message("TronSignMessage")
38+
39+
def test_sign_text_roundtrip(self):
40+
"""TIP-191 sign of printable text, then verify with the same client."""
41+
self.requires_fullFeature()
42+
self.setup_mnemonic_allallall()
43+
44+
message = b"Hello, TRON!"
45+
sig_resp = self.client.tron_sign_message(
46+
parse_path(TRON_DEFAULT_PATH),
47+
message,
48+
)
49+
50+
# Signature shape: 65 bytes (r || s || v=27|28)
51+
self.assertEqual(len(sig_resp.signature), 65)
52+
self.assertIn(sig_resp.signature[64], (27, 28))
53+
54+
# Address shape: 34-char Base58Check starting with 'T'
55+
self.assertEqual(len(sig_resp.address), 34)
56+
self.assertTrue(sig_resp.address.startswith('T'))
57+
58+
# Round-trip verify: same address, same signature, same message → Success
59+
verify_resp = self.client.tron_verify_message(
60+
address=sig_resp.address,
61+
signature=sig_resp.signature,
62+
message=message,
63+
)
64+
self.assertEqual(verify_resp.message, "Message verified")
65+
66+
def test_sign_bytes_roundtrip(self):
67+
"""TIP-191 sign of non-printable bytes; falls into the 'Sign Bytes' UX path."""
68+
self.requires_fullFeature()
69+
self.setup_mnemonic_allallall()
70+
71+
message = bytes.fromhex(
72+
"1df3d10be935abc63b6561cb6148b745eef81f6d2517f420fc0f59684fb3d4cb"
73+
)
74+
sig_resp = self.client.tron_sign_message(
75+
parse_path(TRON_DEFAULT_PATH),
76+
message,
77+
)
78+
self.assertEqual(len(sig_resp.signature), 65)
79+
80+
verify_resp = self.client.tron_verify_message(
81+
address=sig_resp.address,
82+
signature=sig_resp.signature,
83+
message=message,
84+
)
85+
self.assertEqual(verify_resp.message, "Message verified")
86+
87+
def test_sign_empty_message(self):
88+
"""TIP-191 permits zero-length message (hash includes ASCII '0' length)."""
89+
self.requires_fullFeature()
90+
self.setup_mnemonic_allallall()
91+
92+
sig_resp = self.client.tron_sign_message(
93+
parse_path(TRON_DEFAULT_PATH),
94+
b"",
95+
)
96+
self.assertEqual(len(sig_resp.signature), 65)
97+
98+
verify_resp = self.client.tron_verify_message(
99+
address=sig_resp.address,
100+
signature=sig_resp.signature,
101+
message=b"",
102+
)
103+
self.assertEqual(verify_resp.message, "Message verified")
104+
105+
def test_sign_address_matches_get_address(self):
106+
"""The address returned in the signature must match tron_get_address()."""
107+
self.requires_fullFeature()
108+
self.setup_mnemonic_allallall()
109+
110+
addr_resp = self.client.tron_get_address(
111+
parse_path(TRON_DEFAULT_PATH),
112+
show_display=False,
113+
)
114+
sig_resp = self.client.tron_sign_message(
115+
parse_path(TRON_DEFAULT_PATH),
116+
b"address-binding-check",
117+
)
118+
self.assertEqual(sig_resp.address, addr_resp.address)
119+
120+
def test_verify_rejects_corrupted_signature(self):
121+
"""A signature with one flipped byte must fail verification."""
122+
self.requires_fullFeature()
123+
self.setup_mnemonic_allallall()
124+
125+
message = b"original message"
126+
sig_resp = self.client.tron_sign_message(
127+
parse_path(TRON_DEFAULT_PATH),
128+
message,
129+
)
130+
131+
# Flip a byte inside r||s (NOT the recovery id) so recovery still
132+
# produces some pubkey but not the signer's.
133+
bad = bytearray(sig_resp.signature)
134+
bad[10] ^= 0xFF
135+
136+
with pytest.raises(CallException):
137+
self.client.tron_verify_message(
138+
address=sig_resp.address,
139+
signature=bytes(bad),
140+
message=message,
141+
)
142+
143+
def test_verify_rejects_wrong_message(self):
144+
"""Verifying a valid signature against a different message must fail."""
145+
self.requires_fullFeature()
146+
self.setup_mnemonic_allallall()
147+
148+
sig_resp = self.client.tron_sign_message(
149+
parse_path(TRON_DEFAULT_PATH),
150+
b"message A",
151+
)
152+
153+
with pytest.raises(CallException):
154+
self.client.tron_verify_message(
155+
address=sig_resp.address,
156+
signature=sig_resp.signature,
157+
message=b"message B",
158+
)
159+
160+
def test_invalid_path_rejected(self):
161+
"""Non-TRON BIP-44 paths must be rejected by the firmware path guard."""
162+
self.requires_fullFeature()
163+
self.setup_mnemonic_allallall()
164+
165+
# m/44'/60'/0'/0/0 is Ethereum, not TRON
166+
with pytest.raises(CallException):
167+
self.client.tron_sign_message(
168+
parse_path("m/44'/60'/0'/0/0"),
169+
b"wrong-chain",
170+
)
171+
172+
173+
if __name__ == "__main__":
174+
unittest.main()

0 commit comments

Comments
 (0)