Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion deps/device-protocol
1 change: 1 addition & 0 deletions include/keepkey/firmware/fsm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
47 changes: 47 additions & 0 deletions include/keepkey/firmware/hive.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ────────────────────────────────────────────────────────────
/**
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions include/keepkey/transport/messages-hive.options
Original file line number Diff line number Diff line change
Expand Up @@ -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
161 changes: 161 additions & 0 deletions lib/firmware/fsm_msg_hive.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading
Loading