Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
978ac19
PR0 base: pin device-protocol + python-keepkey to canonical 7.15 upst…
BitHighlander Jul 7, 2026
212b99b
fix: widen TokenType.chain_id from uint8_t to uint32_t
BitHighlander May 21, 2026
730f1c1
fix: EIP-712 cancel propagation and strtoll input validation
BitHighlander May 21, 2026
a618ba7
fix(maya): show 'Maya data' instead of 'Thorchain data' for Maya Prot…
BitHighlander Mar 25, 2026
b1216c3
style: clang-format-20 the staged fix
BitHighlander Jun 30, 2026
a9f15ce
fix(maya): declare conf as const char* (string literals are const; -W…
BitHighlander Jun 30, 2026
b487786
test(coins): allowlist 8 legacy tokens with no live token entry in Ta…
BitHighlander Jun 30, 2026
6b08803
fix(eth): canonicalise zero-valued RLP integer fields (strip leading …
BitHighlander Jul 1, 2026
0847c48
fix(eth): strip leading zeros in RLP list-length calc to match hashed…
BitHighlander Jun 29, 2026
0f08a94
fix(eth): always hash EIP-1559 priority fee so Stage 1/2 agree
BitHighlander Jun 29, 2026
becf7d8
style: clang-format RLP length helper call sites in ethereum.c
BitHighlander Jun 29, 2026
3e2e069
fix(eth): enforce EIP-1559 tx-type/fee-field consistency before signing
BitHighlander Jun 29, 2026
0e767af
fix(eth): clear-sign handler gating + 0x drainer hardening (port alph…
BitHighlander Jul 1, 2026
a06e4c0
fix(eth): pin THORChain router to current v4 + validate memo offset (…
BitHighlander Jul 1, 2026
43129de
feat(insight): signed-metadata clear-signing + AdvancedMode blind-sig…
BitHighlander Jul 1, 2026
9c72ec7
feat(clearsign): runtime-loaded signers with mandatory warning screen…
BitHighlander Jul 2, 2026
2d6613f
fix(clearsign): drop loaded signers on device wipe
BitHighlander Jul 2, 2026
83023c6
fix(clearsign): strict alias allowlist; fix FW_VERSION detection for …
BitHighlander Jul 2, 2026
f949e7e
style: clang-format-20 wrap in clearsign load handler + fingerprint sig
BitHighlander Jul 2, 2026
fc9aaf7
feat(clearsign): human-readable WHAT — STRING + TOKEN_AMOUNT arg formats
BitHighlander Jul 2, 2026
d5da9c6
feat(clearsign): v2 static-schema blobs + on-device calldata decode
BitHighlander Jul 3, 2026
0386211
fix(clearsign): harden v2 enforce with explicit decode flag; idempote…
BitHighlander Jul 3, 2026
55d028c
fix(clearsign): reset v2 decode flag per matches_tx call (no stale pr…
BitHighlander Jul 3, 2026
5c89df2
fix(proto): add firmware-side max_size for LoadClearsignSigner.icon
BitHighlander Jul 7, 2026
b0c279d
chore(proto): nanopb max_size for ripple.memo + thorchain.denom (supe…
BitHighlander Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "deps/device-protocol"]
path = deps/device-protocol
url = https://github.com/keepkey/device-protocol.git
branch = master
branch = up/release-protocol
[submodule "deps/trezor-firmware"]
path = deps/crypto/trezor-firmware
url = https://github.com/keepkey/trezor-firmware.git
Expand All @@ -14,7 +14,7 @@ url = https://github.com/keepkey/code-signing-keys.git
[submodule "deps/python-keepkey"]
path = deps/python-keepkey
url = https://github.com/keepkey/python-keepkey.git
branch = master
branch = reconcile/upstream-sync
[submodule "deps/qrenc/QR-Code-generator"]
path = deps/qrenc/QR-Code-generator
url = https://github.com/keepkey/QR-Code-generator.git
Expand Down
2 changes: 1 addition & 1 deletion deps/python-keepkey
Submodule python-keepkey updated 39 files
+19 −0 .github/workflows/copilot-review.yml
+1 −1 .gitmodules
+1 −1 build_pb.sh
+1 −1 device-protocol
+81 −0 keepkeylib/clearsign_abi.py
+1,003 −0 keepkeylib/clearsign_catalog.py
+129 −3 keepkeylib/client.py
+69 −0 keepkeylib/hive.py
+25 −1 keepkeylib/mapping.py
+66 −13 keepkeylib/messages_ethereum_pb2.py
+702 −0 keepkeylib/messages_hive_pb2.py
+219 −114 keepkeylib/messages_pb2.py
+13 −6 keepkeylib/messages_ripple_pb2.py
+13 −6 keepkeylib/messages_thorchain_pb2.py
+227 −57 keepkeylib/messages_zcash_pb2.py
+355 −39 keepkeylib/signed_metadata.py
+44 −0 keepkeylib/zcash.py
+628 −89 scripts/generate-test-report.py
+19 −0 tests/common.py
+7 −0 tests/probe.py
+2 −3 tests/test_msg_bip85.py
+2 −2 tests/test_msg_cosmos_signtx.py
+862 −6 tests/test_msg_ethereum_clear_signing.py
+10 −0 tests/test_msg_ethereum_erc20_0x_signtx.py
+7 −0 tests/test_msg_ethereum_erc20_uniswap_liquidity.py
+147 −0 tests/test_msg_ethereum_signing_guards.py
+1 −1 tests/test_msg_ethereum_signtx.py
+142 −0 tests/test_msg_ethereum_thorchain_deposit.py
+307 −0 tests/test_msg_hive.py
+16 −10 tests/test_msg_mayachain_signtx.py
+1 −1 tests/test_msg_recoverydevice_cipher.py
+51 −0 tests/test_msg_ripple_sign_tx.py
+65 −4 tests/test_msg_solana_signtx.py
+16 −10 tests/test_msg_thorchain_signtx.py
+13 −1 tests/test_msg_tron_signtx.py
+13 −20 tests/test_msg_zcash_display_address.py
+140 −0 tests/test_msg_zcash_seed_fingerprint.py
+4 −0 tests/test_msg_zcash_sign_pczt.py
+54 −0 tests/test_zcash_seed_fingerprint_helper.py
4 changes: 4 additions & 0 deletions include/keepkey/board/confirm_sm.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,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)));
Expand Down
1 change: 1 addition & 0 deletions include/keepkey/board/layout.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ typedef enum {
typedef enum {
NO_ICON = 0,
ETHEREUM_ICON,
VERIFIED_ICON,
} IconType;

typedef void (*AnimateCallback)(void* data, uint32_t duration,
Expand Down
3 changes: 2 additions & 1 deletion include/keepkey/firmware/eip712.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ typedef enum { DOMAIN = 1, MESSAGE } dm;
#define JSON_TYPE_T_NOVAL 31
#define ADDR_STRING_NULL 32
#define JSON_TYPE_WNOVAL 33
#define USER_CANCELLED 34

#define LAST_ERROR JSON_TYPE_WNOVAL
#define LAST_ERROR USER_CANCELLED

int encode(const json_t* jsonTypes, const json_t* jsonVals, const char* typeS,
uint8_t* hashRet);
Expand Down
12 changes: 11 additions & 1 deletion include/keepkey/firmware/ethereum_contracts/thortx.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@
"\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee" \
"\xee\xee"

#define THOR_ROUTER "42a5ed456650a09dc10ebc6361a7480fdd61f27b"
/* THORChain ETH router (mainnet), current v4.1.1.
* NOTE: THORChain migrates this router periodically (v1 42a5ed.. -> v3
* 3624525.. -> v4 d37bbe..). A hardcoded pin must be updated on each migration;
* the durable path is the signed-metadata clear-sign protocol (host-signed,
* key-pinned) which needs no firmware update per router change. */
#define THOR_ROUTER "d37bbe5744d730a1d98d8dc97c42f0ca46ad7146"

/* Maya Protocol ETH router (mainnet) */
#define MAYA_ROUTER "d89dce570de35a6f42d3bca7dba50a6d89bfc2a2"

/* deposit(address,address,uint256,string) — legacy selector */
#define THOR_SELECTOR_DEPOSIT "\x1f\xec\xe7\xb4"
Expand All @@ -43,6 +51,8 @@ typedef struct _EthereumSignTx EthereumSignTx;
bool thor_has_deposit_selector(const EthereumSignTx* msg);
bool thor_is_expiry_variant(const EthereumSignTx* msg);
bool thor_isThorchainTx(const EthereumSignTx* msg);
bool thor_isMayachainTx(const EthereumSignTx* msg);
bool thor_confirmThorTx(uint32_t data_total, const EthereumSignTx* msg);
bool thor_confirmMayaTx(uint32_t data_total, const EthereumSignTx* msg);

#endif
6 changes: 3 additions & 3 deletions include/keepkey/firmware/ethereum_tokens.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ enum {
typedef struct _TokenType {
const char* const address;
const char* const ticker;
uint8_t chain_id;
uint32_t chain_id;
uint8_t decimals;
} TokenType;

Expand All @@ -51,7 +51,7 @@ extern const TokenType* UnknownToken;

const TokenType* tokenIter(int32_t* ctr);

const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address);
const TokenType* tokenByChainAddress(uint32_t chain_id, const uint8_t* address);

/// Tokens don't have unique tickers, so this might not return the one you're
/// looking for :/
Expand All @@ -64,7 +64,7 @@ const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address);
/// \param[out] token The found token, assuming it was uniquely determinable.
/// \returns true iff the token can be uniquely found in the list of known
/// tokens.
bool tokenByTicker(uint8_t chain_id, const char* ticker,
bool tokenByTicker(uint32_t chain_id, const char* ticker,
const TokenType** token);

void coinFromToken(CoinType* coin, const TokenType* token);
Expand Down
2 changes: 2 additions & 0 deletions include/keepkey/firmware/fsm.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ void fsm_msgEthereumSignMessage(EthereumSignMessage* msg);
void fsm_msgEthereumVerifyMessage(const EthereumVerifyMessage* msg);
void fsm_msgEthereumSignTypedHash(const EthereumSignTypedHash* msg);
void fsm_msgEthereum712TypesValues(Ethereum712TypesValues* msg);
void fsm_msgEthereumTxMetadata(const EthereumTxMetadata* msg);
void fsm_msgLoadClearsignSigner(const LoadClearsignSigner* msg);

void fsm_msgNanoGetAddress(NanoGetAddress* msg);
void fsm_msgNanoSignTx(NanoSignTx* msg);
Expand Down
179 changes: 179 additions & 0 deletions include/keepkey/firmware/signed_metadata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#ifndef KEEPKEY_FIRMWARE_SIGNED_METADATA_H
#define KEEPKEY_FIRMWARE_SIGNED_METADATA_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

typedef struct _EthereumSignTx EthereumSignTx;

#define METADATA_MAX_ARGS 8
#define METADATA_MAX_METHOD_LEN 64
#define METADATA_MAX_ARG_NAME_LEN 32
/* Sized for TOKEN_AMOUNT: decimals(1) + symbol_len(1) + symbol(<=10) +
* amount(<=32). Other formats remain capped at 32 by their own guards. */
#define METADATA_MAX_ARG_VALUE_LEN 44
#define METADATA_MAX_TOKEN_SYMBOL_LEN 10
#define METADATA_MAX_KEYS 4
#define METADATA_ALIAS_MAX_LEN 31
/* hex(first 4 bytes of sha256(pubkey)) + NUL */
#define METADATA_FINGERPRINT_LEN 9

typedef enum {
METADATA_OPAQUE = 0,
METADATA_VERIFIED = 1,
METADATA_MALFORMED = 2,
} MetadataClassification;

/*
* Blob format versions (the first payload byte).
*
* LEGACY (v1): per-transaction. The blob carries a committed tx_hash and the
* pre-decoded argument VALUES; the host is trusted for the decode and the
* device only binds it to the signed digest (signed_metadata_enforce). This is
* the format that requires an online, per-tx signer holding the attestation
* key.
*
* SCHEMA (v2): static. The blob carries NO tx_hash and NO values — only how to
* decode the call: (chainId, contract, selector, method, per-arg name + display
* format [+ static decimals/symbol]). The DEVICE decodes the argument values
* from the exact calldata it is about to sign, so the display is bound to the
* signature by construction. No tx_hash, no per-tx signing: the catalog is
* signed ONCE, offline, and can be served from a host CDN (no hot key).
*/
#define METADATA_VERSION_LEGACY 0x01
#define METADATA_VERSION_SCHEMA 0x02

/*
* Argument display formats. The goal of clear-signing is that the device
* answers WHO the user is dealing with (validated contract address, protocol
* name), WHAT the transaction does (method + human-readable typed args:
* recipient, "Amount: 1,000 USDC"), and WHY the decode can be trusted
* (signer attestation bound to the exact tx hash). RAW/BYTES hex dumps are
* the fallback, not the product.
*/
typedef enum {
ARG_FORMAT_RAW = 0, /* hex dump (first 16 bytes) */
ARG_FORMAT_ADDRESS = 1, /* 20 bytes -> full EIP-55 address, never truncated */
ARG_FORMAT_AMOUNT = 2, /* big-endian uint256 -> raw integer, "wei" */
ARG_FORMAT_BYTES = 3, /* hex dump (first 16 bytes) */
/* Attested printable label, e.g. protocol: "Uniswap V2". Same character
* rules as the signer alias minus length (printable subset, no '%'). */
ARG_FORMAT_STRING = 4,
/* decimals(1) + symbol_len(1) + symbol(<=10, [A-Za-z0-9]) + amount(1..32
* big-endian). Rendered as a decimal-scaled amount with the symbol, e.g.
* "1000 USDC"; all-0xFF 32-byte amounts render "UNLIMITED <symbol>". */
ARG_FORMAT_TOKEN_AMOUNT = 5,
} 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);

/* True when the stored v2 (schema) metadata was decoded from the current tx's
* calldata by the most recent signed_metadata_matches_tx() call. Reset at the
* top of every matches_tx() so it reflects only that call (never a stale prior
* match). The v2 enforce path requires it; exported for unit testing. */
bool signed_metadata_schema_decoded(void);

void signed_metadata_clear(void);

/*
* Runtime-loaded clearsign signers (phase 1: the ONLY verification path).
*
* A signer is a compressed secp256k1 pubkey + display alias loaded into a
* key slot at the host's request, gated by a mandatory on-device confirm
* (see fsm_msgLoadClearsignSigner). Loaded signers live in RAM only and are
* gone on reboot. Metadata verified by a loaded signer always shows a
* warning screen naming the alias before any clearsign page — only the
* built-in (phase 2) keys sign warning-free.
*/

/* Pure validation: slot in range and not occupied by a built-in key, pubkey a
* valid compressed secp256k1 point, alias non-empty printable ASCII within
* METADATA_ALIAS_MAX_LEN. No state, no I/O. */
bool signed_metadata_signer_valid(uint8_t key_id, const uint8_t *pubkey,
size_t pubkey_len, const char *alias);

/* Store a signer into a slot. Caller (the FSM handler) MUST have passed
* signed_metadata_signer_valid() and obtained on-device user confirmation
* first — this function is the post-consent write, nothing more. */
void signed_metadata_store_signer(uint8_t key_id, const uint8_t *pubkey,
const char *alias);

/* Drop all runtime-loaded signers (and any metadata they verified). */
void signed_metadata_clear_signers(void);

/* out = hex of the first 4 bytes of sha256(pubkey[33]), NUL-terminated.
* Shown at load-confirm and on the per-tx warning screen so the user can
* correlate the two. */
void signed_metadata_pubkey_fingerprint(const uint8_t pubkey[33],
char out[METADATA_FINGERPRINT_LEN]);

/* True when the currently stored metadata was verified by a runtime-loaded
* signer (=> its confirm flow is warning-first, never "Insight Verified"). */
bool signed_metadata_from_loaded_signer(void);
MetadataClassification signed_metadata_process(const uint8_t *payload,
size_t payload_len,
uint8_t key_id);
/* Display gate: does this metadata plausibly describe `msg`? Binds contract
* address, selector and chain id so the wrong method is never shown. The
* authoritative full-tx binding is enforced later by signed_metadata_enforce().
*/
bool signed_metadata_matches_tx(const EthereumSignTx *msg);
bool signed_metadata_confirm(void);

/* True once a verified confirm has suppressed the raw-data confirmation, i.e.
* the signature is now gated on the metadata matching the final tx hash. */
bool signed_metadata_relied(void);

/* Authoritative binding, called after the real Ethereum sighash is finalized
* (in send_signature, the only point it exists). Returns true if signing may
* proceed: either no metadata was relied upon, or the relied-upon metadata's
* committed tx_hash equals `hash`. Fail-closed on any mismatch. */
bool signed_metadata_enforce(const uint8_t hash[32]);

/* Pure enforcement decision, exported for unit testing. Given the module flags
* and the metadata's committed tx hash, decides whether signing may proceed for
* the just-finalized `hash`. signed_metadata_enforce() is a thin wrapper that
* feeds the module state into this function. No state, no I/O. */
bool signed_metadata_enforce_decision(bool relied, bool available,
int classification,
const uint8_t *stored_hash,
const uint8_t *hash);

/* Pure enforcement decision for v2 (static schema) blobs, exported for unit
* testing. v2 has no committed tx_hash; the binding is structural (args decoded
* from the signed calldata), so signing proceeds when the relied-upon metadata
* is available, VERIFIED, and was actually decoded (`decoded`) — no digest
* comparison. `decoded` must be the recorded result of decode_v2_args() for
* this signing operation, not inferred from call order.
* signed_metadata_enforce() dispatches here when the stored blob's version is
* METADATA_VERSION_SCHEMA. */
bool signed_metadata_enforce_schema_decision(bool relied, bool available,
bool decoded, int classification);

const SignedMetadata *signed_metadata_get(void);

#endif
5 changes: 5 additions & 0 deletions include/keepkey/firmware/storage.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ void storage_loadDevice(LoadDevice* msg);
/// \return true iff the root node was found.
bool storage_getRootNode(const char* curve, bool usePassphrase, HDNode* node);

/// \brief Get the raw 64-byte BIP-39 seed, triggering passphrase if needed.
/// \param usePassphrase[in] Whether to use the passphrase.
/// \returns pointer to 64-byte seed, or NULL on failure.
const uint8_t* storage_getRawSeed(bool usePassphrase);

/// \brief Fetch the node used for U2F signing.
/// \returns true iff retrieval was successful.
bool storage_getU2FRoot(HDNode* node);
Expand Down
3 changes: 3 additions & 0 deletions include/keepkey/transport/messages-ethereum.options
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ Ethereum712TypesValues.eip712data max_size:2048

EthereumTxMetadata.signed_payload max_size:1024
EthereumMetadataAck.display_summary max_size:32
LoadClearsignSigner.pubkey max_size:33
LoadClearsignSigner.alias max_size:32
LoadClearsignSigner.icon max_size:384

2 changes: 2 additions & 0 deletions include/keepkey/transport/messages-ripple.options
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ RippleSignTx.address_n max_count:8

RipplePayment.destination max_size:36

RippleSignTx.memo max_size:200

RippleSignedTx.signature max_size:75
RippleSignedTx.serialized_tx max_size:1024
1 change: 1 addition & 0 deletions include/keepkey/transport/messages-thorchain.options
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ThorchainSignTx.memo max_size:256

ThorchainMsgSend.from_address max_size:46
ThorchainMsgSend.to_address max_size:46
ThorchainMsgSend.denom max_size:69

ThorchainMsgDeposit.asset max_size:20
ThorchainMsgDeposit.memo max_size:256
Expand Down
23 changes: 23 additions & 0 deletions lib/board/confirm_sm.c
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,29 @@ bool confirm(ButtonRequestType type, const char* request_title,
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, ...) {
button_request_acked = false;
Expand Down
4 changes: 4 additions & 0 deletions lib/board/layout.c
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ void layout_standard_notification(const char* str1, const char* str2,
void layout_add_icon(IconType type) {
switch (type) {
case ETHEREUM_ICON:
/* ponytail: reuse the ETH glyph as the "verified" mark — it's an ETH tx.
* Swap in a dedicated checkmark bitmap if the trust mark needs to differ.
*/
case VERIFIED_ICON:
draw_bitmap_mono_rle(canvas, get_ethereum_icon_frame(), false);
break;

Expand Down
1 change: 1 addition & 0 deletions lib/firmware/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ set(sources
reset.c
ripple.c
ripple_base58.c
signed_metadata.c
signing.c
signtx_tendermint.c
solana.c
Expand Down
Loading