diff --git a/deps/device-protocol b/deps/device-protocol index 3883e2cd2..0ebbd2f49 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit 3883e2cd297d021c7b1cb12591657b9f0a11d447 +Subproject commit 0ebbd2f49956735ae36069e89d96dc838a057310 diff --git a/deps/python-keepkey b/deps/python-keepkey index 81de77b3c..0f6be2b4d 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 81de77b3c8c4703c026c0fceef3deb77ee0c3605 +Subproject commit 0f6be2b4d3c358a52f8c2598c1c2c38a0c43ed1b diff --git a/include/keepkey/board/confirm_sm.h b/include/keepkey/board/confirm_sm.h index eaa89f1c4..830613f2a 100644 --- a/include/keepkey/board/confirm_sm.h +++ b/include/keepkey/board/confirm_sm.h @@ -96,6 +96,10 @@ bool confirm(ButtonRequestType type, const char *request_title, const char *request_body, ...) __attribute__((format(printf, 3, 4))); +bool confirm_with_icon(ButtonRequestType type, IconType iconNum, + const char *request_title, const char *request_body, + ...) __attribute__((format(printf, 4, 5))); + bool confirm_constant_power(ButtonRequestType type, const char *request_title, const char *request_body, ...) __attribute__((format(printf, 3, 4))); diff --git a/include/keepkey/board/layout.h b/include/keepkey/board/layout.h index b3be90b96..ff54a8a90 100644 --- a/include/keepkey/board/layout.h +++ b/include/keepkey/board/layout.h @@ -79,6 +79,7 @@ typedef enum { typedef enum { NO_ICON=0, ETHEREUM_ICON, + VERIFIED_ICON, } IconType; typedef void (*AnimateCallback)(void *data, uint32_t duration, diff --git a/include/keepkey/firmware/policy.h b/include/keepkey/firmware/policy.h index 9007d7bcd..7c3166eed 100644 --- a/include/keepkey/firmware/policy.h +++ b/include/keepkey/firmware/policy.h @@ -33,6 +33,7 @@ static const PolicyType policies[] = { {true, "Experimental", true, false}, {true, "AdvancedMode", true, false}, {true, "SolBlindSign", true, false}, + {true, "EthBlindSign", true, true}, }; int run_policy_compile_output(const CoinType *coin, const HDNode *root, diff --git a/include/keepkey/firmware/signed_metadata.h b/include/keepkey/firmware/signed_metadata.h new file mode 100644 index 000000000..3eefa48b0 --- /dev/null +++ b/include/keepkey/firmware/signed_metadata.h @@ -0,0 +1,62 @@ +#ifndef KEEPKEY_FIRMWARE_SIGNED_METADATA_H +#define KEEPKEY_FIRMWARE_SIGNED_METADATA_H + +#include +#include +#include + +typedef struct _EthereumSignTx EthereumSignTx; + +#define METADATA_MAX_ARGS 8 +#define METADATA_MAX_METHOD_LEN 64 +#define METADATA_MAX_ARG_NAME_LEN 32 +#define METADATA_MAX_ARG_VALUE_LEN 32 +#define METADATA_MAX_KEYS 4 + +typedef enum { + METADATA_OPAQUE = 0, + METADATA_VERIFIED = 1, + METADATA_MALFORMED = 2, +} MetadataClassification; + +typedef enum { + ARG_FORMAT_RAW = 0, + ARG_FORMAT_ADDRESS = 1, + ARG_FORMAT_AMOUNT = 2, + ARG_FORMAT_BYTES = 3, +} ArgFormat; + +typedef struct { + char name[METADATA_MAX_ARG_NAME_LEN + 1]; + ArgFormat format; + uint8_t value[METADATA_MAX_ARG_VALUE_LEN]; + uint16_t value_len; +} MetadataArg; + +typedef struct { + uint8_t version; + uint32_t chain_id; + uint8_t contract_address[20]; + uint8_t selector[4]; + uint8_t tx_hash[32]; + char method_name[METADATA_MAX_METHOD_LEN + 1]; + uint8_t num_args; + MetadataArg args[METADATA_MAX_ARGS]; + MetadataClassification classification; + uint32_t timestamp; + uint8_t key_id; + uint8_t signature[64]; + uint8_t recovery; +} SignedMetadata; + +bool signed_metadata_available(void); +void signed_metadata_clear(void); +MetadataClassification signed_metadata_process(const uint8_t *payload, + size_t payload_len, + uint8_t key_id); +bool signed_metadata_matches_tx(const EthereumSignTx *msg, + const uint8_t *tx_hash); +bool signed_metadata_confirm(void); +const SignedMetadata *signed_metadata_get(void); + +#endif diff --git a/include/keepkey/transport/messages-ethereum.options b/include/keepkey/transport/messages-ethereum.options index 74a251eba..75f070505 100644 --- a/include/keepkey/transport/messages-ethereum.options +++ b/include/keepkey/transport/messages-ethereum.options @@ -47,3 +47,6 @@ Ethereum712TypesValues.eip712types max_size:2048 Ethereum712TypesValues.eip712primetype max_size:80 Ethereum712TypesValues.eip712data max_size:2048 +EthereumTxMetadata.signed_payload max_size:1024 +EthereumMetadataAck.display_summary max_size:32 + diff --git a/include/keepkey/transport/messages.options b/include/keepkey/transport/messages.options index d39a8a566..ef5022978 100644 --- a/include/keepkey/transport/messages.options +++ b/include/keepkey/transport/messages.options @@ -5,7 +5,7 @@ Features.label max_size:33 Features.coins max_count:0 Features.revision max_size:41 Features.bootloader_hash max_size:32 -Features.policies max_count:5 +Features.policies max_count:6 Features.model max_size:32 Features.firmware_variant max_size:32 Features.firmware_hash max_size:32 diff --git a/lib/board/confirm_sm.c b/lib/board/confirm_sm.c index 534930f06..63745b8d7 100644 --- a/lib/board/confirm_sm.c +++ b/lib/board/confirm_sm.c @@ -318,6 +318,28 @@ bool confirm(ButtonRequestType type, const char *request_title, const char *requ return ret; } +bool confirm_with_icon(ButtonRequestType type, IconType iconNum, + const char *request_title, const char *request_body, + ...) +{ + button_request_acked = false; + + va_list vl; + va_start(vl, request_body); + vsnprintf(strbuf, sizeof(strbuf), request_body, vl); + va_end(vl); + + ButtonRequest resp; + memset(&resp, 0, sizeof(ButtonRequest)); + resp.has_code = true; + resp.code = type; + msg_write(MessageType_MessageType_ButtonRequest, &resp); + + bool ret = confirm_helper(request_title, strbuf, &layout_standard_notification, false, iconNum, false); + memzero(strbuf, sizeof(strbuf)); + return ret; +} + bool confirm_constant_power(ButtonRequestType type, const char *request_title, const char *request_body, ...) { diff --git a/lib/board/layout.c b/lib/board/layout.c index c9726ef0f..10032fb3f 100644 --- a/lib/board/layout.c +++ b/lib/board/layout.c @@ -331,6 +331,12 @@ void layout_add_icon(IconType type) { draw_bitmap_mono_rle(canvas, get_ethereum_icon_frame(), false); break; + case VERIFIED_ICON: + /* TODO: replace with dedicated verified/shield bitmap. + * Using Ethereum icon as placeholder until bitmap is designed. */ + draw_bitmap_mono_rle(canvas, get_ethereum_icon_frame(), false); + break; + default: /* no action requires */ break; diff --git a/lib/firmware/CMakeLists.txt b/lib/firmware/CMakeLists.txt index 5987d0a45..b5433849d 100644 --- a/lib/firmware/CMakeLists.txt +++ b/lib/firmware/CMakeLists.txt @@ -29,6 +29,7 @@ if(NOT "${COIN_SUPPORT}" STREQUAL "BTC") eos-contracts/eosio.token.c ethereum.c ethereum_contracts.c + signed_metadata.c ethereum_contracts/makerdao.c ethereum_contracts/saproxy.c ethereum_contracts/zxappliquid.c diff --git a/lib/firmware/ethereum.c b/lib/firmware/ethereum.c index 2bdfc2426..3339a73d6 100644 --- a/lib/firmware/ethereum.c +++ b/lib/firmware/ethereum.c @@ -33,6 +33,7 @@ #include "keepkey/firmware/eip712.h" #include "keepkey/firmware/ethereum_contracts.h" #include "keepkey/firmware/ethereum_contracts/makerdao.h" +#include "keepkey/firmware/signed_metadata.h" #include "keepkey/firmware/ethereum_tokens.h" #include "keepkey/firmware/storage.h" #include "keepkey/firmware/thorchain.h" @@ -689,6 +690,26 @@ void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node, data_needs_confirm = false; } + // Signed metadata clear signing (backwards compatible). + // Only fires if host sent EthereumTxMetadata before this EthereumSignTx. + if (data_needs_confirm && data_total > 0 && signed_metadata_available()) { + if (signed_metadata_matches_tx(msg, NULL)) { + if (signed_metadata_confirm()) { + needs_confirm = false; + data_needs_confirm = false; + } else { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Signing cancelled by user"); + ethereum_signing_abort(); + signed_metadata_clear(); + return; + } + } + } + // Always clear metadata after use — prevents stale data from persisting + // when contractHandled or ERC-20 paths bypass the metadata check above. + signed_metadata_clear(); + // detect ERC-20 token if (data_total == 68 && ethereum_isStandardERC20Transfer(msg)) { token = tokenByChainAddress(chain_id, msg->to.bytes); @@ -734,10 +755,19 @@ void ethereum_signing_init(EthereumSignTx *msg, const HDNode *node, memset(confirm_body_message, 0, sizeof(confirm_body_message)); if (token == NULL && data_total > 0 && data_needs_confirm) { + // EthBlindSign policy: hard gate when disabled + if (!storage_isPolicyEnabled("EthBlindSign")) { + (void)review(ButtonRequestType_ButtonRequest_Other, "Blocked", + "Blind signing is disabled. Enable " + "'EthBlindSign' policy to allow."); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Blind signing disabled by policy"); + ethereum_signing_abort(); + return; + } + // KeepKey custom: warn the user that they're trying to do something - // that is potentially dangerous. People (generally) aren't great at - // parsing raw transaction data, and we can't effectively show them - // what they're about to do in the general case. + // that is potentially dangerous. if (!storage_isPolicyEnabled("AdvancedMode")) { (void)review( ButtonRequestType_ButtonRequest_Other, "Warning", diff --git a/lib/firmware/fsm.c b/lib/firmware/fsm.c index 5751b74c3..840d63ed6 100644 --- a/lib/firmware/fsm.c +++ b/lib/firmware/fsm.c @@ -100,6 +100,11 @@ #include "messages-zcash.pb.h" #endif // BITCOIN_ONLY +// Forward declaration for handler defined in fsm_msg_ethereum.h +#ifndef BITCOIN_ONLY +void fsm_msgEthereumTxMetadata(const EthereumTxMetadata *); +#endif + #define _(X) (X) static uint8_t msg_resp[MAX_FRAME_SIZE] __attribute__((aligned(4))); diff --git a/lib/firmware/fsm_msg_ethereum.h b/lib/firmware/fsm_msg_ethereum.h index 5ce121cbd..554255595 100644 --- a/lib/firmware/fsm_msg_ethereum.h +++ b/lib/firmware/fsm_msg_ethereum.h @@ -19,6 +19,42 @@ * along with this library. If not, see . */ +#include "keepkey/firmware/signed_metadata.h" + +void fsm_msgEthereumTxMetadata(const EthereumTxMetadata *msg) { + CHECK_INITIALIZED + CHECK_PIN + + RESP_INIT(EthereumMetadataAck); + + MetadataClassification result = signed_metadata_process( + msg->signed_payload.bytes, + msg->signed_payload.size, + msg->has_key_id ? msg->key_id : 0 + ); + + resp->classification = (uint32_t)result; + resp->has_display_summary = true; + + switch (result) { + case METADATA_VERIFIED: + strlcpy(resp->display_summary, "Verified", + sizeof(resp->display_summary)); + break; + case METADATA_OPAQUE: + strlcpy(resp->display_summary, "Unverified", + sizeof(resp->display_summary)); + break; + case METADATA_MALFORMED: + default: + strlcpy(resp->display_summary, "Invalid", + sizeof(resp->display_summary)); + break; + } + + msg_write(MessageType_MessageType_EthereumMetadataAck, resp); +} + static int process_ethereum_xfer(const CoinType *coin, EthereumSignTx *msg) { if (!ethereum_isStandardERC20Transfer(msg) && msg->data_length != 0) return TXOUT_COMPILE_ERROR; diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index 059abdaa2..88778736d 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -37,6 +37,7 @@ MSG_IN(MessageType_MessageType_EthereumGetAddress, EthereumGetAddress, fsm_msgEthereumGetAddress) MSG_IN(MessageType_MessageType_EthereumSignTx, EthereumSignTx, fsm_msgEthereumSignTx) MSG_IN(MessageType_MessageType_EthereumTxAck, EthereumTxAck, fsm_msgEthereumTxAck) + MSG_IN(MessageType_MessageType_EthereumTxMetadata, EthereumTxMetadata, fsm_msgEthereumTxMetadata) MSG_IN(MessageType_MessageType_EthereumSignMessage, EthereumSignMessage, fsm_msgEthereumSignMessage) MSG_IN(MessageType_MessageType_EthereumVerifyMessage, EthereumVerifyMessage, fsm_msgEthereumVerifyMessage) @@ -118,6 +119,7 @@ MSG_OUT(MessageType_MessageType_EthereumMessageSignature, EthereumMessageSignature, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_EthereumTypedDataSignature, EthereumTypedDataSignature, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_EthereumMetadataAck, EthereumMetadataAck, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_NanoAddress, NanoAddress, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_NanoSignedTx, NanoSignedTx, NO_PROCESS_FUNC) diff --git a/lib/firmware/signed_metadata.c b/lib/firmware/signed_metadata.c new file mode 100644 index 000000000..41bc75f62 --- /dev/null +++ b/lib/firmware/signed_metadata.c @@ -0,0 +1,355 @@ +#include "keepkey/firmware/signed_metadata.h" + +#include "keepkey/board/confirm_sm.h" +#include "keepkey/board/util.h" +#include "keepkey/firmware/ethereum.h" +#include "trezor/crypto/address.h" +#include "trezor/crypto/bignum.h" +#include "trezor/crypto/ecdsa.h" +#include "trezor/crypto/memzero.h" +#include "trezor/crypto/secp256k1.h" +#include "trezor/crypto/sha2.h" + +#include +#include + +#define _(X) (X) + +static bool metadata_available = false; +static SignedMetadata stored_metadata; + +/* + * Metadata verification public keys. + * Slot 0: active production key + * Slot 1: rotation target + * Slots 2-3: reserved + * + * Keys are derived via KeepKey SignIdentity at keepkey.com/insight. + * Only the public key is stored here — the signing mnemonic is held + * offline and never appears in source code. + * + * To rotate: generate new key with pioneer-insight keygen, + * replace the slot below, ship firmware update. + */ +static const uint8_t METADATA_PUBKEYS[METADATA_MAX_KEYS][33] = { + /* Key 0: production */ + {0x02, 0x18, 0x62, 0x1d, 0x9c, 0x14, 0x47, 0x34, 0x58, 0x71, 0x3b, + 0xd3, 0xe6, 0x72, 0xe5, 0x34, 0x80, 0xaa, 0x70, 0x32, 0xca, 0x9b, + 0x67, 0x35, 0x63, 0x95, 0xe8, 0x87, 0x09, 0xbb, 0x45, 0x22, 0x6a}, + /* Key 1: rotation slot */ + {0x00}, + {0x00}, +#if DEBUG_LINK + /* Key 3: CI test key — only available in emulator/debug builds */ + {0x02, 0xe3, 0xb3, 0x01, 0x5c, 0x47, 0xdd, 0xca, 0xab, 0xe4, 0xf8, + 0xe8, 0x72, 0xf1, 0xed, 0x8f, 0x09, 0xca, 0x14, 0x5a, 0x8d, 0x81, + 0x77, 0x0d, 0x92, 0x21, 0x3d, 0x56, 0xda, 0x31, 0xab, 0x51, 0x07}, +#else + {0x00}, +#endif +}; + +static bool read_u8(const uint8_t **cursor, const uint8_t *end, uint8_t *out) { + if ((size_t)(end - *cursor) < 1) { + return false; + } + + *out = **cursor; + *cursor += 1; + return true; +} + +static bool read_be_u16(const uint8_t **cursor, const uint8_t *end, + uint16_t *out) { + if ((size_t)(end - *cursor) < 2) { + return false; + } + + *out = ((uint16_t)(*cursor)[0] << 8) | (*cursor)[1]; + *cursor += 2; + return true; +} + +static bool read_be_u32(const uint8_t **cursor, const uint8_t *end, + uint32_t *out) { + if ((size_t)(end - *cursor) < 4) { + return false; + } + + *out = ((uint32_t)(*cursor)[0] << 24) | ((uint32_t)(*cursor)[1] << 16) | + ((uint32_t)(*cursor)[2] << 8) | (*cursor)[3]; + *cursor += 4; + return true; +} + +static bool read_bytes(const uint8_t **cursor, const uint8_t *end, + uint8_t *out, size_t size) { + if ((size_t)(end - *cursor) < size) { + return false; + } + + memcpy(out, *cursor, size); + *cursor += size; + return true; +} + +static bool read_string(const uint8_t **cursor, const uint8_t *end, char *out, + size_t max_len) { + uint16_t value_len = 0; + if (!read_be_u16(cursor, end, &value_len) || value_len == 0 || + value_len > max_len || (size_t)(end - *cursor) < value_len) { + return false; + } + + memcpy(out, *cursor, value_len); + out[value_len] = '\0'; + *cursor += value_len; + return true; +} + +static bool read_arg_name(const uint8_t **cursor, const uint8_t *end, char *out, + size_t max_len) { + uint8_t value_len = 0; + if (!read_u8(cursor, end, &value_len) || value_len == 0 || + value_len > max_len || (size_t)(end - *cursor) < value_len) { + return false; + } + + memcpy(out, *cursor, value_len); + out[value_len] = '\0'; + *cursor += value_len; + return true; +} + +static bool parse_metadata_binary(const uint8_t *payload, size_t payload_len, + SignedMetadata *out) { + /* Minimum: version(1) + chain_id(4) + contract(20) + selector(4) + + * tx_hash(32) + method_len(2) + method(1) + num_args(1) + + * classification(1) + timestamp(4) + key_id(1) + sig(64) + recovery(1) + * = 136 bytes */ + if (payload_len < 136) { + return false; + } + + const uint8_t *cursor = payload; + const uint8_t *end = payload + payload_len; + memset(out, 0, sizeof(*out)); + + if (!read_u8(&cursor, end, &out->version) || out->version != 0x01 || + !read_be_u32(&cursor, end, &out->chain_id) || + !read_bytes(&cursor, end, out->contract_address, + sizeof(out->contract_address)) || + !read_bytes(&cursor, end, out->selector, sizeof(out->selector)) || + !read_bytes(&cursor, end, out->tx_hash, sizeof(out->tx_hash)) || + !read_string(&cursor, end, out->method_name, METADATA_MAX_METHOD_LEN) || + !read_u8(&cursor, end, &out->num_args) || + out->num_args > METADATA_MAX_ARGS) { + return false; + } + + for (uint8_t i = 0; i < out->num_args; i++) { + uint8_t format = 0; + uint16_t value_len = 0; + MetadataArg *arg = &out->args[i]; + + if (!read_arg_name(&cursor, end, arg->name, METADATA_MAX_ARG_NAME_LEN) || + !read_u8(&cursor, end, &format) || format > ARG_FORMAT_BYTES || + !read_be_u16(&cursor, end, &value_len) || + value_len > METADATA_MAX_ARG_VALUE_LEN || + !read_bytes(&cursor, end, arg->value, value_len)) { + return false; + } + + arg->format = (ArgFormat)format; + arg->value_len = value_len; + } + + uint8_t classification = 0; + if (!read_u8(&cursor, end, &classification) || classification > 2 || + !read_be_u32(&cursor, end, &out->timestamp) || + !read_u8(&cursor, end, &out->key_id) || + !read_bytes(&cursor, end, out->signature, sizeof(out->signature)) || + !read_u8(&cursor, end, &out->recovery) || cursor != end) { + return false; + } + + out->classification = (MetadataClassification)classification; + return true; +} + +static void bn_from_metadata_bytes(const uint8_t *value, size_t value_len, + bignum256 *out) { + uint8_t padded[32] = {0}; + if (value_len > sizeof(padded)) { + value_len = sizeof(padded); + } + memcpy(padded + (sizeof(padded) - value_len), value, value_len); + bn_read_be(padded, out); + memzero(padded, sizeof(padded)); +} + +bool signed_metadata_available(void) { return metadata_available; } + +void signed_metadata_clear(void) { + memzero(&stored_metadata, sizeof(stored_metadata)); + metadata_available = false; +} + +MetadataClassification signed_metadata_process(const uint8_t *payload, + size_t payload_len, + uint8_t key_id) { + uint8_t digest[32]; + size_t signed_len; + + signed_metadata_clear(); + + if (key_id >= METADATA_MAX_KEYS || METADATA_PUBKEYS[key_id][0] == 0x00 || + !payload || payload_len < 65) { + return METADATA_MALFORMED; + } + + if (!parse_metadata_binary(payload, payload_len, &stored_metadata) || + stored_metadata.key_id != key_id) { + signed_metadata_clear(); + return METADATA_MALFORMED; + } + + signed_len = payload_len - sizeof(stored_metadata.signature) - 1; + sha256_Raw(payload, signed_len, digest); + + if (ecdsa_verify_digest(&secp256k1, METADATA_PUBKEYS[key_id], + stored_metadata.signature, digest) != 0) { + signed_metadata_clear(); + return METADATA_MALFORMED; + } + + metadata_available = true; + return stored_metadata.classification; +} + +bool signed_metadata_matches_tx(const EthereumSignTx *msg, + const uint8_t *tx_hash) { + if (!metadata_available || !msg || + stored_metadata.classification != METADATA_VERIFIED || + msg->to.size != sizeof(stored_metadata.contract_address) || + msg->data_initial_chunk.size < sizeof(stored_metadata.selector)) { + return false; + } + + /* Contract address binding */ + if (memcmp(stored_metadata.contract_address, msg->to.bytes, + sizeof(stored_metadata.contract_address)) != 0) { + return false; + } + + /* Function selector binding */ + if (memcmp(stored_metadata.selector, msg->data_initial_chunk.bytes, + sizeof(stored_metadata.selector)) != 0) { + return false; + } + + /* Chain ID binding */ + if ((msg->has_chain_id ? msg->chain_id : 0) != stored_metadata.chain_id) { + return false; + } + + /* tx_hash binding — optional in phase 1 (NULL = skip check). + * Full tx_hash verification requires pre-computing keccak before + * confirmation screens, which is a phase 2 change. */ + if (tx_hash != NULL && + memcmp(stored_metadata.tx_hash, tx_hash, + sizeof(stored_metadata.tx_hash)) != 0) { + return false; + } + + return true; +} + +bool signed_metadata_confirm(void) { + char body[128]; + + if (!metadata_available || + stored_metadata.classification != METADATA_VERIFIED) { + return false; + } + + /* Screen 1: Verified method — use review_with_icon for trust indicator */ + memset(body, 0, sizeof(body)); + snprintf(body, sizeof(body), "Verified call:\n%s", + stored_metadata.method_name); + if (!confirm_with_icon(ButtonRequestType_ButtonRequest_ConfirmOutput, + VERIFIED_ICON, "Insight Verified", "%s", body)) { + return false; + } + + /* Screen 2: Contract address — ALWAYS show full address, never truncate. + * Truncation is a spoofing vector (attacker crafts matching prefix+suffix). */ + char contract_addr[43] = "0x"; + ethereum_address_checksum(stored_metadata.contract_address, contract_addr + 2, + false, stored_metadata.chain_id); + memset(body, 0, sizeof(body)); + snprintf(body, sizeof(body), "Contract:\n%s", contract_addr); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + stored_metadata.method_name, "%s", body)) { + return false; + } + + /* Screen 3..N: Each decoded argument */ + for (uint8_t i = 0; i < stored_metadata.num_args; i++) { + MetadataArg *arg = &stored_metadata.args[i]; + memset(body, 0, sizeof(body)); + + switch (arg->format) { + case ARG_FORMAT_ADDRESS: { + char addr_full[43] = "0x"; + if (arg->value_len != 20) { + return false; + } + ethereum_address_checksum(arg->value, addr_full + 2, false, + stored_metadata.chain_id); + snprintf(body, sizeof(body), "%s:\n%s", arg->name, addr_full); + break; + } + case ARG_FORMAT_AMOUNT: { + bignum256 amount; + char formatted[48]; + bn_from_metadata_bytes(arg->value, arg->value_len, &amount); + /* Check for MAX_UINT256 (unlimited approval) */ + bool is_max = true; + for (uint16_t j = 0; j < arg->value_len; j++) { + if (arg->value[j] != 0xFF) { is_max = false; break; } + } + if (is_max && arg->value_len == 32) { + snprintf(body, sizeof(body), "%s:\nUNLIMITED", arg->name); + } else { + bn_format(&amount, NULL, " wei", 0, 0, false, formatted, + sizeof(formatted)); + snprintf(body, sizeof(body), "%s:\n%s", arg->name, formatted); + } + break; + } + case ARG_FORMAT_BYTES: + case ARG_FORMAT_RAW: + default: { + char hex[(METADATA_MAX_ARG_VALUE_LEN * 2) + 1]; + size_t display_len = + arg->value_len > 16 ? 16 : (size_t)arg->value_len; + data2hex(arg->value, display_len, hex); + snprintf(body, sizeof(body), "%s:\n%s%s", arg->name, hex, + arg->value_len > 16 ? "..." : ""); + break; + } + } + + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + stored_metadata.method_name, "%s", body)) { + return false; + } + } + + return true; +} + +const SignedMetadata *signed_metadata_get(void) { + return metadata_available ? &stored_metadata : NULL; +}