From b5ddeb09eb8597b259cdb4f1ad62e622a5fd0f32 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 15 Jul 2026 12:46:48 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(hive):=20HiveSignOperations=20(1616/16?= =?UTF-8?q?17)=20=E2=80=94=20parsed=20generic=20op=20signing,=20unblocks?= =?UTF-8?q?=20vote/post/custom=5Fjson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P4 phase 1: the host serializes the Graphene transaction; firmware parses the bytes, clear-signs the ops it recognizes (vote 0, comment 1, custom_json 18), and refuses everything else. No blind-sign fallback. Everything displayed is re-derived from the bytes being signed. Parser (hive.c): - bounded LEB128 (max 5 bytes, fits uint32, overlong rejected) - field caps per hived: names 1..16, permlinks 1..256, cj id 1..32, vote weight int16 in [-10000,10000] - op types 2/9/10 PERMANENTLY excluded: transfer keeps the dedicated HiveSignTx display path; account ops keep the device-derived-keys-only invariant (a raw-bytes path would let a host slip third-party authorities into an account_update) - op count 1..4, extensions must be empty, trailing bytes rejected - tier detection: vote/comment/cj-posting = posting; cj with required_auths = active; mixed tiers rejected as malformed (one signature cannot satisfy both post-HF28) Handler (fsm_msg_hive.h): - dedicated {posting', active'} path validator pinned to the tx tier (memo' and owner' rejected — no Graphene op uses them) - one confirm per op + final sign confirm; uniform display rules: explicit (+N more) truncation markers, non-ASCII slices fall back to byte-count + hex preview - digest SHA256(chain_id || serialized_tx) identical to HiveSignTx, hashed straight from the decoded message (no 2KB stack copy) --- deps/device-protocol | 2 +- include/keepkey/firmware/fsm.h | 1 + include/keepkey/firmware/hive.h | 47 +++++ .../keepkey/transport/messages-hive.options | 6 + lib/firmware/fsm_msg_hive.h | 161 ++++++++++++++ lib/firmware/hive.c | 197 ++++++++++++++++++ lib/firmware/messagemap.def | 2 + 7 files changed, 415 insertions(+), 1 deletion(-) diff --git a/deps/device-protocol b/deps/device-protocol index a793934d0..f0b454981 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit a793934d09a883c9944ba3b9da15d23969906343 +Subproject commit f0b454981e093ac4f7c157281ed5cc1bfa395f73 diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index f3b918c70..81e3e841d 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -147,6 +147,7 @@ void fsm_msgHiveSignTx(const HiveSignTx* msg); void fsm_msgHiveSignAccountCreate(const HiveSignAccountCreate* msg); void fsm_msgHiveSignAccountUpdate(const HiveSignAccountUpdate* msg); void fsm_msgHiveSignMessage(const HiveSignMessage* msg); +void fsm_msgHiveSignOperations(const HiveSignOperations* msg); #if DEBUG_LINK // void fsm_msgDebugLinkDecision(DebugLinkDecision *msg); diff --git a/include/keepkey/firmware/hive.h b/include/keepkey/firmware/hive.h index 2b1579b6e..fdeeea0a0 100644 --- a/include/keepkey/firmware/hive.h +++ b/include/keepkey/firmware/hive.h @@ -31,9 +31,12 @@ #define HIVE_ROLE_POSTING (0x80000004u) // 4' — votes, posts, follows // ── Graphene operation type IDs ─────────────────────────────────────────── +#define HIVE_OP_VOTE 0 +#define HIVE_OP_COMMENT 1 #define HIVE_OP_TRANSFER 2 #define HIVE_OP_ACCOUNT_CREATE 9 #define HIVE_OP_ACCOUNT_UPDATE 10 +#define HIVE_OP_CUSTOM_JSON 18 // ── Protocol limits ─────────────────────────────────────────────────────── #define HIVE_MAX_ACCOUNT_LEN 16 // max Hive username length @@ -45,6 +48,11 @@ // Maximum signable message length. MUST match HiveSignMessage.message // max_size in messages-hive.options (proto cap and code cap kept in sync). #define HIVE_MAX_MESSAGE_LEN 1024 +// Maximum host-serialized transaction length for HiveSignOperations. MUST +// match HiveSignOperations.serialized_tx max_size in messages-hive.options. +#define HIVE_MAX_OPS_TX_LEN 2048 +// Maximum operations per HiveSignOperations transaction. +#define HIVE_MAX_TX_OPS 4 // ── Public API ──────────────────────────────────────────────────────────── /** @@ -78,6 +86,45 @@ bool hive_getPublicKeys(const HDNode* root, uint32_t account_index, */ void hive_signTx(const HDNode* node, const HiveSignTx* msg, HiveSignedTx* resp); +// ── Parsed operations (HiveSignOperations) ──────────────────────────────── + +typedef struct { + uint32_t op_type; + bool needs_active; // custom_json with required_auths; false = posting tier + // Borrowed slices into the request's serialized_tx (NOT NUL-terminated): + const uint8_t* acct; // vote: voter / comment: author / cj: first auth name + uint16_t acct_len; + const uint8_t* target; // vote: author / comment: title-or-permlink / cj: id + uint16_t target_len; + const uint8_t* detail; // vote: permlink / comment: body / cj: json + uint16_t detail_len; + int16_t weight; // vote only (-10000..10000) + bool is_top_level; // comment only: parent_author empty + uint8_t n_auths; // custom_json only: total auth account names +} HiveTxOp; + +typedef struct { + uint8_t num_ops; + bool needs_active; // tx tier: active' path required, else posting' + HiveTxOp ops[HIVE_MAX_TX_OPS]; +} HiveParsedTx; + +/** + * Parse and validate a host-serialized Graphene transaction against the + * phase-1 op table (vote, comment, custom_json). Returns NULL on success or + * a static error message. Slices in `out` borrow from `tx` — keep it alive. + */ +const char* hive_parseOperations(const uint8_t* tx, size_t len, + HiveParsedTx* out); + +/** + * Sign a parsed HiveSignOperations transaction: digest is + * SHA256(chain_id || serialized_tx), identical to HiveSignTx. The caller + * (FSM handler) is responsible for parsing, display, and role checks. + */ +void hive_signOperations(const HDNode* node, const HiveSignOperations* msg, + HiveSignedOperations* resp); + /** * Sign an arbitrary message per the Hive Keychain signBuffer contract: * signature over SHA256(message bytes) only — no chain_id prepend, no diff --git a/include/keepkey/transport/messages-hive.options b/include/keepkey/transport/messages-hive.options index 04ba85d68..d39d59215 100644 --- a/include/keepkey/transport/messages-hive.options +++ b/include/keepkey/transport/messages-hive.options @@ -50,3 +50,9 @@ HiveSignMessage.message max_size:1024 HiveSignedMessage.signature max_size:65 HiveSignedMessage.public_key max_size:33 + +HiveSignOperations.address_n max_count:8 +HiveSignOperations.chain_id max_size:32 +HiveSignOperations.serialized_tx max_size:2048 + +HiveSignedOperations.signature max_size:65 diff --git a/lib/firmware/fsm_msg_hive.h b/lib/firmware/fsm_msg_hive.h index d12881ea8..d912bbede 100644 --- a/lib/firmware/fsm_msg_hive.h +++ b/lib/firmware/fsm_msg_hive.h @@ -647,3 +647,164 @@ void fsm_msgHiveSignMessage(const HiveSignMessage* msg) { msg_write(MessageType_MessageType_HiveSignedMessage, resp); layoutHome(); } + +// ── HiveSignOperations (parsed generic op signing) ──────────────────────── +// The host serializes the transaction; firmware parses the Graphene bytes, +// clear-signs the ops it recognizes (vote, comment, custom_json), and +// refuses everything else — no blind-sign fallback. Everything shown on the +// OLED is re-derived from the bytes being signed, so a host serializer bug +// can only produce a node rejection, never a silent wrong-sign. + +// Dedicated path validator: {posting', active'} ONLY, pinned to the tx tier. +// Do NOT fold into hive_slip48_message_path_ok — that one deliberately +// accepts memo' (a legitimate signBuffer target), but no Graphene operation +// uses memo authority; a memo-path vote must be refused here, not +// discovered at the chain. owner' is likewise excluded. +static bool hive_slip48_ops_path_ok(const uint32_t* address_n, uint32_t count, + bool needs_active) { + if (count != 5) return false; + if (address_n[0] != HIVE_SLIP48_PURPOSE) return false; + if (address_n[1] != HIVE_SLIP48_NETWORK) return false; + if ((address_n[3] & 0x80000000u) == 0) return false; + if (address_n[4] != 0x80000000u) return false; // key index 0' + return address_n[2] == (needs_active ? HIVE_ROLE_ACTIVE : HIVE_ROLE_POSTING); +} + +// Uniform display rules (PR #306 finding-1): printable-ASCII within the +// budget is copied verbatim; a truncated slice carries an explicit +// "(+N more)" marker; any non-ASCII byte switches the whole slice to a +// byte-count + short hex preview (partial UTF-8 sequences garble the OLED). +static void hive_format_slice(const uint8_t* s, uint16_t len, char* out, + size_t out_len) { + bool ascii = true; + for (uint16_t i = 0; i < len; i++) { + if (s[i] < 0x20 || s[i] > 0x7e) { + ascii = false; + break; + } + } + if (ascii) { + uint16_t show = len > 120 ? 120 : len; + if ((size_t)show + 16 > out_len) show = (uint16_t)(out_len - 16); + memcpy(out, s, show); + if (show < len) { + snprintf(out + show, out_len - show, "(+%u more)", + (unsigned)(len - show)); + } else { + out[show] = '\0'; + } + } else { + unsigned show = len > 8 ? 8 : len; + int off = snprintf(out, out_len, "<%u bytes> ", (unsigned)len); + for (unsigned i = 0; i < show && off + 2 < (int)out_len; i++, off += 2) { + snprintf(out + off, out_len - off, "%02x", s[i]); + } + } +} + +void fsm_msgHiveSignOperations(const HiveSignOperations* msg) { + RESP_INIT(HiveSignedOperations); + + CHECK_INITIALIZED + CHECK_PIN + + if (!msg->has_serialized_tx || msg->serialized_tx.size == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing serialized transaction")); + layoutHome(); + return; + } + + static HiveParsedTx parsed; // slices borrow from the static msg buffer + const char* parse_err = hive_parseOperations( + msg->serialized_tx.bytes, msg->serialized_tx.size, &parsed); + if (parse_err) { + fsm_sendFailure(FailureType_Failure_SyntaxError, _(parse_err)); + layoutHome(); + return; + } + + if (!hive_slip48_ops_path_ok(msg->address_n, msg->address_n_count, + parsed.needs_active)) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + parsed.needs_active + ? _("Invalid Hive SLIP-0048 path (needs active')") + : _("Invalid Hive SLIP-0048 path (needs posting')")); + layoutHome(); + return; + } + + HDNode* node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + hdnode_fill_public_key(node); + + // One confirm per operation, then a final sign confirm (transfer pattern). + for (uint8_t i = 0; i < parsed.num_ops; i++) { + const HiveTxOp* op = &parsed.ops[i]; + char name[17]; // hive account names are <= 16 chars, length-validated + memcpy(name, op->acct, op->acct_len); + name[op->acct_len] = '\0'; + char target[140], detail[140]; + hive_format_slice(op->target, op->target_len, target, sizeof(target)); + hive_format_slice(op->detail, op->detail_len, detail, sizeof(detail)); + + bool approved = false; + switch (op->op_type) { + case HIVE_OP_VOTE: { + int w = op->weight < 0 ? -op->weight : op->weight; + approved = confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + op->weight < 0 ? "Downvote" : "Vote", + "@%s -> @%s/%s at %d.%02d%%", name, target, detail, + w / 100, w % 100); + break; + } + case HIVE_OP_COMMENT: + approved = confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + op->is_top_level ? "Post" : "Comment", + "@%s: %s\n%s", name, target, detail); + break; + case HIVE_OP_CUSTOM_JSON: { + char extra[12] = ""; + if (op->n_auths > 1) { + snprintf(extra, sizeof(extra), " +%u", (unsigned)(op->n_auths - 1)); + } + approved = confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, + "Custom JSON", "id: %s\nby @%s%s\n%s", target, name, + extra, detail); + break; + } + default: + break; // unreachable — parser rejected unknown ops + } + if (!approved) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + } + + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Sign Transaction", + "Sign %u Hive operation%s with the %s key?", + (unsigned)parsed.num_ops, parsed.num_ops == 1 ? "" : "s", + parsed.needs_active ? "active" : "posting")) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + hive_signOperations(node, msg, resp); + memzero(node, sizeof(*node)); + + if (!resp->has_signature) { + fsm_sendFailure(FailureType_Failure_FirmwareError, + _("Hive operation signing failed")); + layoutHome(); + return; + } + + msg_write(MessageType_MessageType_HiveSignedOperations, resp); + layoutHome(); +} diff --git a/lib/firmware/hive.c b/lib/firmware/hive.c index 2f71afdc7..4ae4f3b7e 100644 --- a/lib/firmware/hive.c +++ b/lib/firmware/hive.c @@ -228,6 +228,203 @@ static bool hive_sign_digest(const HDNode* node, const uint8_t* chain_id, return ok; } +// ── Parsed operation signing (HiveSignOperations) ───────────────────────── +// +// The host serializes the transaction; firmware re-derives everything it +// displays from the bytes and refuses anything outside the phase-1 op table. +// Digest/signature are identical to HiveSignTx: SHA256(chain_id || tx). + +typedef struct { + const uint8_t* p; + const uint8_t* end; +} HiveCur; + +/* + * Bounded unsigned LEB128: at most 5 bytes, must fit uint32, overlong + * encodings rejected (an unbounded shift is a classic overflow hole). + */ +static bool cur_varint(HiveCur* c, uint32_t* out) { + uint32_t v = 0; + for (int shift = 0; shift <= 28; shift += 7) { + if (c->p >= c->end) return false; + uint8_t b = *c->p++; + if (shift == 28 && (b & 0xF0)) return false; // overflow or 6th byte + v |= (uint32_t)(b & 0x7F) << shift; + if (!(b & 0x80)) { + *out = v; + return true; + } + } + return false; +} + +/* varint length + bytes, bounds-checked against the buffer AND field caps. */ +static bool cur_string(HiveCur* c, const uint8_t** s, uint16_t* slen, + uint32_t min_len, uint32_t max_len) { + uint32_t n; + if (!cur_varint(c, &n)) return false; + if (n < min_len || n > max_len) return false; + if ((size_t)(c->end - c->p) < n) return false; + *s = c->p; + *slen = (uint16_t)n; + c->p += n; + return true; +} + +const char* hive_parseOperations(const uint8_t* tx, size_t len, + HiveParsedTx* out) { + memzero(out, sizeof(*out)); + // 10-byte header + op_count + at least one op-type byte + extensions + if (len < 13) return "Hive tx too short"; + if (len > HIVE_MAX_OPS_TX_LEN) return "Hive tx too long"; // = proto cap + + // Header (ref_block_num u16, ref_block_prefix u32, expiration u32) is + // covered by the signature but carries nothing to confirm on-device. + HiveCur c = {tx + 10, tx + len}; + + uint32_t op_count; + if (!cur_varint(&c, &op_count)) return "Hive tx: malformed op count"; + if (op_count < 1 || op_count > HIVE_MAX_TX_OPS) + return "Hive tx: op count must be 1-4"; + out->num_ops = (uint8_t)op_count; + + bool any_posting = false, any_active = false; + + for (uint32_t i = 0; i < op_count; i++) { + HiveTxOp* op = &out->ops[i]; + uint32_t op_type; + if (!cur_varint(&c, &op_type)) return "Hive tx: malformed op type"; + op->op_type = op_type; + + switch (op_type) { + case HIVE_OP_VOTE: { // posting authority + if (!cur_string(&c, &op->acct, &op->acct_len, 1, 16) || + !cur_string(&c, &op->target, &op->target_len, 1, 16) || + !cur_string(&c, &op->detail, &op->detail_len, 1, 256)) + return "Hive vote: malformed fields"; + if ((size_t)(c.end - c.p) < 2) return "Hive vote: missing weight"; + int16_t w = (int16_t)((uint16_t)c.p[0] | ((uint16_t)c.p[1] << 8)); + c.p += 2; + if (w < -10000 || w > 10000) return "Hive vote: weight out of range"; + op->weight = w; + any_posting = true; + break; + } + case HIVE_OP_COMMENT: { // posting authority + const uint8_t *pa, *ppl, *permlink, *jm; + uint16_t pa_len, ppl_len, permlink_len, jm_len; + if (!cur_string(&c, &pa, &pa_len, 0, 16) || + !cur_string(&c, &ppl, &ppl_len, 1, 256) || + !cur_string(&c, &op->acct, &op->acct_len, 1, 16) || + !cur_string(&c, &permlink, &permlink_len, 1, 256) || + !cur_string(&c, &op->target, &op->target_len, 0, 256) || + !cur_string(&c, &op->detail, &op->detail_len, 1, + HIVE_MAX_OPS_TX_LEN) || + !cur_string(&c, &jm, &jm_len, 0, HIVE_MAX_OPS_TX_LEN)) + return "Hive comment: malformed fields"; + if (op->target_len == 0) { // no title — show the permlink instead + op->target = permlink; + op->target_len = permlink_len; + } + op->is_top_level = (pa_len == 0); + any_posting = true; + break; + } + case HIVE_OP_CUSTOM_JSON: { // posting OR active authority + uint32_t n_active, n_posting; + if (!cur_varint(&c, &n_active)) + return "Hive custom_json: malformed auths"; + for (uint32_t k = 0; k < n_active; k++) { + const uint8_t* s; + uint16_t sl; + if (!cur_string(&c, &s, &sl, 1, 16)) + return "Hive custom_json: malformed auths"; + if (!op->acct) { + op->acct = s; + op->acct_len = sl; + } + } + if (!cur_varint(&c, &n_posting)) + return "Hive custom_json: malformed auths"; + for (uint32_t k = 0; k < n_posting; k++) { + const uint8_t* s; + uint16_t sl; + if (!cur_string(&c, &s, &sl, 1, 16)) + return "Hive custom_json: malformed auths"; + if (!op->acct) { + op->acct = s; + op->acct_len = sl; + } + } + if (n_active + n_posting == 0) + return "Hive custom_json: no auth accounts"; + // Both tiers on one op can never be satisfied by a single signature + // (post-HF28 hived requires the exact authority) — malformed input. + if (n_active > 0 && n_posting > 0) + return "Hive custom_json: mixed active+posting auths"; + if (!cur_string(&c, &op->target, &op->target_len, 1, 32) || + !cur_string(&c, &op->detail, &op->detail_len, 1, + HIVE_MAX_OPS_TX_LEN)) + return "Hive custom_json: malformed id/json"; + op->n_auths = (uint8_t)(n_active + n_posting); + op->needs_active = (n_active > 0); + if (op->needs_active) + any_active = true; + else + any_posting = true; + break; + } + case HIVE_OP_TRANSFER: + case HIVE_OP_ACCOUNT_CREATE: + case HIVE_OP_ACCOUNT_UPDATE: + // PERMANENTLY excluded from this table: transfer keeps the stronger + // dedicated HiveSignTx display path; the account ops keep the + // device-derived-keys-only invariant (a generic raw-bytes path + // would let a host slip third-party authorities into an + // account_update). Never add these here. + return "Hive tx: op requires its dedicated message type"; + default: + return "Hive tx: unsupported operation type"; + } + } + + uint32_t ext_count; + if (!cur_varint(&c, &ext_count)) return "Hive tx: malformed extensions"; + if (ext_count != 0) return "Hive tx: extensions must be empty"; + if (c.p != c.end) return "Hive tx: trailing bytes"; + + // One signature cannot satisfy posting- and active-tier ops at once. + if (any_posting && any_active) return "Hive tx: mixed posting/active ops"; + out->needs_active = any_active; + return NULL; +} + +void hive_signOperations(const HDNode* node, const HiveSignOperations* msg, + HiveSignedOperations* resp) { + if (!msg->has_serialized_tx || msg->serialized_tx.size == 0 || + msg->serialized_tx.size > HIVE_MAX_OPS_TX_LEN) + return; + + const uint8_t default_chain_id[32] = HIVE_CHAIN_ID; + const uint8_t* chain_id = + (msg->has_chain_id && msg->chain_id.size == HIVE_CHAIN_ID_LEN) + ? msg->chain_id.bytes + : default_chain_id; + + // Hash straight from the decoded message — no stack copy of the 2KB tx. + uint8_t sig[65]; + if (!hive_sign_digest(node, chain_id, msg->serialized_tx.bytes, + msg->serialized_tx.size, sig)) { + memzero(sig, sizeof(sig)); + return; + } + + resp->has_signature = true; + resp->signature.size = 65; + memcpy(resp->signature.bytes, sig, 65); + memzero(sig, sizeof(sig)); +} + // ── Message signing (Keychain signBuffer contract) ──────────────────────── // Digest is SHA256(message bytes) ONLY: no chain_id prepend (unlike // transactions) and no Bitcoin/Solana-style message prefix. hive-js diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index 18514f5ee..e7ec2dd0d 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -196,6 +196,7 @@ MSG_IN(MessageType_MessageType_HiveSignAccountCreate, HiveSignAccountCreate, fsm_msgHiveSignAccountCreate) MSG_IN(MessageType_MessageType_HiveSignAccountUpdate, HiveSignAccountUpdate, fsm_msgHiveSignAccountUpdate) MSG_IN(MessageType_MessageType_HiveSignMessage, HiveSignMessage, fsm_msgHiveSignMessage) + MSG_IN(MessageType_MessageType_HiveSignOperations, HiveSignOperations, fsm_msgHiveSignOperations) MSG_OUT(MessageType_MessageType_HivePublicKey, HivePublicKey, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_HivePublicKeys, HivePublicKeys, NO_PROCESS_FUNC) @@ -203,6 +204,7 @@ MSG_OUT(MessageType_MessageType_HiveSignedAccountCreate, HiveSignedAccountCreate, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_HiveSignedAccountUpdate, HiveSignedAccountUpdate, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_HiveSignedMessage, HiveSignedMessage, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_HiveSignedOperations, HiveSignedOperations, NO_PROCESS_FUNC) #endif // !BITCOIN_ONLY #if DEBUG_LINK From 895996bd8841f36aa1f36c3816f91e4f1f6a7430 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 15 Jul 2026 12:49:29 -0300 Subject: [PATCH 2/4] chore: bump python-keepkey pin (sign_operations + parsed-ops tests) --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index ed8cbdf92..a4332c779 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit ed8cbdf92097945239cb2a084edb909247c520b8 +Subproject commit a4332c7799fe92dd48bbcacd4a251c9afac15576 From c6c7474baacb85cb921e69cb2c86d83167768231 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 15 Jul 2026 12:55:02 -0300 Subject: [PATCH 3/4] chore: bump python-keepkey pin (oversize boundary fix; 28/28 on emulator) --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index a4332c779..15d95eccc 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit a4332c7799fe92dd48bbcacd4a251c9afac15576 +Subproject commit 15d95eccc8908a994d37834f62dc10b98d49a230 From 767276d28789c1111a27bbe330f7fa4703aac880 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 15 Jul 2026 15:29:27 -0300 Subject: [PATCH 4/4] fix(hive): accurate min-length reject boundary + clang-format pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit len==12 (header + op_count + extensions varints) now falls through to the specific 'op count must be 1-4' error instead of the generic too-short reject; op bodies are bounds-checked as they parse. Behavior-equivalent for security — pure error-message accuracy. Formatting: clang-format 20 (CI version) over the two files the lint job flagged. --- include/keepkey/firmware/hive.h | 6 +++--- lib/firmware/fsm_msg_hive.h | 4 ++-- lib/firmware/hive.c | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/include/keepkey/firmware/hive.h b/include/keepkey/firmware/hive.h index fdeeea0a0..0c268a40d 100644 --- a/include/keepkey/firmware/hive.h +++ b/include/keepkey/firmware/hive.h @@ -98,9 +98,9 @@ typedef struct { uint16_t target_len; const uint8_t* detail; // vote: permlink / comment: body / cj: json uint16_t detail_len; - int16_t weight; // vote only (-10000..10000) - bool is_top_level; // comment only: parent_author empty - uint8_t n_auths; // custom_json only: total auth account names + int16_t weight; // vote only (-10000..10000) + bool is_top_level; // comment only: parent_author empty + uint8_t n_auths; // custom_json only: total auth account names } HiveTxOp; typedef struct { diff --git a/lib/firmware/fsm_msg_hive.h b/lib/firmware/fsm_msg_hive.h index d912bbede..436b1cddf 100644 --- a/lib/firmware/fsm_msg_hive.h +++ b/lib/firmware/fsm_msg_hive.h @@ -761,8 +761,8 @@ void fsm_msgHiveSignOperations(const HiveSignOperations* msg) { } case HIVE_OP_COMMENT: approved = confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, - op->is_top_level ? "Post" : "Comment", - "@%s: %s\n%s", name, target, detail); + op->is_top_level ? "Post" : "Comment", "@%s: %s\n%s", + name, target, detail); break; case HIVE_OP_CUSTOM_JSON: { char extra[12] = ""; diff --git a/lib/firmware/hive.c b/lib/firmware/hive.c index 4ae4f3b7e..747baa07d 100644 --- a/lib/firmware/hive.c +++ b/lib/firmware/hive.c @@ -274,8 +274,9 @@ static bool cur_string(HiveCur* c, const uint8_t** s, uint16_t* slen, const char* hive_parseOperations(const uint8_t* tx, size_t len, HiveParsedTx* out) { memzero(out, sizeof(*out)); - // 10-byte header + op_count + at least one op-type byte + extensions - if (len < 13) return "Hive tx too short"; + // 10-byte header + op_count varint + extensions varint is the structural + // minimum; op bodies are bounds-checked as they parse. + if (len < 12) return "Hive tx too short"; if (len > HIVE_MAX_OPS_TX_LEN) return "Hive tx too long"; // = proto cap // Header (ref_block_num u16, ref_block_prefix u32, expiration u32) is