From 19e73c87c48c014cfe25642a00e60bc19754bebb Mon Sep 17 00:00:00 2001 From: highlander Date: Fri, 21 Aug 2026 13:46:26 -0500 Subject: [PATCH] feat(solana): KKSOLSW1 lookup-table account attestation tests Four tests for the transaction-bound attestation that upgrades an ALT transaction from a blind sign to a described one. The positive test asserts the SCREEN COUNT, not just that signing succeeded, and that is the whole reason it was worth writing: it caught a real firmware bug on first run. nanopb hands each repeated `bytes` element back as a {size, bytes[32]} struct, and the firmware was casting the array to (uint8_t(*)[32]) -- hashing the size word plus 28 bytes of the first key. The signature never verified. A test that only checked "did it sign" would have passed, because the fallback path signs perfectly well. test_attested_accounts_are_shown_and_blind_sign_still_follows base flow + 1 identity screen + 1 per account, and the baseline codes still present at the TAIL -- the additive invariant, restated for Solana test_bad_signature_degrades_to_todays_flow an unverifiable attestation adds nothing and refuses nothing test_attestation_does_not_replay_onto_another_transaction the same signature against a perturbed tx describes nothing test_no_signer_loaded_means_no_extra_screens trust is opt-in per session; without a provider the payload is inert Note for whoever extends these: the three negative cases pass trivially if the signer is not loaded, because "no extra screens" is also what a broken positive path produces. Only the count assertion in the first test distinguishes them. That is why it exists, and why it should not be relaxed into "signature is 64 bytes". messages_solana_pb2.py regenerated for the new tags. It must be generated with a protoc whose output the pinned protobuf==3.20.3 runtime accepts -- the system protoc emits code importing google.protobuf.runtime_version, which 3.20 does not have. grpcio-tools==1.48.2 produces compatible output. --- keepkeylib/messages_solana_pb2.py | 572 +++-------------------- tests/test_msg_solana_lut_attestation.py | 215 +++++++++ 2 files changed, 286 insertions(+), 501 deletions(-) create mode 100644 tests/test_msg_solana_lut_attestation.py diff --git a/keepkeylib/messages_solana_pb2.py b/keepkeylib/messages_solana_pb2.py index dfdd0674..d1a7d18d 100644 --- a/keepkeylib/messages_solana_pb2.py +++ b/keepkeylib/messages_solana_pb2.py @@ -1,13 +1,12 @@ +# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: messages-solana.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +"""Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -15,531 +14,102 @@ -DESCRIPTOR = _descriptor.FileDescriptor( - name='messages-solana.proto', - package='', - syntax='proto2', - serialized_pb=_b('\n\x15messages-solana.proto\"V\n\x10SolanaGetAddress\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\" \n\rSolanaAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\"k\n\x0fSolanaTokenInfo\x12\x0c\n\x04mint\x18\x01 \x01(\x0c\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x10\n\x08\x64\x65\x63imals\x18\x03 \x01(\r\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x15\n\rsigner_key_id\x18\x05 \x01(\r\"\xe7\x01\n\x0cSolanaSignTx\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x0e\n\x06raw_tx\x18\x03 \x01(\x0c\x12$\n\ntoken_info\x18\x04 \x03(\x0b\x32\x10.SolanaTokenInfo\x12\x16\n\x0eschema_payload\x18\t \x01(\x0c\x12\x18\n\x10schema_signature\x18\n \x01(\x0c\x12\x1c\n\x14schema_signer_key_id\x18\x0b \x01(\r\x12\x1d\n\x15token_recipient_owner\x18\x0c \x03(\x0cJ\x04\x08\x05\x10\t\"#\n\x0eSolanaSignedTx\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"h\n\x11SolanaSignMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x14\n\x0cshow_display\x18\x04 \x01(\x08\"?\n\x16SolanaMessageSignature\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"\x9c\x01\n\x19SolanaSignOffchainMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x12\n\x07version\x18\x03 \x01(\r:\x01\x30\x12\x16\n\x0emessage_format\x18\x04 \x01(\r\x12\x0f\n\x07message\x18\x05 \x01(\x0c\x12\x14\n\x0cshow_display\x18\x06 \x01(\x08\"G\n\x1eSolanaOffchainMessageSignature\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x42\x32\n\x1a\x63om.keepkey.deviceprotocolB\x14KeepKeyMessageSolana') -) - - - - -_SOLANAGETADDRESS = _descriptor.Descriptor( - name='SolanaGetAddress', - full_name='SolanaGetAddress', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address_n', full_name='SolanaGetAddress.address_n', index=0, - number=1, type=13, cpp_type=3, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='coin_name', full_name='SolanaGetAddress.coin_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=_b("Solana").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='show_display', full_name='SolanaGetAddress.show_display', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=25, - serialized_end=111, -) - - -_SOLANAADDRESS = _descriptor.Descriptor( - name='SolanaAddress', - full_name='SolanaAddress', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address', full_name='SolanaAddress.address', index=0, - number=1, 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=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=113, - serialized_end=145, -) - - -_SOLANATOKENINFO = _descriptor.Descriptor( - name='SolanaTokenInfo', - full_name='SolanaTokenInfo', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='mint', full_name='SolanaTokenInfo.mint', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='symbol', full_name='SolanaTokenInfo.symbol', index=1, - number=2, 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='decimals', full_name='SolanaTokenInfo.decimals', index=2, - number=3, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='signature', full_name='SolanaTokenInfo.signature', index=3, - number=4, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='signer_key_id', full_name='SolanaTokenInfo.signer_key_id', index=4, - number=5, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=147, - serialized_end=254, -) - - -_SOLANASIGNTX = _descriptor.Descriptor( - name='SolanaSignTx', - full_name='SolanaSignTx', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address_n', full_name='SolanaSignTx.address_n', index=0, - number=1, type=13, cpp_type=3, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='coin_name', full_name='SolanaSignTx.coin_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=_b("Solana").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='raw_tx', full_name='SolanaSignTx.raw_tx', index=2, - number=3, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='token_info', full_name='SolanaSignTx.token_info', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schema_payload', full_name='SolanaSignTx.schema_payload', index=4, - number=9, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schema_signature', full_name='SolanaSignTx.schema_signature', index=5, - number=10, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='schema_signer_key_id', full_name='SolanaSignTx.schema_signer_key_id', index=6, - number=11, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='token_recipient_owner', full_name='SolanaSignTx.token_recipient_owner', index=7, - number=12, type=12, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=257, - serialized_end=488, -) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15messages-solana.proto\"V\n\x10SolanaGetAddress\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\" \n\rSolanaAddress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\"k\n\x0fSolanaTokenInfo\x12\x0c\n\x04mint\x18\x01 \x01(\x0c\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x10\n\x08\x64\x65\x63imals\x18\x03 \x01(\r\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x15\n\rsigner_key_id\x18\x05 \x01(\r\"\xae\x02\n\x0cSolanaSignTx\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x0e\n\x06raw_tx\x18\x03 \x01(\x0c\x12$\n\ntoken_info\x18\x04 \x03(\x0b\x32\x10.SolanaTokenInfo\x12\x13\n\x0blut_account\x18\x05 \x03(\x0c\x12\x15\n\rlut_signature\x18\x06 \x01(\x0c\x12\x19\n\x11lut_signer_key_id\x18\x07 \x01(\r\x12\x16\n\x0eschema_payload\x18\t \x01(\x0c\x12\x18\n\x10schema_signature\x18\n \x01(\x0c\x12\x1c\n\x14schema_signer_key_id\x18\x0b \x01(\r\x12\x1d\n\x15token_recipient_owner\x18\x0c \x03(\x0cJ\x04\x08\x08\x10\t\"#\n\x0eSolanaSignedTx\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"h\n\x11SolanaSignMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x14\n\x0cshow_display\x18\x04 \x01(\x08\"?\n\x16SolanaMessageSignature\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"\x9c\x01\n\x19SolanaSignOffchainMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x19\n\tcoin_name\x18\x02 \x01(\t:\x06Solana\x12\x12\n\x07version\x18\x03 \x01(\r:\x01\x30\x12\x16\n\x0emessage_format\x18\x04 \x01(\r\x12\x0f\n\x07message\x18\x05 \x01(\x0c\x12\x14\n\x0cshow_display\x18\x06 \x01(\x08\"G\n\x1eSolanaOffchainMessageSignature\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x42\x32\n\x1a\x63om.keepkey.deviceprotocolB\x14KeepKeyMessageSolana') -_SOLANASIGNEDTX = _descriptor.Descriptor( - name='SolanaSignedTx', - full_name='SolanaSignedTx', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='signature', full_name='SolanaSignedTx.signature', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=490, - serialized_end=525, -) - -_SOLANASIGNMESSAGE = _descriptor.Descriptor( - name='SolanaSignMessage', - full_name='SolanaSignMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address_n', full_name='SolanaSignMessage.address_n', index=0, - number=1, type=13, cpp_type=3, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='coin_name', full_name='SolanaSignMessage.coin_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=_b("Solana").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='message', full_name='SolanaSignMessage.message', index=2, - number=3, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='show_display', full_name='SolanaSignMessage.show_display', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=527, - serialized_end=631, -) - - -_SOLANAMESSAGESIGNATURE = _descriptor.Descriptor( - name='SolanaMessageSignature', - full_name='SolanaMessageSignature', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='public_key', full_name='SolanaMessageSignature.public_key', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='signature', full_name='SolanaMessageSignature.signature', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=633, - serialized_end=696, -) - - -_SOLANASIGNOFFCHAINMESSAGE = _descriptor.Descriptor( - name='SolanaSignOffchainMessage', - full_name='SolanaSignOffchainMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='address_n', full_name='SolanaSignOffchainMessage.address_n', index=0, - number=1, type=13, cpp_type=3, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='coin_name', full_name='SolanaSignOffchainMessage.coin_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=_b("Solana").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='version', full_name='SolanaSignOffchainMessage.version', index=2, - number=3, type=13, cpp_type=3, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='message_format', full_name='SolanaSignOffchainMessage.message_format', index=3, - number=4, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='message', full_name='SolanaSignOffchainMessage.message', index=4, - number=5, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='show_display', full_name='SolanaSignOffchainMessage.show_display', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=699, - serialized_end=855, -) - - -_SOLANAOFFCHAINMESSAGESIGNATURE = _descriptor.Descriptor( - name='SolanaOffchainMessageSignature', - full_name='SolanaOffchainMessageSignature', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='public_key', full_name='SolanaOffchainMessageSignature.public_key', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='signature', full_name='SolanaOffchainMessageSignature.signature', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=857, - serialized_end=928, -) - -_SOLANASIGNTX.fields_by_name['token_info'].message_type = _SOLANATOKENINFO -DESCRIPTOR.message_types_by_name['SolanaGetAddress'] = _SOLANAGETADDRESS -DESCRIPTOR.message_types_by_name['SolanaAddress'] = _SOLANAADDRESS -DESCRIPTOR.message_types_by_name['SolanaTokenInfo'] = _SOLANATOKENINFO -DESCRIPTOR.message_types_by_name['SolanaSignTx'] = _SOLANASIGNTX -DESCRIPTOR.message_types_by_name['SolanaSignedTx'] = _SOLANASIGNEDTX -DESCRIPTOR.message_types_by_name['SolanaSignMessage'] = _SOLANASIGNMESSAGE -DESCRIPTOR.message_types_by_name['SolanaMessageSignature'] = _SOLANAMESSAGESIGNATURE -DESCRIPTOR.message_types_by_name['SolanaSignOffchainMessage'] = _SOLANASIGNOFFCHAINMESSAGE -DESCRIPTOR.message_types_by_name['SolanaOffchainMessageSignature'] = _SOLANAOFFCHAINMESSAGESIGNATURE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -SolanaGetAddress = _reflection.GeneratedProtocolMessageType('SolanaGetAddress', (_message.Message,), dict( - DESCRIPTOR = _SOLANAGETADDRESS, - __module__ = 'messages_solana_pb2' +_SOLANAGETADDRESS = DESCRIPTOR.message_types_by_name['SolanaGetAddress'] +_SOLANAADDRESS = DESCRIPTOR.message_types_by_name['SolanaAddress'] +_SOLANATOKENINFO = DESCRIPTOR.message_types_by_name['SolanaTokenInfo'] +_SOLANASIGNTX = DESCRIPTOR.message_types_by_name['SolanaSignTx'] +_SOLANASIGNEDTX = DESCRIPTOR.message_types_by_name['SolanaSignedTx'] +_SOLANASIGNMESSAGE = DESCRIPTOR.message_types_by_name['SolanaSignMessage'] +_SOLANAMESSAGESIGNATURE = DESCRIPTOR.message_types_by_name['SolanaMessageSignature'] +_SOLANASIGNOFFCHAINMESSAGE = DESCRIPTOR.message_types_by_name['SolanaSignOffchainMessage'] +_SOLANAOFFCHAINMESSAGESIGNATURE = DESCRIPTOR.message_types_by_name['SolanaOffchainMessageSignature'] +SolanaGetAddress = _reflection.GeneratedProtocolMessageType('SolanaGetAddress', (_message.Message,), { + 'DESCRIPTOR' : _SOLANAGETADDRESS, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaGetAddress) - )) + }) _sym_db.RegisterMessage(SolanaGetAddress) -SolanaAddress = _reflection.GeneratedProtocolMessageType('SolanaAddress', (_message.Message,), dict( - DESCRIPTOR = _SOLANAADDRESS, - __module__ = 'messages_solana_pb2' +SolanaAddress = _reflection.GeneratedProtocolMessageType('SolanaAddress', (_message.Message,), { + 'DESCRIPTOR' : _SOLANAADDRESS, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaAddress) - )) + }) _sym_db.RegisterMessage(SolanaAddress) -SolanaTokenInfo = _reflection.GeneratedProtocolMessageType('SolanaTokenInfo', (_message.Message,), dict( - DESCRIPTOR = _SOLANATOKENINFO, - __module__ = 'messages_solana_pb2' +SolanaTokenInfo = _reflection.GeneratedProtocolMessageType('SolanaTokenInfo', (_message.Message,), { + 'DESCRIPTOR' : _SOLANATOKENINFO, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaTokenInfo) - )) + }) _sym_db.RegisterMessage(SolanaTokenInfo) -SolanaSignTx = _reflection.GeneratedProtocolMessageType('SolanaSignTx', (_message.Message,), dict( - DESCRIPTOR = _SOLANASIGNTX, - __module__ = 'messages_solana_pb2' +SolanaSignTx = _reflection.GeneratedProtocolMessageType('SolanaSignTx', (_message.Message,), { + 'DESCRIPTOR' : _SOLANASIGNTX, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaSignTx) - )) + }) _sym_db.RegisterMessage(SolanaSignTx) -SolanaSignedTx = _reflection.GeneratedProtocolMessageType('SolanaSignedTx', (_message.Message,), dict( - DESCRIPTOR = _SOLANASIGNEDTX, - __module__ = 'messages_solana_pb2' +SolanaSignedTx = _reflection.GeneratedProtocolMessageType('SolanaSignedTx', (_message.Message,), { + 'DESCRIPTOR' : _SOLANASIGNEDTX, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaSignedTx) - )) + }) _sym_db.RegisterMessage(SolanaSignedTx) -SolanaSignMessage = _reflection.GeneratedProtocolMessageType('SolanaSignMessage', (_message.Message,), dict( - DESCRIPTOR = _SOLANASIGNMESSAGE, - __module__ = 'messages_solana_pb2' +SolanaSignMessage = _reflection.GeneratedProtocolMessageType('SolanaSignMessage', (_message.Message,), { + 'DESCRIPTOR' : _SOLANASIGNMESSAGE, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaSignMessage) - )) + }) _sym_db.RegisterMessage(SolanaSignMessage) -SolanaMessageSignature = _reflection.GeneratedProtocolMessageType('SolanaMessageSignature', (_message.Message,), dict( - DESCRIPTOR = _SOLANAMESSAGESIGNATURE, - __module__ = 'messages_solana_pb2' +SolanaMessageSignature = _reflection.GeneratedProtocolMessageType('SolanaMessageSignature', (_message.Message,), { + 'DESCRIPTOR' : _SOLANAMESSAGESIGNATURE, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaMessageSignature) - )) + }) _sym_db.RegisterMessage(SolanaMessageSignature) -SolanaSignOffchainMessage = _reflection.GeneratedProtocolMessageType('SolanaSignOffchainMessage', (_message.Message,), dict( - DESCRIPTOR = _SOLANASIGNOFFCHAINMESSAGE, - __module__ = 'messages_solana_pb2' +SolanaSignOffchainMessage = _reflection.GeneratedProtocolMessageType('SolanaSignOffchainMessage', (_message.Message,), { + 'DESCRIPTOR' : _SOLANASIGNOFFCHAINMESSAGE, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaSignOffchainMessage) - )) + }) _sym_db.RegisterMessage(SolanaSignOffchainMessage) -SolanaOffchainMessageSignature = _reflection.GeneratedProtocolMessageType('SolanaOffchainMessageSignature', (_message.Message,), dict( - DESCRIPTOR = _SOLANAOFFCHAINMESSAGESIGNATURE, - __module__ = 'messages_solana_pb2' +SolanaOffchainMessageSignature = _reflection.GeneratedProtocolMessageType('SolanaOffchainMessageSignature', (_message.Message,), { + 'DESCRIPTOR' : _SOLANAOFFCHAINMESSAGESIGNATURE, + '__module__' : 'messages_solana_pb2' # @@protoc_insertion_point(class_scope:SolanaOffchainMessageSignature) - )) + }) _sym_db.RegisterMessage(SolanaOffchainMessageSignature) - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\032com.keepkey.deviceprotocolB\024KeepKeyMessageSolana')) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\032com.keepkey.deviceprotocolB\024KeepKeyMessageSolana' + _SOLANAGETADDRESS._serialized_start=25 + _SOLANAGETADDRESS._serialized_end=111 + _SOLANAADDRESS._serialized_start=113 + _SOLANAADDRESS._serialized_end=145 + _SOLANATOKENINFO._serialized_start=147 + _SOLANATOKENINFO._serialized_end=254 + _SOLANASIGNTX._serialized_start=257 + _SOLANASIGNTX._serialized_end=559 + _SOLANASIGNEDTX._serialized_start=561 + _SOLANASIGNEDTX._serialized_end=596 + _SOLANASIGNMESSAGE._serialized_start=598 + _SOLANASIGNMESSAGE._serialized_end=702 + _SOLANAMESSAGESIGNATURE._serialized_start=704 + _SOLANAMESSAGESIGNATURE._serialized_end=767 + _SOLANASIGNOFFCHAINMESSAGE._serialized_start=770 + _SOLANASIGNOFFCHAINMESSAGE._serialized_end=926 + _SOLANAOFFCHAINMESSAGESIGNATURE._serialized_start=928 + _SOLANAOFFCHAINMESSAGESIGNATURE._serialized_end=999 # @@protoc_insertion_point(module_scope) diff --git a/tests/test_msg_solana_lut_attestation.py b/tests/test_msg_solana_lut_attestation.py new file mode 100644 index 00000000..cbca5b26 --- /dev/null +++ b/tests/test_msg_solana_lut_attestation.py @@ -0,0 +1,215 @@ +"""KKSOLSW1 -- transaction-bound lookup-table account attestation. + +A Solana v0 message may source instruction accounts from an Address Lookup +Table. Those accounts are NOT in the bytes being signed, so the device cannot +derive them: it forces the whole transaction to SOL_TX_REVIEW_OPAQUE, refuses +it outright without AdvancedMode, and treats it as an explicit BLIND SIGN with +AdvancedMode on. The instruction's meaning is never shown. + +A clear-sign provider may attest the resolved account list for THIS exact +transaction, turning that blind sign into a described one. The attestation is: + + * DOMAIN-TAGGED -- "KeepKeySolanaTxAccounts/1", so a signature made for any + other purpose (an EVM metadata blob, a token definition) + cannot be replayed as one; + * TX-BOUND -- over sha256(raw_tx), so it cannot be replayed onto a + different transaction; + * ADDITIVE -- the blind-sign warning still follows it. A runtime signer + is annotation, never authority. + +These tests assert all three, and assert that every failure mode degrades to +exactly the flow that exists today rather than to something new. +""" +import struct +import unittest + +import common +import keepkeylib.messages_solana_pb2 as messages +import keepkeylib.types_pb2 as proto_types +from keepkeylib.client import CallException +from keepkeylib.tools import parse_path + +TAG = b"KeepKeySolanaTxAccounts/1" +SLOT = 3 + + +class TestSolanaLutAttestation(common.KeepKeyTest): + + SYSTEM_PROGRAM = b'\x00' * 32 + + def setUp(self): + super(TestSolanaLutAttestation, self).setUp() + self.requires_firmware("7.15.0") + self.requires_fullFeature() + self.requires_message("LoadClearsignSigner") + self.setup_mnemonic_allallall() + + # ---------------------------------------------------------------- helpers + + def _raw_pubkey(self): + """The device's Solana (ed25519) pubkey, decoded from the base58 + address. get_public_node() would hand back a secp256k1 key, which is + not what signs a Solana transaction -- the device would then reject the + tx with "Derived key is not a signer".""" + addr = self.client.call(messages.SolanaGetAddress( + address_n=parse_path("m/44'/501'/0'/0'"), + show_display=False)).address + ALPHABET = ('123456789ABCDEFGHJKLMNPQRSTUVWXYZ' + 'abcdefghijkmnopqrstuvwxyz') + n = 0 + for c in addr: + n = n * 58 + ALPHABET.index(c) + return n.to_bytes(32, 'big') + + def _build_lut_tx(self, from_pubkey): + """A v0 message carrying a lookup-table section. + + The ALT section is what forces the device opaque -- exactly the case + KKSOLSW1 exists for. Built by hand rather than reused from another test + so the shape under test is visible here. + """ + tx = bytearray() + tx.append(0x80) # versioned, v0 + tx.extend([1, 0, 1]) # header: 1 sig, 0 ro-signed, 1 ro-unsigned + tx.append(2) # 2 static accounts + tx.extend(from_pubkey) + tx.extend(self.SYSTEM_PROGRAM) + tx.extend(b'\xbb' * 32) # recent blockhash + tx.append(1) # 1 instruction + tx.extend(bytes([1])) # program index -> SYSTEM_PROGRAM + tx.append(1) # 1 account index + tx.append(3) # index 3: BEYOND the static table -> external + tx.append(4) # data len + tx.extend(struct.pack('