From b745562be6a13cc83b2a547d7ef86637f4556dbb Mon Sep 17 00:00:00 2001 From: highlander Date: Tue, 22 Sep 2026 23:40:36 -0500 Subject: [PATCH 01/11] test(ripple): assert canonical MemoData field and length boundaries --- tests/test_msg_ripple_sign_tx.py | 34 ++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_msg_ripple_sign_tx.py b/tests/test_msg_ripple_sign_tx.py index af0db89c..5cb8c4bd 100644 --- a/tests/test_msg_ripple_sign_tx.py +++ b/tests/test_msg_ripple_sign_tx.py @@ -122,11 +122,11 @@ def test_sign_with_thorchain_memo(self): resp = self.client.call(msg) # Verify the XRPL Memos array is appended to the serialized tx. - # Format: 0xF9 (STArray[9]) 0xEA (STObject[10]) 0x72 (MemoData VL[2]) + # Format: 0xF9 (STArray[9]) 0xEA (STObject[10]) 0x7D (MemoData VL[13]) # 0xE1 (end object) 0xF1 (end array) memo_bytes = memo.encode('ascii') expected_tail = ( - bytes([0xF9, 0xEA, 0x72, len(memo_bytes)]) + + bytes([0xF9, 0xEA, 0x7D, len(memo_bytes)]) + memo_bytes + bytes([0xE1, 0xF1]) ) @@ -152,6 +152,36 @@ def test_sign_with_thorchain_memo(self): "plain send must not contain Memos array (0xF9 0xEA marker sequence)" ) + def test_memo_length_prefix_boundaries(self): + self.requires_release_capability("ripple-memo-policy") + self.requires_fullFeature() + self.requires_firmware("7.15.0") + self.setup_mnemonic_allallall() + + for length in (191, 192, 193, 199): + # A distinct suffix makes the last display page inspectable. + memo = "A" * (length - 8) + ("TAIL%04d" % length) + msg = messages.RippleSignTx( + address_n=parse_path("m/44'/144'/0'/0/0"), + payment=messages.RipplePayment( + amount=100000000, + destination="rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws" + ), + flags=0x80000000, + fee=100000, + sequence=25 + length, + memo=memo + ) + resp = self.client.call(msg) + prefix = (bytes([length]) if length <= 192 else + bytes([193, length - 193])) + expected_tail = (b'\xf9\xea\x7d' + prefix + + memo.encode('ascii') + b'\xe1\xf1') + self.assertTrue( + resp.serialized_tx.endswith(expected_tail), + "serialized Ripple memo has wrong prefix at length %d" % length + ) + def test_unsupported_memo_is_rejected(self): self.requires_release_capability("ripple-memo-policy") self.requires_fullFeature() From 1d42a1fa401129da292a8b0fe4f6dc9bee727117 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 24 Sep 2026 01:18:48 -0500 Subject: [PATCH 02/11] fix(report): describe immediate unlimited-approval refusal --- scripts/generate-test-report.py | 26 ++++++++++++++++-------- tests/test_msg_ethereum_clear_signing.py | 16 +++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/scripts/generate-test-report.py b/scripts/generate-test-report.py index 6e405b3a..3661162f 100644 --- a/scripts/generate-test-report.py +++ b/scripts/generate-test-report.py @@ -432,7 +432,6 @@ def parse_junit(path): ('test_msg_ethereum_clearsign_additive', 'test_failed_signature_falls_back_to_the_unverified_review'), ('test_msg_ethereum_clear_signing', 'test_binding_happy_path_signs_and_recovers'), - ('test_msg_ethereum_clear_signing', 'test_clearsign_erc20_approve_unlimited'), ('test_msg_ethereum_clear_signing', 'test_clearsign_uniswap_v2_eth_to_token'), # The newest/highest-stakes tx shapes get the full ordered walkthrough too. ('test_msg_ethereum_clear_signing', 'test_clearsign_eip7702_setcode_authorization'), @@ -460,10 +459,9 @@ def _v_catalog_tests(start_id=17): silently go stale (as happened when the old hand-written V17-V23 test names drifted from the dynamically-generated ones). - Every entry gets a NON-EMPTY screenshots hint: screenshot_filter() below - only includes tests whose hint list is non-empty in the Phase-1 capture - filter, so an empty list here would silently exclude a flow from ever - getting an OLED screenshot. + Signing entries get screenshot hints. The unlimited-approval refusal is + deliberately screenless: its wire assertion requires immediate Failure, + so setup or home frames must never be used as approval evidence. """ if not CLEARSIGN_FLOWS: return [] @@ -473,6 +471,18 @@ def _v_catalog_tests(start_id=17): if f['key'] == 'aave-v3-supply': continue method = 'test_clearsign_' + f['key'].replace('-', '_').replace('.', '_') + if f['key'] == 'erc20-approve-unlimited': + out.append(( + 'V%d' % i, 'test_msg_ethereum_clear_signing', method, + 'Unlimited ERC-20 approval refused before confirmation', + 'Even with AdvancedMode and valid runtime metadata, unlimited ' + 'approval returns Failure before any ButtonRequest or signature. ' + 'The wire test requires that immediate refusal; no OLED approval ' + 'screen is expected.', + [], + )) + i += 1 + continue def _arg_shown(a): # Render what the OLED will actually show for this arg: @@ -502,14 +512,14 @@ def _arg_shown(a): # Prefer any TOKEN_AMOUNT/ADDRESS/STRING label as the screenshot hint # so it reads like what the OLED will actually show. hint_names = [a['name'] for a in f['args'][:2]] or [f['method']] - ctx = ('%s.%s (%s). %s AdvancedMode OFF; the bound metadata is the ' - 'only reason this contract data may sign. Real tx: to=0x%s..%s, ' + ctx = ('%s.%s (%s). %s AdvancedMode ON; runtime annotations are ' + 'additional to ordinary raw review. Real tx: to=0x%s..%s, ' 'chainId %d. Decode: %s.' % ( f['protocol'], f['method'], f['category'], f.get('why', ''), f['to'].hex()[:4], f['to'].hex()[-4:], f['chain_id'], shows)) out.append(( 'V%d' % i, 'test_msg_ethereum_clear_signing', method, - '%s %s — clear-signed, zero hex' % (f['protocol'], f['method']), + '%s %s — annotated with ordinary review' % (f['protocol'], f['method']), ctx, hint_names, )) diff --git a/tests/test_msg_ethereum_clear_signing.py b/tests/test_msg_ethereum_clear_signing.py index c3e4a1c9..509d16cb 100644 --- a/tests/test_msg_ethereum_clear_signing.py +++ b/tests/test_msg_ethereum_clear_signing.py @@ -29,6 +29,7 @@ import struct from keepkeylib import messages_ethereum_pb2 as messages_eth +from keepkeylib import messages_pb2 as messages try: import common @@ -1029,12 +1030,15 @@ def _clearsign_flow(self, flow, chain_id=1): self.assertEqual(resp.classification, CLASSIFICATION_VERIFIED) if flow['key'] == 'erc20-approve-unlimited': - with self.assertRaises(CallException) as ctx: - self.client.ethereum_sign_tx( - n=n, nonce=FLOW_NONCE, gas_price=FLOW_GAS_PRICE, - gas_limit=FLOW_GAS_LIMIT, to=flow['to'], - value=flow['value'], data=flow['data'], - chain_id=chain_id) + with self.client: + self.client.set_expected_responses([messages.Failure( + message='Unlimited ERC20 approval is disabled')]) + with self.assertRaises(CallException) as ctx: + self.client.ethereum_sign_tx( + n=n, nonce=FLOW_NONCE, gas_price=FLOW_GAS_PRICE, + gas_limit=FLOW_GAS_LIMIT, to=flow['to'], + value=flow['value'], data=flow['data'], + chain_id=chain_id) self.assertIn('Unlimited ERC20 approval is disabled', str(ctx.exception)) return From e97672ca452d414646b6db3372258c442bde4dbd Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 24 Sep 2026 20:01:47 -0500 Subject: [PATCH 03/11] test(eip712): check streamed array digests against an independent encoder Array-of-structs, Permit2 batch and multidimensional device walks now compare the device's domain/message hashes with a spec-derived reference encoder and recover the signature to the device address. The multidim walk is Stack 07 work and is gated by the structured-EIP-712 probe, not by the later evm-unknown-token-review capability. --- tests/test_msg_eip712_streaming.py | 119 +++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/tests/test_msg_eip712_streaming.py b/tests/test_msg_eip712_streaming.py index f3e5862e..298d7176 100644 --- a/tests/test_msg_eip712_streaming.py +++ b/tests/test_msg_eip712_streaming.py @@ -50,8 +50,104 @@ SPEC_MESSAGE_HASH = "c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e" +# --- Independent EIP-712 reference encoder ---------------------------------- +# Written from the EIP-712 specification text and deliberately NOT built on +# keepkeylib.eip712_stream (the host half of the protocol under test), so a +# shared misreading of arrays or nested dimensions cannot make both sides +# agree. keccak comes from pycryptodome directly. +def _keccak(data): + from Crypto.Hash import keccak + return keccak.new(digest_bits=256, data=data).digest() + + +def _ref_base_struct(type_name): + return type_name.split('[', 1)[0] + + +def _ref_encode_type(primary, types): + deps = set() + + def collect(name): + if name in deps or name not in types: + return + deps.add(name) + for field in types[name]: + collect(_ref_base_struct(field['type'])) + + collect(primary) + deps.discard(primary) + return ''.join( + '%s(%s)' % (name, ','.join('%s %s' % (f['type'], f['name']) + for f in types[name])) + for name in [primary] + sorted(deps)) + + +def _ref_int(value): + if isinstance(value, str): + return int(value, 16) if value.startswith('0x') else int(value) + return int(value) + + +def _ref_encode_value(type_name, value, types): + if type_name.endswith(']'): + # Arrays: keccak of the concatenated encodings of the elements, where + # each element of T[n][m] is itself an encoded T[n] array. + inner = type_name[:type_name.rindex('[')] + return _keccak(b''.join( + _ref_encode_value(inner, item, types) for item in value)) + if type_name in types: + return _ref_hash_struct(type_name, value, types) + if type_name == 'string': + return _keccak(value.encode('utf-8')) + if type_name == 'bytes': + return _keccak(bytes.fromhex(value[2:])) + if type_name == 'address': + return bytes(12) + bytes.fromhex(value[2:]) + if type_name == 'bool': + return (1 if value else 0).to_bytes(32, 'big') + if type_name.startswith('bytes'): + return bytes.fromhex(value[2:]).ljust(32, b'\0') + if type_name.startswith('uint'): + return _ref_int(value).to_bytes(32, 'big') + if type_name.startswith('int'): + return (_ref_int(value) % (1 << 256)).to_bytes(32, 'big') + raise ValueError('reference encoder: unsupported type ' + type_name) + + +def _ref_hash_struct(name, data, types): + encoded = _keccak(_ref_encode_type(name, types).encode('ascii')) + for field in types[name]: + encoded += _ref_encode_value(field['type'], data[field['name']], types) + return _keccak(encoded) + + +def reference_eip712_hashes(doc): + """Return (domain_separator, message_hash, signing_digest).""" + types = doc['types'] + domain = _ref_hash_struct('EIP712Domain', doc['domain'], types) + message = _ref_hash_struct(doc['primaryType'], doc['message'], types) + return domain, message, _keccak(b'\x19\x01' + domain + message) + + +def recover_signer(signature, digest): + from ecdsa import VerifyingKey, SECP256k1, util + rec = signature[64] - 27 + if rec not in (0, 1): + raise AssertionError('unexpected recovery byte %d' % signature[64]) + keys = VerifyingKey.from_public_key_recovery_with_digest( + signature[:64], digest, SECP256k1, hashfunc=None, + sigdecode=util.sigdecode_string) + return _keccak(keys[rec].to_string())[-20:] + + class TestEip712StreamHelpers(unittest.TestCase): + def test_reference_encoder_reproduces_the_published_spec_hashes(self): + """The independent encoder is itself checked against Example.js.""" + domain, message, _ = reference_eip712_hashes(SPEC_MAIL) + self.assertEqual(domain.hex(), SPEC_DOMAIN_SEPARATOR) + self.assertEqual(message.hex(), SPEC_MESSAGE_HASH) + def test_review_identifiers_are_exact_and_unambiguous(self): doc = { 'types': { @@ -160,6 +256,17 @@ def _walk(self, doc, max_steps=400): return resp raise AssertionError('walk did not terminate') + def _assert_reference_signature(self, doc, resp): + """The device's hashes AND signature match an independent encoder.""" + self.assertIsInstance(resp, eth.EthereumTypedDataSignature) + domain, message, digest = reference_eip712_hashes(doc) + self.assertEqual(resp.domain_separator_hash.hex(), domain.hex()) + self.assertEqual(resp.message_hash.hex(), message.hex()) + self.assertEqual(len(resp.signature), 65) + address = self.client.ethereum_get_address(PATH) + self.assertEqual(recover_signer(resp.signature, digest).hex(), + address.hex()) + def setUp(self): super(TestMsgEip712Streaming, self).setUp() self.requires_firmware("7.15.0") @@ -189,7 +296,7 @@ def test_spec_example_matches_the_published_hashes(self): self.assertIsInstance(resp, eth.EthereumTypedDataSignature) self.assertEqual(resp.domain_separator_hash.hex(), SPEC_DOMAIN_SEPARATOR) self.assertEqual(resp.message_hash.hex(), SPEC_MESSAGE_HASH) - self.assertEqual(len(resp.signature), 65) + self._assert_reference_signature(SPEC_MAIL, resp) def test_array_of_structs_walks(self): """Arrays, which the walk refused until the decode buffer was reclaimed. @@ -209,12 +316,10 @@ def test_array_of_structs_walks(self): "message": {"items": [{"id": 1}, {"id": 2}]}, } resp = self._walk(doc) - self.assertIsInstance(resp, eth.EthereumTypedDataSignature) - self.assertEqual(len(resp.signature), 65) + self._assert_reference_signature(doc, resp) def test_multidimensional_arrays_walk_outermost_first_on_device(self): """Host and device must traverse asymmetric Solidity dimensions alike.""" - self.requires_release_capability("evm-unknown-token-review") doc = { 'types': { 'EIP712Domain': [], @@ -233,8 +338,7 @@ def test_multidimensional_arrays_walk_outermost_first_on_device(self): } resp = self._walk(doc) - self.assertIsInstance(resp, eth.EthereumTypedDataSignature) - self.assertEqual(len(resp.signature), 65) + self._assert_reference_signature(doc, resp) def test_permit2_batch_walks_realistic_nested_array(self): """The production Permit2 Batch shape, including trailing root fields. @@ -288,8 +392,7 @@ def test_permit2_batch_walks_realistic_nested_array(self): }, } resp = self._walk(doc) - self.assertIsInstance(resp, eth.EthereumTypedDataSignature) - self.assertEqual(len(resp.signature), 65) + self._assert_reference_signature(doc, resp) def test_fixed_array_length_must_match_the_declared_size(self): """A declared dimension is part of the type string and so of typeHash. From 546dcb01acdba1076b47f419b3b97fc5ae35493f Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 24 Sep 2026 20:02:31 -0500 Subject: [PATCH 04/11] test(clearsign): assert the device's unlimited-approval refusal on the wire set_expected_responses() raised a CallException whose text embedded the expected message, so the old assertIn passed whatever the device sent. Send EthereumSignTx with call_raw and require the first response to be Failure_ActionCancelled with the exact message (no ButtonRequest first). --- tests/test_msg_ethereum_clear_signing.py | 33 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/test_msg_ethereum_clear_signing.py b/tests/test_msg_ethereum_clear_signing.py index 509d16cb..a6729cdd 100644 --- a/tests/test_msg_ethereum_clear_signing.py +++ b/tests/test_msg_ethereum_clear_signing.py @@ -30,6 +30,8 @@ from keepkeylib import messages_ethereum_pb2 as messages_eth from keepkeylib import messages_pb2 as messages +from keepkeylib import types_pb2 as types +from keepkeylib.tools import int_to_big_endian try: import common @@ -1030,17 +1032,26 @@ def _clearsign_flow(self, flow, chain_id=1): self.assertEqual(resp.classification, CLASSIFICATION_VERIFIED) if flow['key'] == 'erc20-approve-unlimited': - with self.client: - self.client.set_expected_responses([messages.Failure( - message='Unlimited ERC20 approval is disabled')]) - with self.assertRaises(CallException) as ctx: - self.client.ethereum_sign_tx( - n=n, nonce=FLOW_NONCE, gas_price=FLOW_GAS_PRICE, - gas_limit=FLOW_GAS_LIMIT, to=flow['to'], - value=flow['value'], data=flow['data'], - chain_id=chain_id) - self.assertIn('Unlimited ERC20 approval is disabled', - str(ctx.exception)) + # Drive the wire directly: the policy is refused on the FIRST + # response, before any ButtonRequest. Expected-response helpers + # would raise their own exception text containing the expected + # message, so they cannot prove what the device actually sent. + msg = messages_eth.EthereumSignTx( + address_n=n, + nonce=int_to_big_endian(FLOW_NONCE), + gas_price=int_to_big_endian(FLOW_GAS_PRICE), + gas_limit=int_to_big_endian(FLOW_GAS_LIMIT), + value=int_to_big_endian(flow['value']), + to=flow['to'], chain_id=chain_id, + data_length=len(flow['data']), + data_initial_chunk=flow['data'][:1024]) + self.assertEqual(len(flow['data']), 68) + response = self.client.call_raw(msg) + self.assertIsInstance(response, messages.Failure) + self.assertEqual(response.code, + types.Failure_ActionCancelled) + self.assertEqual(response.message, + 'Unlimited ERC20 approval is disabled') return sig_v, sig_r, sig_s = self.client.ethereum_sign_tx( From 0aa3c1a0ba184f3347100478321ffa04842380f6 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 24 Sep 2026 20:03:48 -0500 Subject: [PATCH 05/11] test(erc7730): never pass firmware/registry conformance without its inputs The firmware-validation step ran only behind 'if validator:' so compile tests passed without ever reaching firmware. Missing validator or registry is now an explicit skip, and a failure when CI sets KK_REQUIRE_ERC7730_EVIDENCE=1. --- tests/test_erc7730_compiler.py | 117 ++++++++++++--------------------- 1 file changed, 41 insertions(+), 76 deletions(-) diff --git a/tests/test_erc7730_compiler.py b/tests/test_erc7730_compiler.py index 576d8284..2e170b99 100644 --- a/tests/test_erc7730_compiler.py +++ b/tests/test_erc7730_compiler.py @@ -16,6 +16,32 @@ from keepkeylib.signed_metadata import keccak256 +# CI sets KK_REQUIRE_ERC7730_EVIDENCE=1. There, a missing firmware validator or +# official registry is a FAILURE: these tests are reported as firmware and +# registry conformance evidence, and silently skipping that step would let a +# host-only compile masquerade as firmware acceptance. Elsewhere the absence is +# reported as an explicit skip, never as a pass. +REQUIRE_EVIDENCE = os.environ.get("KK_REQUIRE_ERC7730_EVIDENCE") == "1" + + +def _evidence_input(name): + value = os.environ.get(name) + if value: + return value + message = "%s is not configured" % name + if REQUIRE_EVIDENCE: + pytest.fail(message + " (required by KK_REQUIRE_ERC7730_EVIDENCE=1)") + pytest.skip(message) + + +def _firmware_validate(program): + validator = _evidence_input("ERC7730_FIRMWARE_VALIDATOR") + with tempfile.NamedTemporaryFile() as compiled: + compiled.write(program) + compiled.flush() + subprocess.check_call([validator, compiled.name]) + + def _sections(program): count = program[178] offset = HEADER_SIZE @@ -127,18 +153,11 @@ def test_compiles_deterministic_canonical_calldata_program(): assert set(sections) == {1, 2, 3, 6, 7, 8, 9} assert hashlib.sha256(first).digest() == hashlib.sha256(second).digest() assert len(first) < 16384 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as compiled: - compiled.write(first) - compiled.flush() - subprocess.check_call([validator, compiled.name]) + _firmware_validate(first) def test_compiles_official_uniswap_tuple_fixture_through_firmware(): - registry = os.environ.get("ERC7730_REGISTRY") - if not registry: - pytest.skip("official ERC-7730 registry not configured") + registry = _evidence_input("ERC7730_REGISTRY") path = os.path.join( registry, "registry", "uniswap", "calldata-UniswapV3Router02.json") with open(path, "r") as source: @@ -168,12 +187,7 @@ def test_compiles_official_uniswap_tuple_fixture_through_firmware(): assert calldata[:4] == compiled[38:42] assert fixtures[1]["txHash"] == ( "0xb25281abb3e6bbfe18c746187522c2e915aa02fdb8175082005340e00c1f0b30") - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_compiles_and_checks_exact_keepkey_sdk_thorchain_swap(): @@ -199,12 +213,7 @@ def test_compiles_and_checks_exact_keepkey_sdk_thorchain_swap(): fixture["descriptor"], fixture["signature"], fixture["chainId"], fixture["to"], network_records=[(1, "Ethereum", "ETH", 18)]) assert compiled[38:42].hex() == expected["selector"] - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_compiles_array_iteration_separator_and_optional_visibility(): @@ -236,12 +245,7 @@ def test_compiles_array_iteration_separator_and_optional_visibility(): end = display[26:34] assert int.from_bytes(begin[6:8], "big") == 3 assert int.from_bytes(end[2:4], "big") == 1 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_compiles_recursive_array_iteration_frames(): @@ -261,12 +265,7 @@ def test_compiles_recursive_array_iteration_frames(): assert [item[0] for item in instructions] == [1, 7, 7, 4, 8, 8, 10] assert int.from_bytes(instructions[1][6:8], "big") == 5 assert int.from_bytes(instructions[2][6:8], "big") == 4 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_token_amount_without_token_uses_firmware_raw_fallback(): @@ -283,12 +282,7 @@ def test_token_amount_without_token_uses_firmware_raw_fallback(): formatter = _sections(compiled)[6] assert formatter[2:5] == bytes([3, 0, 1]) assert formatter[5:9] == bytes([1, 1, 0, 0]) - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_compiles_typed_if_not_in_and_must_match_conditions(): @@ -315,12 +309,7 @@ def test_compiles_typed_if_not_in_and_must_match_conditions(): assert conditions[10] == 8 literals = sections[4] assert int.from_bytes(literals[:2], "big") == 5 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_compiles_interpolated_intent_and_metadata_enum(): @@ -353,12 +342,7 @@ def test_compiles_interpolated_intent_and_metadata_enum(): assert formatters[2] == 8 literals = sections[4] assert int.from_bytes(literals[:2], "big") == 3 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_compiles_nested_field_group_with_balanced_links(): @@ -383,12 +367,7 @@ def test_compiles_nested_field_group_with_balanced_links(): assert [item[0] for item in instructions] == [1, 5, 4, 4, 6, 10] assert int.from_bytes(instructions[1][6:8], "big") == 4 assert int.from_bytes(instructions[4][2:4], "big") == 1 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): @@ -423,19 +402,12 @@ def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): count = int.from_bytes(display[:2], "big") opcodes = [display[2 + i * 8] for i in range(count)] assert opcodes == [1, 7, 5, 4, 4, 6, 8, 10] - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) def test_official_registry_all_calldata_formats_reach_firmware(): - registry = os.environ.get("ERC7730_REGISTRY") - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if not registry or not validator: - pytest.skip("official registry and firmware validator are required") + registry = _evidence_input("ERC7730_REGISTRY") + validator = _evidence_input("ERC7730_FIRMWARE_VALIDATOR") import glob failures = [] checked = 0 @@ -470,9 +442,7 @@ def test_official_registry_all_calldata_formats_reach_firmware(): def test_compiles_official_uniswap_eip712_fixture_through_firmware(): - registry = os.environ.get("ERC7730_REGISTRY") - if not registry: - pytest.skip("official ERC-7730 registry not configured") + registry = _evidence_input("ERC7730_REGISTRY") with open(os.path.join(registry, "registry", "uniswap", "eip712-uniswap-permit2.json"), "r") as source: descriptor = json.load(source) @@ -496,9 +466,4 @@ def test_compiles_official_uniswap_eip712_fixture_through_firmware(): binding = sections[8] # deployment + name/chain/contract domain facts + token + network assert int.from_bytes(binding[:2], "big") == 6 - validator = os.environ.get("ERC7730_FIRMWARE_VALIDATOR") - if validator: - with tempfile.NamedTemporaryFile() as output: - output.write(compiled) - output.flush() - subprocess.check_call([validator, output.name]) + _firmware_validate(compiled) From d64749cb18a7db3f8454ab47867f73f560918b3d Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 24 Sep 2026 20:20:23 -0500 Subject: [PATCH 06/11] test(eip712): cover the final sign screen, domain-only, permits and long bytes Every signing walk now requires exactly one ButtonRequest_SignTx "Sign Typed Data" screen at the end. New device tests: declining that screen returns ActionCancelled and no signature; primaryType EIP712Domain signs keccak(0x1901||domainSeparator) (checked by the independent encoder and signer recovery); unlimited EIP-2612 and Permit2 amounts are refused before their leaf is displayed; a 1024-byte bytes leaf signs and verifies independently. --- tests/test_msg_eip712_streaming.py | 165 ++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git a/tests/test_msg_eip712_streaming.py b/tests/test_msg_eip712_streaming.py index 298d7176..52f99bf7 100644 --- a/tests/test_msg_eip712_streaming.py +++ b/tests/test_msg_eip712_streaming.py @@ -8,15 +8,20 @@ # reading is self-consistent. Only an outside number can catch a shared # misreading. +import copy +import time import unittest import common +import oled_text from keepkeylib import eip712_stream as es from keepkeylib import messages_ethereum_pb2 as eth from keepkeylib import messages_pb2 as proto +from keepkeylib import types_pb2 as types from keepkeylib.client import CallException PATH = [0x8000002C, 0x8000003C, 0x80000000, 0, 0] +SETTLE = 0.3 # seconds for a confirm screen to finish drawing # assets/eip-712/Example.js in ethereum/EIPs publishes every intermediate. SPEC_MAIL = { @@ -122,9 +127,14 @@ def _ref_hash_struct(name, data, types): def reference_eip712_hashes(doc): - """Return (domain_separator, message_hash, signing_digest).""" + """Return (domain_separator, message_hash, signing_digest). + + primaryType EIP712Domain signs keccak(0x1901 || domainSeparator) with no + message hash (eth-sig-util v4); message_hash is None then.""" types = doc['types'] domain = _ref_hash_struct('EIP712Domain', doc['domain'], types) + if doc['primaryType'] == 'EIP712Domain': + return domain, None, _keccak(b'\x19\x01' + domain) message = _ref_hash_struct(doc['primaryType'], doc['message'], types) return domain, message, _keccak(b'\x19\x01' + domain + message) @@ -221,7 +231,7 @@ def test_innermost_fixed_array_length_is_checked(self): class TestMsgEip712Streaming(common.KeepKeyTest): - def _walk(self, doc, max_steps=400): + def _walk(self, doc, max_steps=400, decline_final=False): """Answer whatever the device asks until it returns a signature. The DEVICE leads. Nothing here chooses the order, which is the property @@ -234,6 +244,8 @@ def _walk(self, doc, max_steps=400): msg.primary_type = doc['primaryType'] msg.metamask_v4_compat = True + # (ButtonRequest code, framebuffer) for every screen, in order. + self.frames = [] resp = self.client.call_raw(msg) for _ in range(max_steps): if isinstance(resp, proto.ButtonRequest): @@ -241,7 +253,15 @@ def _walk(self, doc, max_steps=400): # callback_ButtonRequest() will capture the field being # approved. Retain it while that exact field is still active. self.client.capture_oled() - self.client.debug.press_yes() + # capture_oled() only settles when screenshots are enabled; + # the text assertions need the finished frame either way. + time.sleep(SETTLE) + self.frames.append( + (resp.code, bytes(self.client.debug.read_layout()))) + if decline_final and resp.code == types.ButtonRequest_SignTx: + self.client.debug.press_no() + else: + self.client.debug.press_yes() resp = self.client.call_raw(proto.ButtonAck()) elif isinstance(resp, eth.EthereumTypedDataStructRequest): resp = self.client.call_raw( @@ -256,12 +276,38 @@ def _walk(self, doc, max_steps=400): return resp raise AssertionError('walk did not terminate') + def _assert_final_sign_screen(self, first_line): + """Every typed-data signature ends with ONE "Sign Typed Data" screen + (ButtonRequest_SignTx); only its own continuation pages follow it.""" + codes = [code for code, _ in self.frames] + self.assertEqual(codes.count(types.ButtonRequest_SignTx), 1) + final = codes.index(types.ButtonRequest_SignTx) + self.assertTrue(all(code == types.ButtonRequest_Other + for code in codes[final + 1:])) + layout = self.frames[final][1] + self.assertIsNotNone(oled_text.find_line( + layout, 'SIGN TYPED DATA', oled_text.TITLE_FONT)) + self.assertIsNotNone(oled_text.find_line(layout, first_line)) + return final + + def _leaf_index(self, path): + """Index of the first screen whose body starts with `path`.""" + for index, (_, layout) in enumerate(self.frames): + if oled_text.find_line(layout, path) is not None: + return index + self.fail('no screen shows the %r leaf' % path) + def _assert_reference_signature(self, doc, resp): """The device's hashes AND signature match an independent encoder.""" self.assertIsInstance(resp, eth.EthereumTypedDataSignature) domain, message, digest = reference_eip712_hashes(doc) self.assertEqual(resp.domain_separator_hash.hex(), domain.hex()) - self.assertEqual(resp.message_hash.hex(), message.hex()) + if doc['primaryType'] == 'EIP712Domain': + self.assertFalse(resp.has_msg_hash) + self.assertEqual(resp.message_hash, b'') + else: + self.assertEqual(resp.message_hash.hex(), message.hex()) + self._assert_final_sign_screen('Sign ' + doc['primaryType']) self.assertEqual(len(resp.signature), 65) address = self.client.ethereum_get_address(PATH) self.assertEqual(recover_signer(resp.signature, digest).hex(), @@ -423,6 +469,117 @@ def test_advanced_mode_is_not_required_for_structured_review(self): self.assertEqual(resp.domain_separator_hash.hex(), SPEC_DOMAIN_SEPARATOR) self.assertEqual(resp.message_hash.hex(), SPEC_MESSAGE_HASH) + def test_declining_the_final_sign_screen_returns_no_signature(self): + """The final "Sign Typed Data" screen is a real consent: declining it + after every leaf was approved signs nothing.""" + resp = self._walk(SPEC_MAIL, decline_final=True) + self.assertIsInstance(resp, proto.Failure) + self.assertEqual((resp.code, resp.message), + (types.Failure_ActionCancelled, + 'Signing cancelled by user')) + codes = [code for code, _ in self.frames] + self.assertEqual(codes.count(types.ButtonRequest_SignTx), 1) + self.assertEqual(codes[-1], types.ButtonRequest_SignTx) + + def test_domain_only_signature_matches_an_independent_digest(self): + """primaryType EIP712Domain signs keccak(0x1901 || domainSeparator).""" + doc = dict(SPEC_MAIL, primaryType='EIP712Domain', message={}) + resp = self._walk(doc) + self.assertIsInstance(resp, eth.EthereumTypedDataSignature) + self.assertEqual(resp.domain_separator_hash.hex(), SPEC_DOMAIN_SEPARATOR) + self._assert_reference_signature(doc, resp) + + def test_unlimited_permits_are_refused_before_the_amount_screen(self): + """EIP-2612 Permit.value and Permit2 PermitDetails.amount at their + all-ones maximum are refused before that leaf is ever displayed.""" + permit = { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + "Permit": [ + {"name": "owner", "type": "address"}, + {"name": "spender", "type": "address"}, + {"name": "value", "type": "uint256"}, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + ], + }, + "primaryType": "Permit", + "domain": {"name": "USD Coin", "version": "2", "chainId": 1, + "verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, + "message": { + "owner": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8", + "spender": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + "value": "1000000", "nonce": "0", "deadline": "1893456000"}, + } + permit2 = { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + "PermitDetails": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint160"}, + {"name": "expiration", "type": "uint48"}, + {"name": "nonce", "type": "uint48"}, + ], + "PermitSingle": [ + {"name": "details", "type": "PermitDetails"}, + {"name": "spender", "type": "address"}, + {"name": "sigDeadline", "type": "uint256"}, + ], + }, + "primaryType": "PermitSingle", + "domain": {"name": "Permit2", "chainId": 1, + "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"}, + "message": { + "details": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amount": "250000000", "expiration": "1893456000", + "nonce": "1"}, + "spender": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + "sigDeadline": "1893456000"}, + } + for doc, path, leaf, bits in ((permit, ("value",), "value", 256), + (permit2, ("details", "amount"), + "details.amount", 160)): + # A finite amount signs and shows the leaf; its position is where + # the unlimited amount must stop. + self._assert_reference_signature(doc, self._walk(doc)) + before_leaf = self._leaf_index(leaf) + unlimited = copy.deepcopy(doc) + target = unlimited["message"] + for key in path[:-1]: + target = target[key] + target[path[-1]] = str((1 << bits) - 1) + resp = self._walk(unlimited) + self.assertIsInstance(resp, proto.Failure) + self.assertEqual((resp.code, resp.message), + (types.Failure_SyntaxError, + 'Unlimited ERC20 approval is disabled')) + self.assertEqual(len(self.frames), before_leaf) + + def test_1024_byte_dynamic_bytes_leaf_signs(self): + """A 1024-byte `bytes` leaf is reviewed in parts and signs; the digest + and signer are checked independently.""" + doc = { + "types": { + "EIP712Domain": [{"name": "name", "type": "string"}], + "Blob": [{"name": "data", "type": "bytes"}], + }, + "primaryType": "Blob", + "domain": {"name": "Blob"}, + "message": {"data": "0x" + bytes(range(256)).hex() * 4}, + } + resp = self._walk(doc) + self._assert_reference_signature(doc, resp) + if __name__ == '__main__': unittest.main() From 4f80b087f643df713fb8492b463e6f885d2e90bc Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 24 Sep 2026 20:31:37 -0500 Subject: [PATCH 07/11] fix(erc7730): compile only programs the device verifier accepts The registry conformance test had never run against a firmware validator. Run through the firmware's own catalog verifier, 56 of the 1,450 official calldata formats compiled to programs the device refuses: - 46 tokenAmount fields with no token compiled to formatter kind 3, which the verifier refuses without a token argument. With no token the device can only show the raw integer, so compile the raw formatter. - 8 paths iterated two nested arrays; the verifier accepts one "[]" step per path. Refuse them by name. - 2 ABIs nested deeper than the device's 8 levels. Refuse them by name. The registry test now requires every format to pass the firmware validator or be refused for one of those named device limits, and pins that count at 10. --- keepkeylib/erc7730_compiler.py | 15 ++++++++++++ tests/test_erc7730_compiler.py | 45 ++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/keepkeylib/erc7730_compiler.py b/keepkeylib/erc7730_compiler.py index 08501b09..0ce65fe6 100644 --- a/keepkeylib/erc7730_compiler.py +++ b/keepkeylib/erc7730_compiler.py @@ -670,6 +670,11 @@ def intern_path(steps): "threshold", "message", "chainId", "chainIdPath")): raise ValueError( "unknown token amount cannot use token metadata parameters") + else: + # No token is named, so the device can only show the raw + # integer. Its verifier refuses a tokenAmount without a + # token argument, so compile the raw formatter it runs. + kind = 1 if "threshold" in params: threshold = descriptor_value(params["threshold"]) if isinstance(threshold, str) and threshold.startswith("0x"): @@ -847,6 +852,11 @@ def depth(node): return 1 + (max(depth(child) for child in node.children) if node.children else 0) max_depth = depth(root) + # The device's ABI verifier counts the root as depth 1, as depth() + # does, and refuses a node deeper than ERC7730_ABI_MAX_DEPTH (8). + if max_depth > 8: + raise ValueError( + "ERC-7730 ABI nests deeper than the device supports") for node, first, count in flat: array_length = node.array_length if node.kind == 9 else 0 payload += bytes([node.kind]) + _u16(node.size) + _u16(first) + _u16(count) + _u16(array_length or 0) @@ -859,6 +869,11 @@ def depth(node): if steps and steps[0][0] == "container": payload += bytes([2, 0]) + _u16(steps[0][1]) continue + # The device iterates at most one array per path ("[]" step). + if sum(1 for step in steps if step[0] == 2) > 1: + raise ValueError( + "ERC-7730 path iterates more than one array; the device " + "supports one") payload += bytes([1, len(steps)]) + _u16(ABSENT) for step in steps: if step[0] == 1: diff --git a/tests/test_erc7730_compiler.py b/tests/test_erc7730_compiler.py index 2e170b99..7f44302e 100644 --- a/tests/test_erc7730_compiler.py +++ b/tests/test_erc7730_compiler.py @@ -248,7 +248,10 @@ def test_compiles_array_iteration_separator_and_optional_visibility(): _firmware_validate(compiled) -def test_compiles_recursive_array_iteration_frames(): +def test_refuses_nested_array_iteration_the_device_cannot_verify(): + # The device's catalog verifier accepts one "[]" step per path, so a + # program iterating a nested array could never be loaded. The compiler + # refuses it by name instead of emitting a program the device rejects. descriptor = {"display": {"formats": { "matrix(uint256[][] values)": { "intent": "Review matrix", @@ -256,16 +259,10 @@ def test_compiles_recursive_array_iteration_frames(): "format": "raw", "separator": "Next row"}], } }}} - compiled = compile_calldata( - descriptor, "matrix(uint256[][] values)", 1, - "0x1111111111111111111111111111111111111111") - display = _sections(compiled)[7] - count = int.from_bytes(display[:2], "big") - instructions = [display[2 + i * 8:10 + i * 8] for i in range(count)] - assert [item[0] for item in instructions] == [1, 7, 7, 4, 8, 8, 10] - assert int.from_bytes(instructions[1][6:8], "big") == 5 - assert int.from_bytes(instructions[2][6:8], "big") == 4 - _firmware_validate(compiled) + with pytest.raises(ValueError, match="iterates more than one array"): + compile_calldata( + descriptor, "matrix(uint256[][] values)", 1, + "0x1111111111111111111111111111111111111111") def test_token_amount_without_token_uses_firmware_raw_fallback(): @@ -279,8 +276,10 @@ def test_token_amount_without_token_uses_firmware_raw_fallback(): compiled = compile_calldata( descriptor, "quote(uint256 amount)", 1, "0x1111111111111111111111111111111111111111") + # With no token named the device can only show the raw integer, and its + # verifier refuses a tokenAmount formatter without a token argument. formatter = _sections(compiled)[6] - assert formatter[2:5] == bytes([3, 0, 1]) + assert formatter[2:5] == bytes([1, 0, 1]) assert formatter[5:9] == bytes([1, 1, 0, 0]) _firmware_validate(compiled) @@ -405,11 +404,18 @@ def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): _firmware_validate(compiled) +DEVICE_LIMITS = ( + "iterates more than one array", + "nests deeper than the device supports", +) + + def test_official_registry_all_calldata_formats_reach_firmware(): registry = _evidence_input("ERC7730_REGISTRY") validator = _evidence_input("ERC7730_FIRMWARE_VALIDATOR") import glob failures = [] + unsupported = [] checked = 0 for path in glob.glob(os.path.join( registry, "registry", "**", "calldata-*.json"), recursive=True): @@ -425,9 +431,15 @@ def test_official_registry_all_calldata_formats_reach_firmware(): for signature in descriptor.get("display", {}).get("formats", {}): checked += 1 try: - program = compile_calldata( - descriptor, signature, deployment["chainId"], - deployment["address"]) + try: + program = compile_calldata( + descriptor, signature, deployment["chainId"], + deployment["address"]) + except ValueError as exc: + if any(reason in str(exc) for reason in DEVICE_LIMITS): + unsupported.append(signature) + continue + raise with tempfile.NamedTemporaryFile() as output: output.write(program) output.flush() @@ -439,6 +451,9 @@ def test_official_registry_all_calldata_formats_reach_firmware(): os.path.basename(path), signature, exc)) assert checked == 1450 assert failures == [] + # Every other format is refused by the compiler for a named device limit: + # 8 iterate nested arrays, 2 nest their ABI deeper than 8 levels. + assert len(unsupported) == 10 def test_compiles_official_uniswap_eip712_fixture_through_firmware(): From e97f6250a241245f4ab9f28fa82806b6396b5919 Mon Sep 17 00:00:00 2001 From: highlander Date: Fri, 25 Sep 2026 00:37:26 -0500 Subject: [PATCH 08/11] test(eip712): assert the typed-data response reports the signing address The device response buffer is shared with DebugLink, and a state read during the final sign screen erased an address written before it. The signature still recovered to the right key, so only the reported address was wrong. Under screenshot capture the unfixed firmware returns an empty address and these assertions fail. --- tests/test_msg_eip712_streaming.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_msg_eip712_streaming.py b/tests/test_msg_eip712_streaming.py index 52f99bf7..6a6a48fe 100644 --- a/tests/test_msg_eip712_streaming.py +++ b/tests/test_msg_eip712_streaming.py @@ -312,6 +312,9 @@ def _assert_reference_signature(self, doc, resp): address = self.client.ethereum_get_address(PATH) self.assertEqual(recover_signer(resp.signature, digest).hex(), address.hex()) + # The reported signer must be intact too: the response is built after + # the final screen, so a DebugLink read during it cannot erase it. + self.assertEqual(resp.address.lower(), '0x' + address.hex()) def setUp(self): super(TestMsgEip712Streaming, self).setUp() From 9cea0adfab4a5d99a81a42ee09f5caba9616edbb Mon Sep 17 00:00:00 2001 From: highlander Date: Fri, 25 Sep 2026 01:07:29 -0500 Subject: [PATCH 09/11] fix(erc7730): refuse paths deeper than device captures accept Firmware calldata and typed-data captures refuse ERC7730_ABI_MAX_DEPTH (8) or more path steps, and the catalog verifier now does too. Refuse such paths by name at compile time. No official registry format is affected. --- keepkeylib/erc7730_compiler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keepkeylib/erc7730_compiler.py b/keepkeylib/erc7730_compiler.py index 0ce65fe6..fb80742c 100644 --- a/keepkeylib/erc7730_compiler.py +++ b/keepkeylib/erc7730_compiler.py @@ -869,6 +869,11 @@ def depth(node): if steps and steps[0][0] == "container": payload += bytes([2, 0]) + _u16(steps[0][1]) continue + # Device captures descend one ABI level per step and refuse + # ERC7730_ABI_MAX_DEPTH (8) or more steps. + if len(steps) >= 8: + raise ValueError( + "ERC-7730 path nests deeper than the device supports") # The device iterates at most one array per path ("[]" step). if sum(1 for step in steps if step[0] == 2) > 1: raise ValueError( From 67f561ca3adfab4f0a7ddc1616ac9122d6c3ccde Mon Sep 17 00:00:00 2001 From: highlander Date: Fri, 25 Sep 2026 03:23:03 -0500 Subject: [PATCH 10/11] feat(erc7730): compile only what the device executes Firmware now refuses at preload every ERC-7730 program its runtime cannot finish, using one capability table shared by the verifier and the runtime. Mirror that table here as DEVICE_CAPABILITIES: - compile_calldata/compile_eip712 raise DeviceCannotExecute with the exact reason unless executable_only=False; - device_refusal() reports why a compiled program would be refused; - the registry test asserts that the device verifier and this mirror agree on every compiled format in both directions, and that exactly 92 of the 1,450 official formats are signable (not 94: two raw fields read the @.from container, which the runtime never captured). Also pin device-protocol at the canonical release protocol plus the Solana LUT merge and DebugLinkState.confirm_title/confirm_body, and add DebugLink.read_confirm_text() so tests can assert exact screen text. --- device-protocol | 2 +- keepkeylib/debuglink.py | 6 ++ keepkeylib/erc7730_compiler.py | 138 +++++++++++++++++++++++++++++++-- keepkeylib/messages_pb2.py | 54 ++++++++----- tests/test_erc7730_compiler.py | 105 +++++++++++++++++-------- 5 files changed, 244 insertions(+), 61 deletions(-) diff --git a/device-protocol b/device-protocol index dd9c85dc..5fec9e69 160000 --- a/device-protocol +++ b/device-protocol @@ -1 +1 @@ -Subproject commit dd9c85dc747cf965fb0e7bf49615dc9e7568ee65 +Subproject commit 5fec9e6906a340be5eb3d795ec746769065b2db8 diff --git a/keepkeylib/debuglink.py b/keepkeylib/debuglink.py index efd308c9..34abcb87 100644 --- a/keepkeylib/debuglink.py +++ b/keepkeylib/debuglink.py @@ -91,6 +91,12 @@ def read_dice_digest(self): obj = self._call(proto.DebugLinkGetState()) return obj.dice_digest + def read_confirm_text(self): + """(title, body) of the confirmation currently or last shown, exactly + as the firmware formatted them, before the body is paged.""" + obj = self._call(proto.DebugLinkGetState()) + return (obj.confirm_title, obj.confirm_body) + def read_passphrase_protection(self): obj = self._call(proto.DebugLinkGetState()) return obj.passphrase_protection diff --git a/keepkeylib/erc7730_compiler.py b/keepkeylib/erc7730_compiler.py index fb80742c..458b22f4 100644 --- a/keepkeylib/erc7730_compiler.py +++ b/keepkeylib/erc7730_compiler.py @@ -967,9 +967,135 @@ def depth(node): for kind, value in sections) -def compile_calldata(descriptor, signature, chain_id, address, **kwargs): - return CalldataCompiler(descriptor, signature, chain_id, address, - **kwargs).compile() +# What the device executes: a mirror of keepkey-firmware's +# include/keepkey/firmware/erc7730_capabilities.h. The device refuses every +# program outside it at preload, before the first screen. Widen this only +# together with the firmware table. +DEVICE_CAPABILITIES = { + "display_opcodes": frozenset((1, 4, 10)), + # formatter kind -> {argument role -> permitted sources} + "formatters": {1: {1: frozenset((1,))}}, + "path_sources": frozenset((1,)), + "path_step_opcodes": frozenset((1,)), + "conditions": False, +} +MAX_ARRAY_ELEMENTS = 64 + + +class DeviceCannotExecute(ValueError): + """The program is valid ERC-7730 but outside the device's capabilities.""" + + +def _program_sections(program): + offset = HEADER_SIZE + sections = {} + for _ in range(program[178]): + kind = program[offset] + length = struct.unpack(">I", program[offset + 1:offset + 5])[0] + sections[kind] = program[offset + 5:offset + 5 + length] + offset += 5 + length + return sections + + +def device_refusal(program, capabilities=DEVICE_CAPABILITIES): + """Why the device would refuse `program` at preload, or None.""" + sections = _program_sections(program) + u16 = lambda data, at: struct.unpack(">H", data[at:at + 2])[0] + + abi = sections.get(2, b"\0\0") + nodes = [struct.unpack(">BHHHH", abi[2 + 9 * i:11 + 9 * i]) + for i in range(u16(abi, 0))] + + def walk(steps): + node = 0 + for step in steps: + if node >= len(nodes): + return "path leaves the ABI" + kind, _, first, count, length = nodes[node] + if kind == 8 and count and 0 <= step < count: + node = first + step + elif kind == 9: + limit = MAX_ARRAY_ELEMENTS if length == ABSENT else length + if not -limit <= step < limit: + return "path indexes beyond the array" + node = first + else: + return "path does not name an ABI member" + if node >= len(nodes) or nodes[node][0] > 7: + return "path does not end at a value" + return None + + paths = sections.get(3, b"\0\0") + at = 2 + for _ in range(u16(paths, 0)): + source, count = paths[at], paths[at + 1] + at += 4 + if source not in capabilities["path_sources"]: + return "path source %d is not executed" % source + steps = [] + for _ in range(count): + opcode = paths[at] + at += 1 + if opcode not in capabilities["path_step_opcodes"]: + return "path step opcode %d is not executed" % opcode + steps.append(struct.unpack(">i", paths[at:at + 4])[0]) + at += 4 + reason = walk(steps) + if reason: + return reason + + conditions = sections.get(5, b"\0\0") + if u16(conditions, 0) and not capabilities["conditions"]: + return "display conditions are not executed" + + formatters = sections.get(6, b"\0\0") + at = 2 + for _ in range(u16(formatters, 0)): + kind, argc = formatters[at], formatters[at + 2] + at += 3 + roles = capabilities["formatters"].get(kind) + if roles is None: + return "formatter kind %d is not executed" % kind + seen = set() + for _ in range(argc): + role, source = formatters[at], formatters[at + 1] + at += 4 + if source not in roles.get(role, ()): + return "formatter kind %d argument role %d is not executed" % ( + kind, role) + seen.add(role) + if not set(roles) <= seen: + return "formatter kind %d lacks a required argument" % kind + + displays = sections.get(7, b"\0\0") + for pc in range(u16(displays, 0)): + opcode, _, a, b, c = struct.unpack( + ">BBHHH", displays[2 + 8 * pc:10 + 8 * pc]) + if opcode not in capabilities["display_opcodes"]: + return "display opcode %d is not executed" % opcode + if (opcode == 1) != (pc == 0): + return "the intent must be the first display instruction only" + if opcode == 4 and c != ABSENT and not capabilities["conditions"]: + return "display conditions are not executed" + return None + + +def _executable(program, executable_only): + if executable_only: + reason = device_refusal(program) + if reason: + raise DeviceCannotExecute("device cannot execute: " + reason) + return program + + +def compile_calldata(descriptor, signature, chain_id, address, + executable_only=True, **kwargs): + """Compile one calldata format. By default only a program the device + executes is returned; pass executable_only=False to obtain the program + anyway, for example to prove that the device refuses it.""" + return _executable(CalldataCompiler(descriptor, signature, chain_id, + address, **kwargs).compile(), + executable_only) def _typed_base(type_name, types, active): @@ -1017,7 +1143,7 @@ def declaration(name): def compile_eip712(descriptor, typed_data, chain_id=None, address=None, - **kwargs): + executable_only=True, **kwargs): """Compile one EIP-712 descriptor against its exact typed-data schema.""" types = typed_data["types"] primary = typed_data["primaryType"] @@ -1060,7 +1186,7 @@ def named(node): if name in domain: constraints.append((field, domain[name])) type_hash = keccak256(eip712_encode_type(primary, types).encode("ascii")) - return CalldataCompiler( + return _executable(CalldataCompiler( synthetic, signature, chain_id, address, definition_kind=2, primary_type_hash=type_hash, domain_constraints=constraints, - **kwargs).compile() + **kwargs).compile(), executable_only) diff --git a/keepkeylib/messages_pb2.py b/keepkeylib/messages_pb2.py index dc6abeb3..500bcef1 100644 --- a/keepkeylib/messages_pb2.py +++ b/keepkeylib/messages_pb2.py @@ -21,7 +21,7 @@ name='messages.proto', package='', syntax='proto2', - serialized_pb=_b('\n\x0emessages.proto\x1a\x0btypes.proto\"\x0c\n\nInitialize\"\r\n\x0bGetFeatures\"\x8a\x05\n\x08\x46\x65\x61tures\x12\x0e\n\x06vendor\x18\x01 \x01(\t\x12\x15\n\rmajor_version\x18\x02 \x01(\r\x12\x15\n\rminor_version\x18\x03 \x01(\r\x12\x15\n\rpatch_version\x18\x04 \x01(\r\x12\x17\n\x0f\x62ootloader_mode\x18\x05 \x01(\x08\x12\x11\n\tdevice_id\x18\x06 \x01(\t\x12\x16\n\x0epin_protection\x18\x07 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x08 \x01(\x08\x12\x10\n\x08language\x18\t \x01(\t\x12\r\n\x05label\x18\n \x01(\t\x12\x18\n\x05\x63oins\x18\x0b \x03(\x0b\x32\t.CoinType\x12\x13\n\x0binitialized\x18\x0c \x01(\x08\x12\x10\n\x08revision\x18\r \x01(\x0c\x12\x17\n\x0f\x62ootloader_hash\x18\x0e \x01(\x0c\x12\x10\n\x08imported\x18\x0f \x01(\x08\x12\x12\n\npin_cached\x18\x10 \x01(\x08\x12\x19\n\x11passphrase_cached\x18\x11 \x01(\x08\x12\x1d\n\x08policies\x18\x12 \x03(\x0b\x32\x0b.PolicyType\x12\r\n\x05model\x18\x15 \x01(\t\x12\x18\n\x10\x66irmware_variant\x18\x16 \x01(\t\x12\x15\n\rfirmware_hash\x18\x17 \x01(\x0c\x12\x11\n\tno_backup\x18\x18 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x19 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x1a \x01(\r\x12\x18\n\x10supports_taproot\x18\x1b \x01(\x08\x12\x1b\n\x13supports_dice_modes\x18\x1c \x01(\x08\x12\'\n\x1fsupports_solana_lut_attestation\x18\x1d \x01(\x08\"*\n\x0cGetCoinTable\x12\r\n\x05start\x18\x01 \x01(\r\x12\x0b\n\x03\x65nd\x18\x02 \x01(\r\"L\n\tCoinTable\x12\x18\n\x05table\x18\x01 \x03(\x0b\x32\t.CoinType\x12\x11\n\tnum_coins\x18\x02 \x01(\r\x12\x12\n\nchunk_size\x18\x03 \x01(\r\"\x0e\n\x0c\x43learSession\"y\n\rApplySettings\x12\x10\n\x08language\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x16\n\x0euse_passphrase\x18\x03 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x04 \x01(\r\x12\x13\n\x0bu2f_counter\x18\x05 \x01(\r\"\x1b\n\tChangePin\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x87\x01\n\x04Ping\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x19\n\x11\x62utton_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x05 \x01(\x08\"\x1a\n\x07Success\x12\x0f\n\x07message\x18\x01 \x01(\t\"6\n\x07\x46\x61ilure\x12\x1a\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x0c.FailureType\x12\x0f\n\x07message\x18\x02 \x01(\t\"?\n\rButtonRequest\x12 \n\x04\x63ode\x18\x01 \x01(\x0e\x32\x12.ButtonRequestType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"\x0b\n\tButtonAck\"7\n\x10PinMatrixRequest\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.PinMatrixRequestType\"\x1b\n\x0cPinMatrixAck\x12\x0b\n\x03pin\x18\x01 \x02(\t\"\x08\n\x06\x43\x61ncel\"\x13\n\x11PassphraseRequest\"#\n\rPassphraseAck\x12\x12\n\npassphrase\x18\x01 \x02(\t\"\x1a\n\nGetEntropy\x12\x0c\n\x04size\x18\x01 \x02(\r\"\x1a\n\x07\x45ntropy\x12\x0f\n\x07\x65ntropy\x18\x01 \x02(\x0c\"\xa2\x01\n\x0cGetPublicKey\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x02 \x01(\t\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"4\n\tPublicKey\x12\x19\n\x04node\x18\x01 \x02(\x0b\x32\x0b.HDNodeType\x12\x0c\n\x04xpub\x18\x02 \x01(\t\"\xb3\x01\n\nGetAddress\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x1a\n\tcoin_name\x18\x02 \x01(\t:\x07\x42itcoin\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12+\n\x08multisig\x18\x04 \x01(\x0b\x32\x19.MultisigRedeemScriptType\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"\x1a\n\x07\x41\x64\x64ress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x02(\t\"\x0c\n\nWipeDevice\"\xbb\x01\n\nLoadDevice\x12\x10\n\x08mnemonic\x18\x01 \x01(\t\x12\x19\n\x04node\x18\x02 \x01(\x0b\x32\x0b.HDNodeType\x12\x0b\n\x03pin\x18\x03 \x01(\t\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x15\n\rskip_checksum\x18\x07 \x01(\x08\x12\x13\n\x0bu2f_counter\x18\x08 \x01(\r\"\x8a\x02\n\x0bResetDevice\x12\x16\n\x0e\x64isplay_random\x18\x01 \x01(\x08\x12\x15\n\x08strength\x18\x02 \x01(\r:\x03\x32\x35\x36\x12\x1d\n\x15passphrase_protection\x18\x03 \x01(\x08\x12\x16\n\x0epin_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x11\n\tno_backup\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x14\n\x0c\x64ice_entropy\x18\n \x01(\x08\x12\x11\n\tdice_only\x18\x0b \x01(\x08\"\x10\n\x0e\x45ntropyRequest\"\x1d\n\nEntropyAck\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\xff\x01\n\x0eRecoveryDevice\x12\x12\n\nword_count\x18\x01 \x01(\r\x12\x1d\n\x15passphrase_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x19\n\x08language\x18\x04 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x05 \x01(\t\x12\x18\n\x10\x65nforce_wordlist\x18\x06 \x01(\x08\x12\x1c\n\x14use_character_cipher\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x0f\n\x07\x64ry_run\x18\n \x01(\x08\"\r\n\x0bWordRequest\"\x17\n\x07WordAck\x12\x0c\n\x04word\x18\x01 \x02(\t\";\n\x10\x43haracterRequest\x12\x10\n\x08word_pos\x18\x01 \x02(\r\x12\x15\n\rcharacter_pos\x18\x02 \x02(\r\"?\n\x0c\x43haracterAck\x12\x11\n\tcharacter\x18\x01 \x01(\t\x12\x0e\n\x06\x64\x65lete\x18\x02 \x01(\x08\x12\x0c\n\x04\x64one\x18\x03 \x01(\x08\"\x82\x01\n\x0bSignMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0f\n\x07message\x18\x02 \x02(\x0c\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"`\n\rVerifyMessage\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\"6\n\x10MessageSignature\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"v\n\x0e\x45ncryptMessage\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64isplay_only\x18\x03 \x01(\x08\x12\x11\n\taddress_n\x18\x04 \x03(\r\x12\x1a\n\tcoin_name\x18\x05 \x01(\t:\x07\x42itcoin\"@\n\x10\x45ncryptedMessage\x12\r\n\x05nonce\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x0c\n\x04hmac\x18\x03 \x01(\x0c\"Q\n\x0e\x44\x65\x63ryptMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\r\n\x05nonce\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x0c\n\x04hmac\x18\x04 \x01(\x0c\"4\n\x10\x44\x65\x63ryptedMessage\x12\x0f\n\x07message\x18\x01 \x01(\x0c\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"\x8c\x01\n\x0e\x43ipherKeyValue\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\x12\x0f\n\x07\x65ncrypt\x18\x04 \x01(\x08\x12\x16\n\x0e\x61sk_on_encrypt\x18\x05 \x01(\x08\x12\x16\n\x0e\x61sk_on_decrypt\x18\x06 \x01(\x08\x12\n\n\x02iv\x18\x07 \x01(\x0c\"!\n\x10\x43ipheredKeyValue\x12\r\n\x05value\x18\x01 \x01(\x0c\"5\n\x10GetBip85Mnemonic\x12\x12\n\nword_count\x18\x01 \x02(\r\x12\r\n\x05index\x18\x02 \x02(\r\"!\n\rBip85Mnemonic\x12\x10\n\x08mnemonic\x18\x01 \x02(\t\"\xce\x01\n\x06SignTx\x12\x15\n\routputs_count\x18\x01 \x02(\r\x12\x14\n\x0cinputs_count\x18\x02 \x02(\r\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x12\n\x07version\x18\x04 \x01(\r:\x01\x31\x12\x14\n\tlock_time\x18\x05 \x01(\r:\x01\x30\x12\x0e\n\x06\x65xpiry\x18\x06 \x01(\r\x12\x14\n\x0coverwintered\x18\x07 \x01(\x08\x12\x18\n\x10version_group_id\x18\x08 \x01(\r\x12\x11\n\tbranch_id\x18\n \x01(\r\"\x85\x01\n\tTxRequest\x12\"\n\x0crequest_type\x18\x01 \x01(\x0e\x32\x0c.RequestType\x12&\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x15.TxRequestDetailsType\x12,\n\nserialized\x18\x03 \x01(\x0b\x32\x18.TxRequestSerializedType\"%\n\x05TxAck\x12\x1c\n\x02tx\x18\x01 \x01(\x0b\x32\x10.TransactionType\"+\n\x08RawTxAck\x12\x1f\n\x02tx\x18\x01 \x01(\x0b\x32\x13.RawTransactionType\"}\n\x0cSignIdentity\x12\x1f\n\x08identity\x18\x01 \x01(\x0b\x32\r.IdentityType\x12\x18\n\x10\x63hallenge_hidden\x18\x02 \x01(\x0c\x12\x18\n\x10\x63hallenge_visual\x18\x03 \x01(\t\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x04 \x01(\t\"H\n\x0eSignedIdentity\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\",\n\rApplyPolicies\x12\x1b\n\x06policy\x18\x01 \x03(\x0b\x32\x0b.PolicyType\"?\n\tFlashHash\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\x12\x11\n\tchallenge\x18\x03 \x01(\x0c\":\n\nFlashWrite\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\r\n\x05\x65rase\x18\x03 \x01(\x08\"!\n\x11\x46lashHashResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"5\n\x12\x44\x65\x62ugLinkFlashDump\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\"*\n\x1a\x44\x65\x62ugLinkFlashDumpResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x0b\n\tSoftReset\"\x0f\n\rFirmwareErase\"7\n\x0e\x46irmwareUpload\x12\x14\n\x0cpayload_hash\x18\x01 \x02(\x0c\x12\x0f\n\x07payload\x18\x02 \x02(\x0c\"2\n\x11\x44\x65\x62ugLinkDecision\x12\x0e\n\x06yes_no\x18\x01 \x02(\x08\x12\r\n\x05input\x18\x02 \x01(\t\"\x13\n\x11\x44\x65\x62ugLinkGetState\"\xec\x02\n\x0e\x44\x65\x62ugLinkState\x12\x0e\n\x06layout\x18\x01 \x01(\x0c\x12\x0b\n\x03pin\x18\x02 \x01(\t\x12\x0e\n\x06matrix\x18\x03 \x01(\t\x12\x10\n\x08mnemonic\x18\x04 \x01(\t\x12\x19\n\x04node\x18\x05 \x01(\x0b\x32\x0b.HDNodeType\x12\x1d\n\x15passphrase_protection\x18\x06 \x01(\x08\x12\x12\n\nreset_word\x18\x07 \x01(\t\x12\x15\n\rreset_entropy\x18\x08 \x01(\x0c\x12\x1a\n\x12recovery_fake_word\x18\t \x01(\t\x12\x19\n\x11recovery_word_pos\x18\n \x01(\r\x12\x17\n\x0frecovery_cipher\x18\x0b \x01(\t\x12$\n\x1crecovery_auto_completed_word\x18\x0c \x01(\t\x12\x15\n\rfirmware_hash\x18\r \x01(\x0c\x12\x14\n\x0cstorage_hash\x18\x0e \x01(\x0c\x12\x13\n\x0b\x64ice_digest\x18\x0f \x01(\x0c\"\x0f\n\rDebugLinkStop\";\n\x0c\x44\x65\x62ugLinkLog\x12\r\n\x05level\x18\x01 \x01(\r\x12\x0e\n\x06\x62ucket\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\"\x15\n\x13\x44\x65\x62ugLinkFillConfig\" \n\x0e\x43hangeWipeCode\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x1f\n\x1d\x43learsignAttestorGetPublicKey\"0\n\x1a\x43learsignAttestorPublicKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"(\n\x15\x43learsignAttestorSign\x12\x0f\n\x07payload\x18\x01 \x01(\x0c\"C\n\x1a\x43learsignAttestorSignature\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x12\n\npublic_key\x18\x02 \x01(\x0c*\xc3\x46\n\x0bMessageType\x12 \n\x16MessageType_Initialize\x10\x00\x1a\x04\x90\xb5\x18\x01\x12\x1a\n\x10MessageType_Ping\x10\x01\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Success\x10\x02\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_Failure\x10\x03\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ChangePin\x10\x04\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_WipeDevice\x10\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_FirmwareErase\x10\x06\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_FirmwareUpload\x10\x07\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetEntropy\x10\t\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Entropy\x10\n\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_GetPublicKey\x10\x0b\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_PublicKey\x10\x0c\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_LoadDevice\x10\r\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_ResetDevice\x10\x0e\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_SignTx\x10\x0f\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_Features\x10\x11\x1a\x04\x98\xb5\x18\x01\x12&\n\x1cMessageType_PinMatrixRequest\x10\x12\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_PinMatrixAck\x10\x13\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_Cancel\x10\x14\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_TxRequest\x10\x15\x1a\x04\x98\xb5\x18\x01\x12\x1b\n\x11MessageType_TxAck\x10\x16\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_CipherKeyValue\x10\x17\x1a\x04\x90\xb5\x18\x01\x12\"\n\x18MessageType_ClearSession\x10\x18\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplySettings\x10\x19\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ButtonRequest\x10\x1a\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ButtonAck\x10\x1b\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetAddress\x10\x1d\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Address\x10\x1e\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EntropyRequest\x10#\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_EntropyAck\x10$\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_SignMessage\x10&\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_VerifyMessage\x10\'\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_MessageSignature\x10(\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1dMessageType_PassphraseRequest\x10)\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_PassphraseAck\x10*\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_RecoveryDevice\x10-\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_WordRequest\x10.\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_WordAck\x10/\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CipheredKeyValue\x10\x30\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EncryptMessage\x10\x31\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_EncryptedMessage\x10\x32\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_DecryptMessage\x10\x33\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_DecryptedMessage\x10\x34\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_SignIdentity\x10\x35\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_SignedIdentity\x10\x36\x1a\x04\x98\xb5\x18\x01\x12!\n\x17MessageType_GetFeatures\x10\x37\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumGetAddress\x10\x38\x1a\x04\x90\xb5\x18\x01\x12%\n\x1bMessageType_EthereumAddress\x10\x39\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EthereumSignTx\x10:\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_EthereumTxRequest\x10;\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_EthereumTxAck\x10<\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CharacterRequest\x10P\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_CharacterAck\x10Q\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_RawTxAck\x10R\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplyPolicies\x10S\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_FlashHash\x10T\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_FlashWrite\x10U\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_FlashHashResponse\x10V\x1a\x04\x98\xb5\x18\x01\x12(\n\x1eMessageType_DebugLinkFlashDump\x10W\x1a\x04\xa0\xb5\x18\x01\x12\x30\n&MessageType_DebugLinkFlashDumpResponse\x10X\x1a\x04\xa8\xb5\x18\x01\x12\x1f\n\x15MessageType_SoftReset\x10Y\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkDecision\x10\x64\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkGetState\x10\x65\x1a\x04\xa0\xb5\x18\x01\x12$\n\x1aMessageType_DebugLinkState\x10\x66\x1a\x04\xa8\xb5\x18\x01\x12#\n\x19MessageType_DebugLinkStop\x10g\x1a\x04\xa0\xb5\x18\x01\x12\"\n\x18MessageType_DebugLinkLog\x10h\x1a\x04\xa8\xb5\x18\x01\x12)\n\x1fMessageType_DebugLinkFillConfig\x10i\x1a\x04\xa8\xb5\x18\x01\x12\"\n\x18MessageType_GetCoinTable\x10j\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_CoinTable\x10k\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_EthereumSignMessage\x10l\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumVerifyMessage\x10m\x1a\x04\x90\xb5\x18\x01\x12.\n$MessageType_EthereumMessageSignature\x10n\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_ChangeWipeCode\x10o\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumSignTypedHash\x10p\x1a\x04\x90\xb5\x18\x01\x12\x30\n&MessageType_EthereumTypedDataSignature\x10q\x1a\x04\x98\xb5\x18\x01\x12,\n\"MessageType_Ethereum712TypesValues\x10r\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumTxMetadata\x10s\x1a\x04\x90\xb5\x18\x01\x12)\n\x1fMessageType_EthereumMetadataAck\x10t\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_LoadClearsignSigner\x10u\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedData\x10\xa8\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumTypedDataStructRequest\x10\xa9\r\x1a\x04\x98\xb5\x18\x01\x12\x31\n&MessageType_EthereumTypedDataStructAck\x10\xaa\r\x1a\x04\x90\xb5\x18\x01\x12\x34\n)MessageType_EthereumTypedDataValueRequest\x10\xab\r\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_EthereumTypedDataValueAck\x10\xac\r\x1a\x04\x90\xb5\x18\x01\x12\x32\n\'MessageType_EthereumClearSignDefinition\x10\xad\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumClearSignDefinitionAck\x10\xae\r\x1a\x04\x98\xb5\x18\x01\x12\x39\n.MessageType_EthereumClearSignDefinitionRequest\x10\xaf\r\x1a\x04\x98\xb5\x18\x01\x12\x37\n,MessageType_EthereumClearSignDefinitionChunk\x10\xb0\r\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_GetBip85Mnemonic\x10x\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_Bip85Mnemonic\x10y\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_RippleGetAddress\x10\x90\x03\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_RippleAddress\x10\x91\x03\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_RippleSignTx\x10\x92\x03\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_RippleSignedTx\x10\x93\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainGetAddress\x10\xf4\x03\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_ThorchainAddress\x10\xf5\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainSignTx\x10\xf6\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainMsgRequest\x10\xf7\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainMsgAck\x10\xf8\x03\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_ThorchainSignedTx\x10\xf9\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_EosGetPublicKey\x10\xd8\x04\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_EosPublicKey\x10\xd9\x04\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_EosSignTx\x10\xda\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_EosTxActionRequest\x10\xdb\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_EosTxActionAck\x10\xdc\x04\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_EosSignedTx\x10\xdd\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NanoGetAddress\x10\xbc\x05\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NanoAddress\x10\xbd\x05\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NanoSignTx\x10\xbe\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NanoSignedTx\x10\xbf\x05\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_SolanaGetAddress\x10\xee\x05\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\xef\x05\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\xf0\x05\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_SolanaSignedTx\x10\xf1\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_SolanaSignMessage\x10\xf2\x05\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_SolanaMessageSignature\x10\xf3\x05\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_SolanaSignOffchainMessage\x10\xf4\x05\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_SolanaOffchainMessageSignature\x10\xf5\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_BinanceGetAddress\x10\xa0\x06\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_BinanceAddress\x10\xa1\x06\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_BinanceGetPublicKey\x10\xa2\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinancePublicKey\x10\xa3\x06\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_BinanceSignTx\x10\xa4\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceTxRequest\x10\xa5\x06\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_BinanceTransferMsg\x10\xa6\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceOrderMsg\x10\xa7\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceCancelMsg\x10\xa8\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceSignedTx\x10\xa9\x06\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosGetAddress\x10\x84\x07\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_CosmosAddress\x10\x85\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosSignTx\x10\x86\x07\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRequest\x10\x87\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosMsgAck\x10\x88\x07\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CosmosSignedTx\x10\x89\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CosmosMsgDelegate\x10\x8a\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgUndelegate\x10\x8b\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgRedelegate\x10\x8c\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRewards\x10\x8d\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_CosmosMsgIBCTransfer\x10\x8e\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintGetAddress\x10\xe8\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintAddress\x10\xe9\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintSignTx\x10\xea\x07\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TendermintMsgRequest\x10\xeb\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintMsgAck\x10\xec\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintMsgSend\x10\xed\x07\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_TendermintSignedTx\x10\xee\x07\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_TendermintMsgDelegate\x10\xef\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgUndelegate\x10\xf0\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgRedelegate\x10\xf1\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintMsgRewards\x10\xf2\x07\x1a\x04\x98\xb5\x18\x01\x12/\n$MessageType_TendermintMsgIBCTransfer\x10\xf3\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisGetAddress\x10\xcc\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisAddress\x10\xcd\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisSignTx\x10\xce\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRequest\x10\xcf\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisMsgAck\x10\xd0\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSend\x10\xd1\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgDelegate\x10\xd2\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgUndelegate\x10\xd3\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgRedelegate\x10\xd4\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRewards\x10\xd5\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisMsgLPAdd\x10\xd6\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgLPRemove\x10\xd7\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgLPStake\x10\xd8\x08\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_OsmosisMsgLPUnstake\x10\xd9\x08\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_OsmosisMsgIBCTransfer\x10\xda\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSwap\x10\xdb\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisSignedTx\x10\xdc\x08\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_MayachainGetAddress\x10\xb0\t\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_MayachainAddress\x10\xb1\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainSignTx\x10\xb2\t\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_MayachainMsgRequest\x10\xb3\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainMsgAck\x10\xb4\t\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_MayachainSignedTx\x10\xb5\t\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_ZcashSignPCZT\x10\x94\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashPCZTAction\x10\x95\n\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_ZcashPCZTActionAck\x10\x96\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ZcashSignedPCZT\x10\x97\n\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_ZcashGetOrchardFVK\x10\x98\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashOrchardFVK\x10\x99\n\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ZcashTransparentInput\x10\x9a\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentSigned\x10\x9b\n\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_ZcashDisplayAddress\x10\x9c\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_ZcashAddress\x10\x9d\n\x1a\x04\x98\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentOutput\x10\x9e\n\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ZcashTransparentAck\x10\x9f\n\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TronGetAddress\x10\xf8\n\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TronAddress\x10\xf9\n\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_TronSignTx\x10\xfa\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_TronSignedTx\x10\xfb\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_TronSignMessage\x10\xfc\n\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TronMessageSignature\x10\xfd\n\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_TronVerifyMessage\x10\xfe\n\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TronSignTypedHash\x10\xff\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_TronTypedDataSignature\x10\x80\x0b\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_TonGetAddress\x10\xdc\x0b\x1a\x04\x90\xb5\x18\x01\x12!\n\x16MessageType_TonAddress\x10\xdd\x0b\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_TonSignTx\x10\xde\x0b\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TonSignedTx\x10\xdf\x0b\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TonSignMessage\x10\xe0\x0b\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_TonMessageSignature\x10\xe1\x0b\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_HiveGetPublicKey\x10\xc0\x0c\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_HivePublicKey\x10\xc1\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_HiveSignTx\x10\xc2\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_HiveSignedTx\x10\xc3\x0c\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_HiveGetPublicKeys\x10\xc4\x0c\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_HivePublicKeys\x10\xc5\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountCreate\x10\xc6\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountCreate\x10\xc7\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountUpdate\x10\xc8\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountUpdate\x10\xc9\x0c\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NearGetAddress\x10\xca\x0c\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NearAddress\x10\xcb\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NearSignTx\x10\xcc\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NearSignedTx\x10\xcd\x0c\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_HiveSignMessage\x10\xce\x0c\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_HiveSignedMessage\x10\xcf\x0c\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_HiveSignOperations\x10\xd0\x0c\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_HiveSignedOperations\x10\xd1\x0c\x1a\x04\x98\xb5\x18\x01\x12\x34\n)MessageType_ClearsignAttestorGetPublicKey\x10\xa4\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorPublicKey\x10\xa5\r\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ClearsignAttestorSign\x10\xa6\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorSignature\x10\xa7\r\x1a\x04\x98\xb5\x18\x01\x42,\n\x1a\x63om.keepkey.deviceprotocolB\x0eKeepKeyMessage') + serialized_pb=_b('\n\x0emessages.proto\x1a\x0btypes.proto\"\x0c\n\nInitialize\"\r\n\x0bGetFeatures\"\x8a\x05\n\x08\x46\x65\x61tures\x12\x0e\n\x06vendor\x18\x01 \x01(\t\x12\x15\n\rmajor_version\x18\x02 \x01(\r\x12\x15\n\rminor_version\x18\x03 \x01(\r\x12\x15\n\rpatch_version\x18\x04 \x01(\r\x12\x17\n\x0f\x62ootloader_mode\x18\x05 \x01(\x08\x12\x11\n\tdevice_id\x18\x06 \x01(\t\x12\x16\n\x0epin_protection\x18\x07 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x08 \x01(\x08\x12\x10\n\x08language\x18\t \x01(\t\x12\r\n\x05label\x18\n \x01(\t\x12\x18\n\x05\x63oins\x18\x0b \x03(\x0b\x32\t.CoinType\x12\x13\n\x0binitialized\x18\x0c \x01(\x08\x12\x10\n\x08revision\x18\r \x01(\x0c\x12\x17\n\x0f\x62ootloader_hash\x18\x0e \x01(\x0c\x12\x10\n\x08imported\x18\x0f \x01(\x08\x12\x12\n\npin_cached\x18\x10 \x01(\x08\x12\x19\n\x11passphrase_cached\x18\x11 \x01(\x08\x12\x1d\n\x08policies\x18\x12 \x03(\x0b\x32\x0b.PolicyType\x12\r\n\x05model\x18\x15 \x01(\t\x12\x18\n\x10\x66irmware_variant\x18\x16 \x01(\t\x12\x15\n\rfirmware_hash\x18\x17 \x01(\x0c\x12\x11\n\tno_backup\x18\x18 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x19 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x1a \x01(\r\x12\x18\n\x10supports_taproot\x18\x1b \x01(\x08\x12\x1b\n\x13supports_dice_modes\x18\x1c \x01(\x08\x12\'\n\x1fsupports_solana_lut_attestation\x18\x1d \x01(\x08\"*\n\x0cGetCoinTable\x12\r\n\x05start\x18\x01 \x01(\r\x12\x0b\n\x03\x65nd\x18\x02 \x01(\r\"L\n\tCoinTable\x12\x18\n\x05table\x18\x01 \x03(\x0b\x32\t.CoinType\x12\x11\n\tnum_coins\x18\x02 \x01(\r\x12\x12\n\nchunk_size\x18\x03 \x01(\r\"\x0e\n\x0c\x43learSession\"y\n\rApplySettings\x12\x10\n\x08language\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x16\n\x0euse_passphrase\x18\x03 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x04 \x01(\r\x12\x13\n\x0bu2f_counter\x18\x05 \x01(\r\"\x1b\n\tChangePin\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x87\x01\n\x04Ping\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x19\n\x11\x62utton_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x05 \x01(\x08\"\x1a\n\x07Success\x12\x0f\n\x07message\x18\x01 \x01(\t\"6\n\x07\x46\x61ilure\x12\x1a\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x0c.FailureType\x12\x0f\n\x07message\x18\x02 \x01(\t\"?\n\rButtonRequest\x12 \n\x04\x63ode\x18\x01 \x01(\x0e\x32\x12.ButtonRequestType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"\x0b\n\tButtonAck\"7\n\x10PinMatrixRequest\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.PinMatrixRequestType\"\x1b\n\x0cPinMatrixAck\x12\x0b\n\x03pin\x18\x01 \x02(\t\"\x08\n\x06\x43\x61ncel\"\x13\n\x11PassphraseRequest\"#\n\rPassphraseAck\x12\x12\n\npassphrase\x18\x01 \x02(\t\"\x1a\n\nGetEntropy\x12\x0c\n\x04size\x18\x01 \x02(\r\"\x1a\n\x07\x45ntropy\x12\x0f\n\x07\x65ntropy\x18\x01 \x02(\x0c\"\xa2\x01\n\x0cGetPublicKey\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x02 \x01(\t\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"4\n\tPublicKey\x12\x19\n\x04node\x18\x01 \x02(\x0b\x32\x0b.HDNodeType\x12\x0c\n\x04xpub\x18\x02 \x01(\t\"\xb3\x01\n\nGetAddress\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x1a\n\tcoin_name\x18\x02 \x01(\t:\x07\x42itcoin\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12+\n\x08multisig\x18\x04 \x01(\x0b\x32\x19.MultisigRedeemScriptType\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"\x1a\n\x07\x41\x64\x64ress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x02(\t\"\x0c\n\nWipeDevice\"\xbb\x01\n\nLoadDevice\x12\x10\n\x08mnemonic\x18\x01 \x01(\t\x12\x19\n\x04node\x18\x02 \x01(\x0b\x32\x0b.HDNodeType\x12\x0b\n\x03pin\x18\x03 \x01(\t\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x15\n\rskip_checksum\x18\x07 \x01(\x08\x12\x13\n\x0bu2f_counter\x18\x08 \x01(\r\"\x8a\x02\n\x0bResetDevice\x12\x16\n\x0e\x64isplay_random\x18\x01 \x01(\x08\x12\x15\n\x08strength\x18\x02 \x01(\r:\x03\x32\x35\x36\x12\x1d\n\x15passphrase_protection\x18\x03 \x01(\x08\x12\x16\n\x0epin_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x11\n\tno_backup\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x14\n\x0c\x64ice_entropy\x18\n \x01(\x08\x12\x11\n\tdice_only\x18\x0b \x01(\x08\"\x10\n\x0e\x45ntropyRequest\"\x1d\n\nEntropyAck\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\xff\x01\n\x0eRecoveryDevice\x12\x12\n\nword_count\x18\x01 \x01(\r\x12\x1d\n\x15passphrase_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x19\n\x08language\x18\x04 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x05 \x01(\t\x12\x18\n\x10\x65nforce_wordlist\x18\x06 \x01(\x08\x12\x1c\n\x14use_character_cipher\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x0f\n\x07\x64ry_run\x18\n \x01(\x08\"\r\n\x0bWordRequest\"\x17\n\x07WordAck\x12\x0c\n\x04word\x18\x01 \x02(\t\";\n\x10\x43haracterRequest\x12\x10\n\x08word_pos\x18\x01 \x02(\r\x12\x15\n\rcharacter_pos\x18\x02 \x02(\r\"?\n\x0c\x43haracterAck\x12\x11\n\tcharacter\x18\x01 \x01(\t\x12\x0e\n\x06\x64\x65lete\x18\x02 \x01(\x08\x12\x0c\n\x04\x64one\x18\x03 \x01(\x08\"\x82\x01\n\x0bSignMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0f\n\x07message\x18\x02 \x02(\x0c\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"`\n\rVerifyMessage\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\"6\n\x10MessageSignature\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"v\n\x0e\x45ncryptMessage\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64isplay_only\x18\x03 \x01(\x08\x12\x11\n\taddress_n\x18\x04 \x03(\r\x12\x1a\n\tcoin_name\x18\x05 \x01(\t:\x07\x42itcoin\"@\n\x10\x45ncryptedMessage\x12\r\n\x05nonce\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x0c\n\x04hmac\x18\x03 \x01(\x0c\"Q\n\x0e\x44\x65\x63ryptMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\r\n\x05nonce\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x0c\n\x04hmac\x18\x04 \x01(\x0c\"4\n\x10\x44\x65\x63ryptedMessage\x12\x0f\n\x07message\x18\x01 \x01(\x0c\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"\x8c\x01\n\x0e\x43ipherKeyValue\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\x12\x0f\n\x07\x65ncrypt\x18\x04 \x01(\x08\x12\x16\n\x0e\x61sk_on_encrypt\x18\x05 \x01(\x08\x12\x16\n\x0e\x61sk_on_decrypt\x18\x06 \x01(\x08\x12\n\n\x02iv\x18\x07 \x01(\x0c\"!\n\x10\x43ipheredKeyValue\x12\r\n\x05value\x18\x01 \x01(\x0c\"5\n\x10GetBip85Mnemonic\x12\x12\n\nword_count\x18\x01 \x02(\r\x12\r\n\x05index\x18\x02 \x02(\r\"!\n\rBip85Mnemonic\x12\x10\n\x08mnemonic\x18\x01 \x02(\t\"\xce\x01\n\x06SignTx\x12\x15\n\routputs_count\x18\x01 \x02(\r\x12\x14\n\x0cinputs_count\x18\x02 \x02(\r\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x12\n\x07version\x18\x04 \x01(\r:\x01\x31\x12\x14\n\tlock_time\x18\x05 \x01(\r:\x01\x30\x12\x0e\n\x06\x65xpiry\x18\x06 \x01(\r\x12\x14\n\x0coverwintered\x18\x07 \x01(\x08\x12\x18\n\x10version_group_id\x18\x08 \x01(\r\x12\x11\n\tbranch_id\x18\n \x01(\r\"\x85\x01\n\tTxRequest\x12\"\n\x0crequest_type\x18\x01 \x01(\x0e\x32\x0c.RequestType\x12&\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x15.TxRequestDetailsType\x12,\n\nserialized\x18\x03 \x01(\x0b\x32\x18.TxRequestSerializedType\"%\n\x05TxAck\x12\x1c\n\x02tx\x18\x01 \x01(\x0b\x32\x10.TransactionType\"+\n\x08RawTxAck\x12\x1f\n\x02tx\x18\x01 \x01(\x0b\x32\x13.RawTransactionType\"}\n\x0cSignIdentity\x12\x1f\n\x08identity\x18\x01 \x01(\x0b\x32\r.IdentityType\x12\x18\n\x10\x63hallenge_hidden\x18\x02 \x01(\x0c\x12\x18\n\x10\x63hallenge_visual\x18\x03 \x01(\t\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x04 \x01(\t\"H\n\x0eSignedIdentity\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\",\n\rApplyPolicies\x12\x1b\n\x06policy\x18\x01 \x03(\x0b\x32\x0b.PolicyType\"?\n\tFlashHash\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\x12\x11\n\tchallenge\x18\x03 \x01(\x0c\":\n\nFlashWrite\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\r\n\x05\x65rase\x18\x03 \x01(\x08\"!\n\x11\x46lashHashResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"5\n\x12\x44\x65\x62ugLinkFlashDump\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\"*\n\x1a\x44\x65\x62ugLinkFlashDumpResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x0b\n\tSoftReset\"\x0f\n\rFirmwareErase\"7\n\x0e\x46irmwareUpload\x12\x14\n\x0cpayload_hash\x18\x01 \x02(\x0c\x12\x0f\n\x07payload\x18\x02 \x02(\x0c\"2\n\x11\x44\x65\x62ugLinkDecision\x12\x0e\n\x06yes_no\x18\x01 \x02(\x08\x12\r\n\x05input\x18\x02 \x01(\t\"\x13\n\x11\x44\x65\x62ugLinkGetState\"\x99\x03\n\x0e\x44\x65\x62ugLinkState\x12\x0e\n\x06layout\x18\x01 \x01(\x0c\x12\x0b\n\x03pin\x18\x02 \x01(\t\x12\x0e\n\x06matrix\x18\x03 \x01(\t\x12\x10\n\x08mnemonic\x18\x04 \x01(\t\x12\x19\n\x04node\x18\x05 \x01(\x0b\x32\x0b.HDNodeType\x12\x1d\n\x15passphrase_protection\x18\x06 \x01(\x08\x12\x12\n\nreset_word\x18\x07 \x01(\t\x12\x15\n\rreset_entropy\x18\x08 \x01(\x0c\x12\x1a\n\x12recovery_fake_word\x18\t \x01(\t\x12\x19\n\x11recovery_word_pos\x18\n \x01(\r\x12\x17\n\x0frecovery_cipher\x18\x0b \x01(\t\x12$\n\x1crecovery_auto_completed_word\x18\x0c \x01(\t\x12\x15\n\rfirmware_hash\x18\r \x01(\x0c\x12\x14\n\x0cstorage_hash\x18\x0e \x01(\x0c\x12\x13\n\x0b\x64ice_digest\x18\x0f \x01(\x0c\x12\x15\n\rconfirm_title\x18\x10 \x01(\t\x12\x14\n\x0c\x63onfirm_body\x18\x11 \x01(\t\"\x0f\n\rDebugLinkStop\";\n\x0c\x44\x65\x62ugLinkLog\x12\r\n\x05level\x18\x01 \x01(\r\x12\x0e\n\x06\x62ucket\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\"\x15\n\x13\x44\x65\x62ugLinkFillConfig\" \n\x0e\x43hangeWipeCode\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x1f\n\x1d\x43learsignAttestorGetPublicKey\"0\n\x1a\x43learsignAttestorPublicKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"(\n\x15\x43learsignAttestorSign\x12\x0f\n\x07payload\x18\x01 \x01(\x0c\"C\n\x1a\x43learsignAttestorSignature\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x12\n\npublic_key\x18\x02 \x01(\x0c*\xc3\x46\n\x0bMessageType\x12 \n\x16MessageType_Initialize\x10\x00\x1a\x04\x90\xb5\x18\x01\x12\x1a\n\x10MessageType_Ping\x10\x01\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Success\x10\x02\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_Failure\x10\x03\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ChangePin\x10\x04\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_WipeDevice\x10\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_FirmwareErase\x10\x06\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_FirmwareUpload\x10\x07\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetEntropy\x10\t\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Entropy\x10\n\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_GetPublicKey\x10\x0b\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_PublicKey\x10\x0c\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_LoadDevice\x10\r\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_ResetDevice\x10\x0e\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_SignTx\x10\x0f\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_Features\x10\x11\x1a\x04\x98\xb5\x18\x01\x12&\n\x1cMessageType_PinMatrixRequest\x10\x12\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_PinMatrixAck\x10\x13\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_Cancel\x10\x14\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_TxRequest\x10\x15\x1a\x04\x98\xb5\x18\x01\x12\x1b\n\x11MessageType_TxAck\x10\x16\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_CipherKeyValue\x10\x17\x1a\x04\x90\xb5\x18\x01\x12\"\n\x18MessageType_ClearSession\x10\x18\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplySettings\x10\x19\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ButtonRequest\x10\x1a\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ButtonAck\x10\x1b\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetAddress\x10\x1d\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Address\x10\x1e\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EntropyRequest\x10#\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_EntropyAck\x10$\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_SignMessage\x10&\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_VerifyMessage\x10\'\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_MessageSignature\x10(\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1dMessageType_PassphraseRequest\x10)\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_PassphraseAck\x10*\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_RecoveryDevice\x10-\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_WordRequest\x10.\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_WordAck\x10/\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CipheredKeyValue\x10\x30\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EncryptMessage\x10\x31\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_EncryptedMessage\x10\x32\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_DecryptMessage\x10\x33\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_DecryptedMessage\x10\x34\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_SignIdentity\x10\x35\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_SignedIdentity\x10\x36\x1a\x04\x98\xb5\x18\x01\x12!\n\x17MessageType_GetFeatures\x10\x37\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumGetAddress\x10\x38\x1a\x04\x90\xb5\x18\x01\x12%\n\x1bMessageType_EthereumAddress\x10\x39\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EthereumSignTx\x10:\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_EthereumTxRequest\x10;\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_EthereumTxAck\x10<\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CharacterRequest\x10P\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_CharacterAck\x10Q\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_RawTxAck\x10R\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplyPolicies\x10S\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_FlashHash\x10T\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_FlashWrite\x10U\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_FlashHashResponse\x10V\x1a\x04\x98\xb5\x18\x01\x12(\n\x1eMessageType_DebugLinkFlashDump\x10W\x1a\x04\xa0\xb5\x18\x01\x12\x30\n&MessageType_DebugLinkFlashDumpResponse\x10X\x1a\x04\xa8\xb5\x18\x01\x12\x1f\n\x15MessageType_SoftReset\x10Y\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkDecision\x10\x64\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkGetState\x10\x65\x1a\x04\xa0\xb5\x18\x01\x12$\n\x1aMessageType_DebugLinkState\x10\x66\x1a\x04\xa8\xb5\x18\x01\x12#\n\x19MessageType_DebugLinkStop\x10g\x1a\x04\xa0\xb5\x18\x01\x12\"\n\x18MessageType_DebugLinkLog\x10h\x1a\x04\xa8\xb5\x18\x01\x12)\n\x1fMessageType_DebugLinkFillConfig\x10i\x1a\x04\xa8\xb5\x18\x01\x12\"\n\x18MessageType_GetCoinTable\x10j\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_CoinTable\x10k\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_EthereumSignMessage\x10l\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumVerifyMessage\x10m\x1a\x04\x90\xb5\x18\x01\x12.\n$MessageType_EthereumMessageSignature\x10n\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_ChangeWipeCode\x10o\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumSignTypedHash\x10p\x1a\x04\x90\xb5\x18\x01\x12\x30\n&MessageType_EthereumTypedDataSignature\x10q\x1a\x04\x98\xb5\x18\x01\x12,\n\"MessageType_Ethereum712TypesValues\x10r\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumTxMetadata\x10s\x1a\x04\x90\xb5\x18\x01\x12)\n\x1fMessageType_EthereumMetadataAck\x10t\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_LoadClearsignSigner\x10u\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedData\x10\xa8\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumTypedDataStructRequest\x10\xa9\r\x1a\x04\x98\xb5\x18\x01\x12\x31\n&MessageType_EthereumTypedDataStructAck\x10\xaa\r\x1a\x04\x90\xb5\x18\x01\x12\x34\n)MessageType_EthereumTypedDataValueRequest\x10\xab\r\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_EthereumTypedDataValueAck\x10\xac\r\x1a\x04\x90\xb5\x18\x01\x12\x32\n\'MessageType_EthereumClearSignDefinition\x10\xad\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumClearSignDefinitionAck\x10\xae\r\x1a\x04\x98\xb5\x18\x01\x12\x39\n.MessageType_EthereumClearSignDefinitionRequest\x10\xaf\r\x1a\x04\x98\xb5\x18\x01\x12\x37\n,MessageType_EthereumClearSignDefinitionChunk\x10\xb0\r\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_GetBip85Mnemonic\x10x\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_Bip85Mnemonic\x10y\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_RippleGetAddress\x10\x90\x03\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_RippleAddress\x10\x91\x03\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_RippleSignTx\x10\x92\x03\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_RippleSignedTx\x10\x93\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainGetAddress\x10\xf4\x03\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_ThorchainAddress\x10\xf5\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainSignTx\x10\xf6\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainMsgRequest\x10\xf7\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainMsgAck\x10\xf8\x03\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_ThorchainSignedTx\x10\xf9\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_EosGetPublicKey\x10\xd8\x04\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_EosPublicKey\x10\xd9\x04\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_EosSignTx\x10\xda\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_EosTxActionRequest\x10\xdb\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_EosTxActionAck\x10\xdc\x04\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_EosSignedTx\x10\xdd\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NanoGetAddress\x10\xbc\x05\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NanoAddress\x10\xbd\x05\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NanoSignTx\x10\xbe\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NanoSignedTx\x10\xbf\x05\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_SolanaGetAddress\x10\xee\x05\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\xef\x05\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\xf0\x05\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_SolanaSignedTx\x10\xf1\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_SolanaSignMessage\x10\xf2\x05\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_SolanaMessageSignature\x10\xf3\x05\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_SolanaSignOffchainMessage\x10\xf4\x05\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_SolanaOffchainMessageSignature\x10\xf5\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_BinanceGetAddress\x10\xa0\x06\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_BinanceAddress\x10\xa1\x06\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_BinanceGetPublicKey\x10\xa2\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinancePublicKey\x10\xa3\x06\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_BinanceSignTx\x10\xa4\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceTxRequest\x10\xa5\x06\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_BinanceTransferMsg\x10\xa6\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceOrderMsg\x10\xa7\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceCancelMsg\x10\xa8\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceSignedTx\x10\xa9\x06\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosGetAddress\x10\x84\x07\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_CosmosAddress\x10\x85\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosSignTx\x10\x86\x07\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRequest\x10\x87\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosMsgAck\x10\x88\x07\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CosmosSignedTx\x10\x89\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CosmosMsgDelegate\x10\x8a\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgUndelegate\x10\x8b\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgRedelegate\x10\x8c\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRewards\x10\x8d\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_CosmosMsgIBCTransfer\x10\x8e\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintGetAddress\x10\xe8\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintAddress\x10\xe9\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintSignTx\x10\xea\x07\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TendermintMsgRequest\x10\xeb\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintMsgAck\x10\xec\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintMsgSend\x10\xed\x07\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_TendermintSignedTx\x10\xee\x07\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_TendermintMsgDelegate\x10\xef\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgUndelegate\x10\xf0\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgRedelegate\x10\xf1\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintMsgRewards\x10\xf2\x07\x1a\x04\x98\xb5\x18\x01\x12/\n$MessageType_TendermintMsgIBCTransfer\x10\xf3\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisGetAddress\x10\xcc\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisAddress\x10\xcd\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisSignTx\x10\xce\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRequest\x10\xcf\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisMsgAck\x10\xd0\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSend\x10\xd1\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgDelegate\x10\xd2\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgUndelegate\x10\xd3\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgRedelegate\x10\xd4\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRewards\x10\xd5\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisMsgLPAdd\x10\xd6\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgLPRemove\x10\xd7\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgLPStake\x10\xd8\x08\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_OsmosisMsgLPUnstake\x10\xd9\x08\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_OsmosisMsgIBCTransfer\x10\xda\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSwap\x10\xdb\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisSignedTx\x10\xdc\x08\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_MayachainGetAddress\x10\xb0\t\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_MayachainAddress\x10\xb1\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainSignTx\x10\xb2\t\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_MayachainMsgRequest\x10\xb3\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainMsgAck\x10\xb4\t\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_MayachainSignedTx\x10\xb5\t\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_ZcashSignPCZT\x10\x94\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashPCZTAction\x10\x95\n\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_ZcashPCZTActionAck\x10\x96\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ZcashSignedPCZT\x10\x97\n\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_ZcashGetOrchardFVK\x10\x98\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashOrchardFVK\x10\x99\n\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ZcashTransparentInput\x10\x9a\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentSigned\x10\x9b\n\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_ZcashDisplayAddress\x10\x9c\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_ZcashAddress\x10\x9d\n\x1a\x04\x98\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentOutput\x10\x9e\n\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ZcashTransparentAck\x10\x9f\n\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TronGetAddress\x10\xf8\n\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TronAddress\x10\xf9\n\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_TronSignTx\x10\xfa\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_TronSignedTx\x10\xfb\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_TronSignMessage\x10\xfc\n\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TronMessageSignature\x10\xfd\n\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_TronVerifyMessage\x10\xfe\n\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TronSignTypedHash\x10\xff\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_TronTypedDataSignature\x10\x80\x0b\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_TonGetAddress\x10\xdc\x0b\x1a\x04\x90\xb5\x18\x01\x12!\n\x16MessageType_TonAddress\x10\xdd\x0b\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_TonSignTx\x10\xde\x0b\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TonSignedTx\x10\xdf\x0b\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TonSignMessage\x10\xe0\x0b\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_TonMessageSignature\x10\xe1\x0b\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_HiveGetPublicKey\x10\xc0\x0c\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_HivePublicKey\x10\xc1\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_HiveSignTx\x10\xc2\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_HiveSignedTx\x10\xc3\x0c\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_HiveGetPublicKeys\x10\xc4\x0c\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_HivePublicKeys\x10\xc5\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountCreate\x10\xc6\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountCreate\x10\xc7\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountUpdate\x10\xc8\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountUpdate\x10\xc9\x0c\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NearGetAddress\x10\xca\x0c\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NearAddress\x10\xcb\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NearSignTx\x10\xcc\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NearSignedTx\x10\xcd\x0c\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_HiveSignMessage\x10\xce\x0c\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_HiveSignedMessage\x10\xcf\x0c\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_HiveSignOperations\x10\xd0\x0c\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_HiveSignedOperations\x10\xd1\x0c\x1a\x04\x98\xb5\x18\x01\x12\x34\n)MessageType_ClearsignAttestorGetPublicKey\x10\xa4\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorPublicKey\x10\xa5\r\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ClearsignAttestorSign\x10\xa6\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorSignature\x10\xa7\r\x1a\x04\x98\xb5\x18\x01\x42,\n\x1a\x63om.keepkey.deviceprotocolB\x0eKeepKeyMessage') , dependencies=[types__pb2.DESCRIPTOR,]) @@ -926,8 +926,8 @@ ], containing_type=None, options=None, - serialized_start=5558, - serialized_end=14585, + serialized_start=5603, + serialized_end=14630, ) _sym_db.RegisterEnumDescriptor(_MESSAGETYPE) @@ -3969,6 +3969,20 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='confirm_title', full_name='DebugLinkState.confirm_title', index=15, + number=16, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='confirm_body', full_name='DebugLinkState.confirm_body', index=16, + number=17, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -3982,7 +3996,7 @@ oneofs=[ ], serialized_start=4862, - serialized_end=5226, + serialized_end=5271, ) @@ -4005,8 +4019,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5228, - serialized_end=5243, + serialized_start=5273, + serialized_end=5288, ) @@ -4050,8 +4064,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5245, - serialized_end=5304, + serialized_start=5290, + serialized_end=5349, ) @@ -4074,8 +4088,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5306, - serialized_end=5327, + serialized_start=5351, + serialized_end=5372, ) @@ -4105,8 +4119,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5329, - serialized_end=5361, + serialized_start=5374, + serialized_end=5406, ) @@ -4129,8 +4143,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5363, - serialized_end=5394, + serialized_start=5408, + serialized_end=5439, ) @@ -4160,8 +4174,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5396, - serialized_end=5444, + serialized_start=5441, + serialized_end=5489, ) @@ -4191,8 +4205,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5446, - serialized_end=5486, + serialized_start=5491, + serialized_end=5531, ) @@ -4229,8 +4243,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5488, - serialized_end=5555, + serialized_start=5533, + serialized_end=5600, ) _FEATURES.fields_by_name['coins'].message_type = types__pb2._COINTYPE diff --git a/tests/test_erc7730_compiler.py b/tests/test_erc7730_compiler.py index 7f44302e..c6219af5 100644 --- a/tests/test_erc7730_compiler.py +++ b/tests/test_erc7730_compiler.py @@ -10,8 +10,9 @@ import pytest from keepkeylib.erc7730_compiler import ( - HEADER_SIZE, compile_calldata, compile_eip712, eip712_encode_type, - load_descriptor, parse_function_signature, + HEADER_SIZE, DeviceCannotExecute, compile_calldata, compile_eip712, + device_refusal, eip712_encode_type, load_descriptor, + parse_function_signature, ) from keepkeylib.signed_metadata import keccak256 @@ -34,12 +35,26 @@ def _evidence_input(name): pytest.skip(message) -def _firmware_validate(program): +def _firmware_accepts(program): validator = _evidence_input("ERC7730_FIRMWARE_VALIDATOR") with tempfile.NamedTemporaryFile() as compiled: compiled.write(program) compiled.flush() - subprocess.check_call([validator, compiled.name]) + return subprocess.call([validator, compiled.name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) == 0 + + +def _unchecked(compile, *args, **kwargs): + return compile(*args, executable_only=False, **kwargs) + + +def _firmware_validate(program, refusal=None): + """The device verifier accepts `program` exactly when the compiler's + capability mirror does, and `refusal` names the expected reason (None + for a program the device executes).""" + assert device_refusal(program) == refusal + assert _firmware_accepts(program) == (refusal is None) def _sections(program): @@ -137,8 +152,8 @@ def test_compiles_deterministic_canonical_calldata_program(): ], network_records=[(1, "Ethereum", "ETH", 18)], ) - first = compile_calldata(descriptor, **kwargs) - second = compile_calldata(descriptor, **kwargs) + first = _unchecked(compile_calldata, descriptor, **kwargs) + second = _unchecked(compile_calldata, descriptor, **kwargs) assert first == second assert first[:4] == b"C773" assert first[4:8] == bytes([1, 2, 0, 1]) @@ -153,7 +168,8 @@ def test_compiles_deterministic_canonical_calldata_program(): assert set(sections) == {1, 2, 3, 6, 7, 8, 9} assert hashlib.sha256(first).digest() == hashlib.sha256(second).digest() assert len(first) < 16384 - _firmware_validate(first) + _firmware_validate(first, + "formatter kind 3 is not executed") def test_compiles_official_uniswap_tuple_fixture_through_firmware(): @@ -171,7 +187,7 @@ def test_compiles_official_uniswap_tuple_fixture_through_firmware(): "exactInputSingle((address tokenIn, address tokenOut, uint24 fee, " "address recipient, uint256 amountIn, uint256 amountOutMinimum, " "uint160 sqrtPriceLimitX96) params)") - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, signature, 1, "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45", token_records=[ @@ -187,7 +203,8 @@ def test_compiles_official_uniswap_tuple_fixture_through_firmware(): assert calldata[:4] == compiled[38:42] assert fixtures[1]["txHash"] == ( "0xb25281abb3e6bbfe18c746187522c2e915aa02fdb8175082005340e00c1f0b30") - _firmware_validate(compiled) + _firmware_validate(compiled, + "formatter kind 3 is not executed") def test_compiles_and_checks_exact_keepkey_sdk_thorchain_swap(): @@ -209,11 +226,12 @@ def test_compiles_and_checks_exact_keepkey_sdk_thorchain_swap(): assert memo == expected["memo"] assert int(fixture["value"], 16) == int(expected["amount"]) - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, fixture["descriptor"], fixture["signature"], fixture["chainId"], fixture["to"], network_records=[(1, "Ethereum", "ETH", 18)]) assert compiled[38:42].hex() == expected["selector"] - _firmware_validate(compiled) + _firmware_validate(compiled, + "formatter kind 10 is not executed") def test_compiles_array_iteration_separator_and_optional_visibility(): @@ -231,7 +249,7 @@ def test_compiles_array_iteration_separator_and_optional_visibility(): } }} } - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "batch((address recipient,uint256 amount)[] items)", 1, "0x1111111111111111111111111111111111111111") @@ -245,7 +263,8 @@ def test_compiles_array_iteration_separator_and_optional_visibility(): end = display[26:34] assert int.from_bytes(begin[6:8], "big") == 3 assert int.from_bytes(end[2:4], "big") == 1 - _firmware_validate(compiled) + _firmware_validate(compiled, + "path step opcode 2 is not executed") def test_refuses_nested_array_iteration_the_device_cannot_verify(): @@ -273,7 +292,7 @@ def test_token_amount_without_token_uses_firmware_raw_fallback(): "format": "tokenAmount"}], } }}} - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "quote(uint256 amount)", 1, "0x1111111111111111111111111111111111111111") # With no token named the device can only show the raw integer, and its @@ -298,7 +317,7 @@ def test_compiles_typed_if_not_in_and_must_match_conditions(): ], } }}} - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "guard(uint256 mode,address recipient)", 1, "0x1111111111111111111111111111111111111111") sections = _sections(compiled) @@ -308,7 +327,8 @@ def test_compiles_typed_if_not_in_and_must_match_conditions(): assert conditions[10] == 8 literals = sections[4] assert int.from_bytes(literals[:2], "big") == 5 - _firmware_validate(compiled) + _firmware_validate(compiled, + "display conditions are not executed") def test_compiles_interpolated_intent_and_metadata_enum(): @@ -328,7 +348,7 @@ def test_compiles_interpolated_intent_and_metadata_enum(): } }} } - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "swap(bool selling,uint256 amount)", 1, "0x1111111111111111111111111111111111111111") sections = _sections(compiled) @@ -341,7 +361,8 @@ def test_compiles_interpolated_intent_and_metadata_enum(): assert formatters[2] == 8 literals = sections[4] assert int.from_bytes(literals[:2], "big") == 3 - _firmware_validate(compiled) + _firmware_validate(compiled, + "formatter kind 8 is not executed") def test_compiles_nested_field_group_with_balanced_links(): @@ -357,7 +378,7 @@ def test_compiles_nested_field_group_with_balanced_links(): }], } }}} - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "act((address owner,uint256 amount) details)", 1, "0x1111111111111111111111111111111111111111") display = _sections(compiled)[7] @@ -366,7 +387,8 @@ def test_compiles_nested_field_group_with_balanced_links(): assert [item[0] for item in instructions] == [1, 5, 4, 4, 6, 10] assert int.from_bytes(instructions[1][6:8], "big") == 4 assert int.from_bytes(instructions[4][2:4], "big") == 1 - _firmware_validate(compiled) + _firmware_validate(compiled, + "formatter kind 10 is not executed") def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): @@ -394,14 +416,15 @@ def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): path = tmp_path / "descriptor.json" path.write_text(json.dumps(descriptor)) loaded = load_descriptor(str(path), str(tmp_path)) - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, loaded, "batch((address to,uint256 amount)[] items)", 1, "0x1111111111111111111111111111111111111111") display = _sections(compiled)[7] count = int.from_bytes(display[:2], "big") opcodes = [display[2 + i * 8] for i in range(count)] assert opcodes == [1, 7, 5, 4, 4, 6, 8, 10] - _firmware_validate(compiled) + _firmware_validate(compiled, + "path step opcode 2 is not executed") DEVICE_LIMITS = ( @@ -410,12 +433,21 @@ def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): ) +# Formats the device can fully sign with this firmware's capability table. +# Each later phase of the ERC-7730 formatter plan raises this number +# (docs/security/HANDOFF-ERC7730-715-FORMATTERS.md in keepkey-firmware). +# The plan estimated 94; two 1inch increaseEpoch formats show a raw field read +# from a container path (@.from), which the runtime does not capture. +REGISTRY_SIGNABLE = 92 + + def test_official_registry_all_calldata_formats_reach_firmware(): registry = _evidence_input("ERC7730_REGISTRY") - validator = _evidence_input("ERC7730_FIRMWARE_VALIDATOR") + _evidence_input("ERC7730_FIRMWARE_VALIDATOR") import glob failures = [] unsupported = [] + signable = 0 checked = 0 for path in glob.glob(os.path.join( registry, "registry", "**", "calldata-*.json"), recursive=True): @@ -432,20 +464,22 @@ def test_official_registry_all_calldata_formats_reach_firmware(): checked += 1 try: try: - program = compile_calldata( - descriptor, signature, deployment["chainId"], - deployment["address"]) + program = _unchecked( + compile_calldata, descriptor, signature, + deployment["chainId"], deployment["address"]) except ValueError as exc: if any(reason in str(exc) for reason in DEVICE_LIMITS): unsupported.append(signature) continue raise - with tempfile.NamedTemporaryFile() as output: - output.write(program) - output.flush() - subprocess.check_call( - [validator, output.name], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + # The device verifier and the compiler's capability mirror + # must agree on every format, in both directions. + executable = device_refusal(program) is None + if _firmware_accepts(program) != executable: + raise AssertionError( + "device and compiler disagree: %r" % + device_refusal(program)) + signable += executable except Exception as exc: failures.append("%s :: %s :: %s" % ( os.path.basename(path), signature, exc)) @@ -454,6 +488,8 @@ def test_official_registry_all_calldata_formats_reach_firmware(): # Every other format is refused by the compiler for a named device limit: # 8 iterate nested arrays, 2 nest their ABI deeper than 8 levels. assert len(unsupported) == 10 + # Passing the parser is not signability: only these run end to end. + assert signable == REGISTRY_SIGNABLE def test_compiles_official_uniswap_eip712_fixture_through_firmware(): @@ -468,7 +504,7 @@ def test_compiles_official_uniswap_eip712_fixture_through_firmware(): assert encoded == ( "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)" "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)") - compiled = compile_eip712( + compiled = _unchecked(compile_eip712, descriptor, fixture, token_records=[ (1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "USDC", 6) @@ -481,4 +517,5 @@ def test_compiles_official_uniswap_eip712_fixture_through_firmware(): binding = sections[8] # deployment + name/chain/contract domain facts + token + network assert int.from_bytes(binding[:2], "big") == 6 - _firmware_validate(compiled) + _firmware_validate(compiled, + "formatter kind 3 is not executed") From ce14a489fb59dc2a4818d6cff2dd2a3347cd7939 Mon Sep 17 00:00:00 2001 From: highlander Date: Fri, 25 Sep 2026 03:26:46 -0500 Subject: [PATCH 11/11] fix(erc7730): mirror required roles, containers and step encodings exactly The capability mirror treated every permitted role as required, walked non-value paths against the ABI and read an index after every step opcode. None of that could fire under the Phase 0 table, but each would misfire as soon as the table widens. Separate required roles, list executable containers, and parse whole-array and slice steps as the firmware does. --- keepkeylib/erc7730_compiler.py | 72 ++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/keepkeylib/erc7730_compiler.py b/keepkeylib/erc7730_compiler.py index 458b22f4..939298d4 100644 --- a/keepkeylib/erc7730_compiler.py +++ b/keepkeylib/erc7730_compiler.py @@ -973,9 +973,10 @@ def depth(node): # together with the firmware table. DEVICE_CAPABILITIES = { "display_opcodes": frozenset((1, 4, 10)), - # formatter kind -> {argument role -> permitted sources} - "formatters": {1: {1: frozenset((1,))}}, + # formatter kind -> (argument role -> permitted sources, required roles) + "formatters": {1: ({1: frozenset((1,))}, frozenset((1,)))}, "path_sources": frozenset((1,)), + "containers": frozenset(), "path_step_opcodes": frozenset((1,)), "conditions": False, } @@ -997,6 +998,27 @@ def _program_sections(program): return sections +def _abi_walk(nodes, steps): + """Mirror of the firmware's preload walk; steps are (opcode, index).""" + node = 0 + for opcode, index in steps: + if node >= len(nodes): + return "path leaves the ABI" + kind, _, first, count, length = nodes[node] + if kind == 8 and opcode == 1 and count and 0 <= index < count: + node = first + index + elif kind == 9 and opcode in (1, 2): + limit = MAX_ARRAY_ELEMENTS if length == ABSENT else length + if not -limit <= index < limit: + return "path indexes beyond the array" + node = first + else: + return "path does not name an ABI member" + if node >= len(nodes) or nodes[node][0] > 7: + return "path does not end at a value" + return None + + def device_refusal(program, capabilities=DEVICE_CAPABILITIES): """Why the device would refuse `program` at preload, or None.""" sections = _program_sections(program) @@ -1006,43 +1028,33 @@ def device_refusal(program, capabilities=DEVICE_CAPABILITIES): nodes = [struct.unpack(">BHHHH", abi[2 + 9 * i:11 + 9 * i]) for i in range(u16(abi, 0))] - def walk(steps): - node = 0 - for step in steps: - if node >= len(nodes): - return "path leaves the ABI" - kind, _, first, count, length = nodes[node] - if kind == 8 and count and 0 <= step < count: - node = first + step - elif kind == 9: - limit = MAX_ARRAY_ELEMENTS if length == ABSENT else length - if not -limit <= step < limit: - return "path indexes beyond the array" - node = first - else: - return "path does not name an ABI member" - if node >= len(nodes) or nodes[node][0] > 7: - return "path does not end at a value" - return None - paths = sections.get(3, b"\0\0") at = 2 for _ in range(u16(paths, 0)): - source, count = paths[at], paths[at + 1] + source, count, index = paths[at], paths[at + 1], u16(paths, at + 2) at += 4 if source not in capabilities["path_sources"]: return "path source %d is not executed" % source + if source == 2 and index not in capabilities["containers"]: + return "container %d is not executed" % index steps = [] for _ in range(count): opcode = paths[at] at += 1 if opcode not in capabilities["path_step_opcodes"]: return "path step opcode %d is not executed" % opcode - steps.append(struct.unpack(">i", paths[at:at + 4])[0]) - at += 4 - reason = walk(steps) - if reason: - return reason + if opcode == 1: + steps.append((1, struct.unpack(">i", paths[at:at + 4])[0])) + at += 4 + elif opcode == 2: + steps.append((2, 0)) + else: + flags = paths[at] + at += 1 + 4 * bin(flags).count("1") + if source == 1: + reason = _abi_walk(nodes, steps) + if reason: + return reason conditions = sections.get(5, b"\0\0") if u16(conditions, 0) and not capabilities["conditions"]: @@ -1053,9 +1065,9 @@ def walk(steps): for _ in range(u16(formatters, 0)): kind, argc = formatters[at], formatters[at + 2] at += 3 - roles = capabilities["formatters"].get(kind) - if roles is None: + if kind not in capabilities["formatters"]: return "formatter kind %d is not executed" % kind + roles, required = capabilities["formatters"][kind] seen = set() for _ in range(argc): role, source = formatters[at], formatters[at + 1] @@ -1064,7 +1076,7 @@ def walk(steps): return "formatter kind %d argument role %d is not executed" % ( kind, role) seen.add(role) - if not set(roles) <= seen: + if not required <= seen: return "formatter kind %d lacks a required argument" % kind displays = sections.get(7, b"\0\0")