Skip to content

Commit 02fa3ea

Browse files
committed
test(hive): negative coverage for SLIP-48 path enforcement + memo limit
- reject foreign path (BIP-44), wrong network (3054'), unassigned role - memo >440 fails with the specific error; 440 boundary still signs+recovers - config.py: KK_FORCE_UDP=1 local-only escape hatch to reach the UDP emulator with a real device plugged in (not for CI)
1 parent 560b897 commit 02fa3ea

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

tests/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@
4444
(_explicit_transport, sorted(_KNOWN_TRANSPORTS))
4545
)
4646

47-
if _explicit_transport == "dylib":
47+
if os.getenv("KK_FORCE_UDP") == "1":
48+
# Local-only escape hatch: skip HID/WebUSB autodetect so tests hit the
49+
# UDP emulator even with a real KeepKey plugged in. NOT for CI.
50+
hid_devices = []
51+
webusb_devices = []
52+
elif _explicit_transport == "dylib":
4853
# Skip HID/WebUSB autodetect — dylib is opt-in by env var. Without
4954
# this skip, a connected real KeepKey would win over the explicit
5055
# request and the dylib regression suite would route to hardware.

tests/test_msg_hive.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,5 +303,83 @@ def test_hive_sign_account_update(self):
303303
r.assert_end()
304304

305305

306+
def _transfer_kwargs(self, **overrides):
307+
"""Baseline valid HiveSignTx args; override per negative case."""
308+
kw = dict(
309+
address_n=hive_path(ROLE_ACTIVE),
310+
chain_id=HIVE_CHAIN_ID,
311+
ref_block_num=12345,
312+
ref_block_prefix=67890,
313+
expiration=1700000000,
314+
sender="kktester",
315+
recipient="kkrecipient",
316+
amount=1000,
317+
decimals=3,
318+
asset_symbol="HIVE",
319+
memo="kktest",
320+
)
321+
kw.update(overrides)
322+
return kw
323+
324+
def _assert_sign_tx_fails(self, message_fragment, **overrides):
325+
from keepkeylib.client import CallException
326+
with self.assertRaises(CallException) as ctx:
327+
hive.sign_tx(self.client, **self._transfer_kwargs(**overrides))
328+
self.assertIn(message_fragment, str(ctx.exception))
329+
330+
def test_hive_sign_transfer_rejects_foreign_path(self):
331+
"""A path outside SLIP-0048 (e.g. BIP-44 BTC) must be rejected before
332+
signing — a compromised host cannot obtain a Hive signature with a
333+
key from another coin's derivation tree."""
334+
self.requires_firmware("7.15.0")
335+
self.requires_message("HiveSignTx")
336+
self.setup_mnemonic_nopin_nopassphrase()
337+
self._assert_sign_tx_fails(
338+
"Invalid Hive SLIP-0048 path",
339+
address_n=parse_path("m/44'/0'/0'/0/0"),
340+
)
341+
342+
def test_hive_sign_transfer_rejects_wrong_network(self):
343+
"""Wrong SLIP-0048 network index (registry 3054' instead of the
344+
de-facto 13') must be rejected — keys must be Ledger-compatible."""
345+
self.requires_firmware("7.15.0")
346+
self.requires_message("HiveSignTx")
347+
self.setup_mnemonic_nopin_nopassphrase()
348+
h = 0x80000000
349+
self._assert_sign_tx_fails(
350+
"Invalid Hive SLIP-0048 path",
351+
address_n=[h + 48, h + 3054, h + ROLE_ACTIVE, h, h],
352+
)
353+
354+
def test_hive_sign_transfer_rejects_wrong_role(self):
355+
"""Role index outside {owner, active, memo, posting} must be rejected."""
356+
self.requires_firmware("7.15.0")
357+
self.requires_message("HiveSignTx")
358+
self.setup_mnemonic_nopin_nopassphrase()
359+
h = 0x80000000
360+
self._assert_sign_tx_fails(
361+
"Invalid Hive SLIP-0048 path",
362+
address_n=[h + 48, h + 13, h + 2, h, h], # role 2' is unassigned
363+
)
364+
365+
def test_hive_sign_transfer_rejects_long_memo(self):
366+
"""Memo over the 440-byte serialization limit must fail with a
367+
specific error, not a generic signing failure."""
368+
self.requires_firmware("7.15.0")
369+
self.requires_message("HiveSignTx")
370+
self.setup_mnemonic_nopin_nopassphrase()
371+
self._assert_sign_tx_fails("memo too long", memo="x" * 441)
372+
373+
def test_hive_sign_transfer_max_memo_ok(self):
374+
"""A memo of exactly 440 bytes still signs (boundary check)."""
375+
self.requires_firmware("7.15.0")
376+
self.requires_message("HiveSignTx")
377+
self.setup_mnemonic_nopin_nopassphrase()
378+
active = hive.get_public_key(self.client, hive_path(ROLE_ACTIVE), show_display=False)
379+
resp = hive.sign_tx(self.client, **self._transfer_kwargs(memo="x" * 440))
380+
self.assertEqual(len(resp.signature), 65)
381+
self.assertEqual(recover_compressed(resp.serialized_tx, resp.signature), active.raw_public_key)
382+
383+
306384
if __name__ == "__main__":
307385
unittest.main()

0 commit comments

Comments
 (0)