diff --git a/.circleci/config.yml b/.circleci/config.yml index 95f8a880..ca41ba78 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: name: Checkout current branch over HTTPS command: | git clone --depth 1 -b "$CIRCLE_BRANCH" \ - https://github.com/keepkey/python-keepkey.git . + "https://github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}.git" . git submodule update --init --recursive - run: name: Validate canonical Python and EOS vector contracts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28caa5f0..cc7aaf09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,11 +125,32 @@ jobs: with: repository: BitHighlander/keepkey-firmware # 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: 9b0583a41b758af7fb4c08280ab2a2bd8bb5d503 + # Accepted consolidated 7b (#871): supplies the compiler/runtime + # contracts exercised by the canonical Python stack through 08. + ref: 2483977523d9b6beab52a69429ace9aeefd00f76 path: keepkey-firmware + - name: Bind the staged 7.15 capability ledger to the pinned firmware + run: | + python - <<'PYCODE' + import os + import pathlib + import re + # The preceding checkout is the immutable accepted 7b commit. Its + # owner-reviewed omissions apply only to this staged 7.15 job; + # no Python test assertion or default release behavior is changed. + workflow = pathlib.Path('keepkey-firmware/.github/workflows/ci.yml').read_text() + values = re.findall(r'^ KK_RELEASE_MISSING_CAPABILITIES: ([a-z0-9,-]*)$', + workflow, re.MULTILINE) + if len(values) != 1: + raise SystemExit('Expected one explicit ledger in pinned staged firmware') + with open(os.environ['GITHUB_ENV'], 'a') as env: + env.write('KK_RELEASE_MISSING_CAPABILITIES=' + values[0] + '\n') + with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as summary: + summary.write('Staged 7.15 firmware 2483977523d9b6beab52a69429ace9aeefd00f76; ' + 'deferred capabilities: `' + values[0] + '`.\n') + PYCODE + # NOT `submodules: recursive`. trezor-firmware carries a micropython # vendor tree whose lib/lwip lives on git.savannah.gnu.org, which serves # dumb HTTP and cannot do the shallow clone actions/checkout requests -- @@ -555,7 +576,7 @@ jobs: firmware_ref: 4125e1c7409b1cb7b08ba595bc408e3128fc24ca min_fw: "7.14.3" - release: "7.15" - firmware_ref: 9b0583a41b758af7fb4c08280ab2a2bd8bb5d503 + firmware_ref: 2483977523d9b6beab52a69429ace9aeefd00f76 min_fw: "7.15.0" # KK_BITCOIN_ONLY=ON is a second shipping product, not a build flavour: diff --git a/device-protocol b/device-protocol index dd9c85dc..5fec9e69 160000 --- a/device-protocol +++ b/device-protocol @@ -1 +1 @@ -Subproject commit dd9c85dc747cf965fb0e7bf49615dc9e7568ee65 +Subproject commit 5fec9e6906a340be5eb3d795ec746769065b2db8 diff --git a/keepkeylib/debuglink.py b/keepkeylib/debuglink.py index efd308c9..34abcb87 100644 --- a/keepkeylib/debuglink.py +++ b/keepkeylib/debuglink.py @@ -91,6 +91,12 @@ def read_dice_digest(self): obj = self._call(proto.DebugLinkGetState()) return obj.dice_digest + def read_confirm_text(self): + """(title, body) of the confirmation currently or last shown, exactly + as the firmware formatted them, before the body is paged.""" + obj = self._call(proto.DebugLinkGetState()) + return (obj.confirm_title, obj.confirm_body) + def read_passphrase_protection(self): obj = self._call(proto.DebugLinkGetState()) return obj.passphrase_protection diff --git a/keepkeylib/erc7730.py b/keepkeylib/erc7730.py index 7a9e88db..f2cbe449 100644 --- a/keepkeylib/erc7730.py +++ b/keepkeylib/erc7730.py @@ -166,7 +166,17 @@ def resolve(self, request): return definition def chunk(self, request): - definition = self.resolve(request) + try: + definition = self.resolve(request) + except KeyError: + if (not request.HasField("recursion_depth") or + request.recursion_depth == 0): + raise + # An embedded call's definition that the catalog does not hold: + # the empty chunk means "none". Firmware 7.15 then shows the + # inner call under a blind-sign warning; 7.16 rejects it. + return ethereum.EthereumClearSignDefinitionChunk( + definition_id=bytes(32), offset=0, total_length=0, data=b"") offset = request.offset length = request.length if length == 0 or length > MAX_CHUNK or offset >= len(definition.envelope): diff --git a/keepkeylib/erc7730_compiler.py b/keepkeylib/erc7730_compiler.py index 08501b09..d38c8843 100644 --- a/keepkeylib/erc7730_compiler.py +++ b/keepkeylib/erc7730_compiler.py @@ -339,7 +339,9 @@ def _condition_literal(node, value): if node.kind == 1 and isinstance(value, int) and not isinstance(value, bool): return 1, _unsigned_literal(value) if node.kind == 2 and isinstance(value, int) and not isinstance(value, bool): - width = max(1, (value.bit_length() + 8) // 8) + # Minimal two's complement, as the device requires: -128 is 0x80. + width = max(1, ((value if value >= 0 else ~value).bit_length() + 8) + // 8) return 2, value.to_bytes(width, "big", signed=True) if node.kind == 3 and isinstance(value, str): return 5, _hex_address(value) @@ -476,19 +478,23 @@ def flatten_fields(items, prefix=None): interpolation_tokens = [] interpolation_values = set() if interpolation is not None: + # The device shows each fragment and value as its own numbered + # screen and escapes edge spaces, so trim fragments here; a + # fragment of spaces alone separates nothing and is dropped. + def add_text(text): + text = text.strip() + if text: + strings.add(text) + interpolation_tokens.append(("text", text)) cursor = 0 for match in re.finditer(r"\{([^{}]+)\}", interpolation): if match.start() > cursor: - text = interpolation[cursor:match.start()] - strings.add(text) - interpolation_tokens.append(("text", text)) + add_text(interpolation[cursor:match.start()]) interpolation_tokens.append(("value", match.group(1))) interpolation_values.add(normalized_path(match.group(1))) cursor = match.end() if cursor < len(interpolation): - text = interpolation[cursor:] - strings.add(text) - interpolation_tokens.append(("text", text)) + add_text(interpolation[cursor:]) for record in self.token_records: strings.add(record[2]) for record in self.network_records: @@ -670,6 +676,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"): @@ -768,6 +779,12 @@ def intern_path(steps): if not callee: raise ValueError("embedded calldata requires calleePath") arguments.append((15, 1, intern_path(_resolve_path(root, callee)))) + # 17: the native value the inner call moves; 18: whose + # authority it runs with. + for role, key in ((17, "amountPath"), (18, "spenderPath")): + if params.get(key): + arguments.append((role, 1, intern_path( + _resolve_path(root, params[key])))) formatter_index = len(formatters) formatters.append((kind, arguments)) condition = ABSENT @@ -847,6 +864,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 +881,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: @@ -947,9 +979,380 @@ def depth(node): for kind, value in sections) -def compile_calldata(descriptor, signature, chain_id, address, **kwargs): - return CalldataCompiler(descriptor, signature, chain_id, address, - **kwargs).compile() +# What the device executes: a mirror of keepkey-firmware's +# include/keepkey/firmware/erc7730_capabilities.h. The device refuses every +# program outside it at preload, before the first screen. Widen this only +# together with the firmware table. +_PATH, _LITERAL, _STRING = frozenset((1,)), frozenset((2,)), frozenset((3,)) +DEVICE_CAPABILITIES = { + # 2 and 3 (interpolated intent) only as one run directly after the intent; + # 5/6 groups; 7/8 one iteration at a time, calldata only + "display_opcodes": frozenset((1, 2, 3, 4, 5, 6, 7, 8, 10)), + # formatter kind -> (argument role -> permitted sources, required roles) + "formatters": { + 1: ({1: _PATH}, frozenset((1,))), # raw + 2: ({1: _PATH}, frozenset((1,))), # amount + 3: ({1: _PATH, 2: _PATH, 7: _LITERAL, 8: _STRING, # tokenAmount + 22: _LITERAL}, frozenset((1, 2))), + 4: ({1: _PATH, 3: _PATH}, frozenset((1, 3))), # nftName + 5: ({1: _PATH, 9: _STRING}, frozenset((1,))), # date + 6: ({1: _PATH}, frozenset((1,))), # duration + 7: ({1: _PATH, 4: _LITERAL, 5: _STRING, 6: _LITERAL}, # unit + frozenset((1, 5))), + 8: ({1: _PATH, 10: _LITERAL}, frozenset((1, 10))), # enum + 10: ({1: _PATH}, frozenset((1,))), # addressName + 13: ({1: _PATH, 15: _PATH, 17: _PATH, 18: _PATH}, # calldata + frozenset((1, 15))), + }, + "path_sources": frozenset((1, 2, 3)), + # @.from, @.to and @.value, calldata definitions only + "containers": frozenset((1, 2, 3)), + # 2: every element, bound to the iteration's current element + "path_step_opcodes": frozenset((1, 2)), + # only "optional" (3), which is always shown + "condition_opcodes": frozenset((3,)), + "conditions": True, + "alias_set_max": 4, + "enum_max": 16, + # signer text shown inside a value's screen, and unit decimals + "signer_text_max": 64, + "unit_decimals_max": 77, +} +# The verifier's table limits (erc7730_catalog.c). +TABLE_LIMITS = {1: 96, 2: 64, 3: 64, 4: 64, 5: 32, 6: 64, 7: 64, 8: 64} +# Value classes: 1-7 are ABI leaf kinds; literals map to what they hold. +CLASS_BYTES = 6 +(CLASS_UINT, CLASS_INT, CLASS_ADDRESS, CLASS_BOOL, CLASS_STRING, + CLASS_STRING_REF, CLASS_ALIAS_SET, CLASS_UINT_SMALL, CLASS_DATE_ENCODING, + CLASS_ENUM_MAP, CLASS_FLAG) = 1, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13 + + +def _value_allowed(kind, role, cls): + """Mirror of erc7730_cap_value().""" + if not cls: + return False + unsigned = (CLASS_UINT, CLASS_UINT_SMALL) + rules = { + (1, 1): lambda: cls <= CLASS_STRING_REF or cls == CLASS_UINT_SMALL, + (10, 1): lambda: cls == CLASS_ADDRESS, + (2, 1): lambda: cls == CLASS_UINT, + (6, 1): lambda: cls == CLASS_UINT, + (3, 1): lambda: cls == CLASS_UINT, + (3, 7): lambda: cls in unsigned, + (3, 2): lambda: cls == CLASS_ADDRESS, + (3, 8): lambda: cls in (CLASS_STRING, CLASS_DATE_ENCODING), + (3, 22): lambda: cls == CLASS_ALIAS_SET, + (4, 1): lambda: cls == CLASS_UINT, + (4, 3): lambda: cls == CLASS_ADDRESS, + (5, 1): lambda: cls == CLASS_UINT, + (5, 9): lambda: cls == CLASS_DATE_ENCODING, + (7, 1): lambda: cls == CLASS_UINT, + (7, 4): lambda: cls == CLASS_UINT_SMALL, + (7, 5): lambda: cls in (CLASS_STRING, CLASS_DATE_ENCODING), + (7, 6): lambda: cls == CLASS_FLAG, + (8, 1): lambda: cls in (CLASS_UINT, CLASS_INT, CLASS_BOOL), + (8, 10): lambda: cls == CLASS_ENUM_MAP, + (13, 1): lambda: cls == CLASS_BYTES, + (13, 15): lambda: cls == CLASS_ADDRESS, + (13, 17): lambda: cls == CLASS_UINT, + (13, 18): lambda: cls == CLASS_ADDRESS, + } + rule = rules.get((kind, role)) + return bool(rule and rule()) + + +MAX_ARRAY_ELEMENTS = 64 + + +class DeviceCannotExecute(ValueError): + """The program is valid ERC-7730 but outside the device's capabilities.""" + + +def _program_sections(program): + offset = HEADER_SIZE + sections = {} + for _ in range(program[178]): + kind = program[offset] + length = struct.unpack(">I", program[offset + 1:offset + 5])[0] + sections[kind] = program[offset + 5:offset + 5 + length] + offset += 5 + length + return sections + + +def _abi_walk(nodes, steps): + """Mirror of the firmware's preload walk; steps are (opcode, index). + Returns (refusal, leaf kind or 0 for an iteration path, [] array node).""" + node = 0 + indexed = False + array = None + for opcode, index in steps: + if node >= len(nodes): + return "path leaves the ABI", 0, None + kind, _, first, count, length = nodes[node] + if kind == 8 and opcode == 1 and count and 0 <= index < count: + node = first + index + elif kind == 9 and opcode in (1, 2): + if opcode == 2: + if indexed: + return "path indexes an array before iterating", 0, None + array = node + else: + indexed = True + limit = MAX_ARRAY_ELEMENTS if length == ABSENT else length + if not -limit <= index < limit: + return "path indexes beyond the array", 0, None + node = first + else: + return "path does not name an ABI member", 0, None + if node >= len(nodes): + return "path does not end at a value", 0, None + if steps and steps[-1][0] == 2: + # An iteration path: a value too when its element is a leaf. + leaf = nodes[node][0] + return None, (leaf if leaf <= 7 else 0), array + if nodes[node][0] > 7: + return "path does not end at a value", 0, None + return None, nodes[node][0], array + + +def device_refusal(program, capabilities=DEVICE_CAPABILITIES): + """Why the device would refuse `program` at preload, or None.""" + sections = _program_sections(program) + u16 = lambda data, at: struct.unpack(">H", data[at:at + 2])[0] + + abi = sections.get(2, b"\0\0") + nodes = [struct.unpack(">BHHHH", abi[2 + 9 * i:11 + 9 * i]) + for i in range(u16(abi, 0))] + # A fixed array holds 1-64 elements (0xffff marks a dynamic one). + for kind, _, _, _, length in nodes: + if kind == 9 and (length == 0 or (length > 64 and length != 0xffff)): + return "an ABI array exceeds the device limit" + + string_table = sections.get(1, b"\0\0") + date_strings = set() + short_strings = set() + at = 2 + for index in range(u16(string_table, 0)): + length = u16(string_table, at) + # Printable ASCII or well-formed UTF-8, never a control character. + try: + text = string_table[at + 2:at + 2 + length].decode("utf-8") + except UnicodeDecodeError: + return "a program string is not printable text" + if any(ch < " " or ch == "\x7f" for ch in text): + return "a program string is not printable text" + if length <= capabilities.get("signer_text_max", 128): + short_strings.add(index) + if string_table[at + 2:at + 2 + length] in (b"timestamp", + b"blockheight"): + date_strings.add(index) + at += 2 + length + + literal_table = sections.get(4, b"\0\0") + literal_classes = [] + decimals_literals = set() + at = 2 + for literal_index in range(u16(literal_table, 0)): + kind, length = literal_table[at], u16(literal_table, at + 1) + value = literal_table[at + 3:at + 3 + length] + at += 3 + length + if (kind == 1 and length == 1 and + value[0] <= capabilities.get("unit_decimals_max", 255)): + decimals_literals.add(literal_index) + if kind == 8: + for entry in range(u16(value, 0)): + if u16(value, 4 + 4 * entry) not in short_strings: + return "an enum label is longer than the device shows" + members = u16(value, 0) if kind in (8, 9) else 0 + if kind == 1: + cls = CLASS_UINT_SMALL if length == 1 else CLASS_UINT + elif kind == 8: + cls = (CLASS_ENUM_MAP if 0 < members <= capabilities.get( + "enum_max", 0) else 0) + elif kind == 9: + cls = (CLASS_ALIAS_SET if 0 < members <= capabilities.get( + "alias_set_max", 0) else 0) + else: + cls = {4: CLASS_STRING_REF, 5: CLASS_ADDRESS, + 6: CLASS_FLAG}.get(kind, 0) + literal_classes.append(cls) + + def literal_class(index): + return literal_classes[index] if index < len(literal_classes) else 0 + + for kind, limit in TABLE_LIMITS.items(): + if kind in sections and u16(sections[kind], 0) > limit: + return "program table %d exceeds the device limit" % kind + + calldata = program[7] == 1 + path_classes = [] + path_arrays = [] + iterable = set() + paths = sections.get(3, b"\0\0") + at = 2 + for _ in range(u16(paths, 0)): + source, count, index = paths[at], paths[at + 1], u16(paths, at + 2) + at += 4 + if source not in capabilities["path_sources"]: + return "path source %d is not executed" % source + if source == 2 and (index not in capabilities["containers"] or + not calldata): + return "container %d is not executed" % index + if source == 3 and index >= 64: + return "path names a literal beyond the table" + path_classes.append( + (CLASS_UINT if index == 3 else CLASS_ADDRESS) if source == 2 else + ("literal", index) if source == 3 else None) + path_arrays.append(None) + steps = [] + for _ in range(count): + opcode = paths[at] + at += 1 + if opcode not in capabilities["path_step_opcodes"]: + return "path step opcode %d is not executed" % opcode + if opcode == 1: + steps.append((1, struct.unpack(">i", paths[at:at + 4])[0])) + at += 4 + elif opcode == 2: + steps.append((2, 0)) + else: + flags = paths[at] + at += 1 + 4 * bin(flags).count("1") + if source == 1: + if sum(1 for step in steps if step[0] == 2) > 1: + return "path iterates more than once" + reason, leaf, array = _abi_walk(nodes, steps) + if reason: + return reason + path_classes[-1] = leaf + path_arrays[-1] = array + if steps and steps[-1][0] == 2: + iterable.add(len(path_classes) - 1) + + conditions = sections.get(5, b"\0\0") + if u16(conditions, 0) and not capabilities["conditions"]: + return "display conditions are not executed" + for i in range(u16(conditions, 0)): + if conditions[2 + 8 * i] not in capabilities.get("condition_opcodes", + ()): + return "condition opcode %d is not executed" % conditions[2 + 8 * i] + + formatters = sections.get(6, b"\0\0") + formatter_arrays = [] + at = 2 + for _ in range(u16(formatters, 0)): + value_array, any_array, mixed_arrays, value_literal = ( + None, False, False, False) + kind, argc = formatters[at], formatters[at + 2] + at += 3 + if kind not in capabilities["formatters"]: + return "formatter kind %d is not executed" % kind + roles, required = capabilities["formatters"][kind] + seen = set() + for _ in range(argc): + role, source = formatters[at], formatters[at + 1] + index = u16(formatters, at + 2) + at += 4 + if source not in roles.get(role, ()): + return "formatter kind %d argument role %d is not executed" % ( + kind, role) + if source == 3: + cls = (CLASS_DATE_ENCODING if index in date_strings else + CLASS_STRING) + else: + cls = (literal_class(index) if source == 2 else + path_classes[index] if index < len(path_classes) else 0) + if isinstance(cls, tuple): + cls = literal_class(cls[1]) + if not _value_allowed(kind, role, cls): + return ("formatter kind %d argument role %d has the wrong " + "type" % (kind, role)) + if source == 3 and role in (5, 8) and index not in short_strings: + return "signer text is longer than the device shows" + if kind == 7 and role == 4 and index not in decimals_literals: + return "unit decimals exceed the device limit" + constant = (source == 1 and index < len(path_classes) and + isinstance(path_classes[index], tuple)) + if constant and kind == 13 and role in (15, 17, 18): + return ("an embedded call's callee, value and authority must " + "come from calldata") + if constant and role == 1 and kind != 1: + return "only a raw field may show a signer constant" + if (role == 1 and source == 1 and index < len(path_classes) and + isinstance(path_classes[index], tuple)): + value_literal = True + if source == 1: + argument_array = path_arrays[index] + if argument_array is not None: + any_array = True + if role == 1: + value_array = argument_array + elif argument_array != value_array: + mixed_arrays = True + seen.add(role) + if not required <= seen: + return "formatter kind %d lacks a required argument" % kind + if kind == 13 and not calldata: + return "embedded calldata is executed for calldata only" + formatter_arrays.append((value_array, any_array, mixed_arrays, + kind == 13, value_literal)) + + displays = sections.get(7, b"\0\0") + run_closed = False + iteration = None + for pc in range(u16(displays, 0)): + opcode, _, a, b, c = struct.unpack( + ">BBHHH", displays[2 + 8 * pc:10 + 8 * pc]) + if opcode not in capabilities["display_opcodes"]: + return "display opcode %d is not executed" % opcode + if (opcode == 1) != (pc == 0): + return "the intent must be the first display instruction only" + if pc and opcode in (2, 3): + if run_closed: + return "interpolated intent must directly follow the intent" + elif pc: + run_closed = True + if opcode == 4 and c != ABSENT and not capabilities["conditions"]: + return "display conditions are not executed" + if opcode == 7: + if a not in iterable or iteration is not None or not calldata: + return "iteration is not executed here" + iteration = path_arrays[a] + elif opcode == 8: + iteration = None + elif opcode in (3, 4): + value_array, any_array, mixed_arrays, embedded, value_literal = ( + formatter_arrays[a if opcode == 3 else b]) + if opcode == 3 and embedded: + return "an embedded call cannot be an intent value" + if opcode == 3 and value_literal: + return "a signer constant cannot be an intent value" + if opcode == 4 and a not in short_strings: + return "a field label is longer than the device shows" + if any_array and iteration is None: + return "an iterating value outside an iteration" + if iteration is not None and (value_array != iteration or + mixed_arrays): + return "a field reads another array than its iteration" + return None + + +def _executable(program, executable_only): + if executable_only: + reason = device_refusal(program) + if reason: + raise DeviceCannotExecute("device cannot execute: " + reason) + return program + + +def compile_calldata(descriptor, signature, chain_id, address, + executable_only=True, **kwargs): + """Compile one calldata format. By default only a program the device + executes is returned; pass executable_only=False to obtain the program + anyway, for example to prove that the device refuses it.""" + return _executable(CalldataCompiler(descriptor, signature, chain_id, + address, **kwargs).compile(), + executable_only) def _typed_base(type_name, types, active): @@ -997,7 +1400,7 @@ def declaration(name): def compile_eip712(descriptor, typed_data, chain_id=None, address=None, - **kwargs): + executable_only=True, **kwargs): """Compile one EIP-712 descriptor against its exact typed-data schema.""" types = typed_data["types"] primary = typed_data["primaryType"] @@ -1040,7 +1443,7 @@ def named(node): if name in domain: constraints.append((field, domain[name])) type_hash = keccak256(eip712_encode_type(primary, types).encode("ascii")) - return CalldataCompiler( + return _executable(CalldataCompiler( synthetic, signature, chain_id, address, definition_kind=2, primary_type_hash=type_hash, domain_constraints=constraints, - **kwargs).compile() + **kwargs).compile(), executable_only) diff --git a/keepkeylib/messages_pb2.py b/keepkeylib/messages_pb2.py index dc6abeb3..500bcef1 100644 --- a/keepkeylib/messages_pb2.py +++ b/keepkeylib/messages_pb2.py @@ -21,7 +21,7 @@ name='messages.proto', package='', syntax='proto2', - serialized_pb=_b('\n\x0emessages.proto\x1a\x0btypes.proto\"\x0c\n\nInitialize\"\r\n\x0bGetFeatures\"\x8a\x05\n\x08\x46\x65\x61tures\x12\x0e\n\x06vendor\x18\x01 \x01(\t\x12\x15\n\rmajor_version\x18\x02 \x01(\r\x12\x15\n\rminor_version\x18\x03 \x01(\r\x12\x15\n\rpatch_version\x18\x04 \x01(\r\x12\x17\n\x0f\x62ootloader_mode\x18\x05 \x01(\x08\x12\x11\n\tdevice_id\x18\x06 \x01(\t\x12\x16\n\x0epin_protection\x18\x07 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x08 \x01(\x08\x12\x10\n\x08language\x18\t \x01(\t\x12\r\n\x05label\x18\n \x01(\t\x12\x18\n\x05\x63oins\x18\x0b \x03(\x0b\x32\t.CoinType\x12\x13\n\x0binitialized\x18\x0c \x01(\x08\x12\x10\n\x08revision\x18\r \x01(\x0c\x12\x17\n\x0f\x62ootloader_hash\x18\x0e \x01(\x0c\x12\x10\n\x08imported\x18\x0f \x01(\x08\x12\x12\n\npin_cached\x18\x10 \x01(\x08\x12\x19\n\x11passphrase_cached\x18\x11 \x01(\x08\x12\x1d\n\x08policies\x18\x12 \x03(\x0b\x32\x0b.PolicyType\x12\r\n\x05model\x18\x15 \x01(\t\x12\x18\n\x10\x66irmware_variant\x18\x16 \x01(\t\x12\x15\n\rfirmware_hash\x18\x17 \x01(\x0c\x12\x11\n\tno_backup\x18\x18 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x19 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x1a \x01(\r\x12\x18\n\x10supports_taproot\x18\x1b \x01(\x08\x12\x1b\n\x13supports_dice_modes\x18\x1c \x01(\x08\x12\'\n\x1fsupports_solana_lut_attestation\x18\x1d \x01(\x08\"*\n\x0cGetCoinTable\x12\r\n\x05start\x18\x01 \x01(\r\x12\x0b\n\x03\x65nd\x18\x02 \x01(\r\"L\n\tCoinTable\x12\x18\n\x05table\x18\x01 \x03(\x0b\x32\t.CoinType\x12\x11\n\tnum_coins\x18\x02 \x01(\r\x12\x12\n\nchunk_size\x18\x03 \x01(\r\"\x0e\n\x0c\x43learSession\"y\n\rApplySettings\x12\x10\n\x08language\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x16\n\x0euse_passphrase\x18\x03 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x04 \x01(\r\x12\x13\n\x0bu2f_counter\x18\x05 \x01(\r\"\x1b\n\tChangePin\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x87\x01\n\x04Ping\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x19\n\x11\x62utton_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x05 \x01(\x08\"\x1a\n\x07Success\x12\x0f\n\x07message\x18\x01 \x01(\t\"6\n\x07\x46\x61ilure\x12\x1a\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x0c.FailureType\x12\x0f\n\x07message\x18\x02 \x01(\t\"?\n\rButtonRequest\x12 \n\x04\x63ode\x18\x01 \x01(\x0e\x32\x12.ButtonRequestType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"\x0b\n\tButtonAck\"7\n\x10PinMatrixRequest\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.PinMatrixRequestType\"\x1b\n\x0cPinMatrixAck\x12\x0b\n\x03pin\x18\x01 \x02(\t\"\x08\n\x06\x43\x61ncel\"\x13\n\x11PassphraseRequest\"#\n\rPassphraseAck\x12\x12\n\npassphrase\x18\x01 \x02(\t\"\x1a\n\nGetEntropy\x12\x0c\n\x04size\x18\x01 \x02(\r\"\x1a\n\x07\x45ntropy\x12\x0f\n\x07\x65ntropy\x18\x01 \x02(\x0c\"\xa2\x01\n\x0cGetPublicKey\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x02 \x01(\t\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"4\n\tPublicKey\x12\x19\n\x04node\x18\x01 \x02(\x0b\x32\x0b.HDNodeType\x12\x0c\n\x04xpub\x18\x02 \x01(\t\"\xb3\x01\n\nGetAddress\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x1a\n\tcoin_name\x18\x02 \x01(\t:\x07\x42itcoin\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12+\n\x08multisig\x18\x04 \x01(\x0b\x32\x19.MultisigRedeemScriptType\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"\x1a\n\x07\x41\x64\x64ress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x02(\t\"\x0c\n\nWipeDevice\"\xbb\x01\n\nLoadDevice\x12\x10\n\x08mnemonic\x18\x01 \x01(\t\x12\x19\n\x04node\x18\x02 \x01(\x0b\x32\x0b.HDNodeType\x12\x0b\n\x03pin\x18\x03 \x01(\t\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x15\n\rskip_checksum\x18\x07 \x01(\x08\x12\x13\n\x0bu2f_counter\x18\x08 \x01(\r\"\x8a\x02\n\x0bResetDevice\x12\x16\n\x0e\x64isplay_random\x18\x01 \x01(\x08\x12\x15\n\x08strength\x18\x02 \x01(\r:\x03\x32\x35\x36\x12\x1d\n\x15passphrase_protection\x18\x03 \x01(\x08\x12\x16\n\x0epin_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x11\n\tno_backup\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x14\n\x0c\x64ice_entropy\x18\n \x01(\x08\x12\x11\n\tdice_only\x18\x0b \x01(\x08\"\x10\n\x0e\x45ntropyRequest\"\x1d\n\nEntropyAck\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\xff\x01\n\x0eRecoveryDevice\x12\x12\n\nword_count\x18\x01 \x01(\r\x12\x1d\n\x15passphrase_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x19\n\x08language\x18\x04 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x05 \x01(\t\x12\x18\n\x10\x65nforce_wordlist\x18\x06 \x01(\x08\x12\x1c\n\x14use_character_cipher\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x0f\n\x07\x64ry_run\x18\n \x01(\x08\"\r\n\x0bWordRequest\"\x17\n\x07WordAck\x12\x0c\n\x04word\x18\x01 \x02(\t\";\n\x10\x43haracterRequest\x12\x10\n\x08word_pos\x18\x01 \x02(\r\x12\x15\n\rcharacter_pos\x18\x02 \x02(\r\"?\n\x0c\x43haracterAck\x12\x11\n\tcharacter\x18\x01 \x01(\t\x12\x0e\n\x06\x64\x65lete\x18\x02 \x01(\x08\x12\x0c\n\x04\x64one\x18\x03 \x01(\x08\"\x82\x01\n\x0bSignMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0f\n\x07message\x18\x02 \x02(\x0c\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"`\n\rVerifyMessage\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\"6\n\x10MessageSignature\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"v\n\x0e\x45ncryptMessage\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64isplay_only\x18\x03 \x01(\x08\x12\x11\n\taddress_n\x18\x04 \x03(\r\x12\x1a\n\tcoin_name\x18\x05 \x01(\t:\x07\x42itcoin\"@\n\x10\x45ncryptedMessage\x12\r\n\x05nonce\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x0c\n\x04hmac\x18\x03 \x01(\x0c\"Q\n\x0e\x44\x65\x63ryptMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\r\n\x05nonce\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x0c\n\x04hmac\x18\x04 \x01(\x0c\"4\n\x10\x44\x65\x63ryptedMessage\x12\x0f\n\x07message\x18\x01 \x01(\x0c\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"\x8c\x01\n\x0e\x43ipherKeyValue\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\x12\x0f\n\x07\x65ncrypt\x18\x04 \x01(\x08\x12\x16\n\x0e\x61sk_on_encrypt\x18\x05 \x01(\x08\x12\x16\n\x0e\x61sk_on_decrypt\x18\x06 \x01(\x08\x12\n\n\x02iv\x18\x07 \x01(\x0c\"!\n\x10\x43ipheredKeyValue\x12\r\n\x05value\x18\x01 \x01(\x0c\"5\n\x10GetBip85Mnemonic\x12\x12\n\nword_count\x18\x01 \x02(\r\x12\r\n\x05index\x18\x02 \x02(\r\"!\n\rBip85Mnemonic\x12\x10\n\x08mnemonic\x18\x01 \x02(\t\"\xce\x01\n\x06SignTx\x12\x15\n\routputs_count\x18\x01 \x02(\r\x12\x14\n\x0cinputs_count\x18\x02 \x02(\r\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x12\n\x07version\x18\x04 \x01(\r:\x01\x31\x12\x14\n\tlock_time\x18\x05 \x01(\r:\x01\x30\x12\x0e\n\x06\x65xpiry\x18\x06 \x01(\r\x12\x14\n\x0coverwintered\x18\x07 \x01(\x08\x12\x18\n\x10version_group_id\x18\x08 \x01(\r\x12\x11\n\tbranch_id\x18\n \x01(\r\"\x85\x01\n\tTxRequest\x12\"\n\x0crequest_type\x18\x01 \x01(\x0e\x32\x0c.RequestType\x12&\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x15.TxRequestDetailsType\x12,\n\nserialized\x18\x03 \x01(\x0b\x32\x18.TxRequestSerializedType\"%\n\x05TxAck\x12\x1c\n\x02tx\x18\x01 \x01(\x0b\x32\x10.TransactionType\"+\n\x08RawTxAck\x12\x1f\n\x02tx\x18\x01 \x01(\x0b\x32\x13.RawTransactionType\"}\n\x0cSignIdentity\x12\x1f\n\x08identity\x18\x01 \x01(\x0b\x32\r.IdentityType\x12\x18\n\x10\x63hallenge_hidden\x18\x02 \x01(\x0c\x12\x18\n\x10\x63hallenge_visual\x18\x03 \x01(\t\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x04 \x01(\t\"H\n\x0eSignedIdentity\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\",\n\rApplyPolicies\x12\x1b\n\x06policy\x18\x01 \x03(\x0b\x32\x0b.PolicyType\"?\n\tFlashHash\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\x12\x11\n\tchallenge\x18\x03 \x01(\x0c\":\n\nFlashWrite\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\r\n\x05\x65rase\x18\x03 \x01(\x08\"!\n\x11\x46lashHashResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"5\n\x12\x44\x65\x62ugLinkFlashDump\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\"*\n\x1a\x44\x65\x62ugLinkFlashDumpResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x0b\n\tSoftReset\"\x0f\n\rFirmwareErase\"7\n\x0e\x46irmwareUpload\x12\x14\n\x0cpayload_hash\x18\x01 \x02(\x0c\x12\x0f\n\x07payload\x18\x02 \x02(\x0c\"2\n\x11\x44\x65\x62ugLinkDecision\x12\x0e\n\x06yes_no\x18\x01 \x02(\x08\x12\r\n\x05input\x18\x02 \x01(\t\"\x13\n\x11\x44\x65\x62ugLinkGetState\"\xec\x02\n\x0e\x44\x65\x62ugLinkState\x12\x0e\n\x06layout\x18\x01 \x01(\x0c\x12\x0b\n\x03pin\x18\x02 \x01(\t\x12\x0e\n\x06matrix\x18\x03 \x01(\t\x12\x10\n\x08mnemonic\x18\x04 \x01(\t\x12\x19\n\x04node\x18\x05 \x01(\x0b\x32\x0b.HDNodeType\x12\x1d\n\x15passphrase_protection\x18\x06 \x01(\x08\x12\x12\n\nreset_word\x18\x07 \x01(\t\x12\x15\n\rreset_entropy\x18\x08 \x01(\x0c\x12\x1a\n\x12recovery_fake_word\x18\t \x01(\t\x12\x19\n\x11recovery_word_pos\x18\n \x01(\r\x12\x17\n\x0frecovery_cipher\x18\x0b \x01(\t\x12$\n\x1crecovery_auto_completed_word\x18\x0c \x01(\t\x12\x15\n\rfirmware_hash\x18\r \x01(\x0c\x12\x14\n\x0cstorage_hash\x18\x0e \x01(\x0c\x12\x13\n\x0b\x64ice_digest\x18\x0f \x01(\x0c\"\x0f\n\rDebugLinkStop\";\n\x0c\x44\x65\x62ugLinkLog\x12\r\n\x05level\x18\x01 \x01(\r\x12\x0e\n\x06\x62ucket\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\"\x15\n\x13\x44\x65\x62ugLinkFillConfig\" \n\x0e\x43hangeWipeCode\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x1f\n\x1d\x43learsignAttestorGetPublicKey\"0\n\x1a\x43learsignAttestorPublicKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"(\n\x15\x43learsignAttestorSign\x12\x0f\n\x07payload\x18\x01 \x01(\x0c\"C\n\x1a\x43learsignAttestorSignature\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x12\n\npublic_key\x18\x02 \x01(\x0c*\xc3\x46\n\x0bMessageType\x12 \n\x16MessageType_Initialize\x10\x00\x1a\x04\x90\xb5\x18\x01\x12\x1a\n\x10MessageType_Ping\x10\x01\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Success\x10\x02\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_Failure\x10\x03\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ChangePin\x10\x04\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_WipeDevice\x10\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_FirmwareErase\x10\x06\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_FirmwareUpload\x10\x07\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetEntropy\x10\t\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Entropy\x10\n\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_GetPublicKey\x10\x0b\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_PublicKey\x10\x0c\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_LoadDevice\x10\r\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_ResetDevice\x10\x0e\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_SignTx\x10\x0f\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_Features\x10\x11\x1a\x04\x98\xb5\x18\x01\x12&\n\x1cMessageType_PinMatrixRequest\x10\x12\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_PinMatrixAck\x10\x13\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_Cancel\x10\x14\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_TxRequest\x10\x15\x1a\x04\x98\xb5\x18\x01\x12\x1b\n\x11MessageType_TxAck\x10\x16\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_CipherKeyValue\x10\x17\x1a\x04\x90\xb5\x18\x01\x12\"\n\x18MessageType_ClearSession\x10\x18\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplySettings\x10\x19\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ButtonRequest\x10\x1a\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ButtonAck\x10\x1b\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetAddress\x10\x1d\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Address\x10\x1e\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EntropyRequest\x10#\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_EntropyAck\x10$\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_SignMessage\x10&\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_VerifyMessage\x10\'\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_MessageSignature\x10(\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1dMessageType_PassphraseRequest\x10)\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_PassphraseAck\x10*\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_RecoveryDevice\x10-\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_WordRequest\x10.\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_WordAck\x10/\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CipheredKeyValue\x10\x30\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EncryptMessage\x10\x31\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_EncryptedMessage\x10\x32\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_DecryptMessage\x10\x33\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_DecryptedMessage\x10\x34\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_SignIdentity\x10\x35\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_SignedIdentity\x10\x36\x1a\x04\x98\xb5\x18\x01\x12!\n\x17MessageType_GetFeatures\x10\x37\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumGetAddress\x10\x38\x1a\x04\x90\xb5\x18\x01\x12%\n\x1bMessageType_EthereumAddress\x10\x39\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EthereumSignTx\x10:\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_EthereumTxRequest\x10;\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_EthereumTxAck\x10<\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CharacterRequest\x10P\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_CharacterAck\x10Q\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_RawTxAck\x10R\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplyPolicies\x10S\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_FlashHash\x10T\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_FlashWrite\x10U\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_FlashHashResponse\x10V\x1a\x04\x98\xb5\x18\x01\x12(\n\x1eMessageType_DebugLinkFlashDump\x10W\x1a\x04\xa0\xb5\x18\x01\x12\x30\n&MessageType_DebugLinkFlashDumpResponse\x10X\x1a\x04\xa8\xb5\x18\x01\x12\x1f\n\x15MessageType_SoftReset\x10Y\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkDecision\x10\x64\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkGetState\x10\x65\x1a\x04\xa0\xb5\x18\x01\x12$\n\x1aMessageType_DebugLinkState\x10\x66\x1a\x04\xa8\xb5\x18\x01\x12#\n\x19MessageType_DebugLinkStop\x10g\x1a\x04\xa0\xb5\x18\x01\x12\"\n\x18MessageType_DebugLinkLog\x10h\x1a\x04\xa8\xb5\x18\x01\x12)\n\x1fMessageType_DebugLinkFillConfig\x10i\x1a\x04\xa8\xb5\x18\x01\x12\"\n\x18MessageType_GetCoinTable\x10j\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_CoinTable\x10k\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_EthereumSignMessage\x10l\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumVerifyMessage\x10m\x1a\x04\x90\xb5\x18\x01\x12.\n$MessageType_EthereumMessageSignature\x10n\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_ChangeWipeCode\x10o\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumSignTypedHash\x10p\x1a\x04\x90\xb5\x18\x01\x12\x30\n&MessageType_EthereumTypedDataSignature\x10q\x1a\x04\x98\xb5\x18\x01\x12,\n\"MessageType_Ethereum712TypesValues\x10r\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumTxMetadata\x10s\x1a\x04\x90\xb5\x18\x01\x12)\n\x1fMessageType_EthereumMetadataAck\x10t\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_LoadClearsignSigner\x10u\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedData\x10\xa8\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumTypedDataStructRequest\x10\xa9\r\x1a\x04\x98\xb5\x18\x01\x12\x31\n&MessageType_EthereumTypedDataStructAck\x10\xaa\r\x1a\x04\x90\xb5\x18\x01\x12\x34\n)MessageType_EthereumTypedDataValueRequest\x10\xab\r\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_EthereumTypedDataValueAck\x10\xac\r\x1a\x04\x90\xb5\x18\x01\x12\x32\n\'MessageType_EthereumClearSignDefinition\x10\xad\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumClearSignDefinitionAck\x10\xae\r\x1a\x04\x98\xb5\x18\x01\x12\x39\n.MessageType_EthereumClearSignDefinitionRequest\x10\xaf\r\x1a\x04\x98\xb5\x18\x01\x12\x37\n,MessageType_EthereumClearSignDefinitionChunk\x10\xb0\r\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_GetBip85Mnemonic\x10x\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_Bip85Mnemonic\x10y\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_RippleGetAddress\x10\x90\x03\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_RippleAddress\x10\x91\x03\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_RippleSignTx\x10\x92\x03\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_RippleSignedTx\x10\x93\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainGetAddress\x10\xf4\x03\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_ThorchainAddress\x10\xf5\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainSignTx\x10\xf6\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainMsgRequest\x10\xf7\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainMsgAck\x10\xf8\x03\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_ThorchainSignedTx\x10\xf9\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_EosGetPublicKey\x10\xd8\x04\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_EosPublicKey\x10\xd9\x04\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_EosSignTx\x10\xda\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_EosTxActionRequest\x10\xdb\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_EosTxActionAck\x10\xdc\x04\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_EosSignedTx\x10\xdd\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NanoGetAddress\x10\xbc\x05\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NanoAddress\x10\xbd\x05\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NanoSignTx\x10\xbe\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NanoSignedTx\x10\xbf\x05\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_SolanaGetAddress\x10\xee\x05\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\xef\x05\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\xf0\x05\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_SolanaSignedTx\x10\xf1\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_SolanaSignMessage\x10\xf2\x05\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_SolanaMessageSignature\x10\xf3\x05\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_SolanaSignOffchainMessage\x10\xf4\x05\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_SolanaOffchainMessageSignature\x10\xf5\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_BinanceGetAddress\x10\xa0\x06\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_BinanceAddress\x10\xa1\x06\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_BinanceGetPublicKey\x10\xa2\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinancePublicKey\x10\xa3\x06\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_BinanceSignTx\x10\xa4\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceTxRequest\x10\xa5\x06\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_BinanceTransferMsg\x10\xa6\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceOrderMsg\x10\xa7\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceCancelMsg\x10\xa8\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceSignedTx\x10\xa9\x06\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosGetAddress\x10\x84\x07\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_CosmosAddress\x10\x85\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosSignTx\x10\x86\x07\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRequest\x10\x87\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosMsgAck\x10\x88\x07\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CosmosSignedTx\x10\x89\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CosmosMsgDelegate\x10\x8a\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgUndelegate\x10\x8b\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgRedelegate\x10\x8c\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRewards\x10\x8d\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_CosmosMsgIBCTransfer\x10\x8e\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintGetAddress\x10\xe8\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintAddress\x10\xe9\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintSignTx\x10\xea\x07\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TendermintMsgRequest\x10\xeb\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintMsgAck\x10\xec\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintMsgSend\x10\xed\x07\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_TendermintSignedTx\x10\xee\x07\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_TendermintMsgDelegate\x10\xef\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgUndelegate\x10\xf0\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgRedelegate\x10\xf1\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintMsgRewards\x10\xf2\x07\x1a\x04\x98\xb5\x18\x01\x12/\n$MessageType_TendermintMsgIBCTransfer\x10\xf3\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisGetAddress\x10\xcc\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisAddress\x10\xcd\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisSignTx\x10\xce\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRequest\x10\xcf\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisMsgAck\x10\xd0\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSend\x10\xd1\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgDelegate\x10\xd2\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgUndelegate\x10\xd3\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgRedelegate\x10\xd4\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRewards\x10\xd5\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisMsgLPAdd\x10\xd6\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgLPRemove\x10\xd7\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgLPStake\x10\xd8\x08\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_OsmosisMsgLPUnstake\x10\xd9\x08\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_OsmosisMsgIBCTransfer\x10\xda\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSwap\x10\xdb\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisSignedTx\x10\xdc\x08\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_MayachainGetAddress\x10\xb0\t\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_MayachainAddress\x10\xb1\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainSignTx\x10\xb2\t\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_MayachainMsgRequest\x10\xb3\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainMsgAck\x10\xb4\t\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_MayachainSignedTx\x10\xb5\t\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_ZcashSignPCZT\x10\x94\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashPCZTAction\x10\x95\n\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_ZcashPCZTActionAck\x10\x96\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ZcashSignedPCZT\x10\x97\n\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_ZcashGetOrchardFVK\x10\x98\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashOrchardFVK\x10\x99\n\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ZcashTransparentInput\x10\x9a\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentSigned\x10\x9b\n\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_ZcashDisplayAddress\x10\x9c\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_ZcashAddress\x10\x9d\n\x1a\x04\x98\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentOutput\x10\x9e\n\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ZcashTransparentAck\x10\x9f\n\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TronGetAddress\x10\xf8\n\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TronAddress\x10\xf9\n\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_TronSignTx\x10\xfa\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_TronSignedTx\x10\xfb\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_TronSignMessage\x10\xfc\n\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TronMessageSignature\x10\xfd\n\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_TronVerifyMessage\x10\xfe\n\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TronSignTypedHash\x10\xff\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_TronTypedDataSignature\x10\x80\x0b\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_TonGetAddress\x10\xdc\x0b\x1a\x04\x90\xb5\x18\x01\x12!\n\x16MessageType_TonAddress\x10\xdd\x0b\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_TonSignTx\x10\xde\x0b\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TonSignedTx\x10\xdf\x0b\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TonSignMessage\x10\xe0\x0b\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_TonMessageSignature\x10\xe1\x0b\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_HiveGetPublicKey\x10\xc0\x0c\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_HivePublicKey\x10\xc1\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_HiveSignTx\x10\xc2\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_HiveSignedTx\x10\xc3\x0c\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_HiveGetPublicKeys\x10\xc4\x0c\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_HivePublicKeys\x10\xc5\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountCreate\x10\xc6\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountCreate\x10\xc7\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountUpdate\x10\xc8\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountUpdate\x10\xc9\x0c\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NearGetAddress\x10\xca\x0c\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NearAddress\x10\xcb\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NearSignTx\x10\xcc\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NearSignedTx\x10\xcd\x0c\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_HiveSignMessage\x10\xce\x0c\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_HiveSignedMessage\x10\xcf\x0c\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_HiveSignOperations\x10\xd0\x0c\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_HiveSignedOperations\x10\xd1\x0c\x1a\x04\x98\xb5\x18\x01\x12\x34\n)MessageType_ClearsignAttestorGetPublicKey\x10\xa4\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorPublicKey\x10\xa5\r\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ClearsignAttestorSign\x10\xa6\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorSignature\x10\xa7\r\x1a\x04\x98\xb5\x18\x01\x42,\n\x1a\x63om.keepkey.deviceprotocolB\x0eKeepKeyMessage') + serialized_pb=_b('\n\x0emessages.proto\x1a\x0btypes.proto\"\x0c\n\nInitialize\"\r\n\x0bGetFeatures\"\x8a\x05\n\x08\x46\x65\x61tures\x12\x0e\n\x06vendor\x18\x01 \x01(\t\x12\x15\n\rmajor_version\x18\x02 \x01(\r\x12\x15\n\rminor_version\x18\x03 \x01(\r\x12\x15\n\rpatch_version\x18\x04 \x01(\r\x12\x17\n\x0f\x62ootloader_mode\x18\x05 \x01(\x08\x12\x11\n\tdevice_id\x18\x06 \x01(\t\x12\x16\n\x0epin_protection\x18\x07 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x08 \x01(\x08\x12\x10\n\x08language\x18\t \x01(\t\x12\r\n\x05label\x18\n \x01(\t\x12\x18\n\x05\x63oins\x18\x0b \x03(\x0b\x32\t.CoinType\x12\x13\n\x0binitialized\x18\x0c \x01(\x08\x12\x10\n\x08revision\x18\r \x01(\x0c\x12\x17\n\x0f\x62ootloader_hash\x18\x0e \x01(\x0c\x12\x10\n\x08imported\x18\x0f \x01(\x08\x12\x12\n\npin_cached\x18\x10 \x01(\x08\x12\x19\n\x11passphrase_cached\x18\x11 \x01(\x08\x12\x1d\n\x08policies\x18\x12 \x03(\x0b\x32\x0b.PolicyType\x12\r\n\x05model\x18\x15 \x01(\t\x12\x18\n\x10\x66irmware_variant\x18\x16 \x01(\t\x12\x15\n\rfirmware_hash\x18\x17 \x01(\x0c\x12\x11\n\tno_backup\x18\x18 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x19 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x1a \x01(\r\x12\x18\n\x10supports_taproot\x18\x1b \x01(\x08\x12\x1b\n\x13supports_dice_modes\x18\x1c \x01(\x08\x12\'\n\x1fsupports_solana_lut_attestation\x18\x1d \x01(\x08\"*\n\x0cGetCoinTable\x12\r\n\x05start\x18\x01 \x01(\r\x12\x0b\n\x03\x65nd\x18\x02 \x01(\r\"L\n\tCoinTable\x12\x18\n\x05table\x18\x01 \x03(\x0b\x32\t.CoinType\x12\x11\n\tnum_coins\x18\x02 \x01(\r\x12\x12\n\nchunk_size\x18\x03 \x01(\r\"\x0e\n\x0c\x43learSession\"y\n\rApplySettings\x12\x10\n\x08language\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x16\n\x0euse_passphrase\x18\x03 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x04 \x01(\r\x12\x13\n\x0bu2f_counter\x18\x05 \x01(\r\"\x1b\n\tChangePin\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x87\x01\n\x04Ping\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x19\n\x11\x62utton_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x1c\n\x14wipe_code_protection\x18\x05 \x01(\x08\"\x1a\n\x07Success\x12\x0f\n\x07message\x18\x01 \x01(\t\"6\n\x07\x46\x61ilure\x12\x1a\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x0c.FailureType\x12\x0f\n\x07message\x18\x02 \x01(\t\"?\n\rButtonRequest\x12 \n\x04\x63ode\x18\x01 \x01(\x0e\x32\x12.ButtonRequestType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"\x0b\n\tButtonAck\"7\n\x10PinMatrixRequest\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.PinMatrixRequestType\"\x1b\n\x0cPinMatrixAck\x12\x0b\n\x03pin\x18\x01 \x02(\t\"\x08\n\x06\x43\x61ncel\"\x13\n\x11PassphraseRequest\"#\n\rPassphraseAck\x12\x12\n\npassphrase\x18\x01 \x02(\t\"\x1a\n\nGetEntropy\x12\x0c\n\x04size\x18\x01 \x02(\r\"\x1a\n\x07\x45ntropy\x12\x0f\n\x07\x65ntropy\x18\x01 \x02(\x0c\"\xa2\x01\n\x0cGetPublicKey\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x02 \x01(\t\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"4\n\tPublicKey\x12\x19\n\x04node\x18\x01 \x02(\x0b\x32\x0b.HDNodeType\x12\x0c\n\x04xpub\x18\x02 \x01(\t\"\xb3\x01\n\nGetAddress\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x1a\n\tcoin_name\x18\x02 \x01(\t:\x07\x42itcoin\x12\x14\n\x0cshow_display\x18\x03 \x01(\x08\x12+\n\x08multisig\x18\x04 \x01(\x0b\x32\x19.MultisigRedeemScriptType\x12\x33\n\x0bscript_type\x18\x05 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"\x1a\n\x07\x41\x64\x64ress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x02(\t\"\x0c\n\nWipeDevice\"\xbb\x01\n\nLoadDevice\x12\x10\n\x08mnemonic\x18\x01 \x01(\t\x12\x19\n\x04node\x18\x02 \x01(\x0b\x32\x0b.HDNodeType\x12\x0b\n\x03pin\x18\x03 \x01(\t\x12\x1d\n\x15passphrase_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x15\n\rskip_checksum\x18\x07 \x01(\x08\x12\x13\n\x0bu2f_counter\x18\x08 \x01(\r\"\x8a\x02\n\x0bResetDevice\x12\x16\n\x0e\x64isplay_random\x18\x01 \x01(\x08\x12\x15\n\x08strength\x18\x02 \x01(\r:\x03\x32\x35\x36\x12\x1d\n\x15passphrase_protection\x18\x03 \x01(\x08\x12\x16\n\x0epin_protection\x18\x04 \x01(\x08\x12\x19\n\x08language\x18\x05 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x06 \x01(\t\x12\x11\n\tno_backup\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x14\n\x0c\x64ice_entropy\x18\n \x01(\x08\x12\x11\n\tdice_only\x18\x0b \x01(\x08\"\x10\n\x0e\x45ntropyRequest\"\x1d\n\nEntropyAck\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\xff\x01\n\x0eRecoveryDevice\x12\x12\n\nword_count\x18\x01 \x01(\r\x12\x1d\n\x15passphrase_protection\x18\x02 \x01(\x08\x12\x16\n\x0epin_protection\x18\x03 \x01(\x08\x12\x19\n\x08language\x18\x04 \x01(\t:\x07\x65nglish\x12\r\n\x05label\x18\x05 \x01(\t\x12\x18\n\x10\x65nforce_wordlist\x18\x06 \x01(\x08\x12\x1c\n\x14use_character_cipher\x18\x07 \x01(\x08\x12\x1a\n\x12\x61uto_lock_delay_ms\x18\x08 \x01(\r\x12\x13\n\x0bu2f_counter\x18\t \x01(\r\x12\x0f\n\x07\x64ry_run\x18\n \x01(\x08\"\r\n\x0bWordRequest\"\x17\n\x07WordAck\x12\x0c\n\x04word\x18\x01 \x02(\t\";\n\x10\x43haracterRequest\x12\x10\n\x08word_pos\x18\x01 \x02(\r\x12\x15\n\rcharacter_pos\x18\x02 \x02(\r\"?\n\x0c\x43haracterAck\x12\x11\n\tcharacter\x18\x01 \x01(\t\x12\x0e\n\x06\x64\x65lete\x18\x02 \x01(\x08\x12\x0c\n\x04\x64one\x18\x03 \x01(\x08\"\x82\x01\n\x0bSignMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0f\n\x07message\x18\x02 \x02(\x0c\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x33\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x10.InputScriptType:\x0cSPENDADDRESS\"`\n\rVerifyMessage\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x1a\n\tcoin_name\x18\x04 \x01(\t:\x07\x42itcoin\"6\n\x10MessageSignature\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"v\n\x0e\x45ncryptMessage\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64isplay_only\x18\x03 \x01(\x08\x12\x11\n\taddress_n\x18\x04 \x03(\r\x12\x1a\n\tcoin_name\x18\x05 \x01(\t:\x07\x42itcoin\"@\n\x10\x45ncryptedMessage\x12\r\n\x05nonce\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\x0c\x12\x0c\n\x04hmac\x18\x03 \x01(\x0c\"Q\n\x0e\x44\x65\x63ryptMessage\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\r\n\x05nonce\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\x0c\x12\x0c\n\x04hmac\x18\x04 \x01(\x0c\"4\n\x10\x44\x65\x63ryptedMessage\x12\x0f\n\x07message\x18\x01 \x01(\x0c\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\t\"\x8c\x01\n\x0e\x43ipherKeyValue\x12\x11\n\taddress_n\x18\x01 \x03(\r\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\x12\x0f\n\x07\x65ncrypt\x18\x04 \x01(\x08\x12\x16\n\x0e\x61sk_on_encrypt\x18\x05 \x01(\x08\x12\x16\n\x0e\x61sk_on_decrypt\x18\x06 \x01(\x08\x12\n\n\x02iv\x18\x07 \x01(\x0c\"!\n\x10\x43ipheredKeyValue\x12\r\n\x05value\x18\x01 \x01(\x0c\"5\n\x10GetBip85Mnemonic\x12\x12\n\nword_count\x18\x01 \x02(\r\x12\r\n\x05index\x18\x02 \x02(\r\"!\n\rBip85Mnemonic\x12\x10\n\x08mnemonic\x18\x01 \x02(\t\"\xce\x01\n\x06SignTx\x12\x15\n\routputs_count\x18\x01 \x02(\r\x12\x14\n\x0cinputs_count\x18\x02 \x02(\r\x12\x1a\n\tcoin_name\x18\x03 \x01(\t:\x07\x42itcoin\x12\x12\n\x07version\x18\x04 \x01(\r:\x01\x31\x12\x14\n\tlock_time\x18\x05 \x01(\r:\x01\x30\x12\x0e\n\x06\x65xpiry\x18\x06 \x01(\r\x12\x14\n\x0coverwintered\x18\x07 \x01(\x08\x12\x18\n\x10version_group_id\x18\x08 \x01(\r\x12\x11\n\tbranch_id\x18\n \x01(\r\"\x85\x01\n\tTxRequest\x12\"\n\x0crequest_type\x18\x01 \x01(\x0e\x32\x0c.RequestType\x12&\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x15.TxRequestDetailsType\x12,\n\nserialized\x18\x03 \x01(\x0b\x32\x18.TxRequestSerializedType\"%\n\x05TxAck\x12\x1c\n\x02tx\x18\x01 \x01(\x0b\x32\x10.TransactionType\"+\n\x08RawTxAck\x12\x1f\n\x02tx\x18\x01 \x01(\x0b\x32\x13.RawTransactionType\"}\n\x0cSignIdentity\x12\x1f\n\x08identity\x18\x01 \x01(\x0b\x32\r.IdentityType\x12\x18\n\x10\x63hallenge_hidden\x18\x02 \x01(\x0c\x12\x18\n\x10\x63hallenge_visual\x18\x03 \x01(\t\x12\x18\n\x10\x65\x63\x64sa_curve_name\x18\x04 \x01(\t\"H\n\x0eSignedIdentity\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\",\n\rApplyPolicies\x12\x1b\n\x06policy\x18\x01 \x03(\x0b\x32\x0b.PolicyType\"?\n\tFlashHash\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\x12\x11\n\tchallenge\x18\x03 \x01(\x0c\":\n\nFlashWrite\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\r\n\x05\x65rase\x18\x03 \x01(\x08\"!\n\x11\x46lashHashResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"5\n\x12\x44\x65\x62ugLinkFlashDump\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x0e\n\x06length\x18\x02 \x01(\r\"*\n\x1a\x44\x65\x62ugLinkFlashDumpResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x0b\n\tSoftReset\"\x0f\n\rFirmwareErase\"7\n\x0e\x46irmwareUpload\x12\x14\n\x0cpayload_hash\x18\x01 \x02(\x0c\x12\x0f\n\x07payload\x18\x02 \x02(\x0c\"2\n\x11\x44\x65\x62ugLinkDecision\x12\x0e\n\x06yes_no\x18\x01 \x02(\x08\x12\r\n\x05input\x18\x02 \x01(\t\"\x13\n\x11\x44\x65\x62ugLinkGetState\"\x99\x03\n\x0e\x44\x65\x62ugLinkState\x12\x0e\n\x06layout\x18\x01 \x01(\x0c\x12\x0b\n\x03pin\x18\x02 \x01(\t\x12\x0e\n\x06matrix\x18\x03 \x01(\t\x12\x10\n\x08mnemonic\x18\x04 \x01(\t\x12\x19\n\x04node\x18\x05 \x01(\x0b\x32\x0b.HDNodeType\x12\x1d\n\x15passphrase_protection\x18\x06 \x01(\x08\x12\x12\n\nreset_word\x18\x07 \x01(\t\x12\x15\n\rreset_entropy\x18\x08 \x01(\x0c\x12\x1a\n\x12recovery_fake_word\x18\t \x01(\t\x12\x19\n\x11recovery_word_pos\x18\n \x01(\r\x12\x17\n\x0frecovery_cipher\x18\x0b \x01(\t\x12$\n\x1crecovery_auto_completed_word\x18\x0c \x01(\t\x12\x15\n\rfirmware_hash\x18\r \x01(\x0c\x12\x14\n\x0cstorage_hash\x18\x0e \x01(\x0c\x12\x13\n\x0b\x64ice_digest\x18\x0f \x01(\x0c\x12\x15\n\rconfirm_title\x18\x10 \x01(\t\x12\x14\n\x0c\x63onfirm_body\x18\x11 \x01(\t\"\x0f\n\rDebugLinkStop\";\n\x0c\x44\x65\x62ugLinkLog\x12\r\n\x05level\x18\x01 \x01(\r\x12\x0e\n\x06\x62ucket\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\"\x15\n\x13\x44\x65\x62ugLinkFillConfig\" \n\x0e\x43hangeWipeCode\x12\x0e\n\x06remove\x18\x01 \x01(\x08\"\x1f\n\x1d\x43learsignAttestorGetPublicKey\"0\n\x1a\x43learsignAttestorPublicKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"(\n\x15\x43learsignAttestorSign\x12\x0f\n\x07payload\x18\x01 \x01(\x0c\"C\n\x1a\x43learsignAttestorSignature\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x12\n\npublic_key\x18\x02 \x01(\x0c*\xc3\x46\n\x0bMessageType\x12 \n\x16MessageType_Initialize\x10\x00\x1a\x04\x90\xb5\x18\x01\x12\x1a\n\x10MessageType_Ping\x10\x01\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Success\x10\x02\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_Failure\x10\x03\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ChangePin\x10\x04\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_WipeDevice\x10\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_FirmwareErase\x10\x06\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_FirmwareUpload\x10\x07\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetEntropy\x10\t\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Entropy\x10\n\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_GetPublicKey\x10\x0b\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_PublicKey\x10\x0c\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_LoadDevice\x10\r\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_ResetDevice\x10\x0e\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_SignTx\x10\x0f\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_Features\x10\x11\x1a\x04\x98\xb5\x18\x01\x12&\n\x1cMessageType_PinMatrixRequest\x10\x12\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_PinMatrixAck\x10\x13\x1a\x04\x90\xb5\x18\x01\x12\x1c\n\x12MessageType_Cancel\x10\x14\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_TxRequest\x10\x15\x1a\x04\x98\xb5\x18\x01\x12\x1b\n\x11MessageType_TxAck\x10\x16\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_CipherKeyValue\x10\x17\x1a\x04\x90\xb5\x18\x01\x12\"\n\x18MessageType_ClearSession\x10\x18\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplySettings\x10\x19\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ButtonRequest\x10\x1a\x1a\x04\x98\xb5\x18\x01\x12\x1f\n\x15MessageType_ButtonAck\x10\x1b\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_GetAddress\x10\x1d\x1a\x04\x90\xb5\x18\x01\x12\x1d\n\x13MessageType_Address\x10\x1e\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EntropyRequest\x10#\x1a\x04\x98\xb5\x18\x01\x12 \n\x16MessageType_EntropyAck\x10$\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_SignMessage\x10&\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_VerifyMessage\x10\'\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_MessageSignature\x10(\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1dMessageType_PassphraseRequest\x10)\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_PassphraseAck\x10*\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_RecoveryDevice\x10-\x1a\x04\x90\xb5\x18\x01\x12!\n\x17MessageType_WordRequest\x10.\x1a\x04\x98\xb5\x18\x01\x12\x1d\n\x13MessageType_WordAck\x10/\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CipheredKeyValue\x10\x30\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EncryptMessage\x10\x31\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_EncryptedMessage\x10\x32\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_DecryptMessage\x10\x33\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_DecryptedMessage\x10\x34\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_SignIdentity\x10\x35\x1a\x04\x90\xb5\x18\x01\x12$\n\x1aMessageType_SignedIdentity\x10\x36\x1a\x04\x98\xb5\x18\x01\x12!\n\x17MessageType_GetFeatures\x10\x37\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumGetAddress\x10\x38\x1a\x04\x90\xb5\x18\x01\x12%\n\x1bMessageType_EthereumAddress\x10\x39\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_EthereumSignTx\x10:\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_EthereumTxRequest\x10;\x1a\x04\x98\xb5\x18\x01\x12#\n\x19MessageType_EthereumTxAck\x10<\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_CharacterRequest\x10P\x1a\x04\x98\xb5\x18\x01\x12\"\n\x18MessageType_CharacterAck\x10Q\x1a\x04\x90\xb5\x18\x01\x12\x1e\n\x14MessageType_RawTxAck\x10R\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_ApplyPolicies\x10S\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_FlashHash\x10T\x1a\x04\x90\xb5\x18\x01\x12 \n\x16MessageType_FlashWrite\x10U\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1dMessageType_FlashHashResponse\x10V\x1a\x04\x98\xb5\x18\x01\x12(\n\x1eMessageType_DebugLinkFlashDump\x10W\x1a\x04\xa0\xb5\x18\x01\x12\x30\n&MessageType_DebugLinkFlashDumpResponse\x10X\x1a\x04\xa8\xb5\x18\x01\x12\x1f\n\x15MessageType_SoftReset\x10Y\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkDecision\x10\x64\x1a\x04\xa0\xb5\x18\x01\x12\'\n\x1dMessageType_DebugLinkGetState\x10\x65\x1a\x04\xa0\xb5\x18\x01\x12$\n\x1aMessageType_DebugLinkState\x10\x66\x1a\x04\xa8\xb5\x18\x01\x12#\n\x19MessageType_DebugLinkStop\x10g\x1a\x04\xa0\xb5\x18\x01\x12\"\n\x18MessageType_DebugLinkLog\x10h\x1a\x04\xa8\xb5\x18\x01\x12)\n\x1fMessageType_DebugLinkFillConfig\x10i\x1a\x04\xa8\xb5\x18\x01\x12\"\n\x18MessageType_GetCoinTable\x10j\x1a\x04\x90\xb5\x18\x01\x12\x1f\n\x15MessageType_CoinTable\x10k\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_EthereumSignMessage\x10l\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumVerifyMessage\x10m\x1a\x04\x90\xb5\x18\x01\x12.\n$MessageType_EthereumMessageSignature\x10n\x1a\x04\x98\xb5\x18\x01\x12$\n\x1aMessageType_ChangeWipeCode\x10o\x1a\x04\x90\xb5\x18\x01\x12+\n!MessageType_EthereumSignTypedHash\x10p\x1a\x04\x90\xb5\x18\x01\x12\x30\n&MessageType_EthereumTypedDataSignature\x10q\x1a\x04\x98\xb5\x18\x01\x12,\n\"MessageType_Ethereum712TypesValues\x10r\x1a\x04\x90\xb5\x18\x01\x12(\n\x1eMessageType_EthereumTxMetadata\x10s\x1a\x04\x90\xb5\x18\x01\x12)\n\x1fMessageType_EthereumMetadataAck\x10t\x1a\x04\x98\xb5\x18\x01\x12)\n\x1fMessageType_LoadClearsignSigner\x10u\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_EthereumSignTypedData\x10\xa8\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumTypedDataStructRequest\x10\xa9\r\x1a\x04\x98\xb5\x18\x01\x12\x31\n&MessageType_EthereumTypedDataStructAck\x10\xaa\r\x1a\x04\x90\xb5\x18\x01\x12\x34\n)MessageType_EthereumTypedDataValueRequest\x10\xab\r\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_EthereumTypedDataValueAck\x10\xac\r\x1a\x04\x90\xb5\x18\x01\x12\x32\n\'MessageType_EthereumClearSignDefinition\x10\xad\r\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_EthereumClearSignDefinitionAck\x10\xae\r\x1a\x04\x98\xb5\x18\x01\x12\x39\n.MessageType_EthereumClearSignDefinitionRequest\x10\xaf\r\x1a\x04\x98\xb5\x18\x01\x12\x37\n,MessageType_EthereumClearSignDefinitionChunk\x10\xb0\r\x1a\x04\x90\xb5\x18\x01\x12&\n\x1cMessageType_GetBip85Mnemonic\x10x\x1a\x04\x90\xb5\x18\x01\x12#\n\x19MessageType_Bip85Mnemonic\x10y\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_RippleGetAddress\x10\x90\x03\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_RippleAddress\x10\x91\x03\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_RippleSignTx\x10\x92\x03\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_RippleSignedTx\x10\x93\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainGetAddress\x10\xf4\x03\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_ThorchainAddress\x10\xf5\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainSignTx\x10\xf6\x03\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ThorchainMsgRequest\x10\xf7\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ThorchainMsgAck\x10\xf8\x03\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_ThorchainSignedTx\x10\xf9\x03\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_EosGetPublicKey\x10\xd8\x04\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_EosPublicKey\x10\xd9\x04\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_EosSignTx\x10\xda\x04\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_EosTxActionRequest\x10\xdb\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_EosTxActionAck\x10\xdc\x04\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_EosSignedTx\x10\xdd\x04\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NanoGetAddress\x10\xbc\x05\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NanoAddress\x10\xbd\x05\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NanoSignTx\x10\xbe\x05\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NanoSignedTx\x10\xbf\x05\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_SolanaGetAddress\x10\xee\x05\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\xef\x05\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\xf0\x05\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_SolanaSignedTx\x10\xf1\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_SolanaSignMessage\x10\xf2\x05\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_SolanaMessageSignature\x10\xf3\x05\x1a\x04\x98\xb5\x18\x01\x12\x30\n%MessageType_SolanaSignOffchainMessage\x10\xf4\x05\x1a\x04\x90\xb5\x18\x01\x12\x35\n*MessageType_SolanaOffchainMessageSignature\x10\xf5\x05\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_BinanceGetAddress\x10\xa0\x06\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_BinanceAddress\x10\xa1\x06\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_BinanceGetPublicKey\x10\xa2\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinancePublicKey\x10\xa3\x06\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_BinanceSignTx\x10\xa4\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceTxRequest\x10\xa5\x06\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_BinanceTransferMsg\x10\xa6\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceOrderMsg\x10\xa7\x06\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_BinanceCancelMsg\x10\xa8\x06\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_BinanceSignedTx\x10\xa9\x06\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosGetAddress\x10\x84\x07\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_CosmosAddress\x10\x85\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosSignTx\x10\x86\x07\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRequest\x10\x87\x07\x1a\x04\x98\xb5\x18\x01\x12#\n\x18MessageType_CosmosMsgAck\x10\x88\x07\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_CosmosSignedTx\x10\x89\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_CosmosMsgDelegate\x10\x8a\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgUndelegate\x10\x8b\x07\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_CosmosMsgRedelegate\x10\x8c\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_CosmosMsgRewards\x10\x8d\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_CosmosMsgIBCTransfer\x10\x8e\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintGetAddress\x10\xe8\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintAddress\x10\xe9\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintSignTx\x10\xea\x07\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TendermintMsgRequest\x10\xeb\x07\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_TendermintMsgAck\x10\xec\x07\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TendermintMsgSend\x10\xed\x07\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_TendermintSignedTx\x10\xee\x07\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_TendermintMsgDelegate\x10\xef\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgUndelegate\x10\xf0\x07\x1a\x04\x98\xb5\x18\x01\x12.\n#MessageType_TendermintMsgRedelegate\x10\xf1\x07\x1a\x04\x98\xb5\x18\x01\x12+\n MessageType_TendermintMsgRewards\x10\xf2\x07\x1a\x04\x98\xb5\x18\x01\x12/\n$MessageType_TendermintMsgIBCTransfer\x10\xf3\x07\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisGetAddress\x10\xcc\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisAddress\x10\xcd\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisSignTx\x10\xce\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRequest\x10\xcf\x08\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_OsmosisMsgAck\x10\xd0\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSend\x10\xd1\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgDelegate\x10\xd2\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgUndelegate\x10\xd3\x08\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_OsmosisMsgRedelegate\x10\xd4\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgRewards\x10\xd5\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisMsgLPAdd\x10\xd6\x08\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_OsmosisMsgLPRemove\x10\xd7\x08\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_OsmosisMsgLPStake\x10\xd8\x08\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_OsmosisMsgLPUnstake\x10\xd9\x08\x1a\x04\x90\xb5\x18\x01\x12,\n!MessageType_OsmosisMsgIBCTransfer\x10\xda\x08\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_OsmosisMsgSwap\x10\xdb\x08\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_OsmosisSignedTx\x10\xdc\x08\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_MayachainGetAddress\x10\xb0\t\x1a\x04\x90\xb5\x18\x01\x12\'\n\x1cMessageType_MayachainAddress\x10\xb1\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainSignTx\x10\xb2\t\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_MayachainMsgRequest\x10\xb3\t\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_MayachainMsgAck\x10\xb4\t\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_MayachainSignedTx\x10\xb5\t\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_ZcashSignPCZT\x10\x94\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashPCZTAction\x10\x95\n\x1a\x04\x90\xb5\x18\x01\x12)\n\x1eMessageType_ZcashPCZTActionAck\x10\x96\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_ZcashSignedPCZT\x10\x97\n\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_ZcashGetOrchardFVK\x10\x98\n\x1a\x04\x90\xb5\x18\x01\x12&\n\x1bMessageType_ZcashOrchardFVK\x10\x99\n\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ZcashTransparentInput\x10\x9a\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentSigned\x10\x9b\n\x1a\x04\x98\xb5\x18\x01\x12*\n\x1fMessageType_ZcashDisplayAddress\x10\x9c\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_ZcashAddress\x10\x9d\n\x1a\x04\x98\xb5\x18\x01\x12-\n\"MessageType_ZcashTransparentOutput\x10\x9e\n\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_ZcashTransparentAck\x10\x9f\n\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TronGetAddress\x10\xf8\n\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TronAddress\x10\xf9\n\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_TronSignTx\x10\xfa\n\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_TronSignedTx\x10\xfb\n\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_TronSignMessage\x10\xfc\n\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_TronMessageSignature\x10\xfd\n\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_TronVerifyMessage\x10\xfe\n\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_TronSignTypedHash\x10\xff\n\x1a\x04\x90\xb5\x18\x01\x12-\n\"MessageType_TronTypedDataSignature\x10\x80\x0b\x1a\x04\x98\xb5\x18\x01\x12$\n\x19MessageType_TonGetAddress\x10\xdc\x0b\x1a\x04\x90\xb5\x18\x01\x12!\n\x16MessageType_TonAddress\x10\xdd\x0b\x1a\x04\x98\xb5\x18\x01\x12 \n\x15MessageType_TonSignTx\x10\xde\x0b\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_TonSignedTx\x10\xdf\x0b\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_TonSignMessage\x10\xe0\x0b\x1a\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_TonMessageSignature\x10\xe1\x0b\x1a\x04\x98\xb5\x18\x01\x12\'\n\x1cMessageType_HiveGetPublicKey\x10\xc0\x0c\x1a\x04\x90\xb5\x18\x01\x12$\n\x19MessageType_HivePublicKey\x10\xc1\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_HiveSignTx\x10\xc2\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_HiveSignedTx\x10\xc3\x0c\x1a\x04\x98\xb5\x18\x01\x12(\n\x1dMessageType_HiveGetPublicKeys\x10\xc4\x0c\x1a\x04\x90\xb5\x18\x01\x12%\n\x1aMessageType_HivePublicKeys\x10\xc5\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountCreate\x10\xc6\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountCreate\x10\xc7\x0c\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_HiveSignAccountUpdate\x10\xc8\x0c\x1a\x04\x90\xb5\x18\x01\x12.\n#MessageType_HiveSignedAccountUpdate\x10\xc9\x0c\x1a\x04\x98\xb5\x18\x01\x12%\n\x1aMessageType_NearGetAddress\x10\xca\x0c\x1a\x04\x90\xb5\x18\x01\x12\"\n\x17MessageType_NearAddress\x10\xcb\x0c\x1a\x04\x98\xb5\x18\x01\x12!\n\x16MessageType_NearSignTx\x10\xcc\x0c\x1a\x04\x90\xb5\x18\x01\x12#\n\x18MessageType_NearSignedTx\x10\xcd\x0c\x1a\x04\x98\xb5\x18\x01\x12&\n\x1bMessageType_HiveSignMessage\x10\xce\x0c\x1a\x04\x90\xb5\x18\x01\x12(\n\x1dMessageType_HiveSignedMessage\x10\xcf\x0c\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_HiveSignOperations\x10\xd0\x0c\x1a\x04\x90\xb5\x18\x01\x12+\n MessageType_HiveSignedOperations\x10\xd1\x0c\x1a\x04\x98\xb5\x18\x01\x12\x34\n)MessageType_ClearsignAttestorGetPublicKey\x10\xa4\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorPublicKey\x10\xa5\r\x1a\x04\x98\xb5\x18\x01\x12,\n!MessageType_ClearsignAttestorSign\x10\xa6\r\x1a\x04\x90\xb5\x18\x01\x12\x31\n&MessageType_ClearsignAttestorSignature\x10\xa7\r\x1a\x04\x98\xb5\x18\x01\x42,\n\x1a\x63om.keepkey.deviceprotocolB\x0eKeepKeyMessage') , dependencies=[types__pb2.DESCRIPTOR,]) @@ -926,8 +926,8 @@ ], containing_type=None, options=None, - serialized_start=5558, - serialized_end=14585, + serialized_start=5603, + serialized_end=14630, ) _sym_db.RegisterEnumDescriptor(_MESSAGETYPE) @@ -3969,6 +3969,20 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='confirm_title', full_name='DebugLinkState.confirm_title', index=15, + number=16, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='confirm_body', full_name='DebugLinkState.confirm_body', index=16, + number=17, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -3982,7 +3996,7 @@ oneofs=[ ], serialized_start=4862, - serialized_end=5226, + serialized_end=5271, ) @@ -4005,8 +4019,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5228, - serialized_end=5243, + serialized_start=5273, + serialized_end=5288, ) @@ -4050,8 +4064,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5245, - serialized_end=5304, + serialized_start=5290, + serialized_end=5349, ) @@ -4074,8 +4088,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5306, - serialized_end=5327, + serialized_start=5351, + serialized_end=5372, ) @@ -4105,8 +4119,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5329, - serialized_end=5361, + serialized_start=5374, + serialized_end=5406, ) @@ -4129,8 +4143,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5363, - serialized_end=5394, + serialized_start=5408, + serialized_end=5439, ) @@ -4160,8 +4174,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5396, - serialized_end=5444, + serialized_start=5441, + serialized_end=5489, ) @@ -4191,8 +4205,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5446, - serialized_end=5486, + serialized_start=5491, + serialized_end=5531, ) @@ -4229,8 +4243,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5488, - serialized_end=5555, + serialized_start=5533, + serialized_end=5600, ) _FEATURES.fields_by_name['coins'].message_type = types__pb2._COINTYPE diff --git a/scripts/generate-test-report.py b/scripts/generate-test-report.py index 6e405b3a..290947da 100644 --- a/scripts/generate-test-report.py +++ b/scripts/generate-test-report.py @@ -432,8 +432,13 @@ 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'), + # ERC-7730 runtime: the order of the screens is the claim. + ('test_msg_ethereum_erc7730_runtime', 'test_interpolated_intent_parts_show_the_same_values_as_fields'), + ('test_msg_ethereum_erc7730_runtime', 'test_embedded_call_is_shown_under_a_blind_sign_warning'), + ('test_msg_ethereum_erc7730_runtime', 'test_inner_call_is_clear_signed_with_its_own_definition'), + ('test_msg_ethereum_erc7730_runtime', 'test_restoring_outer_definition_rejects_another_valid_definition'), + ('test_msg_ethereum_erc7730_runtime', 'test_an_inner_definition_the_device_refuses_falls_back_to_blind'), # The newest/highest-stakes tx shapes get the full ordered walkthrough too. ('test_msg_ethereum_clear_signing', 'test_clearsign_eip7702_setcode_authorization'), ('test_msg_ethereum_clear_signing', 'test_clearsign_erc4337_entrypoint_v0_7_handleops'), @@ -460,10 +465,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 +477,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 +518,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, )) @@ -3187,6 +3203,269 @@ def _arg_shown(a): 'selector or type-hash identity fields.', []), ]), + ('EX', 'ERC-7730 Runtime - What 7.15 Shows For A Certified Definition', '7.15.0', + 'A host may send a signed ERC-7730 definition before an Ethereum transaction. The device ' + 'verifies it while it streams and then shows the transaction through it: the contract action, ' + 'the signer\'s labels beside values the device decodes from the signed calldata, and inner ' + 'calls reviewed with their own definition. The definition only adds screens. The ' + 'ordinary review and signature follow unchanged: every test here that signs first signs ' + 'the same transaction on the ordinary path and requires an identical signature.\n' + 'In 7.15 this runs only with AdvancedMode on, and the definition\'s signer is shown as a runtime ' + 'signer, "NOT verified by KeepKey". It is annotation, not KeepKey-verified clear signing.', + ['WHAT THE DEVICE EXECUTES (one capability table, shared by the preload verifier, the', + 'runtime and python-keepkey\'s compiler; anything else is refused at preload, before', + 'any screen):', + '- Formatters: raw, amount, tokenAmount, nftName, date, duration, unit, enum,', + ' addressName, embedded calldata. Containers @.from/@.to/@.value. A signed', + ' constant is shown only as a raw field, as the signer\'s own.', + '- Display: intent; interpolated intent as numbered parts; fields; groups; one array', + ' iterated at a time. Condition "optional" only, and it always shows.', + '- Refused: slices, packed words, never/ifIn/ifNotIn/mustMatch/ifEmpty, nested', + ' iteration, ABIs deeper than 8. The compiler omits visible:never fields.', + '- Official registry (pinned 9f37816a): 1,326 of 1,450 calldata formats pass the', + ' device\'s preload checks (asserted in lockstep by test_erc7730_compiler).', + '', + 'WHERE EACH FACT COMES FROM:', + '- Values: decoded on device from the signed calldata, replayed and hash-checked on', + ' every pass. Token tickers and decimals: the firmware table only. Signer text', + ' (labels, messages, units, enum labels) is escaped and shown beside the raw value.', + '', + 'EMBEDDED CALLS (7.15 rule: what cannot be clear-signed is shown under a', + '"Blind signature" warning; 7.16 will reject it):', + '- The inner definition is requested by chain, callee and selector read from the', + ' signed calldata, and bound before any inner screen. One level deep.', + '- Blind in 7.15: no inner definition; a refused one; one showing @.value with no', + ' amountPath; depth 2; calls inside an array; inner bytes without a selector.', + '', + 'KNOWN LIMITS (fail closed, no signature): parallel arrays pair by index and a', + 'shorter one aborts mid-review; a fixed index into a short dynamic array aborts; inner', + 'bytes not canonical for the inner ABI abort after the outer screens. The board pager', + 'may wrap a checksummed address across two OLED pages; every page must be confirmed.', + 'Open for 7.16: Safe DELEGATECALL is shown only as the operation field.'], + [ + ('EX1', 'test_msg_ethereum_erc7730_runtime', + 'test_program_outside_capability_table_is_refused_at_preload', + 'Unexecutable programs are refused at preload', + "A definition using a formatter, condition or path step outside the runtime's capability table is refused while it streams, before any screen. A review can never start and then fail part-way.", + []), + ('EX2', 'test_msg_ethereum_erc7730_runtime', + 'test_path_outside_abi_is_refused_at_preload', + 'Paths outside the ABI are refused at preload', + "Every value path is walked against the function's ABI at preload: a tuple index out of range, a step into a scalar or a path ending on an array is refused before any screen.", + []), + ('EX3', 'test_msg_ethereum_erc7730_runtime', + 'test_raw_field_screens_show_exact_text', + 'Raw fields show the exact decoded value', + "Each field is its own required confirmation under a device-owned title; the label is the signer's, the value is decoded from the signed calldata.", + ['Signer field with decoded value']), + ('EX4', 'test_msg_ethereum_erc7730_runtime', + 'test_token_amount_uses_the_firmware_token_table', + 'Token amounts use the firmware token table', + "Ticker and decimals come only from the firmware's own table for the chain, rendered exactly as the ordinary Ethereum review renders them.", + ['Amount with firmware ticker']), + ('EX5', 'test_msg_ethereum_erc7730_runtime', + 'test_token_named_by_calldata_wins_over_the_hosts_claim', + 'The token is read from calldata, not the host', + "The transaction goes to one contract; the calldata names another token. The device shows the calldata's token, and an unknown token as the exact integer plus its address.", + ['Unknown token, exact integer and address']), + ('EX6', 'test_msg_ethereum_erc7730_runtime', + 'test_signer_label_cannot_name_an_unknown_token', + 'A signer label cannot name a token', + 'A label saying "USDC amount" over an unknown token still shows the raw integer, "unknown token" and the address.', + ['Signer label beside unknown token']), + ('EX7', 'test_msg_ethereum_erc7730_runtime', + 'test_threshold_message_is_shown_beside_the_exact_amount', + 'Threshold messages sit beside the amount', + 'At or above the threshold the signer\'s message is shown, marked "Signer:", above the exact amount, never instead of it; below it no message appears.', + ['Signer message above the exact amount']), + ('EX8', 'test_msg_ethereum_erc7730_runtime', + 'test_native_alias_discloses_signer_mapping_and_token_address', + 'Native-currency aliases disclose the signer mapping and token address', + "An address in the signer's alias set is marked as a signer native alias and shown in full before the native-unit amount; any other address stays an unknown token. The firmware token table wins over an alias.", + ['Signer alias, token address and native-unit amount']), + ('EX9', 'test_msg_ethereum_erc7730_runtime', + 'test_address_name_marks_only_the_signing_account', + '"(this wallet)" marks only the signing account', + 'The signing address is derived on device; an address one byte away is shown without the mark. Addresses are EIP-55 and never truncated.', + ['Address marked (this wallet)']), + ('EX10', 'test_msg_ethereum_erc7730_runtime', + 'test_containers_and_signed_constants', + 'Transaction containers and signed constants', + '@.to is read from the transaction being signed; a constant comes from the signed definition.', + ['Container and constant fields']), + ('EX11', 'test_msg_ethereum_erc7730_runtime', + 'test_interpolated_intent_parts_show_the_same_values_as_fields', + 'Interpolated intent as numbered parts', + 'The intent sentence is shown as numbered parts after the plain intent: "Intent text i of n" for the signer\'s fragments, "Intent value i of n" for values the device formats, identical to the matching field.', + ['Intent text part', 'Intent value part', 'Matching field']), + ('EX12', 'test_msg_ethereum_erc7730_runtime', + 'test_phase_c_formatters_show_exact_text', + 'Date, duration, unit and enum formatters', + 'Each value is formatted on device and always shown with its raw integer; a unit\'s base is preceded by "unit set by signer", so the mark is never on a later page than the value.', + ['Unit value with raw integer']), + ('EX13', 'test_msg_ethereum_erc7730_runtime', + 'test_enum_labels_are_the_signers_claim_beside_the_value', + 'Enum labels never replace the value', + 'A mapped value shows "label (value)" after "label set by signer"; an unmapped one shows "value (unmapped)".', + ['Unmapped enum value']), + ('EX14', 'test_msg_ethereum_erc7730_runtime', + 'test_nft_shows_the_collection_address', + 'NFTs show the token ID and collection address', + 'No collection name is claimed: the token ID and the full collection address.', + ['Token ID and collection']), + ('EX15', 'test_msg_ethereum_erc7730_runtime', + 'test_malformed_calldata_is_refused_before_a_constant_field', + 'Malformed calldata is refused before any field', + 'The up-front validation pass rejects calldata that does not match the ABI before the first field, even one showing a constant.', + []), + ('EX16', 'test_msg_ethereum_erc7730_runtime', + 'test_iteration_shows_every_element_numbered', + 'Dynamic and fixed arrays show every element, numbered', + 'Dynamic and fixed arrays title each element "Signer field i of N"; an empty dynamic array shows no element and still signs.', + ['Dynamic element 1 of 2', 'Dynamic element 2 of 2', + 'Fixed element 1 of 2', 'Fixed element 2 of 2']), + ('EX17', 'test_msg_ethereum_erc7730_runtime', + 'test_grouped_tuple_iteration_and_optional_fields', + 'Grouped tuple arrays and optional fields', + 'Groups only group; an "optional" field is always shown. Nothing given to the device is hidden.', + ['Tuple element fields', 'Optional field shown']), + ('EX18', 'test_msg_ethereum_erc7730_runtime', + 'test_embedded_call_is_shown_under_a_blind_sign_warning', + '7.15: embedded calls without a definition are blind', + 'A "Blind signature" screen, then the inner call\'s callee, selector, length, value and authority, all read from the signed calldata. 7.16 rejects instead.', + ['Blind signature warning', 'Inner call summary']), + ('EX19', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_call_is_clear_signed_with_its_own_definition', + 'Inner calls are clear-signed with their own definition', + 'The device requests the inner definition by chain, callee and selector read from the signed calldata, binds it before any inner screen, runs it under "Inner" titles, then resumes the outer definition.', + ['Call summary', 'Inner action', 'Inner fields', 'Outer field after the inner call']), + ('EX20', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_call_without_a_definition_is_blind_in_715', + 'No inner definition means a blind-sign warning', + 'When the host has no definition for the inner call, the 7.15 blind path follows; it is never a silent blind sign.', + ['Blind signature warning']), + ('EX21', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_definition_for_another_call_is_refused', + 'An inner definition for another selector is refused', + 'A hostile host substitutes the inner definition; the binding check refuses it before any inner screen.', + []), + ('EX22', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_bytes_changed_in_an_inner_pass_are_refused', + 'Changed inner bytes are refused', + 'Every inner pass replays the whole outer calldata against the reviewed digest; a change on a later pass is refused before any inner field.', + []), + ('EX23', 'test_msg_ethereum_erc7730_runtime', + 'test_only_a_raw_field_shows_a_signer_constant', + 'Only a raw field shows a signer constant', + 'A signer constant formatted as an amount (or any formatter but raw) would look like a decoded value, so preload refuses it; as a raw field it is shown as the signer\'s own field.', + ['Constant as a raw field']), + ('EX24', 'test_msg_ethereum_erc7730_runtime', + 'test_control_characters_in_a_value_are_escaped', + 'Control characters are escaped', + 'A newline or tab inside a value is shown as \\x0a / \\x09, so no value can fake a line break.', + ['Escaped value']), + ('EX25', 'test_msg_ethereum_erc7730_runtime', + 'test_a_raw_value_too_long_to_capture_is_shown_blind', + 'Over-long raw values are shown blind with their length', + 'A value longer than the device can hold gets a "Blind signature" screen and "Not shown: N bytes".', + ['Blind signature warning', 'Length of the unshown value']), + ('EX26', 'test_msg_ethereum_erc7730_runtime', + 'test_multiline_values_split_between_lines', + 'Long values split between lines', + 'A value too long for one confirmation is split into numbered confirmations at line boundaries. Within one confirmation the board pager still wraps by pixel width, so a long line can continue on the next OLED page.', + ['Numbered part', 'Next part']), + ('EX27', 'test_msg_ethereum_erc7730_runtime', + 'test_an_inner_definition_the_device_refuses_falls_back_to_blind', + 'A refused inner definition falls back to blind', + 'An inner definition the device cannot execute, or signed by an unknown key (also one refused on a later chunk of a multi-chunk stream), is dropped and the call is shown blind; the outer review then continues.', + ['Blind signature warning', 'Outer review continues']), + ('EX28', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_definition_for_another_callee_or_chain_is_refused', + 'Inner definitions are bound to callee and chain', + 'Substituting a definition for another contract or another chain is refused before any inner screen.', + []), + ('EX29', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_calls_read_their_own_containers', + 'Inner calls read their own context', + 'Inside the inner call @.value is the value it moves, @.from whose authority it runs with and @.to the callee.', + ['Inner value, authority and callee']), + ('EX30', 'test_msg_ethereum_erc7730_runtime', + 'test_embedded_calls_inside_an_iteration_are_shown_blind', + 'Calls inside an array are shown blind', + 'Each element of a multicall gets its own blind-sign warning and summary (multicall clear-signing is future work).', + ['Blind warning per element']), + ('EX31', 'test_msg_ethereum_erc7730_runtime', + 'test_a_fixed_index_into_a_short_array_fails_closed', + 'A fixed index past a short array fails closed', + "Preload cannot know a dynamic array's length; reaching path.[0] of an empty array aborts without a signature.", + []), + ('EX32', 'test_msg_ethereum_erc7730_runtime', + 'test_an_inner_value_the_calldata_does_not_carry_is_never_shown', + 'An inner value the calldata does not carry is never shown', + 'Without amountPath the calldata does not say what the inner call moves; an inner definition that shows @.value is refused and the call shown blind.', + ['Blind signature warning']), + ('EX33', 'test_msg_ethereum_erc7730_runtime', + 'test_parallel_arrays_are_refused_before_preload', + 'Parallel formatter arrays are refused', + 'A token path in a separate array is refused before preload; no signature or review screen is produced.', + []), + ('EX34', 'test_msg_ethereum_erc7730_runtime', + 'test_a_call_at_depth_two_is_shown_blind', + 'Depth is bounded at one', + 'A call inside an inner call is shown blind; the device never requests a depth-2 definition.', + ['Blind warning inside the inner call']), + ('EX35', 'test_msg_ethereum_erc7730_runtime', + 'test_a_wanchain_transaction_is_never_certified', + 'A Wanchain transaction is never certified', + 'A transaction carrying tx_type (Wanchain, valued in WAN) is refused on the certified path before any screen; the review would otherwise name its value ETH.', + []), + ('EX36', 'test_msg_ethereum_erc7730_runtime', + 'test_declining_any_certified_screen_returns_no_signature', + 'Declining any certified screen returns no signature', + 'Declining a blind-sign warning (long value, no inner definition, refused inner definition), a later part of a split value, or an inner field aborts with no signature.', + []), + ('EX37', 'test_msg_ethereum_erc7730_runtime', + 'test_a_long_typed_data_value_points_to_the_walk_not_blind', + 'A long typed-data value is not a blind sign', + 'The typed-data walk shows every leaf in full; a raw field too long to capture says "Shown above in full: N bytes" with no blind-sign warning, and the signature equals the ordinary one.', + ['Shown above in full']), + ('EX38', 'test_msg_ethereum_erc7730_runtime', + 'test_a_refused_inner_call_does_not_blind_the_next_one', + 'A refused inner call does not blind the next one', + 'With two embedded calls, the first inner definition is refused and shown blind; the second is still clear-signed with its own definition.', + ['Blind first call', 'Second call clear-signed']), + ('EX39', 'test_msg_ethereum_erc7730_runtime', + 'test_groups_and_optional_fields_are_always_visible', + 'Nested optional fields remain visible', + 'Groups preserve every field; zero and nonzero optional amounts and fees both appear with the decoded recipient.', + ['Recipient', 'Amount', 'Fee']), + ('EX40', 'test_msg_ethereum_erc7730_runtime', + 'test_large_inner_call_is_bound_across_transport_chunks', + 'Large inner calls bind across transport chunks', + 'A 1,200-byte memo is streamed in 31- and 1,024-byte replies; the inner amount is shown, the ordinary signature matches, and a changed inner replay is refused.', + []), + ('EX41', 'test_msg_ethereum_erc7730_runtime', + 'test_outer_bytes_changed_during_inner_review_are_refused', + 'Inner review binds the entire outer transaction', + 'Changing only the outer operation word during inner validation is refused before any inner field.', + []), + ('EX42', 'test_msg_ethereum_erc7730_runtime', + 'test_restoring_outer_definition_rejects_another_valid_definition', + 'Restored parent definition keeps its identity', + 'A separately accepted signed definition for the same outer selector cannot replace the parent after inner review.', + ['Inner review before parent restore refusal']), + ('EX43', 'test_msg_ethereum_erc7730_runtime', + 'test_inner_definition_identity_cannot_change_between_chunks', + 'Inner definition identity is stable across chunks', + 'Changing the definition ID after the first 31-byte reply is refused before inner action review.', + []), + ('EX44', 'test_msg_ethereum_erc7730_runtime', + 'test_cancelled_inner_review_clears_preload_in_the_same_session', + 'Inner cancellation clears the preload', + 'Declining inner signer, action or field returns no signature; an immediate ordinary retry without Initialize has no certified screens or definition requests.', + []), + ]), + # Two-character id because all 26 letters were taken. The catalog keys on a # string, not a char, so this costs nothing. ('TD', 'Structured EIP-712 - The Device Reads The Document', '7.15.0', @@ -3620,6 +3899,8 @@ def screenshot_test_list(fw_version): # loading regressed, all four would skip and the report would certify a # feature it never exercised. 'test_msg_solana_lut_attestation': '7.15.0', + # Block 7b: every ERC-7730 runtime row in section EX is part of 7.15. + 'test_msg_ethereum_erc7730_runtime': '7.15.0', } # A module can be mandatory for the regular product while being intentionally @@ -3629,6 +3910,7 @@ def screenshot_test_list(fw_version): FULL_FEATURE_ONLY_MUST_RUN_MODULES = { 'test_msg_ethereum_erc20_uniswap_liquidity', 'test_msg_solana_lut_attestation', + 'test_msg_ethereum_erc7730_runtime', } diff --git a/tests/test_erc7730_catalog.py b/tests/test_erc7730_catalog.py index 9501fa17..1f791c8f 100644 --- a/tests/test_erc7730_catalog.py +++ b/tests/test_erc7730_catalog.py @@ -62,6 +62,19 @@ def test_catalog_refuses_unknown_ambiguous_and_malformed_requests(): catalog.add(definition(b"different-envelope")) +def test_catalog_answers_an_unknown_embedded_call_with_none(): + # A top-level lookup the catalog cannot satisfy is an error; an embedded + # call's (recursion_depth set) gets the empty "none" chunk, which firmware + # 7.15 shows under a blind-sign warning and 7.16 rejects. + catalog = erc7730.Catalog((definition(),)) + none = catalog.chunk(request(selector_or_type_hash=b"\0" * 4, + recursion_depth=1)) + assert (none.offset, none.total_length, none.data) == (0, 0, b"") + assert len(none.definition_id) == 32 + with pytest.raises(KeyError): + catalog.chunk(request(selector_or_type_hash=b"\0" * 4)) + + class PreloadClient(object): def __init__(self, envelope): self.envelope = envelope diff --git a/tests/test_erc7730_compiler.py b/tests/test_erc7730_compiler.py index 576d8284..2b2fa6b0 100644 --- a/tests/test_erc7730_compiler.py +++ b/tests/test_erc7730_compiler.py @@ -10,12 +10,53 @@ import pytest from keepkeylib.erc7730_compiler import ( - HEADER_SIZE, compile_calldata, compile_eip712, eip712_encode_type, - load_descriptor, parse_function_signature, + HEADER_SIZE, DeviceCannotExecute, compile_calldata, compile_eip712, + device_refusal, eip712_encode_type, load_descriptor, + parse_function_signature, ) from keepkeylib.signed_metadata import keccak256 +# 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_accepts(program): + validator = _evidence_input("ERC7730_FIRMWARE_VALIDATOR") + with tempfile.NamedTemporaryFile() as compiled: + compiled.write(program) + compiled.flush() + return subprocess.call([validator, compiled.name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) == 0 + + +def _unchecked(compile, *args, **kwargs): + return compile(*args, executable_only=False, **kwargs) + + +def _firmware_validate(program, refusal=None): + """The device verifier accepts `program` exactly when the compiler's + capability mirror does, and `refusal` names the expected reason (None + for a program the device executes).""" + assert device_refusal(program) == refusal + assert _firmware_accepts(program) == (refusal is None) + + def _sections(program): count = program[178] offset = HEADER_SIZE @@ -111,8 +152,8 @@ def test_compiles_deterministic_canonical_calldata_program(): ], network_records=[(1, "Ethereum", "ETH", 18)], ) - first = compile_calldata(descriptor, **kwargs) - second = compile_calldata(descriptor, **kwargs) + first = _unchecked(compile_calldata, descriptor, **kwargs) + second = _unchecked(compile_calldata, descriptor, **kwargs) assert first == second assert first[:4] == b"C773" assert first[4:8] == bytes([1, 2, 0, 1]) @@ -127,18 +168,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: @@ -152,7 +186,7 @@ def test_compiles_official_uniswap_tuple_fixture_through_firmware(): "exactInputSingle((address tokenIn, address tokenOut, uint24 fee, " "address recipient, uint256 amountIn, uint256 amountOutMinimum, " "uint160 sqrtPriceLimitX96) params)") - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, signature, 1, "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45", token_records=[ @@ -168,12 +202,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(): @@ -195,16 +224,11 @@ def test_compiles_and_checks_exact_keepkey_sdk_thorchain_swap(): assert memo == expected["memo"] assert int(fixture["value"], 16) == int(expected["amount"]) - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, fixture["descriptor"], fixture["signature"], fixture["chainId"], fixture["to"], network_records=[(1, "Ethereum", "ETH", 18)]) assert compiled[38:42].hex() == expected["selector"] - 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(): @@ -222,7 +246,7 @@ def test_compiles_array_iteration_separator_and_optional_visibility(): } }} } - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "batch((address recipient,uint256 amount)[] items)", 1, "0x1111111111111111111111111111111111111111") @@ -236,15 +260,49 @@ 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_scalar_value_repeated_inside_iteration(): + descriptor = {"display": {"formats": { + "batch(address[] recipients,address fallback)": { + "intent": "Batch transfer", + "fields": [ + {"path": "recipients.[]", "label": "Recipient", + "format": "addressName", "separator": "Next recipient"}, + {"path": "fallback", "label": "Fallback", + "format": "addressName"}, + ], + } + }}} + program = bytearray(_unchecked( + compile_calldata, descriptor, + "batch(address[] recipients,address fallback)", 1, + "0x1111111111111111111111111111111111111111")) + _firmware_validate(bytes(program)) + # Replace the iterated formatter's path with the scalar formatter's path. + # The display still contains an iteration, so the device must refuse it. + offset = HEADER_SIZE + while offset < len(program): + kind = program[offset] + length = struct.unpack_from(">I", program, offset + 1)[0] + if kind == 6: + start = offset + 5 + assert struct.unpack_from(">H", program, start)[0] == 2 + assert program[start + 7:start + 9] != program[start + 14:start + 16] + program[start + 7:start + 9] = program[start + 14:start + 16] + break + offset += 5 + length + else: + pytest.fail("formatter section missing") + _firmware_validate(bytes(program), + "a field reads another array than its iteration") + + +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 +310,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(): @@ -277,18 +324,15 @@ def test_token_amount_without_token_uses_firmware_raw_fallback(): "format": "tokenAmount"}], } }}} - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "quote(uint256 amount)", 1, "0x1111111111111111111111111111111111111111") + # With no token named the device can only show the raw integer, and its + # 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(): @@ -305,7 +349,7 @@ def test_compiles_typed_if_not_in_and_must_match_conditions(): ], } }}} - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "guard(uint256 mode,address recipient)", 1, "0x1111111111111111111111111111111111111111") sections = _sections(compiled) @@ -315,12 +359,8 @@ def test_compiles_typed_if_not_in_and_must_match_conditions(): assert conditions[10] == 8 literals = sections[4] assert int.from_bytes(literals[:2], "big") == 5 - 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, + "condition opcode 7 is not executed") def test_compiles_interpolated_intent_and_metadata_enum(): @@ -340,7 +380,7 @@ def test_compiles_interpolated_intent_and_metadata_enum(): } }} } - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "swap(bool selling,uint256 amount)", 1, "0x1111111111111111111111111111111111111111") sections = _sections(compiled) @@ -353,12 +393,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(): @@ -374,7 +409,7 @@ def test_compiles_nested_field_group_with_balanced_links(): }], } }}} - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, descriptor, "act((address owner,uint256 amount) details)", 1, "0x1111111111111111111111111111111111111111") display = _sections(compiled)[7] @@ -383,12 +418,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): @@ -416,28 +446,47 @@ def test_loads_bounded_includes_and_compiles_array_backed_group(tmp_path): path = tmp_path / "descriptor.json" path.write_text(json.dumps(descriptor)) loaded = load_descriptor(str(path), str(tmp_path)) - compiled = compile_calldata( + compiled = _unchecked(compile_calldata, loaded, "batch((address to,uint256 amount)[] items)", 1, "0x1111111111111111111111111111111111111111") display = _sections(compiled)[7] count = int.from_bytes(display[:2], "big") opcodes = [display[2 + i * 8] for i in range(count)] assert opcodes == [1, 7, 5, 4, 4, 6, 8, 10] - 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", +) + + +# Formats the device can fully sign with this firmware's capability table. +# Each later phase of the ERC-7730 formatter plan raises this number +# (docs/security/HANDOFF-ERC7730-715-FORMATTERS.md in keepkey-firmware). +# Phase 0 signed 92 (raw fields only). Phase A adds tokenAmount, addressName, +# @.from/@.to and signed constants: 812. It refuses addressName and +# tokenAmount over bytes32/uint256 words that pack an address or an encrypted +# amount, rather than reinterpret bytes the calldata does not say are one. +# Phase B adds the interpolated intent, shown as numbered parts: 954. +# Phase C adds amount, nftName, date, duration, unit, enum and @.value: 1138. +# Phase D adds groups, single-array iteration and "optional" fields. The +# reviewed array-binding rule refuses 21 registry programs whose auxiliary +# formatter paths do not walk the displayed item's array: 1273. +# Phase E1 adds embedded calldata, shown under a blind-sign warning. The +# strict formatter rule refuses 29 formerly accepted programs in this phase: +# 1297 remain signable across the 1450 checked registry formats. +REGISTRY_SIGNABLE = 1297 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") + _evidence_input("ERC7730_FIRMWARE_VALIDATOR") import glob failures = [] + unsupported = [] + signable = 0 checked = 0 for path in glob.glob(os.path.join( registry, "registry", "**", "calldata-*.json"), recursive=True): @@ -453,26 +502,38 @@ 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"]) - with tempfile.NamedTemporaryFile() as output: - output.write(program) - output.flush() - subprocess.check_call( - [validator, output.name], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + try: + program = _unchecked( + compile_calldata, descriptor, signature, + deployment["chainId"], deployment["address"]) + except ValueError as exc: + if any(reason in str(exc) for reason in DEVICE_LIMITS): + unsupported.append(signature) + continue + raise + # The device verifier and the compiler's capability mirror + # must agree on every format, in both directions. + executable = device_refusal(program) is None + if _firmware_accepts(program) != executable: + raise AssertionError( + "device and compiler disagree: %r" % + device_refusal(program)) + signable += executable except Exception as exc: failures.append("%s :: %s :: %s" % ( os.path.basename(path), signature, exc)) 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 + # Passing the parser is not signability: only these pass the device's + # preload capability checks. + assert signable == REGISTRY_SIGNABLE 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) @@ -483,7 +544,7 @@ def test_compiles_official_uniswap_eip712_fixture_through_firmware(): assert encoded == ( "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)" "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)") - compiled = compile_eip712( + compiled = _unchecked(compile_eip712, descriptor, fixture, token_records=[ (1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "USDC", 6) @@ -496,9 +557,123 @@ 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) + + +def test_mirror_applies_the_devices_abi_and_text_limits(): + # Each shape the device refuses at preload is refused by the mirror too, + # and a neighbour inside the limit is accepted by both. + address = "0x" + "11" * 20 + for length, refusal in ((64, None), + (65, "an ABI array exceeds the device limit")): + signature = "f(uint256[%d] a,uint256 b)" % length + descriptor = {"display": {"formats": {signature: { + "intent": "F", "fields": [ + {"path": "b", "label": "B", "format": "raw"}]}}}} + _firmware_validate(_unchecked(compile_calldata, descriptor, signature, + 1, address), refusal) + for label, refusal in (("Line one", None), + ("Line\none", "a program string is not printable text"), + ("Tab\there", "a program string is not printable text"), + ("Del\x7f", "a program string is not printable text")): + descriptor = {"display": {"formats": {"f(uint256 a)": { + "intent": "F", "fields": [ + {"path": "a", "label": label, "format": "raw"}]}}}} + _firmware_validate(_unchecked(compile_calldata, descriptor, + "f(uint256 a)", 1, address), refusal) + + +def test_signed_enum_keys_are_minimal_twos_complement(): + # -128 fits one byte (0x80); the device refuses a longer encoding. + for signature, key in (("f(int8 side)", -128), ("f(int16 side)", -32768), + ("f(int16 side)", -129), ("f(int8 side)", 127)): + descriptor = { + "metadata": {"enums": {"side": {str(key): "Edge", "1": "Long"}}}, + "display": {"formats": {signature: { + "intent": "F", "fields": [{ + "path": "side", "label": "Side", "format": "enum", + "params": {"$ref": "$.metadata.enums.side"}}]}}}} + _firmware_validate(compile_calldata(descriptor, signature, 1, + "0x" + "11" * 20), None) + +def test_refuses_scalar_value_repeated_inside_iteration(): + descriptor = {"display": {"formats": { + "batch(address[] recipients,address fallback)": { + "intent": "Batch transfer", + "fields": [ + {"path": "recipients.[]", "label": "Recipient", + "format": "addressName", "separator": "Next recipient"}, + {"path": "fallback", "label": "Fallback", + "format": "addressName"}, + ], + } + }}} + program = bytearray(_unchecked( + compile_calldata, descriptor, + "batch(address[] recipients,address fallback)", 1, + "0x1111111111111111111111111111111111111111")) + _firmware_validate(bytes(program)) + # Replace the iterated formatter's path with the scalar formatter's path. + # The display still contains an iteration, so the device must refuse it. + offset = HEADER_SIZE + while offset < len(program): + kind = program[offset] + length = struct.unpack_from(">I", program, offset + 1)[0] + if kind == 6: + start = offset + 5 + assert struct.unpack_from(">H", program, start)[0] == 2 + assert program[start + 7:start + 9] != program[start + 14:start + 16] + program[start + 7:start + 9] = program[start + 14:start + 16] + break + offset += 5 + length + else: + pytest.fail("formatter section missing") + _firmware_validate(bytes(program), + "a field reads another array than its iteration") + + +def test_iteration_binds_every_formatter_path_to_its_array(): + signature = ( + "batch((uint256 amount,address token)[] items,address[] other," + "address fallback)") + for token_path, refusal in ( + ("items.[].token", None), + ("other.[]", "a field reads another array than its iteration"), + ("fallback", "a field reads another array than its iteration")): + descriptor = {"display": {"formats": {signature: { + "intent": "Batch", "fields": [{ + "path": "items.[].amount", "label": "Amount", + "format": "tokenAmount", + "params": {"tokenPath": token_path}}]}}}} + program = _unchecked(compile_calldata, descriptor, signature, 1, + "0x" + "11" * 20) + _firmware_validate(program, refusal) + if refusal: + with pytest.raises(DeviceCannotExecute, match=refusal): + compile_calldata(descriptor, signature, 1, "0x" + "11" * 20) + + +def test_signer_constant_is_not_an_intent_value(): + signature = "pay(address recipient)" + descriptor = {"display": {"formats": {signature: { + "intent": "Pay", "fields": [ + {"value": "Signer claim", "label": "Claim", "format": "raw"}]}}}} + program = bytearray(_unchecked(compile_calldata, descriptor, signature, + 1, "0x" + "11" * 20)) + _firmware_validate(bytes(program)) # a signer-owned field is allowed + offset = HEADER_SIZE + while offset < len(program): + kind = program[offset] + length = struct.unpack_from(">I", program, offset + 1)[0] + if kind == 7: + instruction = offset + 5 + 2 + 8 + assert program[instruction] == 4 + formatter = struct.unpack_from(">H", program, instruction + 4)[0] + program[instruction:instruction + 8] = struct.pack( + ">BBHHH", 3, 0, formatter, 0xffff, 0xffff) + break + offset += 5 + length + else: + pytest.fail("display section missing") + _firmware_validate(bytes(program), + "a signer constant cannot be an intent value") diff --git a/tests/test_msg_eip712_streaming.py b/tests/test_msg_eip712_streaming.py index f3e5862e..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,6 +276,46 @@ 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.15.0") @@ -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,12 +365,10 @@ def test_array_of_structs_walks(self): "message": {"items": [{"id": 1}, {"id": 2}]}, } resp = self._walk(doc) - self.assertIsInstance(resp, eth.EthereumTypedDataSignature) - self.assertEqual(len(resp.signature), 65) + self._assert_reference_signature(doc, resp) def test_multidimensional_arrays_walk_outermost_first_on_device(self): """Host and device must traverse asymmetric Solidity dimensions alike.""" - self.requires_release_capability("evm-unknown-token-review") doc = { 'types': { 'EIP712Domain': [], @@ -233,8 +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. @@ -288,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. @@ -320,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 c3e4a1c9..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 @@ -1029,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( diff --git a/tests/test_msg_ethereum_erc7730_runtime.py b/tests/test_msg_ethereum_erc7730_runtime.py new file mode 100644 index 00000000..a25fb585 --- /dev/null +++ b/tests/test_msg_ethereum_erc7730_runtime.py @@ -0,0 +1,1347 @@ +"""ERC-7730 runtime (7.15): what the device shows for a certified definition. + +Each test signs the same transaction on the ordinary path and then with a +certified ERC-7730 definition, reads every confirmation's exact title and +body over DebugLink, and requires the same signature both times: the +definition adds screens, never changes what is signed. + +The definition is signed by a throwaway catalog key loaded as a runtime +signer ("NOT verified by KeepKey"); AdvancedMode is required. Uses only the +public test mnemonic. Also imported by keepkey-firmware's +scripts/emulator/test_stack07_regressions.py for its harness. +""" + +import copy +import hashlib + +import common +from keepkeylib import erc7730, erc7730_compiler, eip712_stream +from keepkeylib import messages_ethereum_pb2 as eth +from keepkeylib import messages_pb2 as proto +from keepkeylib import types_pb2 as types +from keepkeylib.signed_metadata import TEST_PRIVATE_KEY, test_signer_compressed_pubkey as signer_pubkey + + +PATH = [0x8000002C, 0x8000003C, 0x80000000, 0, 0] +ADDRESS = bytes.fromhex("11" * 20) +OTHER_ADDRESS = bytes.fromhex("33" * 20) +# An unrelated throwaway key that is never loaded into any signer slot. +UNKNOWN_SIGNER_KEY = bytes.fromhex("42" * 32) +# The ordinary review that follows every certified one. Its screens are not +# captured for the report: they are unchanged, which the identical signature +# already proves, and the report's frame picker would otherwise prefer the +# per-transaction data hash over the screens the definition adds. +ORDINARY_REVIEW = ("Send", "Transaction", "Ethereum Data Hash") + + +def assert_failure(test, result, code, message): + test.assertIsInstance(result, proto.Failure) + test.assertEqual((result.code, result.message), (code, message)) + + +class Erc7730Harness(object): + """Preload, sign and walk helpers. Not a TestCase: importing it collects + no tests.""" + + def _envelope(self, program, signer_key=TEST_PRIVATE_KEY, signer_pub=None, + chain=1): + cert = bytearray(139) + cert[0] = 1 + cert[2:6] = chain.to_bytes(4, "big") + cert[10:21] = b"Host alias\0" + cert[42:75] = signer_pub if signer_pub is not None else signer_pubkey() + return erc7730.sign_envelope(program, cert, signer_key) + + def _definition(self, program, envelope): + return erc7730.Definition( + envelope, program[7], 1, ADDRESS, + program[38:42] if program[7] == 1 else program[38:70]) + + def _load_signer(self): + self.client.load_clearsign_signer( + key_id=3, pubkey=signer_pubkey(), alias="Audit signer") + + def _preload(self, program): + self._load_signer() + envelope = self._envelope(program) + erc7730.preload(self.client, self._definition(program, envelope)) + self._drop_setup_screenshots() + return envelope + + def _raw_preload(self, envelope): + """Stream an envelope; return the first non-Ack response. + + Every refusal contract below is a refusal BEFORE any ButtonRequest, + so a ButtonRequest here is returned (and fails the caller's Failure + assertion) rather than acknowledged. + """ + definition_id = hashlib.sha256(envelope).digest() + offset = 0 + while offset < len(envelope): + data = envelope[offset:offset + erc7730.MAX_CHUNK] + response = self.client.call_raw(eth.EthereumClearSignDefinition( + definition_id=definition_id, offset=offset, + total_length=len(envelope), data=data)) + if not isinstance(response, eth.EthereumClearSignDefinitionAck): + return response + offset += len(data) + return response + + def _first_pages(self): + """(title, body) of each confirmation's first page. A body that pages + continues under ButtonRequest_Other with the same text; any other + repeat is a second confirmation and is kept, so a double display + stays visible.""" + return [screen for screen, code in zip(self.screens, self.button_codes) + if code != types.ButtonRequest_Other] + + def _walk(self, start, envelope=b"", doc=None, change_pass=None, cancel_button=None, + arguments=None, catalog=None, altered=None, cancel_title=None, + chunk_size=None): + response = self.client.call_raw(start) + self.definition_requests = 0 + self.button_codes = [] + self.screens = [] + self.calldata_chunk_sizes = [] + buttons = 0 + calldata_passes = 0 + calldata_offset = 0 + typed_passes = 0 + for _ in range(1000): + if isinstance(response, proto.ButtonRequest): + buttons += 1 + self.button_codes.append(response.code) + self.screens.append(self.client.debug.read_confirm_text()) + if self.screens[-1][0] not in ORDINARY_REVIEW: + self.client.capture_oled() + decline = (buttons == cancel_button or + self.screens[-1][0] == cancel_title) + self.client.debug.press_no() if decline else self.client.debug.press_yes() + response = self.client.call_raw(proto.ButtonAck()) + elif isinstance(response, eth.EthereumClearSignDefinitionRequest) and catalog: + self.definition_requests += 1 + response = self.client.call_raw(catalog.chunk(response)) + elif isinstance(response, eth.EthereumClearSignDefinitionRequest): + self.definition_requests += 1 + offset = response.offset + response = self.client.call_raw(eth.EthereumClearSignDefinitionChunk( + definition_id=hashlib.sha256(envelope).digest(), offset=offset, + total_length=len(envelope), data=envelope[offset:offset + response.length])) + elif isinstance(response, eth.EthereumTypedDataStructRequest): + response = self.client.call_raw(eip712_stream.build_struct_ack( + eip712_stream.struct_members(doc, response.name))) + elif isinstance(response, eth.EthereumTypedDataValueRequest): + path = list(response.member_path) + if path == [1, 0]: + typed_passes += 1 + current = copy.deepcopy(doc) + if change_pass == typed_passes: + current["message"]["first"] += 1 + resolved = eip712_stream.resolve_member_path(current, path) + value = (eip712_stream.encode_array_length(resolved[1]) + if resolved[0] == "length" else + eip712_stream.encode_value(resolved[1], resolved[2])) + response = self.client.call_raw(eth.EthereumTypedDataValueAck(value=value)) + elif isinstance(response, eth.EthereumTxRequest) and response.HasField("data_length"): + if calldata_offset == 0: + calldata_passes += 1 + data = arguments + if altered and altered[0] == calldata_passes: + data = altered[1] + if data is None: + data = (43 if change_pass == calldata_passes else 42).to_bytes(32, "big") + data += (7).to_bytes(32, "big") + self.assertGreater(response.data_length, 0) + self.assertLessEqual(response.data_length, + len(data) - calldata_offset) + length = min(response.data_length, + chunk_size or response.data_length) + chunk = data[calldata_offset:calldata_offset + length] + self.calldata_chunk_sizes.append(length) + calldata_offset += length + if calldata_offset == len(data): + calldata_offset = 0 + response = self.client.call_raw(eth.EthereumTxAck(data_chunk=chunk)) + else: + return response, buttons, calldata_passes, typed_passes + self.fail("protocol did not terminate") + + +class TestMsgEthereumErc7730Runtime(Erc7730Harness, common.KeepKeyTest): + def setUp(self): + super().setUp() + self.requires_fullFeature() + self.requires_firmware("7.15.0") + self.setup_mnemonic_nopin_nopassphrase() + self.client.apply_policy("AdvancedMode", 1) + + # Phase 0 of the ERC-7730 formatter plan: the preload verifier and the + # runtime share one capability table, so a program the runtime cannot + # finish is refused before the first screen instead of after the user has + # approved the signer, intent and earlier fields. + def _audit_start(self, program, data_length=68): + return eth.EthereumSignTx(address_n=PATH, nonce=b"", gas_price=b"\x01", + gas_limit=b"\xff\xff", to=ADDRESS, value=b"", chain_id=1, + data_length=data_length, data_initial_chunk=program[38:42]) + + def test_program_outside_capability_table_is_refused_at_preload(self): + signature = "audit(uint256 first,uint256 second)" + # Conditions that could hide or veto a field are never executed. + fields = { + "ifNotIn": {"path": "first", "label": "First value", + "format": "raw", "visible": {"ifNotIn": [0]}}, + "mustMatch": {"path": "first", "label": "First value", + "format": "raw", "visible": {"mustMatch": [42]}}, + } + self._load_signer() + for name, field in sorted(fields.items()): + descriptor = {"display": {"formats": {signature: { + "intent": "Audit action", "fields": [ + field, + {"path": "second", "label": "Second value", + "format": "raw"}]}}}} + with self.assertRaises(erc7730_compiler.DeviceCannotExecute): + erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS, executable_only=False) + result = self._raw_preload(self._envelope(program)) + assert_failure(self, result, types.Failure_SyntaxError, + "Invalid certified ERC-7730 definition") + # Nothing is left preloaded: the transaction takes the ordinary, + # uncertified path and never asks for a definition. + result, _, passes, _ = self._walk(self._audit_start(program)) + self.assertIsInstance(result, eth.EthereumTxRequest, msg=name) + self.assertTrue(result.HasField("signature_r"), msg=name) + self.assertEqual((name, passes, self.definition_requests), + (name, 1, 0)) + + def test_path_outside_abi_is_refused_at_preload(self): + signature = "audit(uint256 first,uint256 second)" + descriptor = {"display": {"formats": {signature: { + "intent": "Audit action", "fields": [ + {"path": "second", "label": "Second value", + "format": "raw"}]}}}} + program = bytearray(erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS)) + # The single value path is (source 1, one step, index 1). Point it at + # a third argument the function does not have. + step = program.index(bytes([1, 1, 0xff, 0xff, 1, 0, 0, 0, 1])) + program[step + 8] = 2 + self._load_signer() + result = self._raw_preload(self._envelope(bytes(program))) + assert_failure(self, result, types.Failure_SyntaxError, + "Invalid certified ERC-7730 definition") + + def test_raw_field_screens_show_exact_text(self): + signature = "audit(uint256 first,uint256 second)" + descriptor = {"display": {"formats": {signature: { + "intent": "Audit action", "fields": [ + {"path": "first", "label": "First value", "format": "raw"}, + {"path": "second", "label": "Second value", + "format": "raw"}]}}}} + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, self._word(42) + self._word(7)) + self.assertEqual(self.calldata_passes, 4) + certified = [screen for screen in self.screens if screen[0] in ( + "Runtime signer", "Unverified data", "Contract action", + "Signer field")] + self.assertEqual(certified[1:], [ + ("Unverified data", "NOT verified by KeepKey"), + ("Contract action", "Audit action"), + ("Signer field", "First value:\n42"), + ("Signer field", "Second value:\n7"), + ]) + self.assertEqual(certified[0][0], "Runtime signer") + self.assertTrue(certified[0][1].startswith("Audit signer ("), + certified[0][1]) + + # Phase A: tokenAmount, addressName, @.from/@.to and signed constants. + # Asset facts come only from the firmware token table; an address is always + # shown in full; the signer's message sits beside the value. + USDC = bytes.fromhex("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") + # Not in the firmware token table on chain 1 (0xeeee..ee is: there the + # table's own entry wins over any signer alias). + NATIVE = bytes.fromhex("44" * 20) + + def _certified(self, program, arguments, preload=None, catalog=None): + """Sign once on the ordinary path, then with the definition, and + require the same signature: the annotations are additive only.""" + start = self._audit_start(program, 4 + len(arguments)) + baseline, _, _, _ = self._walk(start, arguments=arguments) + self.assertIsInstance(baseline, eth.EthereumTxRequest) + self.assertTrue(baseline.HasField("signature_r")) + envelope = self._preload(program) if preload is None else preload() + result, _, passes, _ = self._walk(start, envelope, arguments=arguments, + catalog=catalog) + self.calldata_passes = passes + # The certified path ran: the device asked for its definition. + self.assertGreater(self.definition_requests, 0) + self.assertIsInstance(result, eth.EthereumTxRequest) + self.assertEqual((result.signature_r, result.signature_s), + (baseline.signature_r, baseline.signature_s)) + return result + + def _field_screens(self, descriptor, signature, arguments): + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, arguments) + return [body for title, body in self._first_pages() + if title == "Signer field"] + + @staticmethod + def _word(value): + if isinstance(value, bytes): + return bytes(12) + value + return value.to_bytes(32, "big") + + def _token_screen(self, token, amount, params=None): + signature = "send(address token,uint256 amount)" + fields = [{"path": "amount", "label": "Amount", "format": "tokenAmount", + "params": dict({"tokenPath": "token"}, **(params or {}))}] + descriptor = {"display": {"formats": {signature: { + "intent": "Send", "fields": fields}}}} + return self._field_screens(descriptor, signature, + self._word(token) + self._word(amount)) + + def test_token_amount_uses_the_firmware_token_table(self): + self.assertEqual(self._token_screen(self.USDC, 1500000), + ["Amount:\n1.5 USDC"]) + + def test_token_named_by_calldata_wins_over_the_hosts_claim(self): + # The transaction goes to ADDRESS and a host might call it USDC; the + # calldata names another token, which the firmware table does not know. + self.assertEqual(self._token_screen(OTHER_ADDRESS, 42), [ + "Amount:\n42\nunknown token\n0x" + OTHER_ADDRESS.hex()]) + + def test_signer_label_cannot_name_an_unknown_token(self): + signature = "send(uint256 amount)" + descriptor = {"display": {"formats": {signature: { + "intent": "Send", "fields": [{ + "path": "amount", "label": "USDC amount", + "format": "tokenAmount", + "params": {"token": "0x" + OTHER_ADDRESS.hex()}}]}}}} + self.assertEqual( + self._field_screens(descriptor, signature, self._word(42)), + ["USDC amount:\n42\nunknown token\n0x" + OTHER_ADDRESS.hex()]) + + def test_threshold_message_is_shown_beside_the_exact_amount(self): + params = {"threshold": 1000000, "message": "Large amount"} + self.assertEqual(self._token_screen(self.USDC, 999999, params), + ["Amount:\n0.999999 USDC"]) + self.assertEqual(self._token_screen(self.USDC, 1500000, params), + ["Amount:\nSigner: Large amount\n1.5 USDC"]) + + def test_native_alias_discloses_signer_mapping_and_token_address(self): + params = {"nativeCurrencyAddress": ["0x" + self.NATIVE.hex()]} + # An address outside the alias set is not the native asset. + self.assertEqual( + self._token_screen(OTHER_ADDRESS, 1500000000000000000, params), + ["Amount:\n1500000000000000000\nunknown token\n0x" + + OTHER_ADDRESS.hex()]) + self.assertEqual( + self._token_screen(self.NATIVE, 1500000000000000000, params), + ["Amount:\nSigner native alias:\n0x" + self.NATIVE.hex() + + "\n1.5 ETH"]) + + def test_address_name_marks_only_the_signing_account(self): + signer = self.client.ethereum_get_address(PATH) + if not isinstance(signer, bytes): + signer = bytes.fromhex(signer[2:]) + signature = "pay(address recipient)" + descriptor = {"display": {"formats": {signature: { + "intent": "Pay", "fields": [{ + "path": "recipient", "label": "Recipient", + "format": "addressName"}]}}}} + from keepkeylib.signed_metadata import keccak256 + + def checksummed(address): + digest = keccak256(address.hex().encode("ascii")).hex() + return "0x" + "".join( + c.upper() if c.isalpha() and int(digest[i], 16) >= 8 else c + for i, c in enumerate(address.hex())) + + near = bytes(signer[:19]) + bytes([signer[19] ^ 1]) + self.assertEqual( + self._field_screens(descriptor, signature, self._word(near)), + ["Recipient:\n" + checksummed(near)]) + self.assertEqual( + self._field_screens(descriptor, signature, self._word(signer)), + ["Recipient:\n" + checksummed(signer) + "\n(this wallet)"]) + + def test_containers_and_signed_constants(self): + signature = "audit(uint256 first,uint256 second)" + descriptor = {"display": {"formats": {signature: { + "intent": "Audit action", "fields": [ + {"path": "@.to", "label": "Contract", "format": "addressName"}, + {"value": "Audit protocol", "label": "Protocol"}]}}}} + self.assertEqual( + self._field_screens(descriptor, signature, + self._word(42) + self._word(7)), + ["Contract:\n0x" + ADDRESS.hex(), "Protocol:\nAudit protocol"]) + + # Phase B: the interpolated intent is shown as numbered parts after the + # plain intent. Each value part is formatted exactly as its field is. + def test_interpolated_intent_parts_show_the_same_values_as_fields(self): + signature = "send(address token,uint256 amount)" + descriptor = {"display": {"formats": {signature: { + "intent": "Send tokens", + "interpolatedIntent": "Send {amount} now", + "fields": [{"path": "amount", "label": "Amount", + "format": "tokenAmount", + "params": {"tokenPath": "token"}}]}}}} + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, self._word(self.USDC) + self._word(1500000)) + shown = [screen for screen in self._first_pages() + if screen[0] in ("Contract action", "Intent text 1 of 3", + "Intent value 2 of 3", "Intent text 3 of 3", + "Signer field")] + self.assertEqual(shown, [ + ("Contract action", "Send tokens"), + ("Intent text 1 of 3", "Send"), + ("Intent value 2 of 3", "1.5 USDC"), + ("Intent text 3 of 3", "now"), + ("Signer field", "Amount:\n1.5 USDC"), + ]) + + # Phase C: amount, date, duration, unit, enum and nftName. Signer-supplied + # units and enum labels appear beside the raw value, never instead. + def _one_field(self, field, arguments, signature="act(uint256 a,address b)", + metadata=None): + descriptor = {"display": {"formats": {signature: { + "intent": "Act", "fields": [field]}}}} + if metadata: + descriptor["metadata"] = metadata + return self._field_screens(descriptor, signature, arguments) + + def test_phase_c_formatters_show_exact_text(self): + pad = self._word(OTHER_ADDRESS) + cases = [ + ({"path": "a", "label": "Value", "format": "amount"}, + 1500000000000000000, "Value:\n1.5 ETH"), + ({"path": "a", "label": "When", "format": "date", + "params": {"encoding": "timestamp"}}, + 1700000000, "When:\n2023-11-14 22:13:20 UTC\n(1700000000)"), + ({"path": "a", "label": "At", "format": "date", + "params": {"encoding": "blockheight"}}, + 19000000, "At:\nBlock 19000000"), + ({"path": "a", "label": "Lock", "format": "duration"}, + 93784, "Lock:\n1d 2h 3m 4s\n(93784 s)"), + ({"path": "a", "label": "Weight", "format": "unit", + "params": {"base": "kg", "decimals": 3}}, + 93784, "Weight:\nunit set by signer\n93.784 kg\nraw 93784"), + ] + for field, value, expected in cases: + self.assertEqual( + (field["format"], + self._one_field(field, self._word(value) + pad)), + (field["format"], [expected])) + + def test_enum_labels_are_the_signers_claim_beside_the_value(self): + field = {"path": "a", "label": "Side", "format": "enum", + "params": {"$ref": "$.metadata.enums.side"}} + metadata = {"enums": {"side": {"0": "Buy", "1": "Sell"}}} + pad = self._word(OTHER_ADDRESS) + self.assertEqual(self._one_field(field, self._word(1) + pad, + metadata=metadata), + ["Side:\nlabel set by signer\nSell (1)"]) + self.assertEqual(self._one_field(field, self._word(5) + pad, + metadata=metadata), + ["Side:\n5 (unmapped)"]) + + def test_nft_shows_the_collection_address(self): + field = {"path": "a", "label": "Item", "format": "nftName", + "params": {"collectionPath": "b"}} + self.assertEqual( + self._one_field(field, self._word(42) + self._word(self.USDC)), + ["Item:\nToken ID 42\nCollection\n" + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]) + + def test_malformed_calldata_is_refused_before_a_constant_field(self): + # The first field shows a signed constant and captures nothing, so + # only the up-front validation pass stands between malformed calldata + # and the first screen. + signature = "pay(address recipient)" + descriptor = {"display": {"formats": {signature: { + "intent": "Pay", "fields": [ + {"value": "Audit protocol", "label": "Protocol"}, + {"path": "recipient", "label": "Recipient", + "format": "addressName"}]}}}} + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + envelope = self._preload(program) + dirty = b"\x01" + bytes(11) + OTHER_ADDRESS # non-canonical address + result, buttons, passes, _ = self._walk( + self._audit_start(program, 36), envelope, arguments=dirty) + assert_failure(self, result, types.Failure_SyntaxError, + "ERC-7730 calldata does not match definition") + self.assertEqual((buttons, passes), (0, 1)) + + # Phase D: groups, "optional" fields and one iteration at a time. Every + # element gets its own numbered screens; nothing is ever hidden. + def _titled_fields(self, descriptor, signature, arguments): + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, arguments) + return [screen for screen in self._first_pages() + if screen[0].startswith("Signer field")] + + def test_iteration_shows_every_element_numbered(self): + signature = "pay(address[] recipients)" + descriptor = {"display": {"formats": {signature: { + "intent": "Pay", "fields": [{ + "path": "recipients.[]", "label": "Recipient", + "format": "addressName"}]}}}} + # An empty array shows no element and still signs. + self.assertEqual( + self._titled_fields(descriptor, signature, + self._word(32) + self._word(0)), []) + arguments = (self._word(32) + self._word(2) + + self._word(OTHER_ADDRESS) + self._word(ADDRESS)) + self.assertEqual( + self._titled_fields(descriptor, signature, arguments), [ + ("Signer field 1 of 2", "Recipient:\n0x" + OTHER_ADDRESS.hex()), + ("Signer field 2 of 2", "Recipient:\n0x" + ADDRESS.hex()), + ]) + # A fixed ABI array has no length word. Capture still binds the + # declared count before replaying both element screens. + fixed_signature = "payFixed(address[2] recipients)" + fixed_descriptor = {"display": {"formats": {fixed_signature: { + "intent": "Pay fixed", "fields": [{ + "path": "recipients.[]", "label": "Recipient", + "format": "addressName"}]}}}} + self.assertEqual( + self._titled_fields( + fixed_descriptor, fixed_signature, + self._word(OTHER_ADDRESS) + self._word(ADDRESS)), [ + ("Signer field 1 of 2", "Recipient:\n0x" + + OTHER_ADDRESS.hex()), + ("Signer field 2 of 2", "Recipient:\n0x" + ADDRESS.hex()), + ]) + + def test_grouped_tuple_iteration_and_optional_fields(self): + signature = "batch((address to,uint256 amount)[] items,uint256 fee)" + descriptor = {"display": {"formats": {signature: { + "intent": "Batch", "fields": [ + {"path": "items.[]", "label": "Transfer", "fields": [ + {"path": "to", "label": "To", "format": "addressName"}, + {"path": "amount", "label": "Amount", "format": "raw"}]}, + {"path": "fee", "label": "Fee", "format": "raw", + "visible": "optional"}]}}}} + arguments = (self._word(64) + self._word(9) + self._word(2) + + self._word(OTHER_ADDRESS) + self._word(5) + + self._word(ADDRESS) + self._word(6)) + self.assertEqual( + self._titled_fields(descriptor, signature, arguments), [ + ("Signer field 1 of 2", "To:\n0x" + OTHER_ADDRESS.hex()), + ("Signer field 1 of 2", "Amount:\n5"), + ("Signer field 2 of 2", "To:\n0x" + ADDRESS.hex()), + ("Signer field 2 of 2", "Amount:\n6"), + ("Signer field", "Fee:\n9"), + ]) + + # Phase E1 (7.15): an embedded call the device cannot clear-sign is shown + # under a blind-sign warning: its callee, selector, length, value and + # authority, all read from the signed calldata. 7.16 rejects it instead. + def _exec_screens(self, inner, value=1500000000000000000): + signature = ("execTransaction(address to,uint256 value,bytes data," + "uint8 operation)") + descriptor = {"display": {"formats": {signature: { + "intent": "sign multisig operation", "fields": [ + {"path": "data", "label": "Transaction", "format": "calldata", + "params": {"calleePath": "to", "amountPath": "value", + "spenderPath": "@.to"}}]}}}} + padded = inner + bytes(-len(inner) % 32) + arguments = (self._word(OTHER_ADDRESS) + self._word(value) + + self._word(128) + self._word(0) + + self._word(len(inner)) + padded) + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, arguments) + return [screen for screen in self._first_pages() + if screen[0] in ("Blind signature", "Signer field")] + + def test_embedded_call_is_shown_under_a_blind_sign_warning(self): + inner = bytes.fromhex("a9059cbb") + bytes(296) # 300 bytes: no capture + self.assertEqual(self._exec_screens(inner), [ + ("Blind signature", "The inner call is not clear-signed"), + ("Signer field", + "Transaction:\nTo 0x" + OTHER_ADDRESS.hex() + + "\nFunction 0xa9059cbb\nData 300 bytes\nValue 1.5 ETH\nAs 0x" + + ADDRESS.hex()), + ]) + self.assertEqual(self._exec_screens(b"", value=0), [ + ("Blind signature", "The inner call is not clear-signed"), + ("Signer field", + "Transaction:\nTo 0x" + OTHER_ADDRESS.hex() + + "\nNo data\nValue 0 Wei\nAs 0x" + ADDRESS.hex()), + ]) + + # Phase E2: the inner call is clear-signed with its own definition, bound + # to the callee and selector in the signed calldata. Every inner pass + # replays the whole outer calldata against the reviewed digest. + EXEC = ("execTransaction(address to,uint256 value,bytes data," + "uint8 operation)") + TRANSFER = "transfer(address to,uint256 amount)" + + def _exec_setup(self, amount_path=True, spender="@.to", memo=None): + inner_signature = (self.TRANSFER if memo is None else + "transfer(address to,uint256 amount,bytes memo)") + params = {"calleePath": "to"} + if spender: + params["spenderPath"] = spender + if amount_path: + params["amountPath"] = "value" + outer_descriptor = {"display": {"formats": {self.EXEC: { + "intent": "sign multisig operation", "fields": [ + {"path": "data", "label": "Transaction", "format": "calldata", + "params": params}, + {"path": "operation", "label": "Operation", "format": "raw"}]}}}} + inner_descriptor = {"display": {"formats": {inner_signature: { + "intent": "Transfer", "fields": [ + {"path": "to", "label": "Recipient", "format": "addressName"}, + {"path": "amount", "label": "Amount", "format": "tokenAmount", + "params": {"tokenPath": "@.to"}}]}}}} + outer = erc7730_compiler.compile_calldata( + outer_descriptor, self.EXEC, 1, ADDRESS) + inner = erc7730_compiler.compile_calldata( + inner_descriptor, inner_signature, 1, self.USDC) + self._load_signer() + outer_env = self._envelope(outer) + inner_def = erc7730.Definition(self._envelope(inner), 1, 1, self.USDC, + inner[38:42]) + outer_def = self._definition(outer, outer_env) + self._exec_outer = (outer, outer_def) + call = (bytes.fromhex("a9059cbb") + self._word(OTHER_ADDRESS) + + self._word(1500000)) + if memo is not None: + call = (inner[38:42] + self._word(OTHER_ADDRESS) + + self._word(1500000) + self._word(96) + + self._word(len(memo)) + memo + bytes(-len(memo) % 32)) + arguments = (self._word(self.USDC) + self._word(0) + + self._word(128) + self._word(0) + + self._word(len(call)) + call + + bytes(-len(call) % 32)) + start = self._audit_start(outer, 4 + len(arguments)) + return start, arguments, outer_def, inner_def + + def _exec_certified(self, arguments, catalog): + outer, outer_def = self._exec_outer + + def preload(): + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + return b"" + return self._certified(outer, arguments, preload=preload, + catalog=catalog) + + def _relevant(self): + titles = ("Runtime signer", "Inner signer", "Contract action", + "Inner action", "Signer field", "Inner field", + "Blind signature") + return [screen for screen in self._first_pages() if screen[0] in titles] + + def test_inner_call_is_clear_signed_with_its_own_definition(self): + start, arguments, outer_def, inner_def = self._exec_setup() + self._exec_certified(arguments, erc7730.Catalog((outer_def, inner_def))) + shown = [s for s in self._relevant() + if s[0] not in ("Runtime signer", "Inner signer")] + self.assertEqual(shown, [ + ("Contract action", "sign multisig operation"), + ("Signer field", + "Transaction:\nTo 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + "\nFunction 0xa9059cbb\nData 68 bytes\nValue 0 Wei\nAs 0x" + + ADDRESS.hex()), + ("Inner action", "Transfer"), + ("Inner field", "Recipient:\n0x" + OTHER_ADDRESS.hex()), + ("Inner field", "Amount:\n1.5 USDC"), + ("Signer field", "Operation:\n0"), + ]) + self.assertIn("Inner signer", [s[0] for s in self.screens]) + + def test_inner_call_without_a_definition_is_blind_in_715(self): + start, arguments, outer_def, _ = self._exec_setup() + self._exec_certified(arguments, erc7730.Catalog((outer_def,))) + shown = [s for s in self._relevant() + if s[0] != "Runtime signer"] + self.assertEqual(shown[1:3], [ + ("Blind signature", "The inner call is not clear-signed"), + ("Signer field", + "Transaction:\nTo 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + "\nFunction 0xa9059cbb\nData 68 bytes\nValue 0 Wei\nAs 0x" + + ADDRESS.hex()), + ]) + + def test_inner_definition_for_another_call_is_refused(self): + start, arguments, outer_def, inner_def = self._exec_setup() + # A host that answers the inner request with a definition for another + # selector: the device binds it to the signed selector and refuses. + approve = erc7730_compiler.compile_calldata( + {"display": {"formats": {"approve(address spender,uint256 amount)": { + "intent": "Approve", "fields": []}}}}, + "approve(address spender,uint256 amount)", 1, self.USDC) + wrong = erc7730.Definition(self._envelope(approve), 1, 1, self.USDC, + approve[38:42]) + + class Substituting(erc7730.Catalog): + def chunk(self, request): + if request.HasField("recursion_depth"): + request = copy.deepcopy(request) + request.selector_or_type_hash = wrong.selector_or_type_hash + return erc7730.Catalog.chunk(self, request) + + erc7730.preload(self.client, outer_def) + result, _, _, _ = self._walk( + start, arguments=arguments, + catalog=Substituting((outer_def, inner_def, wrong))) + assert_failure(self, result, types.Failure_SyntaxError, + "ERC-7730 inner definition does not match") + self.assertNotIn("Inner action", [s[0] for s in self.screens]) + + def test_inner_bytes_changed_in_an_inner_pass_are_refused(self): + start, arguments, outer_def, inner_def = self._exec_setup() + # Pass 1 validates and fixes the digest; passes 2-4 capture the outer + # fields up to the inner call. Pass 5 is the inner call's own + # validation pass: change one byte of its amount there. + altered = bytearray(arguments) + altered[-40] ^= 1 + erc7730.preload(self.client, outer_def) + result, buttons, passes, _ = self._walk( + start, arguments=arguments, altered=(5, bytes(altered)), + catalog=erc7730.Catalog((outer_def, inner_def))) + assert_failure(self, result, types.Failure_SyntaxError, + "ERC-7730 calldata does not match definition") + self.assertEqual(passes, 5) + self.assertNotIn("Inner field", [s[0] for s in self.screens]) + + def test_large_inner_call_is_bound_across_transport_chunks(self): + # The inner bytes exceed both the capture buffer and one transport + # reply. A 31-byte reply splits selectors, offsets and ABI words. + for chunk_size in (1024, 31): + with self.subTest(chunk_size=chunk_size): + _, arguments, outer_def, inner_def = self._exec_setup( + memo=b"Z" * 1200) + outer = self._exec_outer[0] + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + start = self._audit_start(outer, 4 + len(arguments)) + reviewed, _, _, _ = self._walk( + start, arguments=arguments, chunk_size=chunk_size, + catalog=erc7730.Catalog((outer_def, inner_def))) + self.assertIsInstance(reviewed, eth.EthereumTxRequest) + self.assertTrue(reviewed.HasField("signature_r")) + reviewed_chunks = list(self.calldata_chunk_sizes) + self.assertGreater(len(reviewed_chunks), 1) + self.assertLessEqual(max(reviewed_chunks), chunk_size) + if chunk_size == 31: + self.assertGreater(len(reviewed_chunks), 40) + self.assertIn(("Inner field", "Amount:\n1.5 USDC"), + self._relevant()) + ordinary, _, _, _ = self._walk( + start, arguments=arguments, chunk_size=chunk_size) + self.assertIsInstance(ordinary, eth.EthereumTxRequest) + self.assertTrue(ordinary.HasField("signature_r")) + self.assertGreater(len(self.calldata_chunk_sizes), 1) + self.assertLessEqual(max(self.calldata_chunk_sizes), chunk_size) + self.assertEqual(self.definition_requests, 0) + self.assertEqual( + (reviewed.signature_r, reviewed.signature_s, + reviewed.signature_v), + (ordinary.signature_r, ordinary.signature_s, + ordinary.signature_v)) + if chunk_size == 31: + # Change an embedded memo byte on the inner validation + # pass, after the outer review fixed the signed digest. + altered = bytearray(arguments) + altered[-40] ^= 1 + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + rejected, _, passes, _ = self._walk( + start, arguments=arguments, altered=(5, bytes(altered)), + chunk_size=chunk_size, + catalog=erc7730.Catalog((outer_def, inner_def))) + assert_failure(self, rejected, types.Failure_SyntaxError, + "ERC-7730 calldata does not match definition") + self.assertEqual(passes, 5) + self.assertNotIn("Inner field", [s[0] for s in self.screens]) + + def test_outer_bytes_changed_during_inner_review_are_refused(self): + start, arguments, outer_def, inner_def = self._exec_setup() + # The operation word belongs to the outer call, outside the inner + # byte range. Inner validation must still bind the whole transaction. + altered = bytearray(arguments) + altered[127] = 1 + erc7730.preload(self.client, outer_def) + result, _, passes, _ = self._walk( + start, arguments=arguments, altered=(5, bytes(altered)), + catalog=erc7730.Catalog((outer_def, inner_def))) + assert_failure(self, result, types.Failure_SyntaxError, + "ERC-7730 calldata does not match definition") + self.assertEqual(passes, 5) + self.assertNotIn("Inner field", [s[0] for s in self.screens]) + + def test_restoring_outer_definition_rejects_another_valid_definition(self): + start, arguments, outer_def, inner_def = self._exec_setup() + replacement = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.EXEC: { + "intent": "Replacement", "fields": []}}}}, + self.EXEC, 1, ADDRESS) + wrong = self._definition(replacement, self._envelope(replacement)) + # Establish that this is a valid signed definition for the same call; + # the failure must come from the frozen outer definition identity. + erc7730.preload(self.client, wrong) + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + + class Substituting(erc7730.Catalog): + substituted = False + + def chunk(self, request): + if (request.HasField("definition_id") and + not request.HasField("contract_address")): + self.substituted = True + return eth.EthereumClearSignDefinitionChunk( + definition_id=wrong.definition_id, offset=0, + total_length=len(wrong.envelope), + data=wrong.envelope[:request.length]) + return super().chunk(request) + + catalog = Substituting((outer_def, inner_def)) + result, _, _, _ = self._walk(start, arguments=arguments, + catalog=catalog) + self.assertTrue(catalog.substituted) + self.assertIn(("Inner field", "Amount:\n1.5 USDC"), self._relevant()) + assert_failure(self, result, types.Failure_SyntaxError, + "Invalid certified ERC-7730 definition") + self.assertNotIn(types.ButtonRequest_SignTx, self.button_codes) + + def test_inner_definition_identity_cannot_change_between_chunks(self): + start, arguments, outer_def, inner_def = self._exec_setup() + erc7730.preload(self.client, outer_def) + + class ChangingIdentity(erc7730.Catalog): + changed = False + + def chunk(self, request): + response = super().chunk(request) + if request.HasField("recursion_depth"): + if request.offset == 0: + response.data = response.data[:31] + else: + self.changed = True + response.definition_id = bytes(32) + return response + + catalog = ChangingIdentity((outer_def, inner_def)) + result, _, _, _ = self._walk(start, arguments=arguments, + catalog=catalog) + self.assertTrue(catalog.changed) + assert_failure(self, result, types.Failure_SyntaxError, + "Invalid certified ERC-7730 definition") + self.assertNotIn("Inner action", [s[0] for s in self.screens]) + + def test_cancelled_inner_review_clears_preload_in_the_same_session(self): + for title in ("Inner signer", "Inner action", "Inner field"): + with self.subTest(title=title): + start, arguments, outer_def, inner_def = self._exec_setup() + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + result, _, _, _ = self._walk( + start, arguments=arguments, cancel_title=title, + catalog=erc7730.Catalog((outer_def, inner_def))) + assert_failure(self, result, types.Failure_ActionCancelled, + "Signing cancelled by user") + self.assertEqual(self.screens[-1][0], title) + self.assertNotIn(types.ButtonRequest_SignTx, self.button_codes) + # No Initialize, signer reset or new preload between calls. + result, _, _, _ = self._walk(start, arguments=arguments) + self.assertIsInstance(result, eth.EthereumTxRequest) + self.assertTrue(result.HasField("signature_r")) + self.assertEqual(self.definition_requests, 0) + self.assertEqual(self._relevant(), []) + + # ---- Audit remediation: each of these used to fail mid-review, take a + # fact from the wrong source, or was untested. ---- + + def test_only_a_raw_field_shows_a_signer_constant(self): + # Formatters show values the device decodes. A signer constant + # formatted as an amount would look decoded, so preload refuses it; + # as a raw field it is the signer's own field, and shows. + signature = "act(uint256 a,address b)" + for constant in (5, 1000): + descriptor = {"display": {"formats": {signature: { + "intent": "Act", "fields": [{ + "value": constant, "label": "Fee", "format": "amount"}]}}}} + with self.assertRaises(erc7730_compiler.DeviceCannotExecute): + erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS, executable_only=False) + self._load_signer() + assert_failure(self, self._raw_preload(self._envelope(program)), + types.Failure_SyntaxError, + "Invalid certified ERC-7730 definition") + self.assertEqual( + self._one_field({"value": 1000, "label": "Fee"}, + self._word(1) + self._word(OTHER_ADDRESS)), + ["Fee:\n1000"]) + + def test_a_wanchain_transaction_is_never_certified(self): + # tx_type marks a Wanchain transaction, whose value is WAN; the + # certified review would name it ETH. Refused before any screen. + signature = "audit(uint256 first,uint256 second)" + program = erc7730_compiler.compile_calldata( + {"display": {"formats": {signature: { + "intent": "Audit action", "fields": [ + {"path": "first", "label": "First", + "format": "amount"}]}}}}, + signature, 1, ADDRESS) + self._preload(program) + start = self._audit_start(program) + start.tx_type = 1 + result, buttons, _, _ = self._walk(start) + assert_failure(self, result, types.Failure_SyntaxError, + "ERC-7730 definition does not match transaction") + self.assertEqual(buttons, 0) + + def test_control_characters_in_a_value_are_escaped(self): + signature = "note(string text)" + descriptor = {"display": {"formats": {signature: { + "intent": "Note", "fields": [ + {"path": "text", "label": "Text", "format": "raw"}]}}}} + text = b"a\nb\tc" + arguments = (self._word(32) + self._word(len(text)) + text + + bytes(-len(text) % 32)) + self.assertEqual(self._field_screens(descriptor, signature, arguments), + ["Text:\na\\x0ab\\x09c"]) + + def test_a_raw_value_too_long_to_capture_is_shown_blind(self): + signature = "blob(bytes data)" + descriptor = {"display": {"formats": {signature: { + "intent": "Blob", "fields": [ + {"path": "data", "label": "Data", "format": "raw"}]}}}} + data = bytes(range(200)) + arguments = (self._word(32) + self._word(len(data)) + data + + bytes(-len(data) % 32)) + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, arguments) + self.assertEqual( + [s for s in self._first_pages() + if s[0] in ("Blind signature", "Signer field")], + [("Blind signature", "The value is too long to show"), + ("Signer field", "Data:\nNot shown: 200 bytes")]) + + def test_a_long_typed_data_value_points_to_the_walk_not_blind(self): + # Typed data: the walk shows every leaf in full, so a raw field too + # long to capture says where it was shown, with no blind warning. + data = bytes(range(200)) + doc = { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}], + "Blob": [{"name": "data", "type": "bytes"}]}, + "primaryType": "Blob", + "domain": {"name": "Audit app", "chainId": 1, + "verifyingContract": "0x" + ADDRESS.hex()}, + "message": {"data": "0x" + data.hex()}} + program = erc7730_compiler.compile_eip712( + {"display": {"formats": {"Blob(bytes data)": { + "intent": "Blob", "fields": [ + {"path": "data", "label": "Data", "format": "raw"}]}}}}, + doc) + start = eth.EthereumSignTypedData(address_n=PATH, primary_type="Blob", + metamask_v4_compat=True) + baseline, _, _, _ = self._walk(start, doc=doc) + self.assertIsInstance(baseline, eth.EthereumTypedDataSignature) + envelope = self._preload(program) + result, _, _, _ = self._walk(start, envelope, doc) + self.assertIsInstance(result, eth.EthereumTypedDataSignature) + self.assertEqual(result.signature, baseline.signature) + self.assertNotIn("Blind signature", [s[0] for s in self.screens]) + self.assertIn(("Signer field", "Data:\nShown above in full: 200 bytes"), + self._first_pages()) + + def test_multiline_values_split_between_lines(self): + # A label of 64 non-ASCII bytes escapes to 256 characters, leaving 93 + # per confirmation: the unknown-token body splits into numbered + # confirmations between lines, so no confirmation ends mid-address. + label = "é" * 32 + signature = "send(address token,uint256 amount)" + descriptor = {"display": {"formats": {signature: { + "intent": "Send", "fields": [{ + "path": "amount", "label": label, "format": "tokenAmount", + "params": {"tokenPath": "token"}}]}}}} + arguments = self._word(OTHER_ADDRESS) + self._word(10 ** 60) + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, arguments) + parts = [body for title, body in self._first_pages() + if title.startswith("Signer field")] + self.assertGreater(len(parts), 1) + address = "0x" + OTHER_ADDRESS.hex() + self.assertEqual(sum(1 for body in parts if body.endswith(address)), 1) + for body in parts: + self.assertFalse(body.endswith(address[:10]), body) + + def _exec_inner_catalog(self, inner_program, chain=1, signer=None): + env = (self._envelope(inner_program, chain=chain) if signer is None + else self._envelope(inner_program, *signer, chain=chain)) + return erc7730.Definition(env, 1, chain, inner_program[18:38], + inner_program[38:42]) + + def test_an_inner_definition_the_device_refuses_falls_back_to_blind(self): + start, arguments, outer_def, _ = self._exec_setup() + refused = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [{ + "path": "to", "label": "Recipient", "format": "raw", + "visible": {"ifNotIn": ["0x" + ADDRESS.hex()]}}]}}}}, + self.TRANSFER, 1, self.USDC, executable_only=False) + from ecdsa import SigningKey, SECP256k1 + unknown = SigningKey.from_string( + UNKNOWN_SIGNER_KEY, curve=SECP256k1).get_verifying_key().to_string( + "compressed") + clear = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": []}}}}, + self.TRANSFER, 1, self.USDC) + # Large enough to stream in several chunks, so the refusal comes on a + # later chunk and the fetch position must be reset for the outer. + large = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [ + {"path": "to", "label": "Recipient %02d %s" % (i, "r" * 44), + "format": "raw"} for i in range(24)]}}}}, + self.TRANSFER, 1, self.USDC) + signed_large = self._exec_inner_catalog( + large, signer=(UNKNOWN_SIGNER_KEY, unknown)) + self.assertGreater(len(signed_large.envelope), 2 * erc7730.MAX_CHUNK) + for inner in (self._exec_inner_catalog(refused), + self._exec_inner_catalog( + clear, signer=(UNKNOWN_SIGNER_KEY, unknown)), + signed_large): + self._exec_certified(arguments, + erc7730.Catalog((outer_def, inner))) + shown = self._relevant() + self.assertIn(("Blind signature", + "The inner call is not clear-signed"), shown) + self.assertNotIn("Inner action", [s[0] for s in shown]) + self.assertEqual(shown[-1], ("Signer field", "Operation:\n0")) + + def test_inner_definition_for_another_callee_or_chain_is_refused(self): + start, arguments, outer_def, inner_def = self._exec_setup() + other_callee = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": []}}}}, + self.TRANSFER, 1, OTHER_ADDRESS) + other_chain = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": []}}}}, + self.TRANSFER, 56, self.USDC) + for wrong in (self._exec_inner_catalog(other_callee), + self._exec_inner_catalog(other_chain, chain=56)): + class Substituting(erc7730.Catalog): + def chunk(self, request): + if request.HasField("recursion_depth") and request.recursion_depth: + replaced = copy.deepcopy(request) + replaced.chain_id = wrong.chain_id + replaced.contract_address = wrong.contract_address + return erc7730.Catalog.chunk(self, replaced) + return erc7730.Catalog.chunk(self, request) + erc7730.preload(self.client, outer_def) + result, _, _, _ = self._walk( + start, arguments=arguments, + catalog=Substituting((outer_def, inner_def, wrong))) + assert_failure(self, result, types.Failure_SyntaxError, + "ERC-7730 inner definition does not match") + self.assertNotIn("Inner action", [s[0] for s in self.screens]) + + def test_inner_calls_read_their_own_containers(self): + # Inside the inner call @.value is the value it moves and @.from + # whose authority it runs with: the outer spenderPath, or without + # one the outer contract. + inner = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [ + {"path": "@.value", "label": "Moves", "format": "amount"}, + {"path": "@.from", "label": "As", "format": "addressName"}, + {"path": "@.to", "label": "Token", "format": "addressName"}, + ]}}}}, + self.TRANSFER, 1, self.USDC) + usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + for spender, authority in ((None, "0x" + ADDRESS.hex()), + ("to", usdc)): + _, arguments, outer_def, _ = self._exec_setup(spender=spender) + # Give the outer call a value: the inner call moves 2 ETH. + moving = bytearray(arguments) + moving[32:64] = self._word(2 * 10 ** 18) + self._exec_certified(bytes(moving), erc7730.Catalog( + (outer_def, self._exec_inner_catalog(inner)))) + inner_fields = [s[1] for s in self._relevant() + if s[0] == "Inner field"] + self.assertEqual(inner_fields, [ + "Moves:\n2 ETH", "As:\n" + authority, "Token:\n" + usdc]) + + def test_embedded_calls_inside_an_iteration_are_shown_blind(self): + signature = "multicall((address callee,bytes data)[] calls)" + descriptor = {"display": {"formats": {signature: { + "intent": "Multicall", "fields": [{ + "path": "calls.[].data", "label": "Call", "format": "calldata", + "params": {"calleePath": "calls.[].callee"}}]}}}} + call = bytes.fromhex("a9059cbb") + self._word(OTHER_ADDRESS) + self._word(1) + padded = call + bytes(-len(call) % 32) + element = (self._word(OTHER_ADDRESS) + self._word(64) + + self._word(len(call)) + padded) + arguments = (self._word(32) + self._word(2) + self._word(64) + + self._word(64 + len(element)) + element + element) + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + self._certified(program, arguments) + shown = [s for s in self._first_pages() + if s[0] == "Blind signature" or s[0].startswith("Signer field")] + body = ("Call:\nTo 0x" + OTHER_ADDRESS.hex() + + "\nFunction 0xa9059cbb\nData 68 bytes") + self.assertEqual(shown, [ + ("Blind signature", "The inner call is not clear-signed"), + ("Signer field 1 of 2", body), + ("Blind signature", "The inner call is not clear-signed"), + ("Signer field 2 of 2", body), + ]) + + def test_a_fixed_index_into_a_short_array_fails_closed(self): + # Data-dependent: preload cannot know the array's length. The device + # refuses without a signature when it reaches the field. + signature = "swap(address[] path,uint256 amount)" + descriptor = {"display": {"formats": {signature: { + "intent": "Swap", "fields": [{ + "path": "amount", "label": "Amount", "format": "tokenAmount", + "params": {"tokenPath": "path.[0]"}}]}}}} + program = erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + arguments = self._word(64) + self._word(5) + self._word(0) + envelope = self._preload(program) + result, _, _, _ = self._walk( + self._audit_start(program, 4 + len(arguments)), envelope, + arguments=arguments) + self.assertIsInstance(result, proto.Failure) + self.assertEqual(result.message, + "ERC-7730 calldata does not match definition") + self.assertNotIn(types.ButtonRequest_SignTx, self.button_codes) + + def test_an_inner_value_the_calldata_does_not_carry_is_never_shown(self): + # Without amountPath the calldata does not say what the inner call + # moves: an inner definition that shows @.value is shown blind, while + # one that does not is still clear-signed. + _, arguments, outer_def, inner_def = self._exec_setup(amount_path=False) + self._exec_certified(arguments, erc7730.Catalog((outer_def, inner_def))) + self.assertIn("Inner field", [s[0] for s in self._relevant()]) + _, arguments, outer_def, _ = self._exec_setup(amount_path=False) + shows_value = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [ + {"path": "@.value", "label": "Moves", "format": "amount"}]}}}}, + self.TRANSFER, 1, self.USDC) + self._exec_certified(arguments, erc7730.Catalog( + (outer_def, self._exec_inner_catalog(shows_value)))) + shown = self._relevant() + self.assertIn(("Blind signature", + "The inner call is not clear-signed"), shown) + self.assertNotIn("Inner field", [s[0] for s in shown]) + + def test_parallel_arrays_are_refused_before_preload(self): + # A token formatter cannot pair amounts[i] with an independent + # tokens[i] array: the auxiliary token path must read the active array. + signature = "batch(address[] tokens,uint256[] amounts)" + descriptor = {"display": {"formats": {signature: { + "intent": "Batch", "fields": [{ + "path": "amounts.[]", "label": "Amount", "format": "tokenAmount", + "params": {"tokenPath": "tokens.[]"}}]}}}} + + with self.assertRaisesRegex( + erc7730_compiler.DeviceCannotExecute, + "a field reads another array than its iteration"): + erc7730_compiler.compile_calldata( + descriptor, signature, 1, ADDRESS) + + def test_a_call_at_depth_two_is_shown_blind(self): + # A Safe executing a call on a second Safe: the second Safe's + # definition is clear-signed one level deep, and the transfer it + # carries is shown blind. The device never fetches a depth-2 + # definition. + from keepkeylib.signed_metadata import keccak256 + _, _, outer_def, _ = self._exec_setup() + second_safe = bytes.fromhex("55" * 20) + middle = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.EXEC: { + "intent": "sign multisig operation", "fields": [ + {"path": "data", "label": "Transaction", "format": "calldata", + "params": {"calleePath": "to", "amountPath": "value", + "spenderPath": "@.to"}}, + {"path": "operation", "label": "Operation", + "format": "raw"}]}}}}, + self.EXEC, 1, second_safe) + + def execute(to, data): + return (self._word(to) + self._word(0) + self._word(128) + + self._word(0) + self._word(len(data)) + data + + bytes(-len(data) % 32)) + selector = keccak256( + b"execTransaction(address,uint256,bytes,uint8)")[:4] + transfer = (bytes.fromhex("a9059cbb") + self._word(OTHER_ADDRESS) + + self._word(1500000)) + arguments = execute(second_safe, + selector + execute(self.USDC, transfer)) + depths = [] + + class Recording(erc7730.Catalog): + def chunk(self, request): + depths.append(request.recursion_depth) + return erc7730.Catalog.chunk(self, request) + self._exec_certified(arguments, Recording( + (outer_def, self._exec_inner_catalog(middle)))) + self.assertEqual(max(depths), 1) + shown = [s for s in self._relevant() + if s[0] not in ("Runtime signer", "Inner signer")] + self.assertEqual([s[0] for s in shown], [ + "Contract action", "Signer field", "Inner action", + "Blind signature", "Inner field", "Inner field", "Signer field"]) + self.assertEqual(shown[3][1], "The inner call is not clear-signed") + self.assertTrue(shown[4][1].startswith( + "Transaction:\nTo 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + "\nFunction 0xa9059cbb\nData 68 bytes"), shown[4][1]) + self.assertEqual(shown[5][1], "Operation:\n0") + self.assertEqual(shown[6][1], "Operation:\n0") + + + def _declined(self, program, arguments, title, preload=None, + catalog=None): + envelope = self._preload(program) if preload is None else preload() + result, _, _, _ = self._walk( + self._audit_start(program, 4 + len(arguments)), envelope, + arguments=arguments, catalog=catalog, cancel_title=title) + assert_failure(self, result, types.Failure_ActionCancelled, + "Signing cancelled by user") + self.assertEqual(self.screens[-1][0], title) + self.assertNotIn(types.ButtonRequest_SignTx, self.button_codes) + + def test_declining_any_certified_screen_returns_no_signature(self): + # Every certified screen is a consent: the blind-sign warnings, a + # later part of a split value and an inner field each abort when + # declined, with no signature. + signature = "blob(bytes data)" + blob = erc7730_compiler.compile_calldata( + {"display": {"formats": {signature: {"intent": "Blob", "fields": [ + {"path": "data", "label": "Data", "format": "raw"}]}}}}, + signature, 1, ADDRESS) + data = bytes(range(200)) + self._declined(blob, self._word(32) + self._word(len(data)) + data + + bytes(-len(data) % 32), "Blind signature") + + signature = "send(address token,uint256 amount)" + split = erc7730_compiler.compile_calldata( + {"display": {"formats": {signature: {"intent": "Send", "fields": [{ + "path": "amount", "label": "\u00e9" * 32, + "format": "tokenAmount", + "params": {"tokenPath": "token"}}]}}}}, + signature, 1, ADDRESS) + self._declined(split, self._word(OTHER_ADDRESS) + self._word(10 ** 60), + "Signer field (2/2)") + + _, arguments, outer_def, inner_def = self._exec_setup() + outer = self._exec_outer[0] + + def preload(): + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + return b"" + # No inner definition: the E1 warning. + self._declined(outer, arguments, "Blind signature", preload=preload, + catalog=erc7730.Catalog((outer_def,))) + # A refused inner definition: the warning on the blind re-run. + refused = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [{ + "path": "to", "label": "Recipient", "format": "raw", + "visible": {"ifNotIn": ["0x" + ADDRESS.hex()]}}]}}}}, + self.TRANSFER, 1, self.USDC, executable_only=False) + self._declined(outer, arguments, "Blind signature", preload=preload, + catalog=erc7730.Catalog( + (outer_def, self._exec_inner_catalog(refused)))) + # A clear-signed inner call: its field. + self._declined(outer, arguments, "Inner field", preload=preload, + catalog=erc7730.Catalog((outer_def, inner_def))) + + + def test_a_refused_inner_call_does_not_blind_the_next_one(self): + # Two embedded calls: the first inner definition is refused and shown + # blind; the second is still clear-signed with its own definition. + signature = "execTwo(address a,bytes da,address b,bytes db)" + other_token = bytes.fromhex("55" * 20) + outer = erc7730_compiler.compile_calldata( + {"display": {"formats": {signature: { + "intent": "Run two calls", "fields": [ + {"path": "da", "label": "First", "format": "calldata", + "params": {"calleePath": "a"}}, + {"path": "db", "label": "Second", "format": "calldata", + "params": {"calleePath": "b"}}]}}}}, + signature, 1, ADDRESS) + refused = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [{ + "path": "to", "label": "Recipient", "format": "raw", + "visible": {"ifNotIn": ["0x" + ADDRESS.hex()]}}]}}}}, + self.TRANSFER, 1, self.USDC, executable_only=False) + second = erc7730_compiler.compile_calldata( + {"display": {"formats": {self.TRANSFER: { + "intent": "Transfer", "fields": [ + {"path": "to", "label": "Recipient", + "format": "addressName"}]}}}}, + self.TRANSFER, 1, other_token) + call = (bytes.fromhex("a9059cbb") + self._word(OTHER_ADDRESS) + + self._word(1500000)) + padded = self._word(len(call)) + call + bytes(-len(call) % 32) + arguments = (self._word(self.USDC) + self._word(128) + + self._word(other_token) + self._word(128 + len(padded)) + + padded + padded) + self._load_signer() + outer_def = self._definition(outer, self._envelope(outer)) + + def preload(): + erc7730.preload(self.client, outer_def) + self._drop_setup_screenshots() + return b"" + self._certified(outer, arguments, preload=preload, + catalog=erc7730.Catalog(( + outer_def, self._exec_inner_catalog(refused), + self._exec_inner_catalog(second)))) + titles = [s[0] for s in self._relevant() + if s[0] not in ("Runtime signer", "Inner signer")] + self.assertEqual(titles, [ + "Contract action", "Blind signature", "Signer field", + "Signer field", "Inner action", "Inner field"]) + self.assertEqual(self._relevant()[-1], + ("Inner field", "Recipient:\n0x" + OTHER_ADDRESS.hex())) + + def test_groups_and_optional_fields_are_always_visible(self): + signature = "pay(address recipient,uint256 amount,uint256 fee)" + descriptor = {"display": {"formats": {signature: { + "intent": "Pay", "fields": [ + {"label": "Payment", "visible": "optional", "fields": [ + {"path": "recipient", "label": "Recipient", + "format": "addressName"}, + {"label": "Details", "fields": [ + {"path": "amount", "label": "Amount", "format": "raw", + "visible": "optional"}]}]}, + {"path": "fee", "label": "Fee", "format": "raw", + "visible": "optional"}]}}}} + for amount, fee in ((0, 0), (5, 9)): + self.assertEqual( + self._field_screens(descriptor, signature, + self._word(OTHER_ADDRESS) + self._word(amount) + self._word(fee)), + ["Recipient:\n0x" + OTHER_ADDRESS.hex(), + "Amount:\n" + str(amount), "Fee:\n" + str(fee)]) diff --git a/tests/test_msg_ripple_sign_tx.py b/tests/test_msg_ripple_sign_tx.py index af0db89c..5cb8c4bd 100644 --- a/tests/test_msg_ripple_sign_tx.py +++ b/tests/test_msg_ripple_sign_tx.py @@ -122,11 +122,11 @@ def test_sign_with_thorchain_memo(self): resp = self.client.call(msg) # Verify the XRPL Memos array is appended to the serialized tx. - # Format: 0xF9 (STArray[9]) 0xEA (STObject[10]) 0x72 (MemoData VL[2]) + # Format: 0xF9 (STArray[9]) 0xEA (STObject[10]) 0x7D (MemoData VL[13]) # 0xE1 (end object) 0xF1 (end array) memo_bytes = memo.encode('ascii') expected_tail = ( - bytes([0xF9, 0xEA, 0x72, len(memo_bytes)]) + + bytes([0xF9, 0xEA, 0x7D, len(memo_bytes)]) + memo_bytes + bytes([0xE1, 0xF1]) ) @@ -152,6 +152,36 @@ def test_sign_with_thorchain_memo(self): "plain send must not contain Memos array (0xF9 0xEA marker sequence)" ) + def test_memo_length_prefix_boundaries(self): + self.requires_release_capability("ripple-memo-policy") + self.requires_fullFeature() + self.requires_firmware("7.15.0") + self.setup_mnemonic_allallall() + + for length in (191, 192, 193, 199): + # A distinct suffix makes the last display page inspectable. + memo = "A" * (length - 8) + ("TAIL%04d" % length) + msg = messages.RippleSignTx( + address_n=parse_path("m/44'/144'/0'/0/0"), + payment=messages.RipplePayment( + amount=100000000, + destination="rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws" + ), + flags=0x80000000, + fee=100000, + sequence=25 + length, + memo=memo + ) + resp = self.client.call(msg) + prefix = (bytes([length]) if length <= 192 else + bytes([193, length - 193])) + expected_tail = (b'\xf9\xea\x7d' + prefix + + memo.encode('ascii') + b'\xe1\xf1') + self.assertTrue( + resp.serialized_tx.endswith(expected_tail), + "serialized Ripple memo has wrong prefix at length %d" % length + ) + def test_unsupported_memo_is_rejected(self): self.requires_release_capability("ripple-memo-policy") self.requires_fullFeature() diff --git a/tests/test_report_variant_validation.py b/tests/test_report_variant_validation.py index f22507c9..5e26709d 100644 --- a/tests/test_report_variant_validation.py +++ b/tests/test_report_variant_validation.py @@ -99,6 +99,35 @@ def test_complete_release_still_requires_staged_controls(self): 'test_attestation_does_not_replay_onto_another_transaction', 'skipped-but-required'), failed) + def test_stack08_runtime_controls_are_required_on_full_only(self): + results = catalog_results_with_solana_lut_skipped('7.15.0') + os.environ['KK_RELEASE_MISSING_CAPABILITIES'] = 'solana-lut-attestation' + controls = [ + (module, method) for section, _, _, _, _, tests in REPORT.SECTIONS + if section == 'EX' for identifier, module, method, _, _, _ in tests + if identifier in ('EX40', 'EX41', 'EX42', 'EX43', 'EX44')] + self.assertEqual(5, len(controls)) + self.assertEqual((True, []), + REPORT.validate_junit('7.15.0', results, 'full')) + for module, method in controls: + for status in ('missing', 'skip', 'fail'): + with self.subTest(method=method, status=status): + changed = dict(results) + key = module + '::' + method + if status == 'missing': + del changed[key] + else: + changed[key] = status + ok, failures = REPORT.validate_junit( + '7.15.0', changed, 'full') + self.assertFalse(ok) + self.assertTrue(any(item[1:3] == (module, method) + for item in failures)) + changed = dict(results) + changed[module + '::' + method] = 'skip' + self.assertEqual((True, []), REPORT.validate_junit( + '7.15.0', changed, 'bitcoin-only')) + class TestReportVariantEnvironmentIsolation(unittest.TestCase):