-
Notifications
You must be signed in to change notification settings - Fork 63
Expand file tree
/
Copy pathtest_msg_osmosis_signtx.py
More file actions
177 lines (144 loc) · 7.31 KB
/
Copy pathtest_msg_osmosis_signtx.py
File metadata and controls
177 lines (144 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Osmosis MsgSend signing — with the confirm-screen amount as the point.
Osmosis had NO device tests at all: the confirm screens that render amounts
were covered only by host-side unit tests of the formatter in isolation. That
matters more than it sounds, because 7.15.0 CHANGED how every Osmosis amount
is drawn.
Before, fsm_msg_osmosis.h rendered amounts with atof() + "%.6f". A float
carries ~7 significant decimal digits, so a large amount was displayed
ROUNDED on the very screen the user approves:
123456789123456 uosmo -> shown as "123456792.000000 OSMO"
actual 123456789.123456 OSMO
The signature was over the correct amount either way — the lie was only on
the screen, which is the half a hardware wallet exists to get right. It now
formats with bn_format_uint64 in integer math.
KNOWN LIMIT, do not overstate this: osmosis_formatAmount converts with an
unchecked strtoull(), so it is exact only for a CANONICAL decimal uint64.
strtoull saturates past UINT64_MAX and also accepts leading whitespace, a
sign, and 0x — so "18446744073709551616" or "-1" display as
18446744073.709551615 OSMO while the original string is what gets hashed.
That is the same display/signature divergence in a different disguise and
needs a firmware-side canonical-range check; these tests deliberately do not
claim otherwise.
These tests are paired with SECTIONS entries carrying screenshot hints, so
the rendered frame is captured as evidence. A test asserting only "it signed"
cannot prove what the OLED drew.
pyk's osmosis_sign_tx currently implements osmosis-sdk/MsgSend only; the
delegate/undelegate/LP/swap/IBC screens share the same formatter but are not
reachable from here until the client learns those message types.
"""
import unittest
import common
from binascii import hexlify
from keepkeylib.tools import parse_path
# Osmosis uses the Cosmos coin type (118), not one of its own.
DEFAULT_BIP32_PATH = "m/44h/118h/0h/0/0"
def make_send(from_address, to_address, amount, denom='uosmo'):
return {
'type': 'osmosis-sdk/MsgSend',
'value': {
'from_address': from_address,
'to_address': to_address,
'amount': [{'denom': denom, 'amount': str(amount)}],
},
}
class TestMsgOsmosisSignTx(common.KeepKeyTest):
def _address(self):
"""Ask the device for its own osmo1 address.
Deliberately NOT a hardcoded constant: the firmware bech32-decodes
to_address and refuses a bad checksum, so a literal invented by
swapping a cosmos1 prefix for osmo1 fails with the opaque "Failed to
include send message in transaction". Deriving it keeps the fixture
honest and makes these self-sends.
"""
# osmosis_get_address is decorated @field('address'), so it already
# returns the string rather than the OsmosisAddress message.
return self.client.osmosis_get_address(
address_n=parse_path(DEFAULT_BIP32_PATH)
)
def _sign(self, amount, denom='uosmo'):
addr = self._address()
return self.client.osmosis_sign_tx(
address_n=parse_path(DEFAULT_BIP32_PATH),
account_number=16359,
chain_id="osmosis-1",
fee=800,
gas=290000,
msgs=[make_send(addr, addr, amount, denom)],
memo="",
sequence=17,
)
def test_osmosis_sign_tx(self):
"""Baseline: a whole-OSMO send signs and returns a well-formed
secp256k1 signature + compressed pubkey."""
self.requires_fullFeature()
self.requires_firmware("7.15.0")
self.setup_mnemonic_nopin_nopassphrase()
sig = self._sign(1500000) # 1.500000 OSMO
self.assertEqual(len(sig.signature), 64)
self.assertEqual(len(sig.public_key), 33)
self.assertIn(hexlify(sig.public_key)[:2], (b'02', b'03'))
def test_osmosis_send_amount_beyond_float_precision(self):
"""THE regression. 123456789123456 uosmo needs 15 significant digits;
a float holds ~7, so the old atof()+"%.6f" path drew
"123456792.000000 OSMO" over a transaction that actually moves
123456789.123456 OSMO. The captured frame is the proof — assert here
only that the device signs it, and read the amount off the screenshot.
"""
self.requires_fullFeature()
self.requires_firmware("7.15.0")
self.setup_mnemonic_nopin_nopassphrase()
sig = self._sign(123456789123456)
self.assertEqual(len(sig.signature), 64)
def test_osmosis_send_subunit_amount(self):
"""500 uosmo is 0.000500 OSMO — six decimal places, no integer part.
The formatter must not collapse it to "0" or drop the tail."""
self.requires_fullFeature()
self.requires_firmware("7.15.0")
self.setup_mnemonic_nopin_nopassphrase()
sig = self._sign(500)
self.assertEqual(len(sig.signature), 64)
def test_osmosis_send_non_uosmo_denom_is_refused(self):
"""A non-uosmo MsgSend must not reach the device at all.
osmosis_signTxUpdateMsgSend takes only (amount, to_address) and
hardcodes "denom":"uosmo" into the amino JSON it hashes, while
fsm_msgOsmosisMsgAck renders whatever denom arrived. Sending uatom
therefore DISPLAYS "1500000 uatom" and SIGNS 1500000 uosmo — the
display/signature divergence a hardware wallet exists to prevent, and
exploitable in the large: a big number of some worthless ibc/... token
on screen, a big number of OSMO in the signature.
This asserts the fence, NOT that arbitrary denoms work. When the
firmware serializer takes a denom, replace this with the real check:
otherwise-identical uosmo and uatom transactions must produce
DIFFERENT signatures.
"""
self.requires_fullFeature()
self.requires_firmware("7.15.0")
self.setup_mnemonic_nopin_nopassphrase()
with self.assertRaises(Exception) as ctx:
self._sign(1500000, denom='uatom')
self.assertIn('uosmo', str(ctx.exception))
def test_osmosis_amount_is_committed_to_the_signature(self):
"""Guards the pairing between what is shown and what is signed: two
sends differing ONLY in amount must produce different signatures. If
they matched, the amount would not be in the digest and the confirm
screen would be decorative."""
self.requires_fullFeature()
self.requires_firmware("7.15.0")
self.setup_mnemonic_nopin_nopassphrase()
a = self._sign(1500000)
b = self._sign(1500001)
self.assertNotEqual(hexlify(a.signature), hexlify(b.signature))
# Same key throughout — only the message differed.
self.assertEqual(hexlify(a.public_key), hexlify(b.public_key))
def test_osmosis_signing_is_deterministic(self):
"""RFC6979: identical input must yield an identical signature. A
mismatch here means nonce generation is not deterministic, which is a
key-recovery risk long before it is a display problem."""
self.requires_fullFeature()
self.requires_firmware("7.15.0")
self.setup_mnemonic_nopin_nopassphrase()
first = self._sign(1500000)
second = self._sign(1500000)
self.assertEqual(hexlify(first.signature), hexlify(second.signature))
if __name__ == '__main__':
unittest.main()