diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..95f8a880 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,27 @@ +version: 2.1 + +# The authoritative multi-release emulator matrix lives in GitHub Actions. +# Keep this smoke job while the legacy CircleCI project remains connected so +# its required status verifies the checkout instead of failing at config load. +jobs: + canonical-smoke: + docker: + - image: cimg/python:3.11 + steps: + - run: + name: Checkout current branch over HTTPS + command: | + git clone --depth 1 -b "$CIRCLE_BRANCH" \ + https://github.com/keepkey/python-keepkey.git . + git submodule update --init --recursive + - run: + name: Validate canonical Python and EOS vector contracts + command: | + python -m py_compile keepkeylib/*.py tests/test_msg_solana_signtx.py + python -m pip install --quiet pytest requests + python -m pytest -q tests/unit/test_eos_updateauth_vector.py + +workflows: + canonical: + jobs: + - canonical-smoke diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17a624cd..40c37938 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,24 +119,15 @@ jobs: submodules: recursive path: python-keepkey - # Immutable fork 7.15 candidate. Once canonical Python merges, the - # firmware branch is repinned to that one upstream commit and rerun. + # Immutable firmware 7.15 code head validated by the release stack. - name: Checkout firmware uses: actions/checkout@v4 with: repository: BitHighlander/keepkey-firmware - # PINNED, not `alpha`. A moving branch means a firmware push can - # change this PR's result with no Python commit, which makes a green - # run unciteable. This SHA is alpha at the time of pinning. - # - # NOTE: this is the exact staged 7.16.0 candidate, NOT - # 7.15.0/RC18. The suite needs firmware - # that only exists after RC18 -- variant_getName() returning - # "EmulatorBTC" (required by requires_bitcoinOnly, so by the whole - # integration-btc job) and the Ironwood known-answer vectors. So this - # job validates 7.16.0; it does not validate the RC18 dependency - # graph. Bump deliberately, and re-read that claim when you do. - ref: 651f2a462b8e71ffb37995094dbe1e9298bc802f + # PINNED, not a moving branch, so a green result remains citeable. + # This is the code/workflow head beneath the handoff-only commit on + # firmware PR #843. + ref: a6effd7205b480c4fb314a1714831ffd863e2a48 path: keepkey-firmware # NOT `submodules: recursive`. trezor-firmware carries a micropython @@ -564,7 +555,7 @@ jobs: firmware_ref: 4125e1c7409b1cb7b08ba595bc408e3128fc24ca min_fw: "7.14.3" - release: "7.15" - firmware_ref: d33f1711c3b2b205f64c5dc35fdec02926a6dc63 + firmware_ref: a6effd7205b480c4fb314a1714831ffd863e2a48 min_fw: "7.15.0" # KK_BITCOIN_ONLY=ON is a second shipping product, not a build flavour: @@ -590,18 +581,10 @@ jobs: uses: actions/checkout@v4 with: repository: BitHighlander/keepkey-firmware - # PINNED, not `alpha`. A moving branch means a firmware push can - # change this PR's result with no Python commit, which makes a green - # run unciteable. This SHA is alpha at the time of pinning. - # - # NOTE: this is the exact staged 7.16.0 candidate, NOT - # 7.15.0/RC18. The suite needs firmware - # that only exists after RC18 -- variant_getName() returning - # "EmulatorBTC" (required by requires_bitcoinOnly, so by the whole - # integration-btc job) and the Ironwood known-answer vectors. So this - # job validates 7.16.0; it does not validate the RC18 dependency - # graph. Bump deliberately, and re-read that claim when you do. - ref: 651f2a462b8e71ffb37995094dbe1e9298bc802f + # Each matrix row is an immutable shipping candidate. Do not replace + # this with a common alpha SHA: that would make both labels test the + # same firmware and silently discard cross-release compatibility. + ref: ${{ matrix.firmware_ref }} path: keepkey-firmware # Same non-recursive init as the regular job: trezor-firmware's diff --git a/device-protocol b/device-protocol index 9800325f..dd9c85dc 160000 --- a/device-protocol +++ b/device-protocol @@ -1 +1 @@ -Subproject commit 9800325f7cfcaa2b2f9fd47f34afbd6632a05fc9 +Subproject commit dd9c85dc747cf965fb0e7bf49615dc9e7568ee65 diff --git a/keepkeylib/erc7730_compiler.py b/keepkeylib/erc7730_compiler.py index 08501b09..fb80742c 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,16 @@ 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( + "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/keepkeylib/messages_pb2.py b/keepkeylib/messages_pb2.py index e3b086a7..dc6abeb3 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\"\xe1\x04\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\"*\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\"\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') , dependencies=[types__pb2.DESCRIPTOR,]) @@ -926,8 +926,8 @@ ], containing_type=None, options=None, - serialized_start=5517, - serialized_end=14544, + serialized_start=5558, + serialized_end=14585, ) _sym_db.RegisterEnumDescriptor(_MESSAGETYPE) @@ -1395,6 +1395,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='supports_solana_lut_attestation', full_name='Features.supports_solana_lut_attestation', index=26, + number=29, 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=[ ], @@ -1408,7 +1415,7 @@ oneofs=[ ], serialized_start=61, - serialized_end=670, + serialized_end=711, ) @@ -1445,8 +1452,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=672, - serialized_end=714, + serialized_start=713, + serialized_end=755, ) @@ -1490,8 +1497,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=716, - serialized_end=792, + serialized_start=757, + serialized_end=833, ) @@ -1514,8 +1521,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=794, - serialized_end=808, + serialized_start=835, + serialized_end=849, ) @@ -1573,8 +1580,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=810, - serialized_end=931, + serialized_start=851, + serialized_end=972, ) @@ -1604,8 +1611,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=933, - serialized_end=960, + serialized_start=974, + serialized_end=1001, ) @@ -1663,8 +1670,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=963, - serialized_end=1098, + serialized_start=1004, + serialized_end=1139, ) @@ -1694,8 +1701,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1100, - serialized_end=1126, + serialized_start=1141, + serialized_end=1167, ) @@ -1732,8 +1739,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1128, - serialized_end=1182, + serialized_start=1169, + serialized_end=1223, ) @@ -1770,8 +1777,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1184, - serialized_end=1247, + serialized_start=1225, + serialized_end=1288, ) @@ -1794,8 +1801,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1249, - serialized_end=1260, + serialized_start=1290, + serialized_end=1301, ) @@ -1825,8 +1832,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1262, - serialized_end=1317, + serialized_start=1303, + serialized_end=1358, ) @@ -1856,8 +1863,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1319, - serialized_end=1346, + serialized_start=1360, + serialized_end=1387, ) @@ -1880,8 +1887,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1348, - serialized_end=1356, + serialized_start=1389, + serialized_end=1397, ) @@ -1904,8 +1911,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1358, - serialized_end=1377, + serialized_start=1399, + serialized_end=1418, ) @@ -1935,8 +1942,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1379, - serialized_end=1414, + serialized_start=1420, + serialized_end=1455, ) @@ -1966,8 +1973,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1416, - serialized_end=1442, + serialized_start=1457, + serialized_end=1483, ) @@ -1997,8 +2004,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1444, - serialized_end=1470, + serialized_start=1485, + serialized_end=1511, ) @@ -2056,8 +2063,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1473, - serialized_end=1635, + serialized_start=1514, + serialized_end=1676, ) @@ -2094,8 +2101,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1637, - serialized_end=1689, + serialized_start=1678, + serialized_end=1730, ) @@ -2153,8 +2160,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1692, - serialized_end=1871, + serialized_start=1733, + serialized_end=1912, ) @@ -2184,8 +2191,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1873, - serialized_end=1899, + serialized_start=1914, + serialized_end=1940, ) @@ -2208,8 +2215,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1901, - serialized_end=1913, + serialized_start=1942, + serialized_end=1954, ) @@ -2288,8 +2295,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1916, - serialized_end=2103, + serialized_start=1957, + serialized_end=2144, ) @@ -2389,8 +2396,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2106, - serialized_end=2372, + serialized_start=2147, + serialized_end=2413, ) @@ -2413,8 +2420,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2374, - serialized_end=2390, + serialized_start=2415, + serialized_end=2431, ) @@ -2444,8 +2451,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2392, - serialized_end=2421, + serialized_start=2433, + serialized_end=2462, ) @@ -2538,8 +2545,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2424, - serialized_end=2679, + serialized_start=2465, + serialized_end=2720, ) @@ -2562,8 +2569,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2681, - serialized_end=2694, + serialized_start=2722, + serialized_end=2735, ) @@ -2593,8 +2600,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2696, - serialized_end=2719, + serialized_start=2737, + serialized_end=2760, ) @@ -2631,8 +2638,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2721, - serialized_end=2780, + serialized_start=2762, + serialized_end=2821, ) @@ -2676,8 +2683,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2782, - serialized_end=2845, + serialized_start=2823, + serialized_end=2886, ) @@ -2728,8 +2735,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2848, - serialized_end=2978, + serialized_start=2889, + serialized_end=3019, ) @@ -2780,8 +2787,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2980, - serialized_end=3076, + serialized_start=3021, + serialized_end=3117, ) @@ -2818,8 +2825,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3078, - serialized_end=3132, + serialized_start=3119, + serialized_end=3173, ) @@ -2877,8 +2884,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3134, - serialized_end=3252, + serialized_start=3175, + serialized_end=3293, ) @@ -2922,8 +2929,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3254, - serialized_end=3318, + serialized_start=3295, + serialized_end=3359, ) @@ -2974,8 +2981,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3320, - serialized_end=3401, + serialized_start=3361, + serialized_end=3442, ) @@ -3012,8 +3019,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3403, - serialized_end=3455, + serialized_start=3444, + serialized_end=3496, ) @@ -3085,8 +3092,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3458, - serialized_end=3598, + serialized_start=3499, + serialized_end=3639, ) @@ -3116,8 +3123,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3600, - serialized_end=3633, + serialized_start=3641, + serialized_end=3674, ) @@ -3154,8 +3161,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3635, - serialized_end=3688, + serialized_start=3676, + serialized_end=3729, ) @@ -3185,8 +3192,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3690, - serialized_end=3723, + serialized_start=3731, + serialized_end=3764, ) @@ -3272,8 +3279,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3726, - serialized_end=3932, + serialized_start=3767, + serialized_end=3973, ) @@ -3317,8 +3324,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3935, - serialized_end=4068, + serialized_start=3976, + serialized_end=4109, ) @@ -3348,8 +3355,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4070, - serialized_end=4107, + serialized_start=4111, + serialized_end=4148, ) @@ -3379,8 +3386,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4109, - serialized_end=4152, + serialized_start=4150, + serialized_end=4193, ) @@ -3431,8 +3438,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4154, - serialized_end=4279, + serialized_start=4195, + serialized_end=4320, ) @@ -3476,8 +3483,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4281, - serialized_end=4353, + serialized_start=4322, + serialized_end=4394, ) @@ -3507,8 +3514,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4355, - serialized_end=4399, + serialized_start=4396, + serialized_end=4440, ) @@ -3552,8 +3559,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4401, - serialized_end=4464, + serialized_start=4442, + serialized_end=4505, ) @@ -3597,8 +3604,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4466, - serialized_end=4524, + serialized_start=4507, + serialized_end=4565, ) @@ -3628,8 +3635,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4526, - serialized_end=4559, + serialized_start=4567, + serialized_end=4600, ) @@ -3666,8 +3673,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4561, - serialized_end=4614, + serialized_start=4602, + serialized_end=4655, ) @@ -3697,8 +3704,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4616, - serialized_end=4658, + serialized_start=4657, + serialized_end=4699, ) @@ -3721,8 +3728,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4660, - serialized_end=4671, + serialized_start=4701, + serialized_end=4712, ) @@ -3745,8 +3752,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4673, - serialized_end=4688, + serialized_start=4714, + serialized_end=4729, ) @@ -3783,8 +3790,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4690, - serialized_end=4745, + serialized_start=4731, + serialized_end=4786, ) @@ -3821,8 +3828,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4747, - serialized_end=4797, + serialized_start=4788, + serialized_end=4838, ) @@ -3845,8 +3852,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4799, - serialized_end=4818, + serialized_start=4840, + serialized_end=4859, ) @@ -3974,8 +3981,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4821, - serialized_end=5185, + serialized_start=4862, + serialized_end=5226, ) @@ -3998,8 +4005,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5187, - serialized_end=5202, + serialized_start=5228, + serialized_end=5243, ) @@ -4043,8 +4050,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5204, - serialized_end=5263, + serialized_start=5245, + serialized_end=5304, ) @@ -4067,8 +4074,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5265, - serialized_end=5286, + serialized_start=5306, + serialized_end=5327, ) @@ -4098,8 +4105,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5288, - serialized_end=5320, + serialized_start=5329, + serialized_end=5361, ) @@ -4122,8 +4129,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5322, - serialized_end=5353, + serialized_start=5363, + serialized_end=5394, ) @@ -4153,8 +4160,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5355, - serialized_end=5403, + serialized_start=5396, + serialized_end=5444, ) @@ -4184,8 +4191,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5405, - serialized_end=5445, + serialized_start=5446, + serialized_end=5486, ) @@ -4222,8 +4229,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5447, - serialized_end=5514, + serialized_start=5488, + serialized_end=5555, ) _FEATURES.fields_by_name['coins'].message_type = types__pb2._COINTYPE diff --git a/scripts/generate-test-report.py b/scripts/generate-test-report.py index 8cb47d5d..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, )) @@ -811,7 +821,7 @@ def _arg_shown(a): ['Wordlist rejection warning']), ]), - ('K', 'Seed Generation Hardening (7.14.3+)', '7.14.3', + ('K', 'Seed Generation Hardening (7.15.0+)', '7.15.0', 'The 7.15 changes to how a seed comes into existence: user-supplied dice entropy folded in ' 'on-device, and the PIN key-derivation rewrap. These ran green from the first 7.15 RC but ' 'appeared nowhere in this report, because the catalog could not reference native firmware ' @@ -1225,12 +1235,13 @@ def _arg_shown(a): ['FOX desired amount', 'FOX minimum', 'Recipient', 'ETH desired amount', 'ETH minimum', 'Deadline', 'Fee and final approval']), ('E19', 'test_msg_ethereum_erc20_uniswap_liquidity', 'test_sign_uni_remove_liquidity_ETH', - 'Uniswap V2 remove liquidity ETH+token', - 'Clear-signs the LP burn amount, minimum FOX and ETH outputs, the non-self signed ' - 'recipient, and deadline before the final fee review. This is the regression for the ' - 'recipient-confirmation path that previously cancelled after the user approved it.', - ['LP burn amount', 'FOX minimum', 'Recipient', 'ETH minimum', 'Deadline', - 'Fee and final approval']), + 'Uniswap V2 unsafe remove-liquidity recipient refused', + 'Enables AdvancedMode and reviews the LP burn amount and minimum FOX output, then ' + 'discloses that the signed recipient is not the signing wallet and refuses before ' + 'the remaining output, deadline, fee, or signing consent. The Failure_ActionCancelled ' + 'response proves the non-self recipient cannot route both withdrawn assets away.', + ['Enable Policy: AdvancedMode', 'LP burn amount', 'FOX minimum', + 'Non-self recipient refusal']), ('E20', 'test_msg_ethereum_thorchain_deposit', 'test_deposit_legacy_selector', 'THORChain router deposit() (legacy selector)', 'Cross-chain swap via the THORChain router contract — a daily-driver EVM<->THORChain ' @@ -2847,31 +2858,30 @@ def _arg_shown(a): 'WARNING: DUPLICATE TRANSACTION! Already signed a tx with the same outputs']), ]), ('U', 'Storage Upgrade Preservation', '7.15.0', - 'A signed UPGRADE must never wipe. A DOWNGRADE wipes, and that is correct. Those two ' - 'sentences are the whole policy (docs/StorageVersionGate.md), and until this section ' - 'nothing in the suite tested either half - every other test creates storage with the ' + 'A signed UPGRADE must never wipe. An older firmware must never parse a newer storage ' + 'format; depending on the tree it either resets that record or explicitly refuses it ' + 'without writing. Until this section nothing in the suite tested those boundaries - every ' + 'other test creates storage with the ' 'firmware under test and never crosses a release boundary, which is exactly where this ' - 'class of defect lives. The mechanism is one function: storage_init() hands whatever is in ' - 'flash to storage_fromFlash(), and if version_from_int() does not recognise the version it ' - 'returns StorageVersion_NONE, the load reports SUS_Invalid, and storage_init() runs ' - 'storage_reset() + storage_commit(). No prompt, no warning - the wallet is gone at boot. ' + 'class of defect lives. The mechanism is one function: storage_init() hands flash to ' + 'storage_fromFlash(). Historical trees map unknown versions to SUS_Invalid and commit a ' + 'reset; trees implementing SUS_TooNew refuse a newer normal-band record without committing. ' 'The flash format this build reads and writes is V17, the same format shipped in v7.14.1. ' '7.15 reverted the RC27 bump to V19 (commit 6bebde7b2) because one boot silently migrated ' '17 to 19 and from that moment no downgrade was possible without a wipe; V18, the ' 'clear-sign identity block, is dead, and the V19 serializer survives only behind ' - 'STORAGE_PIN_KDF_V19 == 0. U5 pins that V17 as a literal, on purpose: the compile-time ' - 'assert compares two numbers in the same header, and raising the baseline to make a build ' - 'compile is the edit the SOP calls its highest-severity review item.', + 'STORAGE_PIN_KDF_V19 == 0. U5 independently ratchets the shipped floor at V17: the ' + 'compile-time assert compares two numbers in the same header, and lowering both to make a ' + 'build compile is the edit the SOP calls its highest-severity review item.', [ 'THE RULE: recognise every version any shipped firmware ever wrote, and never lower', 'STORAGE_VERSION. Both ways of breaking it compile cleanly and pass every other test:', '- lowering STORAGE_VERSION below a version that has shipped;', '- deleting, reordering or renumbering an entry in storage_versions.inc.', '', - 'The reverse direction is NOT a defect. Older firmware cannot read a newer record, so a', - 'DOWNGRADE lands on SUS_Invalid and resets. Do not "fix" that: the reset is what stops', - 'an attacker flashing an older, validly signed image with a known extraction bug and', - 'keeping the seed.', + 'The reverse direction is tree-specific but must be explicit. Older firmware cannot', + 'read a newer record: historical trees reset it, while SUS_TooNew trees lock it and', + 'preserve the bytes. Neither policy may load the incompatible wallet.', '', 'HOW THESE TESTS REACH THE GATE: it only runs at boot, and no host message can reboot', 'the device. SoftReset (messages.proto type 89) has no messagemap entry and no handler', @@ -2890,10 +2900,10 @@ def _arg_shown(a): ' device.', '- U2 restamps a record THIS build wrote rather than replaying one 7.14.x wrote, so the', ' V16 reader runs but the older LAYOUTS (V1-V15) and their fallthrough chain do not.', - '- U1-U4 SKIP wherever no kkemu binary can be started. The CI python-keepkey image', - ' (scripts/emulator/python-keepkey.Dockerfile) copies the source but never builds the', - ' emulator, so as the pipeline stands today only U5-U8 run in CI. A skipped U1-U4 in', - ' this report means the release was NOT audited for upgrade preservation.', + '- U1-U4 SKIP wherever no kkemu binary can be started. The release Compose harness', + ' supplies the exact variant binary built by the firmware-unit service; running the', + ' python-keepkey image by itself does not. A skipped U1-U4 in this report means the', + ' release was NOT audited for upgrade preservation.', ], [ ('U1', 'test_storage_version_gate', 'test_reboot_preserves_the_wallet', @@ -2929,17 +2939,16 @@ def _arg_shown(a): 'Home screen after the migrating boot: wallet still present', 'Bitcoin Account #0 / Address #0 - the same address the V16 record held']), ('U3', 'test_storage_version_gate', 'test_unrecognised_version_wipes_on_boot', - 'An unrecognised version wipes, deliberately', - 'The half of the policy nobody should be tempted to soften. A device that has run ' - 'newer firmware carries a newer stamp; older firmware cannot read it, so ' - 'version_from_int() returns StorageVersion_NONE and storage_init() resets. That reset ' - 'is the rollback protection: without it an attacker could flash an older, validly ' - 'signed image with a known extraction bug and keep the seed. The stamp used is one ' - 'past the version this build just committed - measured from the device, not read out ' - 'of the header - which is exactly what the next format bump will look like from here. ' - 'The device must come up with no wallet, no PIN and no label.', + 'A newer storage version follows the firmware policy', + 'A device that has run newer firmware carries a newer stamp that this firmware cannot ' + 'parse. The canonical test derives the policy from the firmware tree: historical ' + 'trees reset unknown normal-band storage, while trees implementing SUS_TooNew refuse ' + 'the record without modifying flash so reinstalling the newer firmware can recover ' + 'the wallet. In both cases the older firmware must expose no wallet, PIN or label. ' + 'The stamp is one past the value the device just committed, so the test crosses the ' + 'actual next-version boundary rather than pinning python-keepkey to one release.', ['Wipe Device confirm', 'Import Recovery Sentence confirm', - 'Home screen after the boot that reset storage: no wallet']), + 'Home screen after the incompatible boot: no wallet']), ('U4', 'test_storage_version_gate', 'test_bitcoin_only_band_refuses_without_wiping', 'A bitcoin-only wallet is refused, not destroyed', 'Seeds created under bitcoin-only firmware are stamped in a reserved band (10000 + the ' @@ -3273,6 +3282,38 @@ def _arg_shown(a): 'test_certified_soltoshi_join_ignores_blind_sign_policy'): '7.16.0', } +# Intermediate release-stack trees deliberately omit controls that land in a +# later adjacent slice. The firmware workflow records those omissions in the +# JUnit artifact with the same capability names used by the integration tests. +# Only these exact catalog rows are exempt while their capability is declared +# absent; the complete release tree sets no omissions and remains strict. +_TEST_CAPABILITY = { + ('Storage', 'PinKdfRewrapsToActiveVersionAfterCorrectPin'): + 'storage-v19-kdf', + ('Storage', 'PinUnlocksAfterRebootUnderV17'): 'storage-v19-kdf', + ('Storage', 'PinKdfV2FlagIsVersionedInV19'): 'storage-v19-kdf', + ('test_msg_solana_lut_attestation', + 'test_attested_accounts_are_shown_and_blind_sign_still_follows'): + 'solana-lut-attestation', + ('test_msg_solana_lut_attestation', + 'test_bad_signature_degrades_to_todays_flow'): + 'solana-lut-attestation', + ('test_msg_solana_lut_attestation', + 'test_attestation_does_not_replay_onto_another_transaction'): + 'solana-lut-attestation', + ('test_msg_solana_lut_attestation', + 'test_no_signer_loaded_means_no_extra_screens'): + 'solana-lut-attestation', +} + + +def _missing_release_capabilities(): + return { + value.strip() for value in + os.environ.get('KK_RELEASE_MISSING_CAPABILITIES', '').split(',') + if value.strip() + } + def _active_sections(fw_version): active = [] @@ -3652,12 +3693,17 @@ def validate_junit(fw_version, results, variant='full'): unless their module is in MUST_RUN_MODULES. """ active = _active_sections(fw_version) + missing_capabilities = _missing_release_capabilities() failures = [] for letter, title, mf, bg, fl, tests in active: for tid, mod, meth, ttl, ctx, scr in tests: status = _lookup(results, mod, meth) if status in ('fail', 'error'): failures.append((tid, mod, meth, status)) + continue + capability = _TEST_CAPABILITY.get((mod, meth)) + if capability in missing_capabilities: + continue must_run = not ( variant == 'bitcoin-only' and mod in FULL_FEATURE_ONLY_MUST_RUN_MODULES diff --git a/tests/common.py b/tests/common.py index b0382f62..af464f2e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -162,6 +162,25 @@ def requires_firmware(self, ver_required): if version < semver.VersionInfo.parse(ver_required): self.skipTest("Firmware version " + ver_required + " or higher is required to run this test") + def requires_release_capability(self, capability): + """Skip only when staged CI explicitly declares a capability absent. + + A released firmware, a developer invocation, and older firmware all + receive the canonical suite's normal version/device gates. Only the + stacked-release workflow sets ``KK_RELEASE_MISSING_CAPABILITIES`` for + deliberately incomplete intermediate trees. This keeps one canonical + multi-release branch without weakening the final release assertions. + """ + missing_raw = os.environ.get('KK_RELEASE_MISSING_CAPABILITIES') + if missing_raw is None: + return + missing = set(value.strip() for value in missing_raw.split(',') + if value.strip()) + if capability in missing: + self.skipTest( + "Staged release tree does not yet provide capability: " + + capability) + def requires_taproot(self): """Skip unless the firmware reports taproot support. @@ -187,6 +206,21 @@ def requires_dice_modes(self): if not getattr(self.client.features, 'supports_dice_modes', False): self.skipTest("Firmware does not report supports_dice_modes") + def requires_solana_lut_attestation(self): + """Skip unless firmware authenticates and presents resolved LUT data. + + The Solana wire fields are shared across releases, so their presence in + generated Python bindings does not prove the connected firmware trusts + or displays them. Gate the provider-attestation tests on the device's + explicit capability instead of treating every 7.15 build as identical. + """ + self.requires_release_capability('solana-lut-attestation') + self.client.init_device() + if not getattr(self.client.features, + 'supports_solana_lut_attestation', False): + self.skipTest( + "Firmware does not report supports_solana_lut_attestation") + def requires_structured_eip712(self): """Skip unless the FIRMWARE drives the structured EIP-712 walk. @@ -292,4 +326,3 @@ def requires_bitcoinOnly(self): if self.client.features.firmware_variant not in ("KeepKeyBTC", "EmulatorBTC"): self.skipTest("Bitcoin-only firmware required to run this test") - diff --git a/tests/test_erc7730_compiler.py b/tests/test_erc7730_compiler.py index 576d8284..7f44302e 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,15 +245,13 @@ 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(): +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", @@ -252,21 +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 - 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]) + 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(): @@ -280,15 +276,12 @@ 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]) - 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 +308,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 +341,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 +366,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,21 +401,21 @@ 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) + + +DEVICE_LIMITS = ( + "iterates more than one array", + "nests deeper than the device supports", +) 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 = [] + unsupported = [] checked = 0 for path in glob.glob(os.path.join( registry, "registry", "**", "calldata-*.json"), recursive=True): @@ -453,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() @@ -467,12 +451,13 @@ 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(): - 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 +481,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) diff --git a/tests/test_msg_eip712_streaming.py b/tests/test_msg_eip712_streaming.py index ff858fd3..6a6a48fe 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 = { @@ -50,8 +55,109 @@ 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). + + 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) + + +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': { @@ -125,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 @@ -138,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): @@ -145,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( @@ -160,9 +276,49 @@ 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()) + 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(), + 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() - self.requires_firmware("7.16.0") + self.requires_firmware("7.15.0") self.requires_fullFeature() self.requires_structured_eip712() self.setup_mnemonic_nopin_nopassphrase() @@ -189,7 +345,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,8 +365,7 @@ 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.""" @@ -232,8 +387,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. @@ -287,8 +441,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. @@ -319,6 +472,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() diff --git a/tests/test_msg_ethereum_clear_signing.py b/tests/test_msg_ethereum_clear_signing.py index 72cd80db..a6729cdd 100644 --- a/tests/test_msg_ethereum_clear_signing.py +++ b/tests/test_msg_ethereum_clear_signing.py @@ -29,6 +29,9 @@ import struct 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 @@ -832,6 +835,7 @@ class TestEthereumClearSigning(common.KeepKeyTest): def setUp(self): super().setUp() + self.requires_release_capability("erc7730-runtime-review") self.requires_firmware("7.15.0") self.requires_message("EthereumTxMetadata") self.requires_message("LoadClearsignSigner") @@ -1028,14 +1032,26 @@ 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) - 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( @@ -1225,9 +1241,6 @@ def test_load_signer_cancel_refuses(self): signed_payload=sign_metadata(payload), metadata_version=1, key_id=1) self.assertEqual(resp.classification, CLASSIFICATION_MALFORMED) - @unittest.skipUnless( - os.getenv('KK_EXPECT_PERSIST_REJECTED') == '1', - 'requires the exact RC18 firmware security boundary') def test_persistent_signer_rejected_without_session_mutation(self): """RC18 firmware fails closed on persist=true without slot mutation.""" pub = test_signer_compressed_pubkey() @@ -1290,6 +1303,7 @@ class TestClearSignV2Device(common.KeepKeyTest): def setUp(self): super().setUp() + self.requires_release_capability("erc7730-runtime-review") self.requires_firmware(self.V2_FIRMWARE) self.requires_message("EthereumTxMetadata") self.requires_message("LoadClearsignSigner") diff --git a/tests/test_msg_ethereum_clearsign_additive.py b/tests/test_msg_ethereum_clearsign_additive.py index 8ea7194a..ef588605 100644 --- a/tests/test_msg_ethereum_clearsign_additive.py +++ b/tests/test_msg_ethereum_clearsign_additive.py @@ -161,6 +161,7 @@ class TestClearSignAdditiveInvariant(common.KeepKeyTest): def setUp(self): super().setUp() + self.requires_release_capability("erc7730-runtime-review") self.requires_firmware("7.15.0") self.requires_message("EthereumTxMetadata") self.requires_message("LoadClearsignSigner") diff --git a/tests/test_msg_ethereum_erc20_uniswap_liquidity.py b/tests/test_msg_ethereum_erc20_uniswap_liquidity.py index a4508be4..872afab0 100644 --- a/tests/test_msg_ethereum_erc20_uniswap_liquidity.py +++ b/tests/test_msg_ethereum_erc20_uniswap_liquidity.py @@ -34,6 +34,7 @@ def setUp(self): self.requires_firmware("7.15.0") def test_sign_uni_approve_liquidity_ETH(self): + self.requires_release_capability("erc7730-runtime-review") self.requires_fullFeature() self.requires_firmware("7.1.0") self.setup_mnemonic_nopin_nopassphrase() @@ -64,6 +65,7 @@ def test_sign_uni_approve_liquidity_ETH(self): str(caught.exception)) def test_sign_uni_add_liquidity_ETH(self): + self.requires_release_capability("evm-unknown-token-review") self.requires_fullFeature() self.requires_firmware("7.1.0") self.setup_mnemonic_nopin_nopassphrase() @@ -94,34 +96,35 @@ def test_sign_uni_add_liquidity_ETH(self): self.assertEqual(binascii.hexlify(sig_s).decode("ascii"), '0a8eec6856aef8caa234240b06862976f8e238e8b24f5c989279507dd7e51ccd') def test_sign_uni_remove_liquidity_ETH(self): + self.requires_release_capability("evm-unknown-token-review") self.requires_fullFeature() self.requires_firmware("7.1.0") self.setup_mnemonic_nopin_nopassphrase() self.client.apply_policy("AdvancedMode", 1) - # remove liquidity from the ETH/FOX pool - sig_v, sig_r, sig_s = self.client.ethereum_sign_tx( - n=[2147483692,2147483708,2147483648,0,0], - nonce=0xf, - gas_price=0x320313e400, - gas_limit=0x3b754, - value=0x0, - to=binascii.unhexlify('7a250d5630B4cF539739dF2C5dAcb4c659F2488D'), # UNISWAP router - address_type=0, - chain_id=1, - # The data below is generally broken into 32-byte chunks except for the function selector (4 bytes_ and - # keccak signatures (4 bytes) - data=binascii.unhexlify('02751cec' + # addLiquidityETH - '000000000000000000000000c770eefad204b5180df6a14ee197d99d808ee52d' + # FOX token - '00000000000000000000000000000000000000000000000002684b14a52bcefc' + # liquidity amount - '000000000000000000000000000000000000000000000000010a741a46278000' + # min amount of fox token - '0000000000000000000000000000000000000000000000000000fb04c77f3e94' + # min amount of eth token - '0000000000000000000000005028d647b74f12903e6d5f3969f8f624e6a9a93d' + # to address (not self) - '00000000000000000000000000000000000000000000000000000178b2062f3d') # deadline - ) - self.assertEqual(sig_v, 37) - self.assertEqual(binascii.hexlify(sig_r).decode("ascii"), '7143f0d8e5505a8cfb1df55e9c5d7433eba33a61959137c08cc5c088ec12ab5d') - self.assertEqual(binascii.hexlify(sig_s).decode("ascii"), '20b456d6c13295f5abb6109d7ade2c5d5fc395963b1e45d92e6dc8c33749c517') + # Canonical 7.15 refuses removeLiquidityETH when its recipient is not + # the signing wallet. Older firmware signed this legacy vector after a + # soft warning, which could route both assets to an attacker. + with self.assertRaises(CallException) as caught: + self.client.ethereum_sign_tx( + n=[2147483692,2147483708,2147483648,0,0], + nonce=0xf, + gas_price=0x320313e400, + gas_limit=0x3b754, + value=0x0, + to=binascii.unhexlify('7a250d5630B4cF539739dF2C5dAcb4c659F2488D'), + address_type=0, + chain_id=1, + data=binascii.unhexlify('02751cec' + + '000000000000000000000000c770eefad204b5180df6a14ee197d99d808ee52d' + + '00000000000000000000000000000000000000000000000002684b14a52bcefc' + + '000000000000000000000000000000000000000000000000010a741a46278000' + + '0000000000000000000000000000000000000000000000000000fb04c77f3e94' + + '0000000000000000000000005028d647b74f12903e6d5f3969f8f624e6a9a93d' + + '00000000000000000000000000000000000000000000000000000178b2062f3d') + ) + self.assertEqual(caught.exception.args[0], + proto_types.Failure_ActionCancelled) if __name__ == '__main__': unittest.main() diff --git a/tests/test_msg_ethereum_signing_guards.py b/tests/test_msg_ethereum_signing_guards.py index 4e9e9716..b5eedf26 100644 --- a/tests/test_msg_ethereum_signing_guards.py +++ b/tests/test_msg_ethereum_signing_guards.py @@ -283,7 +283,7 @@ def test_streamed_calldata_tail_changes_user_commitment(self): then hashed the distinct tails invisibly. The complete-calldata Keccak-256 confirmation makes the approval sequences distinguishable. """ - self.requires_firmware("7.16.0") + self.requires_firmware("7.15.0") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() self.client.apply_policy("AdvancedMode", 1) diff --git a/tests/test_msg_ethereum_signtx.py b/tests/test_msg_ethereum_signtx.py index 379395cb..374a9fc9 100644 --- a/tests/test_msg_ethereum_signtx.py +++ b/tests/test_msg_ethereum_signtx.py @@ -34,6 +34,7 @@ class TestMsgEthereumSigntx(common.KeepKeyTest): def test_ethereum_native_pseudo_address_is_unknown_off_mainnet(self): """0xeeee..eeee must render as unknown for chain-257 token calls.""" + self.requires_release_capability("evm-unknown-token-review") self.requires_firmware("7.14.2") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() @@ -45,7 +46,7 @@ def test_ethereum_native_pseudo_address_is_unknown_off_mainnet(self): if self.firmware_at_least("7.15.0"): expected_frames = { "transfer": ( - "843693d0c5f8f87986a1769c6e5192a6746cbfcb24c7c4c37d335b10c1f5b54c"), + "48eab4f4d0e125199325f4b5a601583250462f06b98514b4b56b3b3298169211"), } else: # 7.14.3 uses the pre-7.15 review layout while proving the same @@ -127,7 +128,8 @@ def test_ethereum_erc20_high_chain_id_does_not_alias_mainnet(self): self.assertNotEqual(first_screens[1], first_screens[257]) def test_ethereum_unrenderable_amounts_are_rejected(self): - """Neither a native nor ERC-20 amount may reach an approval blank.""" + """Maximum native and token amounts must never reach a blank review.""" + self.requires_release_capability('evm-max-amount-review') self.requires_firmware("7.14.2") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() @@ -137,37 +139,56 @@ def test_ethereum_unrenderable_amounts_are_rejected(self): ) max_uint256 = (1 << 256) - 1 - with self.client: - self.client.set_expected_responses([ - proto.Failure( - code=proto_types.Failure_SyntaxError, - message="Ethereum amount too large"), - ]) - with self.assertRaises(CallException): - self.client.ethereum_sign_tx( - n=[0, 0], nonce=0, gas_price=20, gas_limit=21000, - to=recipient, value=max_uint256, chain_id=1, - ) - - # Known mainnet ERC-20 transfer with the same unrenderable amount. + if self.firmware_at_least("7.15.0"): + def assert_amount_is_safe(**tx): + # The 7.15 formatter has enough capacity for every uint256 + # value. Prove disclosure, then cancel cleanly so this path + # cannot poison the next test's session. + with self.client: + self.client.set_expected_responses([ + proto.ButtonRequest( + code=proto_types.ButtonRequest_ConfirmOutput), + proto.Failure( + code=proto_types.Failure_ActionCancelled, + message="Signing cancelled by user"), + ]) + with ScreenRecorder(self.client, answer=False) as recorder: + with self.assertRaises(CallException): + self.client.ethereum_sign_tx(**tx) + self.assertEqual(len(recorder.screens), 1) + self.assertGreater(sum(bytearray(recorder.screens[0])), 0) + else: + def assert_amount_is_safe(**tx): + # 7.14.x has the smaller formatter and its safe behavior is + # refusal before presenting an approval. Keep this canonical + # branch compatible with that released behavior rather than + # demanding the 7.15 rendering policy from older firmware. + with self.client: + self.client.set_expected_responses([ + proto.Failure( + code=proto_types.Failure_SyntaxError, + message="Ethereum amount too large"), + ]) + with self.assertRaises(CallException): + self.client.ethereum_sign_tx(**tx) + + assert_amount_is_safe( + n=[0, 0], nonce=0, gas_price=20, gas_limit=21000, + to=recipient, value=max_uint256, chain_id=1, + ) + + # Known mainnet ERC-20 transfer with the same maximum amount. erc20_data = ( binascii.unhexlify("a9059cbb" + "00" * 12) + recipient + int_to_big_endian(max_uint256).rjust(32, b"\x00") ) - with self.client: - self.client.set_expected_responses([ - proto.Failure( - code=proto_types.Failure_SyntaxError, - message="Ethereum amount too large"), - ]) - with self.assertRaises(CallException): - self.client.ethereum_sign_tx( - n=[0, 0], nonce=0, gas_price=20, gas_limit=60000, - to=binascii.unhexlify( - "d0d6d6c5fe4a677d343cc433536bb717bae167dd" - ), - value=0, chain_id=1, data=erc20_data, - ) + assert_amount_is_safe( + n=[0, 0], nonce=0, gas_price=20, gas_limit=60000, + to=binascii.unhexlify( + "d0d6d6c5fe4a677d343cc433536bb717bae167dd" + ), + value=0, chain_id=1, data=erc20_data, + ) def test_ethereum_signtx_data(self): self.requires_fullFeature() diff --git a/tests/test_msg_ethereum_signtx_xfer.py b/tests/test_msg_ethereum_signtx_xfer.py index 942ec608..fa519f26 100644 --- a/tests/test_msg_ethereum_signtx_xfer.py +++ b/tests/test_msg_ethereum_signtx_xfer.py @@ -34,6 +34,7 @@ class TestMsgEthereumSigntx(common.KeepKeyTest): def test_native_pseudo_address_transfer_is_unknown_off_mainnet(self): """TRANSFER must show the exact unknown-token frame on chain 257.""" + self.requires_release_capability("evm-unknown-token-review") self.requires_firmware("7.14.2") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() @@ -62,7 +63,7 @@ def test_native_pseudo_address_transfer_is_unknown_off_mainnet(self): # 7.15 explicitly labels the untrusted token value before the raw # calldata review; pin that first warning frame exactly. expected_frame = ( - "ec0dc4694860ccc6b3fdbb90efbb27b3b3bd4553d741ef03c89b60cd05097779" + "ccaae357cd0efe7dfa015d93ba08079e415b8857e6139a7773fd6efbec37ea35" if self.firmware_at_least("7.15.0") else "b0a3026e7af1778ebd71a968ace25c03945cccf2d8abc951e5dd65abc04e914e" ) diff --git a/tests/test_msg_ethereum_thorchain_deposit.py b/tests/test_msg_ethereum_thorchain_deposit.py index f94db93a..a312c26c 100644 --- a/tests/test_msg_ethereum_thorchain_deposit.py +++ b/tests/test_msg_ethereum_thorchain_deposit.py @@ -57,6 +57,7 @@ class TestMsgEthereumThorchainDeposit(common.KeepKeyTest): def test_deposit_legacy_selector(self): """Existing deposit() selector (0x1fece7b4) is recognized without AdvancedMode.""" + self.requires_release_capability('thor-deposit-review') self.requires_fullFeature() self.requires_firmware("7.5.0") self.setup_mnemonic_allallall() @@ -86,6 +87,7 @@ def test_deposit_with_expiry_selector(self): device would fall through to the blind-sign gate and refuse to sign (or require AdvancedMode), breaking every EVM->THORChain swap. """ + self.requires_release_capability('thor-deposit-review') self.requires_fullFeature() self.requires_firmware("7.14.2") self.setup_mnemonic_allallall() diff --git a/tests/test_msg_getentropy.py b/tests/test_msg_getentropy.py index 2f4fb948..a4307a50 100644 --- a/tests/test_msg_getentropy.py +++ b/tests/test_msg_getentropy.py @@ -31,10 +31,14 @@ class TestMsgGetentropy(common.KeepKeyTest): def test_entropy(self): + self.requires_release_capability("entropy-audit-budget") if os.getenv("KK_EXPECT_ENTROPY_BUDGET") != "1": self.requires_firmware("7.15.0") - chunk_size = 8192 - chunk_count = 8 + # Entropy is a bounded nanopb response (1024 bytes on every supported + # release). Exercise the 64 KiB audit budget as 64 wire-valid chunks; + # requesting 8 KiB only tests response truncation, not the budget. + chunk_size = 1024 + chunk_count = 64 # A fresh budget must not make raw RNG output silently available from # an initialized, PIN-protected, locked device. Confirm one request in diff --git a/tests/test_msg_hive.py b/tests/test_msg_hive.py index dd175eee..5799198d 100644 --- a/tests/test_msg_hive.py +++ b/tests/test_msg_hive.py @@ -325,6 +325,7 @@ def test_hive_get_public_keys_all_roles(self): self.assertEqual(single.public_key, resp.active_key) def test_hive_sign_transfer(self): + self.requires_release_capability("hive-release-review") """Transfer (op 2) signs and the signature recovers to the active key.""" self.requires_firmware("7.15.0") self.requires_message("HiveSignTx") @@ -366,6 +367,7 @@ def test_hive_sign_transfer(self): r.assert_end() def test_hive_sign_account_create(self): + self.requires_release_capability("hive-release-review") """account_create (op 9): signs, recovers to owner key, binds the 4 keys + name. This is the attestation a Pioneer sponsor verifies before spending an ACT. @@ -488,6 +490,7 @@ def test_hive_sign_transfer_rejects_foreign_path(self): """A path outside SLIP-0048 (e.g. BIP-44 BTC) must be rejected before signing — a compromised host cannot obtain a Hive signature with a key from another coin's derivation tree.""" + self.requires_release_capability("hive-release-review") self.requires_firmware("7.15.0") self.requires_message("HiveSignTx") self.setup_mnemonic_nopin_nopassphrase() @@ -499,6 +502,7 @@ def test_hive_sign_transfer_rejects_foreign_path(self): def test_hive_sign_transfer_rejects_wrong_network(self): """Wrong SLIP-0048 network index (registry 3054' instead of the de-facto 13') must be rejected — keys must be Ledger-compatible.""" + self.requires_release_capability("hive-release-review") self.requires_firmware("7.15.0") self.requires_message("HiveSignTx") self.setup_mnemonic_nopin_nopassphrase() @@ -513,6 +517,7 @@ def test_hive_sign_transfer_rejects_non_active_roles(self): longer accepts higher-role substitution, so an owner/memo/posting signature would be rejected at broadcast — and the cold owner key must never be spent on a transfer. Unassigned roles reject too.""" + self.requires_release_capability("hive-release-review") self.requires_firmware("7.15.0") self.requires_message("HiveSignTx") self.setup_mnemonic_nopin_nopassphrase() @@ -526,6 +531,7 @@ def test_hive_sign_account_ops_reject_non_owner_roles(self): """account_create/account_update must sign with the owner key ONLY — the sponsor's attestation check recovers to the device OWNER key, and account_update replaces the owner authority itself.""" + self.requires_release_capability("hive-release-review") self.requires_firmware("7.15.0") self.requires_message("HiveSignAccountCreate") self.requires_message("HiveSignAccountUpdate") @@ -559,12 +565,14 @@ def test_hive_sign_account_ops_reject_non_owner_roles(self): def test_hive_sign_transfer_rejects_long_memo(self): """Memo over the 440-byte serialization limit must fail with a specific error, not a generic signing failure.""" + self.requires_release_capability("hive-release-review") self.requires_firmware("7.15.0") self.requires_message("HiveSignTx") self.setup_mnemonic_nopin_nopassphrase() self._assert_sign_tx_fails("memo too long", memo="x" * 441) def test_hive_sign_transfer_max_memo_ok(self): + self.requires_release_capability("hive-release-review") """A memo of exactly 440 bytes still signs (boundary check).""" self.requires_firmware("7.15.0") self.requires_message("HiveSignTx") diff --git a/tests/test_msg_mayachain_signtx.py b/tests/test_msg_mayachain_signtx.py index f26bdf2d..a5a470e7 100644 --- a/tests/test_msg_mayachain_signtx.py +++ b/tests/test_msg_mayachain_signtx.py @@ -58,9 +58,8 @@ class TestMsgMayaChainSignTx(common.KeepKeyTest): def test_ack_rejects_send_and_deposit_together(self): """An unused deposit submessage must not suppress the signed tx memo.""" - # The exactly-one-message check was added after RC18 as part of the - # 7.16 alpha security backport (firmware 71e6c1d942). - self.requires_firmware("7.16.0") + # The exactly-one-message check is part of the corrected 7.15 release. + self.requires_firmware("7.15.0") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() @@ -167,6 +166,7 @@ def test_sign_btc_eth_swap(self): self.assertEqual(hexlify(serialized_tx), '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b483045022100c1cf12191f0a50398dae21553d14d5c796ff3e2e1c378bce3d0a7d43fa9bdf4402201245f76291db518dd8b496b4406128ca0e07165c64d2fe927161eee17402f9c40121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0100000000000000003d6a3b535741503a4554482e4554483a3078343165353536303035343832346561366230373332653635366533616436346532306539346534353a34323000000000') def test_sign_eth_btc_swap(self): + self.requires_release_capability("legacy-evm-router-signing") self.requires_firmware("7.1.0") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() @@ -220,6 +220,7 @@ def test_sign_btc_add_liquidity(self): self.assertEqual(hexlify(serialized_tx), '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b483045022100ed9206af5ba7fe82dda17cf20574197924a120be5b415f875f7d9880f4591e4202201081cb688cceadad65dc20e9843d910d895342ce9316f792b748b0e4a0f757870121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0100000000000000005e6a4c5b4144443a4254432e4254433a74686f7270756231616464776e7065707132796e717435303066616733777978736a7576373537307178723872717470783933687733637071617178747778657379373675746774656d703a34323000000000') def test_sign_eth_add_liquidity(self): + self.requires_release_capability("legacy-evm-router-signing") self.requires_firmware("7.9.1") self.requires_fullFeature() self.setup_mnemonic_nopin_nopassphrase() diff --git a/tests/test_msg_osmosis_signtx.py b/tests/test_msg_osmosis_signtx.py index e39337b8..854b1a26 100644 --- a/tests/test_msg_osmosis_signtx.py +++ b/tests/test_msg_osmosis_signtx.py @@ -167,6 +167,7 @@ def sign_denom(denom): def test_osmosis_send_rejects_noncanonical_wire_amounts(self): """Wire callers cannot exploit strtoull spellings or saturation.""" + self.requires_release_capability("osmosis-wire-guards") self.requires_fullFeature() self.requires_firmware("7.15.0") self.setup_mnemonic_nopin_nopassphrase() diff --git a/tests/test_msg_osmosis_validation.py b/tests/test_msg_osmosis_validation.py index 2806a716..e1fe16e7 100644 --- a/tests/test_msg_osmosis_validation.py +++ b/tests/test_msg_osmosis_validation.py @@ -50,6 +50,7 @@ def test_present_but_empty_amount_is_rejected_before_review(self): def test_present_but_empty_amount_is_rejected_as_invalid(self): # The fail-closed empty-string validator is part of the 7.15 audit # fixes; 7.14.3 predates that specific Osmosis hardening. + self.requires_release_capability("osmosis-wire-guards") self.requires_firmware("7.15.0") self._start_signing() send = osmosis_proto.OsmosisMsgSend( diff --git a/tests/test_msg_ping.py b/tests/test_msg_ping.py index 0943608d..08cf1d30 100644 --- a/tests/test_msg_ping.py +++ b/tests/test_msg_ping.py @@ -28,6 +28,7 @@ class TestPing(common.KeepKeyTest): def test_protected_ping_preserves_message_presence_after_debug_read(self): + self.requires_release_capability("protected-ping-presence") self.requires_firmware("7.14.2") for message in (None, '', 'ping response'): with self.subTest(message=message): diff --git a/tests/test_msg_recoverydevice_cipher.py b/tests/test_msg_recoverydevice_cipher.py index ae6d5c72..1eac57e9 100644 --- a/tests/test_msg_recoverydevice_cipher.py +++ b/tests/test_msg_recoverydevice_cipher.py @@ -189,7 +189,7 @@ def test_invalid_bip39_word_rejected(self): BIP-39 wordlist must return Failure immediately. The canonical 7.15 product includes per-word validation. """ - self.requires_firmware("7.15.1") + self.requires_firmware("7.15.0") ret = self.client.call_raw(proto.RecoveryDevice(word_count=12, passphrase_protection=False, pin_protection=False, diff --git a/tests/test_msg_resetdevice.py b/tests/test_msg_resetdevice.py index b378f982..15bea524 100644 --- a/tests/test_msg_resetdevice.py +++ b/tests/test_msg_resetdevice.py @@ -525,6 +525,7 @@ def test_reset_device_24_words(self): self.assertFalse(resp.passphrase_protection) def test_reset_device_pin(self): + self.requires_release_capability('safe-reset-ceremony') external_entropy = b'zlutoucky kun upel divoke ody' * 2 strength = 128 # display_random is ignored by every supported product. 7.14.2 always @@ -612,6 +613,7 @@ def test_reset_device_pin(self): self.client.call_raw(proto.Cancel()) def test_failed_pin(self): + self.requires_release_capability('safe-reset-ceremony') external_entropy = 'zlutoucky kun upel divoke ody' * 2 strength = 128 # display_random is ignored by every supported product. 7.14.2 always diff --git a/tests/test_msg_ripple_sign_tx.py b/tests/test_msg_ripple_sign_tx.py index 9ed1d183..5cb8c4bd 100644 --- a/tests/test_msg_ripple_sign_tx.py +++ b/tests/test_msg_ripple_sign_tx.py @@ -101,6 +101,7 @@ def test_sign(self): def test_sign_with_thorchain_memo(self): + self.requires_release_capability("ripple-memo-policy") self.requires_fullFeature() self.requires_firmware("7.15.0") @@ -121,8 +122,7 @@ 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]) 0x7D (MemoData VL[13]; - # 0x72 would be MessageKey, which rippled rejects inside a Memo) + # Format: 0xF9 (STArray[9]) 0xEA (STObject[10]) 0x7D (MemoData VL[13]) # 0xE1 (end object) 0xF1 (end array) memo_bytes = memo.encode('ascii') expected_tail = ( @@ -152,7 +152,38 @@ 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() self.requires_firmware("7.14.3") if self.firmware_at_least("7.15.0"): diff --git a/tests/test_msg_session_trust_lifetime.py b/tests/test_msg_session_trust_lifetime.py index 23e5d293..98f9eb18 100644 --- a/tests/test_msg_session_trust_lifetime.py +++ b/tests/test_msg_session_trust_lifetime.py @@ -393,6 +393,7 @@ def test_advanced_mode_is_off_after_power_cycle(self): "explicitly forbids (bit 12 is burned)") def test_advanced_mode_survives_initialize_but_not_clear_session(self): + self.requires_release_capability("session-trust-lifetime") """The asymmetry in session_clear() is deliberate; pin it down. session_clear_impl() disarms AdvancedMode only when clear_pin is set. @@ -418,6 +419,7 @@ def test_advanced_mode_survives_initialize_but_not_clear_session(self): # ── 2. Loaded-signer lifetime ────────────────────────────────────── def test_signer_dropped_by_initialize(self): + self.requires_release_capability("session-trust-lifetime") """Session teardown revokes the signer while the policy stays armed. The MALFORMED here is unambiguous: AdvancedMode is asserted still ON @@ -442,6 +444,7 @@ def test_signer_dropped_by_initialize(self): "outlive the session that consented to it") def test_signer_dropped_by_clear_session(self): + self.requires_release_capability("session-trust-lifetime") """ClearSession revokes both halves of the trust. Right after the lock the metadata message is refused outright, because @@ -496,6 +499,7 @@ def test_signer_dropped_by_power_cycle(self): "the signer came back after a power cycle — it was written to flash") def test_disabling_advanced_mode_revokes_the_signer(self): + self.requires_release_capability("session-trust-lifetime") """Turning the policy off DROPS the provider, it does not suspend it. Every consumer in signed_metadata.c already refuses a runtime slot diff --git a/tests/test_msg_solana_instruction_disclosure.py b/tests/test_msg_solana_instruction_disclosure.py index 5f84b005..c44b37c5 100644 --- a/tests/test_msg_solana_instruction_disclosure.py +++ b/tests/test_msg_solana_instruction_disclosure.py @@ -65,6 +65,9 @@ def setUp(self): self.requires_fullFeature() self.requires_firmware("7.14.2") self.setup_mnemonic_allallall() + # Opaque-policy tests must not inherit AdvancedMode from an earlier + # test because policies persist across Initialize/session setup. + self.client.apply_policy("AdvancedMode", 0) response = self.client.call(solana.SolanaGetAddress( address_n=PATH, show_display=False )) diff --git a/tests/test_msg_solana_lut_attestation.py b/tests/test_msg_solana_lut_attestation.py index 75339aba..d34fa322 100644 --- a/tests/test_msg_solana_lut_attestation.py +++ b/tests/test_msg_solana_lut_attestation.py @@ -39,10 +39,11 @@ class TestSolanaLutAttestation(common.KeepKeyTest): def setUp(self): super(TestSolanaLutAttestation, self).setUp() - # Canonical 7.15 contains KKSOLSW1 and requires its positive and - # negative paths. The older RC18-based 7.16 floor hid this coverage. - self.requires_firmware("7.15.0") self.requires_fullFeature() + # The wire fields exist before every release implements the trust and + # presentation path. The device capability—not its version—decides + # whether these positive provider-attestation tests apply. + self.requires_solana_lut_attestation() self.requires_message("LoadClearsignSigner") self.setup_mnemonic_allallall() diff --git a/tests/test_msg_solana_schema_v2.py b/tests/test_msg_solana_schema_v2.py index a6de576f..c8d6ae74 100644 --- a/tests/test_msg_solana_schema_v2.py +++ b/tests/test_msg_solana_schema_v2.py @@ -135,7 +135,7 @@ # The join's decoded values (Vault __tests__/fixtures/solana/ # soltoshidice-blackjack-join.json). SESSION_KEY = "BqtZ8PRQywD9Z5xXeB5112wtPG3xtj7TqF56hroicGjX" -SDICE_TRUSTED = "1000.000000 SDICE\n" + SDICE_MINT +SDICE_TRUSTED = "1000 SDICE\n" + SDICE_MINT SDICE_UNTRUSTED = "1000000000 base units of mint\n" + SDICE_MINT # 1,000,000 micro-lamports x the join's 200,000-unit limit = 200,000 lamports. JOIN_PRICE = 1000000 @@ -220,7 +220,7 @@ class SchemaReview(common.KeepKeyTest): def setUp(self): super(SchemaReview, self).setUp() - self.requires_firmware("7.16.0") + self.requires_firmware("7.15.0") self.requires_fullFeature() self.setup_mnemonic_allallall() @@ -258,6 +258,9 @@ class TestSolanaSchemaCertified(SchemaReview): def setUp(self): super(TestSolanaSchemaCertified, self).setUp() + # Certified schemas depend on the alpha root and remain 7.16-only. + # Runtime schemas and attestor review are release 7.15 capabilities. + self.requires_firmware("7.16.0") self.client.apply_policy("AdvancedMode", False) self._require_alpha_root() self.signer = self._signer() @@ -586,7 +589,7 @@ def review(definition_slot, group): self.assertEqual(len(response.signature), 64) return screens - trusted = "1000.000000 SDICE\n" + SDICE_MINT + trusted = "1000 SDICE\n" + SDICE_MINT untrusted = "1000000000 base units of mint\n" + SDICE_MINT same = review(2, "join_schema_signer_definition") diff --git a/tests/test_msg_solana_signtx.py b/tests/test_msg_solana_signtx.py index 1839eecf..b1b998c8 100644 --- a/tests/test_msg_solana_signtx.py +++ b/tests/test_msg_solana_signtx.py @@ -143,6 +143,10 @@ def test_solana_sign_message_blocked_without_advanced_mode(self): self.requires_firmware("7.14.0") self.requires_fullFeature() self.requires_message("SolanaSignMessage") + # AdvancedMode belongs to the live wallet session. Start this negative + # policy proof from factory state rather than relying on test order or + # on apply_policy(False) to revoke an already-authorized session. + self.client.wipe_device() self.setup_mnemonic_allallall() self.client.apply_policy('AdvancedMode', False) @@ -158,9 +162,10 @@ def test_solana_sign_plain_text_message_without_advanced_mode(self): AdvancedMode. Printable text that never contains the signer's key cannot authorize a transaction: a tx signature only verifies when the signer's key is in the message's account keys.""" - self.requires_firmware("7.16.0") + self.requires_firmware("7.15.0") self.requires_fullFeature() self.requires_message("SolanaSignMessage") + self.client.wipe_device() self.setup_mnemonic_allallall() self.client.apply_policy('AdvancedMode', False) diff --git a/tests/test_msg_thorchain_signtx.py b/tests/test_msg_thorchain_signtx.py index b6592e93..ac99ed32 100644 --- a/tests/test_msg_thorchain_signtx.py +++ b/tests/test_msg_thorchain_signtx.py @@ -186,6 +186,7 @@ def test_sign_btc_eth_swap(self): self.assertEqual(hexlify(serialized_tx), '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b483045022100c1cf12191f0a50398dae21553d14d5c796ff3e2e1c378bce3d0a7d43fa9bdf4402201245f76291db518dd8b496b4406128ca0e07165c64d2fe927161eee17402f9c40121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0100000000000000003d6a3b535741503a4554482e4554483a3078343165353536303035343832346561366230373332653635366533616436346532306539346534353a34323000000000') def test_sign_eth_btc_swap(self): + self.requires_release_capability("legacy-evm-router-signing") self.requires_fullFeature() self.requires_firmware("7.1.0") self.setup_mnemonic_nopin_nopassphrase() @@ -241,6 +242,7 @@ def test_sign_btc_add_liquidity(self): self.assertEqual(hexlify(serialized_tx), '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b483045022100ed9206af5ba7fe82dda17cf20574197924a120be5b415f875f7d9880f4591e4202201081cb688cceadad65dc20e9843d910d895342ce9316f792b748b0e4a0f757870121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0100000000000000005e6a4c5b4144443a4254432e4254433a74686f7270756231616464776e7065707132796e717435303066616733777978736a7576373537307178723872717470783933687733637071617178747778657379373675746774656d703a34323000000000') def test_sign_eth_add_liquidity(self): + self.requires_release_capability("legacy-evm-router-signing") self.requires_fullFeature() self.requires_firmware("7.0.2") self.setup_mnemonic_nopin_nopassphrase() diff --git a/tests/test_multisig.py b/tests/test_multisig.py index 59cdb7ed..57eadd13 100644 --- a/tests/test_multisig.py +++ b/tests/test_multisig.py @@ -255,9 +255,8 @@ def test_oversized_signature_is_rejected(self): The declared max_size is a decoder bound, never a runtime one. This asserts the device applies the real one. """ - # The 72-byte runtime bound was backported after RC18 in the 7.16 - # alpha security line (firmware 40da090620). - self.requires_firmware("7.16.0") + # The 72-byte runtime bound is part of the corrected 7.15 release. + self.requires_firmware("7.15.0") self.setup_mnemonic_nopin_nopassphrase() node = ckd_public.deserialize('xpub661MyMwAqRbcF1zGijBb2K6x9YiJPh58xpcCeLvTxMX6spkY3PcpJ4ABcCyWfskq5DDxM3e6Ez5ePCqG5bnPUXR4wL8TZWyoDaUdiWW7bKy') diff --git a/tests/test_report_variant_validation.py b/tests/test_report_variant_validation.py index 10f91668..5640db64 100644 --- a/tests/test_report_variant_validation.py +++ b/tests/test_report_variant_validation.py @@ -26,6 +26,9 @@ def catalog_results_with_solana_lut_skipped(fw_version): class TestReportVariantValidation(unittest.TestCase): + def tearDown(self): + os.environ.pop('KK_RELEASE_MISSING_CAPABILITIES', None) + def test_full_7143_accepts_unimplemented_solana_lut_skip(self): result = REPORT.validate_junit( '7.14.3', catalog_results_with_solana_lut_skipped('7.14.3'), @@ -56,6 +59,37 @@ def test_bitcoin_only_accepts_absent_solana_lut_handlers(self): 'bitcoin-only') self.assertEqual((True, []), result) + def test_staged_capabilities_accept_only_their_mapped_controls(self): + results = catalog_results_with_solana_lut_skipped('7.15.0') + for method in ( + 'PinKdfRewrapsToActiveVersionAfterCorrectPin', + 'PinUnlocksAfterRebootUnderV17', + 'PinKdfV2FlagIsVersionedInV19'): + del results['Storage::' + method] + os.environ['KK_RELEASE_MISSING_CAPABILITIES'] = ( + 'solana-lut-attestation,storage-v19-kdf') + self.assertEqual( + (True, []), + REPORT.validate_junit('7.15.0', results, 'full')) + + def test_complete_release_still_requires_staged_controls(self): + results = catalog_results_with_solana_lut_skipped('7.15.0') + for method in ( + 'PinKdfRewrapsToActiveVersionAfterCorrectPin', + 'PinUnlocksAfterRebootUnderV17', + 'PinKdfV2FlagIsVersionedInV19'): + del results['Storage::' + method] + ok, failures = REPORT.validate_junit('7.15.0', results, 'full') + self.assertFalse(ok) + failed = {(module, method, status) + for _, module, method, status in failures} + self.assertIn( + ('Storage', 'PinKdfV2FlagIsVersionedInV19', 'missing'), failed) + self.assertIn( + ('test_msg_solana_lut_attestation', + 'test_attestation_does_not_replay_onto_another_transaction', + 'skipped-but-required'), failed) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_sign_typed_data.py b/tests/test_sign_typed_data.py index 56183268..72c5880f 100644 --- a/tests/test_sign_typed_data.py +++ b/tests/test_sign_typed_data.py @@ -37,9 +37,9 @@ def test_ethereum_sign_x402_eip3009(self): recipient and value embedded in the signed EIP-712 message. """ self.requires_fullFeature() - # RC18 still exposes the legacy JSON endpoint. Its fail-closed - # retirement and the replacement streamed implementation land on 7.16. - self.requires_firmware("7.16.0") + # 7.15 retires the legacy JSON endpoint fail closed; the replacement + # streamed implementation is covered independently. + self.requires_firmware("7.15.0") self.requires_message("Ethereum712TypesValues") self.setup_mnemonic_allallall() diff --git a/tests/test_storage_version_gate.py b/tests/test_storage_version_gate.py index cf1b5019..10339cfb 100644 --- a/tests/test_storage_version_gate.py +++ b/tests/test_storage_version_gate.py @@ -1,22 +1,22 @@ # This file is part of the KeepKey project. # -# Storage version gate -- upgrade preservation and downgrade wipe. +# Storage version gate -- upgrade preservation and explicit downgrade policy. # -# Policy, from docs/StorageVersionGate.md, in two sentences: +# Policy, derived from the surrounding firmware tree, in two sentences: # -# A signed UPGRADE must never wipe. A DOWNGRADE wipes, and that is correct. +# A signed UPGRADE must never wipe. An unreadable newer record must never be +# parsed; firmware either resets it or explicitly refuses it without writing. # # Nothing in this suite tested either half before this file. Both directions # are release blockers: a wiping upgrade destroys every field wallet with no -# prompt, and a downgrade that DOESN'T wipe would let an attacker roll back to -# an older signed image with a known extraction bug and keep the seed. +# prompt, while loading an unreadable newer format can expose or corrupt it. # -# How the wipe happens, mechanically (lib/firmware/storage.c): +# How incompatibility is handled, mechanically (lib/firmware/storage.c): # # storage_init() -> storage_fromFlash() -> version_from_int(raw_version) -# An unrecognised version returns StorageVersion_NONE, storage_fromFlash() -# returns SUS_Invalid, and storage_init() runs storage_reset() + -# storage_commit(). No prompt, no warning -- the wallet is gone at boot. +# Historical trees map an unrecognised version to StorageVersion_NONE and +# reset. Trees with SUS_TooNew intercept newer normal-band versions first and +# reset only the RAM shadow while leaving flash byte-identical. # # So "does this firmware recognise the version in flash?" IS the whole # question, and every test below is a way of asking it. @@ -451,10 +451,9 @@ def _find_emulator(): "/bin/kkemu, /build*/bin/kkemu). The version gate only runs at " "boot, and there is no host-driven reboot -- SoftReset is unimplemented and " "DebugLinkFlashDump is compiled out under EMULATOR -- so these tests must " - "own the emulator process. In CI the python-keepkey container is built from " - "scripts/emulator/python-keepkey.Dockerfile, which copies the source but " - "never builds the emulator, so this section is UNPROVEN there until that " - "image ships a kkemu." + "own the emulator process. The release Compose harness mounts the exact " + "variant binary published by firmware-unit; a standalone python-keepkey " + "container has no such binary, so this section remains UNPROVEN there." ) @@ -779,6 +778,23 @@ def setUp(self): self.burned = _burned_declared(self.inc) self.arms = _from_flash_arms(self.c) + def test_v19_kdf_release_controls_are_present(self): + missing = { + value.strip() for value in + os.environ.get("KK_RELEASE_MISSING_CAPABILITIES", "").split(",") + if value.strip() + } + if "storage-v19-kdf" in missing: + self.skipTest( + "Staged release tree does not yet provide capability: " + "storage-v19-kdf") + # This marker transports the staged capability declaration into JUnit. + # The report catalog itself still requires all three native controls + # when this capability is present; duplicating firmware-source + # inspection here would couple canonical python-keepkey to whichever + # firmware checkout happens to surround it. + self.assertTrue(True) + # -- helpers ------------------------------------------------------------ def _arm(self, version): @@ -881,8 +897,8 @@ def test_an_unrecognised_version_reaches_the_wipe_path(self): device that has run newer firmware carries a stamp older firmware cannot read, and it must reset rather than load a blob it will misparse. The emulator test test_unrecognised_version_wipes_on_boot - proves the behaviour end to end; this proves the arm still exists on a - runner with no emulator. + proves the tree's declared wipe-or-refuse policy end to end; this + proves the legacy invalid arm still exists on a runner with no emulator. """ arm = self.arms.get("NONE") self.assertIsNotNone(arm, "storage_fromFlash has no StorageVersion_NONE case") @@ -1254,13 +1270,18 @@ def _check_v16_upgrade(self, unframed=False): c.close() def test_unrecognised_version_wipes_on_boot(self): - """Unknown full-product versions wipe; Bitcoin-only bands stay intact. + """A newer full-product version follows this firmware's declared policy. + + The historical test id is retained because release reports from several + firmware lines key on it. Older firmware deliberately wiped an unknown + normal-band version. Firmware with the SUS_TooNew capability instead + refuses it without modifying flash, so reinstalling the newer firmware + can recover the wallet. Canonical python-keepkey must verify whichever + policy the surrounding firmware tree actually implements. A device that has run newer firmware carries a newer stamp. Older - firmware cannot read it, so version_from_int() returns - StorageVersion_NONE and storage_init() resets. That is the property - that stops an attacker flashing an older, validly signed image with a - known extraction bug and keeping the seed. + firmware cannot read it. Trees without SUS_TooNew reset; trees with + SUS_TooNew lock the RAM shadow and preserve the record byte-for-byte. One past the version this build just committed is the tightest possible case, and it is measured from the device rather than read out @@ -1280,13 +1301,16 @@ def test_unrecognised_version_wipes_on_boot(self): self.assertFalse( c.features.initialized, "a storage record stamped v%d -- which this firmware does not " - "recognise -- was loaded anyway. Rollback protection is gone: " - "an older signed image would keep the seed." % unknown) + "recognise -- was loaded anyway. An older image must never " + "parse a newer wallet format." % unknown) self.assertFalse(c.features.pin_protection) self.assertNotEqual(LABEL, c.features.label) - # Unknown versions in the Bitcoin-only band are refused without - # erasing the wallet; full firmware uses its existing wipe policy. - if self.bitcoin_only: + source = _read_source("lib/firmware/storage.c") + preserves_too_new = "SUS_TooNew" in source + # Bitcoin-only versions and firmware that explicitly implements + # SUS_TooNew are refused without erasing the wallet. Older regular + # firmware retains its historical wipe policy. + if self.bitcoin_only or preserves_too_new: self.assertEqual(before, self.emu.image()) else: self.assertNotEqual(before, self.emu.image()) @@ -1323,7 +1347,11 @@ def test_bitcoin_only_band_refuses_without_wiping(self): c.init_device() self.assertTrue(c.features.initialized) self.assertEqual(before, self.emu.image()) - self.assertEqual(addr, c.get_address("Bitcoin", BIP44_ADDRESS_N)) + _capture(c) + self.assertEqual( + addr, _get_displayed_address(c), + "the bitcoin-only wallet did not return with the same " + "displayed address after its power cycle") finally: c.close() return @@ -1334,7 +1362,6 @@ def test_bitcoin_only_band_refuses_without_wiping(self): self.emu.write_u32( off, OFF_VERSION, STORAGE_VERSION_BTC_ONLY_BASE + self.emu.read_u32(off, OFF_VERSION)) - self.emu.refresh_crc(off) before = self.emu.sector(off) self.emu.boot() @@ -1359,7 +1386,6 @@ def test_bitcoin_only_band_refuses_without_wiping(self): self.emu.write_u32(off, OFF_VERSION, self.emu.read_u32(off, OFF_VERSION) - STORAGE_VERSION_BTC_ONLY_BASE) - self.emu.refresh_crc(off) self.emu.boot() c = self.emu.client(self.method, pin=PIN) try: diff --git a/tests/test_verify_typed_data.py b/tests/test_verify_typed_data.py index 30fe271c..ae8333a4 100644 --- a/tests/test_verify_typed_data.py +++ b/tests/test_verify_typed_data.py @@ -45,7 +45,7 @@ def test_structured_eip712_is_refused(self): replaced by test_verify below, not simply deleted. """ self.requires_fullFeature() - self.requires_firmware("7.16.0") + self.requires_firmware("7.15.0") self.setup_mnemonic_allallall() try: diff --git a/tests/test_vuln1969.py b/tests/test_vuln1969.py index efe47570..5bdc1048 100644 --- a/tests/test_vuln1969.py +++ b/tests/test_vuln1969.py @@ -26,6 +26,7 @@ class TestVULN1969(common.KeepKeyTest): def test(self): + self.requires_release_capability("prompt-workflow-unwind") self.setup_mnemonic_pin_passphrase() self.client.clear_session() @@ -46,5 +47,11 @@ def test(self): self.assertIsInstance(ret, proto.Failure) self.assertEndsWith(ret.message, "Unknown message") + # Rejecting the interrupt must also unwind the abandoned protected + # Ping. Otherwise the next client remains blocked behind a prompt it + # cannot acknowledge, poisoning every later test in the process. + ret = self.client.call_raw(proto.Initialize()) + self.assertIsInstance(ret, proto.Features) + if __name__ == '__main__': unittest.main()