@@ -75,13 +75,74 @@ def recover_compressed(serialized_tx, sig65):
7575 return candidates [recid ].to_string ("compressed" )
7676
7777
78- class TestMsgHive (common .KeepKeyTest ):
78+ class _Reader :
79+ """Cursor over the device-emitted Graphene bytes. Matches firmware
80+ serialization exactly (see hive.c append_* helpers)."""
81+
82+ def __init__ (self , data ):
83+ self .d = data
84+ self .i = 0
85+
86+ def take (self , n ):
87+ v = self .d [self .i :self .i + n ]
88+ assert len (v ) == n , "truncated serialized_tx"
89+ self .i += n
90+ return v
91+
92+ def u8 (self ):
93+ return self .take (1 )[0 ]
94+
95+ def u16le (self ):
96+ return int .from_bytes (self .take (2 ), "little" )
97+
98+ def u32le (self ):
99+ return int .from_bytes (self .take (4 ), "little" )
100+
101+ def u64le (self ):
102+ return int .from_bytes (self .take (8 ), "little" )
103+
104+ def varint (self ):
105+ shift = result = 0
106+ while True :
107+ b = self .u8 ()
108+ result |= (b & 0x7F ) << shift
109+ if not (b & 0x80 ):
110+ return result
111+ shift += 7
112+
113+ def string (self ):
114+ return self .take (self .varint ())
115+
116+ def asset (self ):
117+ amount = self .u64le ()
118+ precision = self .u8 ()
119+ symbol = self .take (7 ).rstrip (b"\x00 " ).decode ()
120+ return amount , precision , symbol
121+
122+ def authority (self ):
123+ # weight_threshold=1, 0 account auths, 1 key auth, key(33), weight=1
124+ assert self .u32le () == 1 , "weight_threshold must be 1"
125+ assert self .varint () == 0 , "expected 0 account_auths"
126+ assert self .varint () == 1 , "expected 1 key_auth"
127+ key = self .take (33 )
128+ assert self .u16le () == 1 , "key weight must be 1"
129+ return key
130+
131+ def assert_end (self ):
132+ assert self .i == len (self .d ), "trailing bytes after operation (offset %d/%d)" % (self .i , len (self .d ))
133+
134+
135+ def _parse_header (r , expected_op ):
136+ ref_block_num = r .u16le ()
137+ ref_block_prefix = r .u32le ()
138+ expiration = r .u32le ()
139+ assert r .varint () == 1 , "expected exactly one operation"
140+ op_type = r .varint ()
141+ assert op_type == expected_op , "op_type %d != expected %d" % (op_type , expected_op )
142+ return ref_block_num , ref_block_prefix , expiration
79143
80- def _owner_raw (self ):
81- """Device-derived owner key (33-byte compressed) at account 0."""
82- resp = hive .get_public_key (self .client , hive_path (ROLE_OWNER ), show_display = False )
83- self .assertEqual (len (resp .raw_public_key ), 33 )
84- return resp .raw_public_key
144+
145+ class TestMsgHive (common .KeepKeyTest ):
85146
86147 def test_hive_get_public_key_active (self ):
87148 """Active-role key derives and returns an STM-prefixed key + 33-byte raw."""
@@ -133,10 +194,19 @@ def test_hive_sign_transfer(self):
133194 )
134195 self .assertEqual (len (resp .signature ), 65 )
135196 self .assertIn (resp .signature [0 ], (31 , 32 ))
136- self .assertTrue (len (resp .serialized_tx ) > 0 )
137197 self .assertEqual (recover_compressed (resp .serialized_tx , resp .signature ), active .raw_public_key )
138- # op byte sits right after header (u16 + u32 + u32) and the 0x01 op-count varint.
139- self .assertEqual (resp .serialized_tx [11 ], HIVE_OP_TRANSFER )
198+
199+ # Parse the transfer op and bind EVERY field — a rewritten recipient,
200+ # amount, or asset must fail, not just a missing substring.
201+ r = _Reader (resp .serialized_tx )
202+ ref_num , ref_prefix , expiration = _parse_header (r , HIVE_OP_TRANSFER )
203+ self .assertEqual ((ref_num , ref_prefix , expiration ), (12345 , 67890 , 1700000000 ))
204+ self .assertEqual (r .string (), b"kktester" ) # from
205+ self .assertEqual (r .string (), b"kkrecipient" ) # to
206+ self .assertEqual (r .asset (), (1000 , 3 , "HIVE" ))
207+ self .assertEqual (r .string (), b"kktest" ) # memo
208+ self .assertEqual (r .varint (), 0 ) # extensions
209+ r .assert_end ()
140210
141211 def test_hive_sign_account_create (self ):
142212 """account_create (op 9): signs, recovers to owner key, binds the 4 keys + name.
@@ -148,7 +218,9 @@ def test_hive_sign_account_create(self):
148218 self .requires_message ("HiveGetPublicKeys" )
149219 self .setup_mnemonic_nopin_nopassphrase ()
150220
151- owner_raw = self ._owner_raw ()
221+ # Device-derived raw keys per role, for slot-exact comparison.
222+ raw = {role : hive .get_public_key (self .client , hive_path (role ), show_display = False ).raw_public_key
223+ for role in (ROLE_OWNER , ROLE_ACTIVE , ROLE_POSTING , ROLE_MEMO )}
152224 keys = hive .get_public_keys (self .client , account_index = 0 , show_display = False )
153225
154226 resp = hive .sign_account_create (
@@ -170,17 +242,23 @@ def test_hive_sign_account_create(self):
170242 self .assertIn (resp .signature [0 ], (31 , 32 ))
171243
172244 # Attestation: signature recovers to the device owner key.
173- self .assertEqual (recover_compressed (resp .serialized_tx , resp .signature ), owner_raw )
174-
175- tx = resp .serialized_tx
176- self .assertEqual (tx [11 ], HIVE_OP_ACCOUNT_CREATE )
177- # The new account name and all four device-raw role keys are bound into the
178- # signed bytes (per spec §3); a sponsor parses these to confirm what it creates.
179- self .assertIn (b"kktestacct" , tx )
180- single = hive .get_public_key # local alias
181- for role in (ROLE_OWNER , ROLE_ACTIVE , ROLE_POSTING , ROLE_MEMO ):
182- raw = single (self .client , hive_path (role ), show_display = False ).raw_public_key
183- self .assertIn (raw , tx , "role %d key must be embedded in account_create" % role )
245+ self .assertEqual (recover_compressed (resp .serialized_tx , resp .signature ), raw [ROLE_OWNER ])
246+
247+ # Parse op 9 and bind EVERY field at its position. A firmware bug that
248+ # swaps roles, rewrites the creator, or alters the fee must fail here.
249+ r = _Reader (resp .serialized_tx )
250+ ref_num , ref_prefix , expiration = _parse_header (r , HIVE_OP_ACCOUNT_CREATE )
251+ self .assertEqual ((ref_num , ref_prefix , expiration ), (12345 , 67890 , 1700000000 ))
252+ self .assertEqual (r .asset (), (3000 , 3 , "HIVE" )) # fee
253+ self .assertEqual (r .string (), b"kksponsor" ) # creator
254+ self .assertEqual (r .string (), b"kktestacct" ) # new_account_name
255+ self .assertEqual (r .authority (), raw [ROLE_OWNER ], "owner authority slot" )
256+ self .assertEqual (r .authority (), raw [ROLE_ACTIVE ], "active authority slot" )
257+ self .assertEqual (r .authority (), raw [ROLE_POSTING ], "posting authority slot" )
258+ self .assertEqual (r .take (33 ), raw [ROLE_MEMO ], "memo_key slot" )
259+ self .assertEqual (r .string (), b"" ) # json_metadata
260+ self .assertEqual (r .varint (), 0 ) # extensions
261+ r .assert_end ()
184262
185263 def test_hive_sign_account_update (self ):
186264 """account_update (op 10): signs and recovers to the owner key."""
@@ -189,7 +267,8 @@ def test_hive_sign_account_update(self):
189267 self .requires_message ("HiveGetPublicKeys" )
190268 self .setup_mnemonic_nopin_nopassphrase ()
191269
192- owner_raw = self ._owner_raw ()
270+ raw = {role : hive .get_public_key (self .client , hive_path (role ), show_display = False ).raw_public_key
271+ for role in (ROLE_OWNER , ROLE_ACTIVE , ROLE_POSTING , ROLE_MEMO )}
193272 keys = hive .get_public_keys (self .client , account_index = 0 , show_display = False )
194273
195274 resp = hive .sign_account_update (
@@ -207,9 +286,21 @@ def test_hive_sign_account_update(self):
207286 )
208287 self .assertEqual (len (resp .signature ), 65 )
209288 self .assertIn (resp .signature [0 ], (31 , 32 ))
210- self .assertEqual (recover_compressed (resp .serialized_tx , resp .signature ), owner_raw )
211- self .assertEqual (resp .serialized_tx [11 ], HIVE_OP_ACCOUNT_UPDATE )
212- self .assertIn (b"kktestacct" , resp .serialized_tx )
289+ self .assertEqual (recover_compressed (resp .serialized_tx , resp .signature ), raw [ROLE_OWNER ])
290+
291+ # Parse op 10 and bind the replacement keys to their slots. A bad impl
292+ # that updates the wrong authorities must fail even if op/name are right.
293+ r = _Reader (resp .serialized_tx )
294+ ref_num , ref_prefix , expiration = _parse_header (r , HIVE_OP_ACCOUNT_UPDATE )
295+ self .assertEqual ((ref_num , ref_prefix , expiration ), (12345 , 67890 , 1700000000 ))
296+ self .assertEqual (r .string (), b"kktestacct" ) # account
297+ for role , label in ((ROLE_OWNER , "owner" ), (ROLE_ACTIVE , "active" ), (ROLE_POSTING , "posting" )):
298+ self .assertEqual (r .u8 (), 0x01 , "%s optional-present flag" % label )
299+ self .assertEqual (r .authority (), raw [role ], "%s authority slot" % label )
300+ self .assertEqual (r .take (33 ), raw [ROLE_MEMO ], "memo_key slot" )
301+ self .assertEqual (r .string (), b"" ) # json_metadata
302+ self .assertEqual (r .varint (), 0 ) # extensions
303+ r .assert_end ()
213304
214305
215306if __name__ == "__main__" :
0 commit comments