Skip to content

Commit 4ab0d91

Browse files
committed
test: bind Solana signed bytes to OLED review
1 parent 2d54f6d commit 4ab0d91

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
"""Solana display/sign disclosure regressions for firmware 7.14.2."""
10+
11+
from __future__ import print_function
12+
13+
import common
14+
15+
from keepkeylib import messages_solana_pb2 as solana
16+
from keepkeylib.client import CallException
17+
from keepkeylib.tools import parse_path
18+
from test_msg_display_disclosure import ScreenRecorder
19+
20+
21+
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
22+
PATH = parse_path("m/44'/501'/0'/0'")
23+
24+
25+
def b58decode_pubkey(value):
26+
number = 0
27+
for char in value:
28+
number = number * 58 + ALPHABET.index(char)
29+
return number.to_bytes(32, "big")
30+
31+
32+
def compact_u16(value):
33+
encoded = []
34+
while True:
35+
byte = value & 0x7f
36+
value >>= 7
37+
encoded.append(byte | (0x80 if value else 0))
38+
if not value:
39+
return bytes(encoded)
40+
41+
42+
def build_memo_tx(signer, memo):
43+
memo_program = b58decode_pubkey(
44+
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
45+
)
46+
return (
47+
bytes([1, 0, 1, 2])
48+
+ signer
49+
+ memo_program
50+
+ bytes([0xbb]) * 32
51+
+ bytes([1, 1, 0])
52+
+ compact_u16(len(memo))
53+
+ memo
54+
)
55+
56+
57+
class TestSolanaDisplayDisclosure(common.KeepKeyTest):
58+
def setUp(self):
59+
super(TestSolanaDisplayDisclosure, self).setUp()
60+
self.requires_firmware("7.14.2")
61+
self.setup_mnemonic_allallall()
62+
63+
def _capture(self, request):
64+
recorder = ScreenRecorder(self.client, answer=True)
65+
try:
66+
with recorder:
67+
self.client.call(request)
68+
except CallException:
69+
return None
70+
return recorder.fingerprint
71+
72+
def _assert_tail_mutation_changes_review(self, make_request):
73+
payload_a = b"A" * 160
74+
payload_b = payload_a[:96] + b"B" + payload_a[97:]
75+
self.assertEqual(payload_a[:32], payload_b[:32])
76+
77+
screens_a = self._capture(make_request(payload_a))
78+
screens_b = self._capture(make_request(payload_b))
79+
self.assertIsNotNone(screens_a)
80+
self.assertIsNotNone(screens_b)
81+
self.assertGreater(len(screens_a), 1)
82+
self.assertGreater(len(screens_b), 1)
83+
self.assertNotEqual(
84+
screens_a,
85+
screens_b,
86+
"a signed byte after the old 32-byte preview was not disclosed",
87+
)
88+
89+
def test_raw_message_tail_changes_oled_review(self):
90+
self.client.apply_policy("AdvancedMode", True)
91+
self._assert_tail_mutation_changes_review(
92+
lambda payload: solana.SolanaSignMessage(
93+
address_n=PATH,
94+
message=payload,
95+
)
96+
)
97+
98+
def test_offchain_message_tail_changes_oled_review(self):
99+
self._assert_tail_mutation_changes_review(
100+
lambda payload: solana.SolanaSignOffchainMessage(
101+
address_n=PATH,
102+
version=0,
103+
message_format=0,
104+
message=payload,
105+
)
106+
)
107+
108+
def test_offchain_format_changes_oled_review(self):
109+
payload = b"same signed message"
110+
screens_ascii = self._capture(
111+
solana.SolanaSignOffchainMessage(
112+
address_n=PATH,
113+
version=0,
114+
message_format=0,
115+
message=payload,
116+
)
117+
)
118+
screens_utf8 = self._capture(
119+
solana.SolanaSignOffchainMessage(
120+
address_n=PATH,
121+
version=0,
122+
message_format=1,
123+
message=payload,
124+
)
125+
)
126+
self.assertIsNotNone(screens_ascii)
127+
self.assertIsNotNone(screens_utf8)
128+
self.assertNotEqual(
129+
screens_ascii,
130+
screens_utf8,
131+
"the signed off-chain format was not bound to the OLED review",
132+
)
133+
134+
def test_memo_tail_changes_oled_review(self):
135+
address = self.client.call(
136+
solana.SolanaGetAddress(address_n=PATH, show_display=False)
137+
).address
138+
signer = b58decode_pubkey(address)
139+
self._assert_tail_mutation_changes_review(
140+
lambda payload: solana.SolanaSignTx(
141+
address_n=PATH,
142+
raw_tx=build_memo_tx(signer, payload),
143+
)
144+
)
145+

0 commit comments

Comments
 (0)