diff --git a/client/src/ledger_app_clients/ethereum/client.py b/client/src/ledger_app_clients/ethereum/client.py index d9ddcae88..a6008402b 100644 --- a/client/src/ledger_app_clients/ethereum/client.py +++ b/client/src/ledger_app_clients/ethereum/client.py @@ -146,25 +146,30 @@ def eip712_sign_legacy(self, def eip712_filtering_activate(self): return self._exchange_async(self._cmd_builder.eip712_filtering_activate()) + def eip712_filtering_discarded_path(self, path: str): + return self._exchange(self._cmd_builder.eip712_filtering_discarded_path(path)) + def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes): return self._exchange_async(self._cmd_builder.eip712_filtering_message_info(name, filters_count, sig)) - def eip712_filtering_amount_join_token(self, token_idx: int, sig: bytes): + def eip712_filtering_amount_join_token(self, token_idx: int, sig: bytes, discarded: bool): return self._exchange_async(self._cmd_builder.eip712_filtering_amount_join_token(token_idx, - sig)) + sig, + discarded)) - def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: bytes): + def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: bytes, discarded: bool): return self._exchange_async(self._cmd_builder.eip712_filtering_amount_join_value(token_idx, name, - sig)) + sig, + discarded)) - def eip712_filtering_datetime(self, name: str, sig: bytes): - return self._exchange_async(self._cmd_builder.eip712_filtering_datetime(name, sig)) + def eip712_filtering_datetime(self, name: str, sig: bytes, discarded: bool): + return self._exchange_async(self._cmd_builder.eip712_filtering_datetime(name, sig, discarded)) - def eip712_filtering_raw(self, name: str, sig: bytes): - return self._exchange_async(self._cmd_builder.eip712_filtering_raw(name, sig)) + def eip712_filtering_raw(self, name: str, sig: bytes, discarded: bool): + return self._exchange_async(self._cmd_builder.eip712_filtering_raw(name, sig, discarded)) def sign(self, bip32_path: str, diff --git a/client/src/ledger_app_clients/ethereum/command_builder.py b/client/src/ledger_app_clients/ethereum/command_builder.py index bcd505cbe..238a19b66 100644 --- a/client/src/ledger_app_clients/ethereum/command_builder.py +++ b/client/src/ledger_app_clients/ethereum/command_builder.py @@ -41,6 +41,7 @@ class P2Type(IntEnum): LEGACY_IMPLEM = 0x00 NEW_IMPLEM = 0x01 FILTERING_ACTIVATE = 0x00 + FILTERING_DISCARDED_PATH = 0x01 FILTERING_MESSAGE_INFO = 0x0f FILTERING_DATETIME = 0xfc FILTERING_TOKEN_ADDR_CHECK = 0xfd @@ -164,6 +165,15 @@ def _eip712_filtering_send_name(self, name: str, sig: bytes) -> bytes: data += sig return data + def eip712_filtering_discarded_path(self, path: str) -> bytes: + data = bytearray() + data.append(len(path)) + data += path.encode() + return self._serialize(InsType.EIP712_SEND_FILTERING, + P1Type.COMPLETE_SEND, + P2Type.FILTERING_DISCARDED_PATH, + data) + def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes) -> bytes: data = bytearray() data.append(len(name)) @@ -176,17 +186,17 @@ def eip712_filtering_message_info(self, name: str, filters_count: int, sig: byte P2Type.FILTERING_MESSAGE_INFO, data) - def eip712_filtering_amount_join_token(self, token_idx: int, sig: bytes) -> bytes: + def eip712_filtering_amount_join_token(self, token_idx: int, sig: bytes, discarded: bool) -> bytes: data = bytearray() data.append(token_idx) data.append(len(sig)) data += sig return self._serialize(InsType.EIP712_SEND_FILTERING, - P1Type.COMPLETE_SEND, + int(discarded), P2Type.FILTERING_TOKEN_ADDR_CHECK, data) - def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: bytes) -> bytes: + def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: bytes, discarded: bool) -> bytes: data = bytearray() data.append(len(name)) data += name.encode() @@ -194,19 +204,19 @@ def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: byt data.append(len(sig)) data += sig return self._serialize(InsType.EIP712_SEND_FILTERING, - P1Type.COMPLETE_SEND, + int(discarded), P2Type.FILTERING_AMOUNT_FIELD, data) - def eip712_filtering_datetime(self, name: str, sig: bytes) -> bytes: + def eip712_filtering_datetime(self, name: str, sig: bytes, discarded: bool) -> bytes: return self._serialize(InsType.EIP712_SEND_FILTERING, - P1Type.COMPLETE_SEND, + int(discarded), P2Type.FILTERING_DATETIME, self._eip712_filtering_send_name(name, sig)) - def eip712_filtering_raw(self, name: str, sig: bytes) -> bytes: + def eip712_filtering_raw(self, name: str, sig: bytes, discarded: bool) -> bytes: return self._serialize(InsType.EIP712_SEND_FILTERING, - P1Type.COMPLETE_SEND, + int(discarded), P2Type.FILTERING_RAW, self._eip712_filtering_send_name(name, sig)) diff --git a/client/src/ledger_app_clients/ethereum/eip712/InputData.py b/client/src/ledger_app_clients/ethereum/eip712/InputData.py index 966c47b6c..e2d9f3e8d 100644 --- a/client/src/ledger_app_clients/ethereum/eip712/InputData.py +++ b/client/src/ledger_app_clients/ethereum/eip712/InputData.py @@ -194,32 +194,36 @@ def encode_bytes_dyn(value: str, typesize: int) -> bytes: encoding_functions[EIP712FieldType.DYN_BYTES] = encode_bytes_dyn +def send_filter(path: str, discarded: bool): + assert path in filtering_paths.keys() + + if filtering_paths[path]["type"] == "amount_join_token": + send_filtering_amount_join_token(path, filtering_paths[path]["token"], discarded) + elif filtering_paths[path]["type"] == "amount_join_value": + if "token" in filtering_paths[path].keys(): + token = filtering_paths[path]["token"] + else: + # Permit (ERC-2612) + token = 0xff + send_filtering_amount_join_value(path, token, filtering_paths[path]["name"], discarded) + elif filtering_paths[path]["type"] == "datetime": + send_filtering_datetime(path, filtering_paths[path]["name"], discarded) + elif filtering_paths[path]["type"] == "raw": + send_filtering_raw(path, filtering_paths[path]["name"], discarded) + else: + assert False + + def send_struct_impl_field(value, field): - # Something wrong happened if this triggers - if isinstance(value, list) or (field["enum"] == EIP712FieldType.CUSTOM): - breakpoint() + assert not isinstance(value, list) + assert field["enum"] != EIP712FieldType.CUSTOM data = encoding_functions[field["enum"]](value, field["typesize"]) if filtering_paths: path = ".".join(current_path) if path in filtering_paths.keys(): - if filtering_paths[path]["type"] == "amount_join_token": - send_filtering_amount_join_token(filtering_paths[path]["token"]) - elif filtering_paths[path]["type"] == "amount_join_value": - if "token" in filtering_paths[path].keys(): - token = filtering_paths[path]["token"] - else: - # Permit (ERC-2612) - token = 0xff - send_filtering_amount_join_value(token, - filtering_paths[path]["name"]) - elif filtering_paths[path]["type"] == "datetime": - send_filtering_datetime(filtering_paths[path]["name"]) - elif filtering_paths[path]["type"] == "raw": - send_filtering_raw(filtering_paths[path]["name"]) - else: - assert False + send_filter(path, False) with app_client.eip712_send_struct_impl_struct_field(data): enable_autonext() @@ -234,6 +238,12 @@ def evaluate_field(structs, data, field, lvls_left, new_level=True): if len(array_lvls) > 0 and lvls_left > 0: with app_client.eip712_send_struct_impl_array(len(data)): pass + if len(data) == 0: + for path in filtering_paths.keys(): + dpath = ".".join(current_path) + ".[]" + if path.startswith(dpath): + app_client.eip712_filtering_discarded_path(path) + send_filter(path, True) idx = 0 for subdata in data: current_path.append("[]") @@ -295,57 +305,49 @@ def send_filtering_message_info(display_name: str, filters_count: int): disable_autonext() -def send_filtering_amount_join_token(token_idx: int): +def send_filtering_amount_join_token(path: str, token_idx: int, discarded: bool): global sig_ctx - path_str = ".".join(current_path) - to_sign = start_signature_payload(sig_ctx, 11) - to_sign += path_str.encode() + to_sign += path.encode() to_sign.append(token_idx) sig = keychain.sign_data(keychain.Key.CAL, to_sign) - with app_client.eip712_filtering_amount_join_token(token_idx, sig): + with app_client.eip712_filtering_amount_join_token(token_idx, sig, discarded): pass -def send_filtering_amount_join_value(token_idx: int, display_name: str): +def send_filtering_amount_join_value(path: str, token_idx: int, display_name: str, discarded: bool): global sig_ctx - path_str = ".".join(current_path) - to_sign = start_signature_payload(sig_ctx, 22) - to_sign += path_str.encode() + to_sign += path.encode() to_sign += display_name.encode() to_sign.append(token_idx) sig = keychain.sign_data(keychain.Key.CAL, to_sign) - with app_client.eip712_filtering_amount_join_value(token_idx, display_name, sig): + with app_client.eip712_filtering_amount_join_value(token_idx, display_name, sig, discarded): pass -def send_filtering_datetime(display_name: str): +def send_filtering_datetime(path: str, display_name: str, discarded: bool): global sig_ctx - path_str = ".".join(current_path) - to_sign = start_signature_payload(sig_ctx, 33) - to_sign += path_str.encode() + to_sign += path.encode() to_sign += display_name.encode() sig = keychain.sign_data(keychain.Key.CAL, to_sign) - with app_client.eip712_filtering_datetime(display_name, sig): + with app_client.eip712_filtering_datetime(display_name, sig, discarded): pass # ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures -def send_filtering_raw(display_name): +def send_filtering_raw(path: str, display_name: str, discarded: bool): global sig_ctx - path_str = ".".join(current_path) - to_sign = start_signature_payload(sig_ctx, 72) - to_sign += path_str.encode() + to_sign += path.encode() to_sign += display_name.encode() sig = keychain.sign_data(keychain.Key.CAL, to_sign) - with app_client.eip712_filtering_raw(display_name, sig): + with app_client.eip712_filtering_raw(display_name, sig, discarded): pass diff --git a/doc/ethapp.adoc b/doc/ethapp.adoc index 618b422b5..cb77ae79f 100644 --- a/doc/ethapp.adoc +++ b/doc/ethapp.adoc @@ -45,6 +45,9 @@ Application version 1.9.19 - 2022-05-17 - Add EIP-712 amount & date/time filtering - PROVIDE ERC 20 TOKEN INFORMATION & PROVIDE NFT INFORMATION now send back the index where the asset has been stored +### 1.12.0 + - Add EIP-712 discarded filter path support + ## About This application describes the APDU messages interface to communicate with the Ethereum application. @@ -828,6 +831,12 @@ Field substitution will be ignored if the full filtering is not activated. This command should come before the domain & message implementations. If activated, fields will be by default hidden unless they receive a field name substitution. +##### Discarded filter path + +This command gives the app the absolute path of the upcoming filter which will be discarded (because it targets a field within an empty array). + +The next filter should be marked as discarded (with P1) to be able to use this given filter path. + ##### Message info This command should come right after the implementation of the domain has been sent with *SEND STRUCT IMPLEMENTATION*, just before sending the message implementation. @@ -880,8 +889,11 @@ _Command_ [width="80%"] |========================================================================= | *CLA* | *INS* | *P1* | *P2* | *LC* | *Le* -| E0 | 1E | 00 - | 00 : activation +| E0 | 1E | 00 : standard + + 01 : discarded | 00 : activation + + 01 : discarded filter path 0F : message info @@ -901,6 +913,16 @@ _Input data_ None +##### If P2 == discarded filter path + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Path length | 1 +| Path | variable +|========================================== + + ##### If P2 == message info [width="80%"] diff --git a/src/main.c b/src/main.c index 95cda3e08..83a5d0b05 100644 --- a/src/main.c +++ b/src/main.c @@ -214,7 +214,7 @@ static uint16_t handleApdu(command_t *cmd, uint32_t *flags, uint32_t *tx) { break; case INS_EIP712_FILTERING: - sw = handle_eip712_filtering(cmd->p2, cmd->data, cmd->lc, flags); + sw = handle_eip712_filtering(cmd->p1, cmd->p2, cmd->data, cmd->lc, flags); break; #endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/commands_712.c b/src_features/signMessageEIP712/commands_712.c index 70509e633..05438e6c9 100644 --- a/src_features/signMessageEIP712/commands_712.c +++ b/src_features/signMessageEIP712/commands_712.c @@ -27,6 +27,7 @@ #define P2_IMPL_ARRAY 0x0F #define P2_IMPL_FIELD P2_DEF_FIELD #define P2_FILT_ACTIVATE 0x00 +#define P2_FILT_DISCARDED_PATH 0x01 #define P2_FILT_MESSAGE_INFO 0x0F #define P2_FILT_DATE_TIME 0xFC #define P2_FILT_AMOUNT_JOIN_TOKEN 0xFD @@ -70,10 +71,12 @@ void handle_eip712_return_code(bool success) { /** * Process the EIP712 struct definition command * - * @param[in] apdu_buf the APDU payload + * @param[in] p2 instruction parameter 2 + * @param[in] cdata command data + * @param[in] length length of the command data * @return whether the command was successful or not */ -uint16_t handle_eip712_struct_def(uint8_t p2, const uint8_t *dataBuffer, uint8_t dataLength) { +uint16_t handle_eip712_struct_def(uint8_t p2, const uint8_t *cdata, uint8_t length) { bool ret = true; if (eip712_context == NULL) { @@ -86,10 +89,10 @@ uint16_t handle_eip712_struct_def(uint8_t p2, const uint8_t *dataBuffer, uint8_t if (ret) { switch (p2) { case P2_DEF_NAME: - ret = set_struct_name(dataLength, dataBuffer); + ret = set_struct_name(length, cdata); break; case P2_DEF_FIELD: - ret = set_struct_field(dataLength, dataBuffer); + ret = set_struct_field(length, cdata); break; default: PRINTF("Unknown P2 0x%x\n", p2); @@ -104,13 +107,16 @@ uint16_t handle_eip712_struct_def(uint8_t p2, const uint8_t *dataBuffer, uint8_t /** * Process the EIP712 struct implementation command * - * @param[in] apdu_buf the APDU payload + * @param[in] p1 instruction parameter 1 + * @param[in] p2 instruction parameter 2 + * @param[in] cdata command data + * @param[in] length length of the command data * @return whether the command was successful or not */ uint16_t handle_eip712_struct_impl(uint8_t p1, uint8_t p2, - const uint8_t *dataBuffer, - uint8_t dataLength, + const uint8_t *cdata, + uint8_t length, uint32_t *flags) { bool ret = false; bool reply_apdu = true; @@ -121,7 +127,7 @@ uint16_t handle_eip712_struct_impl(uint8_t p1, switch (p2) { case P2_IMPL_NAME: // set root type - ret = path_set_root((char *) dataBuffer, dataLength); + ret = path_set_root((char *) cdata, length); if (ret) { if (N_storage.verbose_eip712) { ui_712_review_struct(path_get_root()); @@ -131,12 +137,12 @@ uint16_t handle_eip712_struct_impl(uint8_t p1, } break; case P2_IMPL_FIELD: - if ((ret = field_hash(dataBuffer, dataLength, p1 != P1_COMPLETE))) { + if ((ret = field_hash(cdata, length, p1 != P1_COMPLETE))) { reply_apdu = false; } break; case P2_IMPL_ARRAY: - ret = path_new_array_depth(dataBuffer, dataLength); + ret = path_new_array_depth(cdata, length); break; default: PRINTF("Unknown P2 0x%x\n", p2); @@ -154,12 +160,16 @@ uint16_t handle_eip712_struct_impl(uint8_t p1, /** * Process the EIP712 filtering command * - * @param[in] apdu_buf the APDU payload + * @param[in] p1 instruction parameter 1 + * @param[in] p2 instruction parameter 2 + * @param[in] cdata command data + * @param[in] length length of the command data * @return whether the command was successful or not */ -uint16_t handle_eip712_filtering(uint8_t p2, - const uint8_t *dataBuffer, - uint8_t dataLength, +uint16_t handle_eip712_filtering(uint8_t p1, + uint8_t p2, + const uint8_t *cdata, + uint8_t length, uint32_t *flags) { bool ret = true; bool reply_apdu = true; @@ -179,23 +189,26 @@ uint16_t handle_eip712_filtering(uint8_t p2, } forget_known_assets(); break; + case P2_FILT_DISCARDED_PATH: + ret = filtering_discarded_path(cdata, length); + break; case P2_FILT_MESSAGE_INFO: - ret = filtering_message_info(dataBuffer, dataLength); + ret = filtering_message_info(cdata, length); if (ret) { reply_apdu = false; } break; case P2_FILT_DATE_TIME: - ret = filtering_date_time(dataBuffer, dataLength); + ret = filtering_date_time(cdata, length, p1 == 1); break; case P2_FILT_AMOUNT_JOIN_TOKEN: - ret = filtering_amount_join_token(dataBuffer, dataLength); + ret = filtering_amount_join_token(cdata, length, p1 == 1); break; case P2_FILT_AMOUNT_JOIN_VALUE: - ret = filtering_amount_join_value(dataBuffer, dataLength); + ret = filtering_amount_join_value(cdata, length, p1 == 1); break; case P2_FILT_RAW_FIELD: - ret = filtering_raw_field(dataBuffer, dataLength); + ret = filtering_raw_field(cdata, length, p1 == 1); break; default: PRINTF("Unknown P2 0x%x\n", p2); @@ -224,7 +237,7 @@ uint16_t handle_eip712_filtering(uint8_t p2, * @param[in] apdu_buf the APDU payload * @return whether the command was successful or not */ -uint16_t handle_eip712_sign(const uint8_t *dataBuffer, uint8_t dataLength, uint32_t *flags) { +uint16_t handle_eip712_sign(const uint8_t *cdata, uint8_t length, uint32_t *flags) { bool ret = false; if (eip712_context == NULL) { @@ -241,7 +254,7 @@ uint16_t handle_eip712_sign(const uint8_t *dataBuffer, uint8_t dataLength, uint3 (ui_712_remaining_filters() != 0)) { PRINTF("%d EIP712 filters are missing\n", ui_712_remaining_filters()); apdu_response_code = APDU_RESPONSE_REF_DATA_NOT_FOUND; - } else if (parseBip32(dataBuffer, &dataLength, &tmpCtx.messageSigningContext.bip32) == NULL) { + } else if (parseBip32(cdata, &length, &tmpCtx.messageSigningContext.bip32) == NULL) { apdu_response_code = APDU_RESPONSE_INVALID_DATA; } else { if (!N_storage.verbose_eip712 && (ui_712_get_filtering_mode() == EIP712_FILTERING_BASIC)) { diff --git a/src_features/signMessageEIP712/commands_712.h b/src_features/signMessageEIP712/commands_712.h index a34d264e4..3bc3377f7 100644 --- a/src_features/signMessageEIP712/commands_712.h +++ b/src_features/signMessageEIP712/commands_712.h @@ -8,16 +8,17 @@ #define DOMAIN_STRUCT_NAME "EIP712Domain" -uint16_t handle_eip712_struct_def(uint8_t p2, const uint8_t *dataBuffer, uint8_t dataLength); +uint16_t handle_eip712_struct_def(uint8_t p2, const uint8_t *cdata, uint8_t length); uint16_t handle_eip712_struct_impl(uint8_t p1, uint8_t p2, - const uint8_t *dataBuffer, - uint8_t dataLength, + const uint8_t *cdata, + uint8_t length, uint32_t *flags); -uint16_t handle_eip712_sign(const uint8_t *dataBuffer, uint8_t dataLength, uint32_t *flags); -uint16_t handle_eip712_filtering(uint8_t p2, - const uint8_t *dataBuffer, - uint8_t dataLength, +uint16_t handle_eip712_sign(const uint8_t *cdata, uint8_t length, uint32_t *flags); +uint16_t handle_eip712_filtering(uint8_t p1, + uint8_t p2, + const uint8_t *cdata, + uint8_t length, uint32_t *flags); void handle_eip712_return_code(bool success); diff --git a/src_features/signMessageEIP712/filtering.c b/src_features/signMessageEIP712/filtering.c index 276fc3649..d491a10a4 100644 --- a/src_features/signMessageEIP712/filtering.c +++ b/src_features/signMessageEIP712/filtering.c @@ -28,33 +28,41 @@ * Reconstruct the field path and hash it * * @param[in] hash_ctx the hashing context + * @param[in] discarded if the filter targets a field that does not exist (within an empty array) */ -static void hash_filtering_path(cx_hash_t *hash_ctx) { +static void hash_filtering_path(cx_hash_t *hash_ctx, bool discarded) { const void *field_ptr; const char *key; uint8_t key_len; - for (uint8_t i = 0; i < path_get_depth_count(); ++i) { - if (i > 0) { - hash_byte('.', hash_ctx); - } - if ((field_ptr = path_get_nth_field(i + 1)) != NULL) { - if ((key = get_struct_field_keyname(field_ptr, &key_len)) != NULL) { - // field name - hash_nbytes((uint8_t *) key, key_len, hash_ctx); - - // array levels - if (struct_field_is_array(field_ptr)) { - uint8_t lvl_count; - - get_struct_field_array_lvls_array(field_ptr, &lvl_count); - for (int j = 0; j < lvl_count; ++j) { - hash_nbytes((uint8_t *) ".[]", 3, hash_ctx); + if (discarded) { + key = ui_712_get_discarded_path(&key_len); + hash_nbytes((uint8_t *) key, key_len, hash_ctx); + } else { + for (uint8_t i = 0; i < path_get_depth_count(); ++i) { + if (i > 0) { + hash_byte('.', hash_ctx); + } + if ((field_ptr = path_get_nth_field(i + 1)) != NULL) { + if ((key = get_struct_field_keyname(field_ptr, &key_len)) != NULL) { + // field name + hash_nbytes((uint8_t *) key, key_len, hash_ctx); + + // array levels + if (struct_field_is_array(field_ptr)) { + uint8_t lvl_count; + + get_struct_field_array_lvls_array(field_ptr, &lvl_count); + for (int j = 0; j < lvl_count; ++j) { + hash_nbytes((uint8_t *) ".[]", 3, hash_ctx); + } } } } } } + // so it is only usable for the following filter + ui_712_set_discarded_path("", 0); } /** @@ -226,14 +234,104 @@ bool filtering_message_info(const uint8_t *payload, uint8_t length) { return true; } +/** + * Check if given path matches the backed-up path + * + * A match is found as long as the given path starts with the backed-up path. + * + * @param[in] path given path + * @param[in] path_len length of the path + * @param[out] offset_ptr offset to where the comparison stopped + * @return whether a match was found or not + */ +static bool matches_backup_path(const char *path, uint8_t path_len, uint8_t *offset_ptr) { + const void *field_ptr; + const char *key; + uint8_t key_len; + uint8_t offset = 0; + uint8_t lvl_count; + + for (uint8_t i = 0; i < path_backup_get_depth_count(); ++i) { + if (i > 0) { + if (((offset + 1) > path_len) || (memcmp(path + offset, ".", 1) != 0)) { + return false; + } + offset += 1; + } + if ((field_ptr = path_backup_get_nth_field(i + 1)) != NULL) { + if ((key = get_struct_field_keyname(field_ptr, &key_len)) != NULL) { + // field name + if (((offset + key_len) > path_len) || (memcmp(path + offset, key, key_len) != 0)) { + return false; + } + offset += key_len; + + // array levels + if (struct_field_is_array(field_ptr)) { + get_struct_field_array_lvls_array(field_ptr, &lvl_count); + for (int j = 0; j < lvl_count; ++j) { + if (((offset + 3) > path_len) || (memcmp(path + offset, ".[]", 3) != 0)) { + return false; + } + offset += 3; + } + } + } + } + } + if (offset_ptr != NULL) { + *offset_ptr = offset; + } + return true; +} + +/** + * Command to provide the filter path of a discarded filtered field + * + * Some filtered fields are discarded/never received because they are contained in an array + * that turns out to be empty. + * + * @param[in] payload the payload to parse + * @param[in] length the payload length + * @return whether it was successful or not + */ +bool filtering_discarded_path(const uint8_t *payload, uint8_t length) { + uint8_t path_len; + const char *path; + uint8_t offset = 0; + uint8_t path_offset; + + if ((offset + sizeof(path_len)) > length) { + return false; + } + path_len = payload[offset++]; + if ((offset + path_len) > length) { + return false; + } + path = (char *) &payload[offset]; + offset += path_len; + if (offset < path_len) { + return false; + } + if (!matches_backup_path(path, path_len, &path_offset)) { + return false; + } + if (!path_exists_in_backup(path + path_offset, path_len - path_offset)) { + return false; + } + ui_712_set_discarded_path(path, path_len); + return true; +} + /** * Command to display a field as a date-time * * @param[in] payload the payload to parse * @param[in] length the payload length + * @param[in] discarded if the filter targets a field that is does not exist (within an empty array) * @return whether it was successful or not */ -bool filtering_date_time(const uint8_t *payload, uint8_t length) { +bool filtering_date_time(const uint8_t *payload, uint8_t length, bool discarded) { uint8_t name_len; const char *name; uint8_t sig_len; @@ -269,7 +367,7 @@ bool filtering_date_time(const uint8_t *payload, uint8_t length) { if (!sig_verif_start(&hash_ctx, FILT_MAGIC_DATETIME)) { return false; } - hash_filtering_path((cx_hash_t *) &hash_ctx); + hash_filtering_path((cx_hash_t *) &hash_ctx, discarded); hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx); if (!sig_verif_end(&hash_ctx, sig, sig_len)) { return false; @@ -291,9 +389,10 @@ bool filtering_date_time(const uint8_t *payload, uint8_t length) { * * @param[in] payload the payload to parse * @param[in] length the payload length + * @param[in] discarded if the filter targets a field that is does not exist (within an empty array) * @return whether it was successful or not */ -bool filtering_amount_join_token(const uint8_t *payload, uint8_t length) { +bool filtering_amount_join_token(const uint8_t *payload, uint8_t length, bool discarded) { uint8_t token_idx; uint8_t sig_len; const uint8_t *sig; @@ -323,7 +422,7 @@ bool filtering_amount_join_token(const uint8_t *payload, uint8_t length) { if (!sig_verif_start(&hash_ctx, FILT_MAGIC_AMOUNT_JOIN_TOKEN)) { return false; } - hash_filtering_path((cx_hash_t *) &hash_ctx); + hash_filtering_path((cx_hash_t *) &hash_ctx, discarded); hash_byte(token_idx, (cx_hash_t *) &hash_ctx); if (!sig_verif_end(&hash_ctx, sig, sig_len)) { return false; @@ -343,9 +442,10 @@ bool filtering_amount_join_token(const uint8_t *payload, uint8_t length) { * * @param[in] payload the payload to parse * @param[in] length the payload length + * @param[in] discarded if the filter targets a field that is does not exist (within an empty array) * @return whether it was successful or not */ -bool filtering_amount_join_value(const uint8_t *payload, uint8_t length) { +bool filtering_amount_join_value(const uint8_t *payload, uint8_t length, bool discarded) { uint8_t name_len; const char *name; uint8_t token_idx; @@ -389,7 +489,7 @@ bool filtering_amount_join_value(const uint8_t *payload, uint8_t length) { if (!sig_verif_start(&hash_ctx, FILT_MAGIC_AMOUNT_JOIN_VALUE)) { return false; } - hash_filtering_path((cx_hash_t *) &hash_ctx); + hash_filtering_path((cx_hash_t *) &hash_ctx, discarded); hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx); hash_byte(token_idx, (cx_hash_t *) &hash_ctx); if (!sig_verif_end(&hash_ctx, sig, sig_len)) { @@ -423,9 +523,10 @@ bool filtering_amount_join_value(const uint8_t *payload, uint8_t length) { * * @param[in] payload the payload to parse * @param[in] length the payload length + * @param[in] discarded if the filter targets a field that is does not exist (within an empty array) * @return whether it was successful or not */ -bool filtering_raw_field(const uint8_t *payload, uint8_t length) { +bool filtering_raw_field(const uint8_t *payload, uint8_t length, bool discarded) { uint8_t name_len; const char *name; uint8_t sig_len; @@ -461,17 +562,19 @@ bool filtering_raw_field(const uint8_t *payload, uint8_t length) { if (!sig_verif_start(&hash_ctx, FILT_MAGIC_RAW_FIELD)) { return false; } - hash_filtering_path((cx_hash_t *) &hash_ctx); + hash_filtering_path((cx_hash_t *) &hash_ctx, discarded); hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx); if (!sig_verif_end(&hash_ctx, sig, sig_len)) { return false; } - // Handling - if (name_len > 0) { // don't substitute for an empty name - ui_712_set_title(name, name_len); + if (!discarded) { + // Handling + if (name_len > 0) { // don't substitute for an empty name + ui_712_set_title(name, name_len); + } + ui_712_flag_field(true, name_len > 0, false, false); } - ui_712_flag_field(true, name_len > 0, false, false); return true; } diff --git a/src_features/signMessageEIP712/filtering.h b/src_features/signMessageEIP712/filtering.h index d35963cd9..8143edfb1 100644 --- a/src_features/signMessageEIP712/filtering.h +++ b/src_features/signMessageEIP712/filtering.h @@ -9,10 +9,11 @@ #define MAX_FILTERS 50 bool filtering_message_info(const uint8_t *payload, uint8_t length); -bool filtering_date_time(const uint8_t *payload, uint8_t length); -bool filtering_amount_join_token(const uint8_t *payload, uint8_t length); -bool filtering_amount_join_value(const uint8_t *payload, uint8_t length); -bool filtering_raw_field(const uint8_t *payload, uint8_t length); +bool filtering_date_time(const uint8_t *payload, uint8_t length, bool discarded); +bool filtering_amount_join_token(const uint8_t *payload, uint8_t length, bool discarded); +bool filtering_amount_join_value(const uint8_t *payload, uint8_t length, bool discarded); +bool filtering_raw_field(const uint8_t *payload, uint8_t length, bool discarded); +bool filtering_discarded_path(const uint8_t *payload, uint8_t length); #endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/path.c b/src_features/signMessageEIP712/path.c index bf7d95a0d..037550440 100644 --- a/src_features/signMessageEIP712/path.c +++ b/src_features/signMessageEIP712/path.c @@ -13,28 +13,30 @@ #include "typed_data.h" static s_path *path_struct = NULL; +static s_path *path_backup = NULL; /** - * Get the field pointer to by the first N depths of the path. + * Get the field pointer to by the first N depths of the given path * + * @param[in] path given path struct * @param[out] fields_count_ptr the number of fields in the last evaluated depth * @param[in] n the number of depths to evaluate * @return the field which the first Nth depths points to */ -static const void *get_nth_field(uint8_t *const fields_count_ptr, uint8_t n) { +static const void *get_nth_field_from(const s_path *path, uint8_t *fields_count_ptr, uint8_t n) { const void *struct_ptr = NULL; const void *field_ptr = NULL; const char *typename; uint8_t length; uint8_t fields_count; - if (path_struct == NULL) { + if (path == NULL) { return NULL; } - struct_ptr = path_struct->root_struct; + struct_ptr = path->root_struct; - if (n > path_struct->depth_count) // sanity check + if (n > path->depth_count) // sanity check { return NULL; } @@ -45,11 +47,11 @@ static const void *get_nth_field(uint8_t *const fields_count_ptr, uint8_t n) { *fields_count_ptr = fields_count; } // check if the index at this depth makes sense - if (path_struct->depths[depth] > fields_count) { + if (path->depths[depth] > fields_count) { return NULL; } - for (uint8_t index = 0; index < path_struct->depths[depth]; ++index) { + for (uint8_t index = 0; index < path->depths[depth]; ++index) { field_ptr = get_next_struct_field(field_ptr); } if (struct_field_type(field_ptr) == TYPE_CUSTOM) { @@ -62,6 +64,10 @@ static const void *get_nth_field(uint8_t *const fields_count_ptr, uint8_t n) { return field_ptr; } +static const void *get_nth_field(uint8_t *fields_count_ptr, uint8_t n) { + return get_nth_field_from(path_struct, fields_count_ptr, n); +} + /** * Get the element the path is pointing to. * @@ -82,6 +88,10 @@ const void *path_get_nth_field(uint8_t n) { return get_nth_field(NULL, n); } +const void *path_backup_get_nth_field(uint8_t n) { + return get_nth_field_from(path_backup, NULL, n); +} + /** * Get Nth to last struct field from path * @@ -452,6 +462,27 @@ static bool check_and_add_array_depth(const void *depth, return true; } +/** + * Back-up the current path + * + * Used for the handling of discarded filtered fields + */ +static void backup_path(void) { + const void *field_ptr; + + memcpy(path_backup, path_struct, sizeof(*path_backup)); + // decrease while it does not point to an array type + while (path_backup->depth_count > 1) { + if ((field_ptr = path_backup_get_nth_field(path_backup->depth_count)) == NULL) { + return; + } + if (struct_field_is_array(field_ptr)) { + break; + } + path_backup->depth_count -= 1; + } +} + /** * Add a new array depth with a given size (number of elements). * @@ -479,6 +510,9 @@ bool path_new_array_depth(const uint8_t *const data, uint8_t length) { } array_size = *data; + if (array_size == 0) { + backup_path(); + } if (!path_update(false, array_size > 0, array_size > 0)) { return false; } @@ -633,15 +667,92 @@ const void *path_get_root(void) { } /** - * Get the current amount of depth + * Get the current amount of depth in a given path struct * + * @param[in] given path struct * @return depth count */ -uint8_t path_get_depth_count(void) { - if (path_struct == NULL) { +static uint8_t get_depth_count(const s_path *path) { + if (path == NULL) { return 0; } - return path_struct->depth_count; + return path->depth_count; +} + +/** + * Get the current amount of depth in the path + * + * @return depth count + */ +uint8_t path_get_depth_count(void) { + return get_depth_count(path_struct); +} + +/** + * Get the current amount of depth in the backup path + * + * @return depth count + */ +uint8_t path_backup_get_depth_count(void) { + return get_depth_count(path_backup); +} + +/** + * Check if the given relative path exists in the backup path + * + * @param[in] path given path + * @param[in] length length of the path + * @return whether it exists or not + */ +bool path_exists_in_backup(const char *path, size_t length) { + size_t offset = 0; + size_t i; + const void *field_ptr; + s_path tmp_path; + const char *typename; + uint8_t typename_len; + const void *struct_ptr; + uint8_t fields_count; + const char *key; + uint8_t key_len; + + memcpy(&tmp_path, path_backup, sizeof(tmp_path)); + field_ptr = get_nth_field_from(&tmp_path, NULL, tmp_path.depth_count); + while (offset < length) { + if (((offset + 1) > length) || (memcmp(path + offset, ".", 1) != 0)) { + return false; + } + offset += 1; + if (((offset + 2) <= length) && (memcmp(path + offset, "[]", 2) == 0)) { + if (!struct_field_is_array(field_ptr)) { + return false; + } + offset += 2; + } else if (offset < length) { + for (i = 0; ((offset + i) < length) && (path[offset + i] != '.'); ++i) + ; + typename = get_struct_field_custom_typename(field_ptr, &typename_len); + if ((struct_ptr = get_structn(typename, typename_len)) == NULL) { + return false; + } + field_ptr = get_struct_fields_array(struct_ptr, &fields_count); + while (fields_count > 0) { + key = get_struct_field_keyname(field_ptr, &key_len); + if ((key_len == i) && (memcmp(key, path + offset, i) == 0)) { + break; + } + field_ptr = get_next_struct_field(field_ptr); + fields_count -= 1; + } + if (fields_count == 0) { + return false; + } + offset += i; + } else { + return false; + } + } + return true; } /** @@ -655,7 +766,7 @@ uint8_t path_get_depth_count(void) { * @return CRC-32 checksum */ uint32_t get_path_crc(void) { - uint32_t value = CX_CRC32_INIT; + uint32_t value = 0; value = cx_crc32_update(value, &path_struct->root_struct, sizeof(path_struct->root_struct)); value = cx_crc32_update(value, &path_struct->depth_count, sizeof(path_struct->depth_count)); @@ -680,13 +791,15 @@ uint32_t get_path_crc(void) { */ bool path_init(void) { if (path_struct == NULL) { - if ((path_struct = MEM_ALLOC_AND_ALIGN_TYPE(*path_struct)) == NULL) { + if (((path_struct = MEM_ALLOC_AND_ALIGN_TYPE(*path_struct)) == NULL) || + ((path_backup = MEM_ALLOC_AND_ALIGN_TYPE(*path_backup)) == NULL)) { apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; } else { - path_struct->depth_count = 0; + explicit_bzero(path_struct, sizeof(*path_struct)); + explicit_bzero(path_backup, sizeof(*path_backup)); } } - return path_struct != NULL; + return (path_struct != NULL) && (path_backup != NULL); } /** diff --git a/src_features/signMessageEIP712/path.h b/src_features/signMessageEIP712/path.h index 336659e85..8285d68f4 100644 --- a/src_features/signMessageEIP712/path.h +++ b/src_features/signMessageEIP712/path.h @@ -35,8 +35,11 @@ bool path_new_array_depth(const uint8_t *const data, uint8_t length); e_root_type path_get_root_type(void); const void *path_get_root(void); const void *path_get_nth_field(uint8_t n); +const void *path_backup_get_nth_field(uint8_t n); +bool path_exists_in_backup(const char *path, size_t length); const void *path_get_nth_field_to_last(uint8_t n); uint8_t path_get_depth_count(void); +uint8_t path_backup_get_depth_count(void); uint32_t get_path_crc(void); #endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/ui_logic.c b/src_features/signMessageEIP712/ui_logic.c index 11d08172b..8694cf6e3 100644 --- a/src_features/signMessageEIP712/ui_logic.c +++ b/src_features/signMessageEIP712/ui_logic.c @@ -59,6 +59,8 @@ typedef struct { s_amount_context amount; uint8_t filters_received; uint32_t filters_crc[MAX_FILTERS]; + uint8_t discarded_path_length; + char discarded_path[255]; #ifdef SCREEN_SIZE_WALLET char ui_pairs_buffer[(SHARED_CTX_FIELD_1_SIZE + SHARED_CTX_FIELD_2_SIZE) * 2]; #endif @@ -805,6 +807,28 @@ bool ui_712_push_new_filter_path(void) { return true; } +/** + * Set a discarded filter path + * + * @param[in] path the given filter path + * @param[in] length the path length + */ +void ui_712_set_discarded_path(const char *path, uint8_t length) { + memcpy(ui_ctx->discarded_path, path, length); + ui_ctx->discarded_path_length = length; +} + +/** + * Get the discarded filter path + * + * @param[out] length the path length + * @return filter path + */ +const char *ui_712_get_discarded_path(uint8_t *length) { + *length = ui_ctx->discarded_path_length; + return ui_ctx->discarded_path; +} + #ifdef SCREEN_SIZE_WALLET /* * Get UI pairs buffer diff --git a/src_features/signMessageEIP712/ui_logic.h b/src_features/signMessageEIP712/ui_logic.h index 9ce4ee152..d04c8db10 100644 --- a/src_features/signMessageEIP712/ui_logic.h +++ b/src_features/signMessageEIP712/ui_logic.h @@ -44,6 +44,8 @@ void ui_712_token_join_prepare_amount(uint8_t index, const char *name, uint8_t n void amount_join_set_token_received(void); bool ui_712_show_raw_key(const void *field_ptr); bool ui_712_push_new_filter_path(void); +void ui_712_set_discarded_path(const char *path, uint8_t length); +const char *ui_712_get_discarded_path(uint8_t *length); #ifdef SCREEN_SIZE_WALLET char *get_ui_pairs_buffer(size_t *size); #endif diff --git a/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00000.png b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00000.png new file mode 100644 index 000000000..14713d83b Binary files /dev/null and b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00000.png differ diff --git a/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00001.png b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00001.png new file mode 100644 index 000000000..7d202e913 Binary files /dev/null and b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00001.png differ diff --git a/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00002.png b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00002.png new file mode 100644 index 000000000..cbaa88901 Binary files /dev/null and b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00002.png differ diff --git a/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00003.png b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00003.png new file mode 100644 index 000000000..8b981d44c Binary files /dev/null and b/tests/ragger/snapshots/flex/test_eip712_filtering_empty_array/00003.png differ diff --git a/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00000.png b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00000.png new file mode 100644 index 000000000..b546f65af Binary files /dev/null and b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00000.png differ diff --git a/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00001.png b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00001.png new file mode 100644 index 000000000..9bc9e1ed7 Binary files /dev/null and b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00001.png differ diff --git a/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00002.png b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00002.png new file mode 100644 index 000000000..f070aac76 Binary files /dev/null and b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00002.png differ diff --git a/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00003.png b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00003.png new file mode 100644 index 000000000..53ae65195 Binary files /dev/null and b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00003.png differ diff --git a/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00004.png b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00004.png new file mode 100644 index 000000000..657887225 Binary files /dev/null and b/tests/ragger/snapshots/nanosp/test_eip712_filtering_empty_array/00004.png differ diff --git a/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00000.png b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00000.png new file mode 100644 index 000000000..b546f65af Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00000.png differ diff --git a/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00001.png b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00001.png new file mode 100644 index 000000000..9bc9e1ed7 Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00001.png differ diff --git a/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00002.png b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00002.png new file mode 100644 index 000000000..f070aac76 Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00002.png differ diff --git a/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00003.png b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00003.png new file mode 100644 index 000000000..53ae65195 Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00003.png differ diff --git a/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00004.png b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00004.png new file mode 100644 index 000000000..657887225 Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_eip712_filtering_empty_array/00004.png differ diff --git a/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00000.png b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00000.png new file mode 100644 index 000000000..eb8072d46 Binary files /dev/null and b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00000.png differ diff --git a/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00001.png b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00001.png new file mode 100644 index 000000000..efce2dfe6 Binary files /dev/null and b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00001.png differ diff --git a/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00002.png b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00002.png new file mode 100644 index 000000000..fbea72d8f Binary files /dev/null and b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00002.png differ diff --git a/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00003.png b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00003.png new file mode 100644 index 000000000..cfee3aec2 Binary files /dev/null and b/tests/ragger/snapshots/stax/test_eip712_filtering_empty_array/00003.png differ diff --git a/tests/ragger/test_eip712.py b/tests/ragger/test_eip712.py index 1f64fb159..830d2a5d3 100644 --- a/tests/ragger/test_eip712.py +++ b/tests/ragger/test_eip712.py @@ -437,3 +437,94 @@ def test_eip712_advanced_filtering(firmware: Firmware, # verify signature addr = recover_message(data_set.data, vrs) assert addr == get_wallet_addr(app_client) + + +def test_eip712_filtering_empty_array(firmware: Firmware, + backend: BackendInterface, + navigator: Navigator, + default_screenshot_path: Path, + test_name: str, + golden_run: bool): + global SNAPS_CONFIG + + app_client = EthAppClient(backend) + if firmware == Firmware.NANOS: + pytest.skip("Not supported on LNS") + + SNAPS_CONFIG = SnapshotsConfig(test_name) + + data = { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + "Person": [ + {"name": "name", "type": "string"}, + {"name": "addr", "type": "address"}, + ], + "Message": [ + {"name": "title", "type": "string"}, + {"name": "to", "type": "Person[]"}, + ], + "Root": [ + {"name": "text", "type": "string"}, + {"name": "subtext", "type": "string[]"}, + {"name": "msg_list1", "type": "Message[]"}, + {"name": "msg_list2", "type": "Message[]"}, + ], + }, + "primaryType": "Root", + "domain": { + "name": "test", + "version": "1", + "verifyingContract": "0x0000000000000000000000000000000000000000", + "chainId": 1, + }, + "message": { + "text": "This is a test", + "subtext": [], + "msg_list1": [ + { + "title": "This is a test", + "to": [], + } + ], + "msg_list2": [], + } + } + filters = { + "name": "Empty array filtering", + "fields": { + "text": { + "type": "raw", + "name": "Text", + }, + "subtext.[]": { + "type": "raw", + "name": "Sub-Text", + }, + "msg_list1.[].to.[].addr": { + "type": "raw", + "name": "(1) Recipient addr", + }, + "msg_list2.[].to.[].addr": { + "type": "raw", + "name": "(2) Recipient addr", + }, + } + } + vrs = eip712_new_common(firmware, + navigator, + default_screenshot_path, + app_client, + data, + filters, + False, + golden_run) + + # verify signature + addr = recover_message(data, vrs) + assert addr == get_wallet_addr(app_client)