diff --git a/deps/device-protocol b/deps/device-protocol index 33521a8fd..a793934d0 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit 33521a8fd6f012c8bbca3a8902eea6c1f5aa3389 +Subproject commit a793934d09a883c9944ba3b9da15d23969906343 diff --git a/deps/python-keepkey b/deps/python-keepkey index 35555d7f1..ed8cbdf92 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 35555d7f127d6be0b96d6b9e4a981130dba50d2b +Subproject commit ed8cbdf92097945239cb2a084edb909247c520b8 diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index 6f38457f3..f3b918c70 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -146,6 +146,7 @@ void fsm_msgHiveGetPublicKeys(const HiveGetPublicKeys* msg); void fsm_msgHiveSignTx(const HiveSignTx* msg); void fsm_msgHiveSignAccountCreate(const HiveSignAccountCreate* msg); void fsm_msgHiveSignAccountUpdate(const HiveSignAccountUpdate* msg); +void fsm_msgHiveSignMessage(const HiveSignMessage* msg); #if DEBUG_LINK // void fsm_msgDebugLinkDecision(DebugLinkDecision *msg); diff --git a/include/keepkey/firmware/hive.h b/include/keepkey/firmware/hive.h index 6cad9cedc..2b1579b6e 100644 --- a/include/keepkey/firmware/hive.h +++ b/include/keepkey/firmware/hive.h @@ -42,6 +42,9 @@ // other fields. Non-memo overhead: header(12) + from(17) + to(17) + asset(16) // + footer(1) = ~63 bytes. 512 - 63 - 3 (varint) = 446; 440 is conservative. #define HIVE_MAX_MEMO_LEN 440 +// 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 // ── Public API ──────────────────────────────────────────────────────────── /** @@ -75,6 +78,15 @@ bool hive_getPublicKeys(const HDNode* root, uint32_t account_index, */ void hive_signTx(const HDNode* node, const HiveSignTx* msg, HiveSignedTx* resp); +/** + * Sign an arbitrary message per the Hive Keychain signBuffer contract: + * signature over SHA256(message bytes) only — no chain_id prepend, no + * message prefix. Emits the 65-byte compact recoverable signature plus the + * signing key's 33-byte compressed public key. + */ +void hive_signMessage(const HDNode* node, const HiveSignMessage* msg, + HiveSignedMessage* resp); + /** * Sign a Hive account_create transaction (op type 9). * owner/active/posting/memo_raw must be device-derived 33-byte compressed keys. diff --git a/include/keepkey/transport/messages-hive.options b/include/keepkey/transport/messages-hive.options index e878f3e3b..04ba85d68 100644 --- a/include/keepkey/transport/messages-hive.options +++ b/include/keepkey/transport/messages-hive.options @@ -44,3 +44,9 @@ HiveSignAccountUpdate.new_memo_key max_size:64 HiveSignedAccountUpdate.signature max_size:65 HiveSignedAccountUpdate.serialized_tx max_size:512 + +HiveSignMessage.address_n max_count:8 +HiveSignMessage.message max_size:1024 + +HiveSignedMessage.signature max_size:65 +HiveSignedMessage.public_key max_size:33 diff --git a/lib/firmware/fsm_msg_hive.h b/lib/firmware/fsm_msg_hive.h index aa9291664..d12881ea8 100644 --- a/lib/firmware/fsm_msg_hive.h +++ b/lib/firmware/fsm_msg_hive.h @@ -514,3 +514,136 @@ void fsm_msgHiveSignAccountUpdate(const HiveSignAccountUpdate* msg) { msg_write(MessageType_MessageType_HiveSignedAccountUpdate, resp); layoutHome(); } + +// ── HiveSignMessage (Keychain signBuffer) ───────────────────────────────── +// The Hive dApp login primitive: Aioha / Keychain-SDK dApps authenticate by +// having the account sign a challenge string, then recover the pubkey and +// check it against the account's authority on-chain. Contract (hive-js +// Signature.signBuffer): sig over SHA256(raw message bytes) — no chain_id, +// no prefix. Roles: posting/active/memo, Keychain's requestSignBuffer +// surface. owner' is deliberately rejected — no consumer offers it, and the +// cold owner key must not be normalized into dApp flows. The full path +// shape is still enforced like the tx handlers. + +static bool hive_slip48_message_path_ok(const uint32_t* address_n, + uint32_t count, + const char** role_label) { + 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' + switch (address_n[2]) { + case HIVE_ROLE_ACTIVE: + *role_label = "active"; + return true; + case HIVE_ROLE_MEMO: + *role_label = "memo"; + return true; + case HIVE_ROLE_POSTING: + *role_label = "posting"; + return true; + default: + return false; + } +} + +void fsm_msgHiveSignMessage(const HiveSignMessage* msg) { + RESP_INIT(HiveSignedMessage); + + CHECK_INITIALIZED + CHECK_PIN + + if (!msg->has_message || msg->message.size == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, _("Missing message")); + layoutHome(); + return; + } + + // Mirrors the proto max_size cap so proto and code can never disagree + // (the memo-length lesson from the transfer handler). + if (msg->message.size > HIVE_MAX_MESSAGE_LEN) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Hive message too long (max 1024 bytes)")); + layoutHome(); + return; + } + + // A Hive TRANSACTION digest is SHA256(chain_id || tx), and this message + // digest is SHA256(message) — so a "message" that begins with the mainnet + // chain-id bytes would hash to a broadcastable transaction's digest. No + // legitimate challenge starts with the chain id; refuse the collision. + const uint8_t hive_chain_id[HIVE_CHAIN_ID_LEN] = HIVE_CHAIN_ID; + if (msg->message.size >= HIVE_CHAIN_ID_LEN && + memcmp(msg->message.bytes, hive_chain_id, HIVE_CHAIN_ID_LEN) == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Message must not start with the Hive chain ID")); + layoutHome(); + return; + } + + const char* role_label = NULL; + if (!hive_slip48_message_path_ok(msg->address_n, msg->address_n_count, + &role_label)) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid Hive SLIP-0048 path")); + layoutHome(); + return; + } + + HDNode* node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + hdnode_fill_public_key(node); + + // Printable ASCII within the display budget is shown verbatim; anything + // longer — or with non-ASCII bytes — gets the hex preview + byte count. + // The budget matters: confirm()'s body buffer silently truncates at + // BODY_CHAR_MAX, so a long "printable" message could show a benign prefix + // while the device signs hidden trailing content. Same 128-byte budget as + // SolanaSignMessage. + bool printable = msg->message.size <= 128; + for (uint32_t i = 0; printable && i < msg->message.size; i++) { + if (msg->message.bytes[i] < 0x20 || msg->message.bytes[i] > 0x7e) { + printable = false; + } + } + + bool approved; + if (printable) { + approved = confirm(ButtonRequestType_ButtonRequest_ProtectCall, + "Sign Hive Message", "(%s key) %.*s", role_label, + (int)msg->message.size, msg->message.bytes); + } else { + char preview[96]; // 64 hex chars + "... (1024 bytes)" + NUL + unsigned show = msg->message.size > 32 ? 32 : msg->message.size; + for (unsigned i = 0; i < show; i++) { + snprintf(&preview[2 * i], 3, "%02x", msg->message.bytes[i]); + } + if (msg->message.size > 32) { + snprintf(&preview[2 * show], sizeof(preview) - 2 * show, "... (%u bytes)", + (unsigned)msg->message.size); + } + approved = confirm(ButtonRequestType_ButtonRequest_ProtectCall, + "Sign Hive Bytes", "(%s key) %s", role_label, preview); + } + if (!approved) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + hive_signMessage(node, msg, resp); + memzero(node, sizeof(*node)); + + if (!resp->has_signature) { + fsm_sendFailure(FailureType_Failure_FirmwareError, + _("Hive message signing failed")); + layoutHome(); + return; + } + + msg_write(MessageType_MessageType_HiveSignedMessage, resp); + layoutHome(); +} diff --git a/lib/firmware/hive.c b/lib/firmware/hive.c index 740ac7144..2f71afdc7 100644 --- a/lib/firmware/hive.c +++ b/lib/firmware/hive.c @@ -193,7 +193,24 @@ static int hive_is_canonic(uint8_t v, uint8_t signature[64]) { } /* - * Sign helper: SHA256(chain_id || serialized_tx) → secp256k1 recoverable sig. + * Core sign helper over an already-computed 32-byte digest → 65-byte + * compact recoverable sig: header (27 + recovery_id + 4 compressed-key + * flag), then r(32) ‖ s(32). + */ +static bool hive_sign_raw_digest(const HDNode* node, const uint8_t digest[32], + uint8_t sig[65]) { + uint8_t pby; + if (ecdsa_sign_digest(&secp256k1, node->private_key, digest, sig + 1, &pby, + hive_is_canonic) != 0) { + return false; + } + // Compact signature header: 27 + recovery_id + 4 (compressed key flag) + sig[0] = 27 + pby + 4; + return true; +} + +/* + * Transaction sign helper: SHA256(chain_id || serialized_tx) → compact sig. * Writes 65 bytes into sig[]. Returns true on success. */ static bool hive_sign_digest(const HDNode* node, const uint8_t* chain_id, @@ -206,16 +223,44 @@ static bool hive_sign_digest(const HDNode* node, const uint8_t* chain_id, uint8_t digest[32]; sha256_Final(&sha, digest); - uint8_t pby; - if (ecdsa_sign_digest(&secp256k1, node->private_key, digest, sig + 1, &pby, - hive_is_canonic) != 0) { + bool ok = hive_sign_raw_digest(node, digest, sig); + memzero(digest, sizeof(digest)); + return ok; +} + +// ── 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 +// Signature.signBuffer — which every Hive dApp verifies against — hashes +// the raw bytes exactly once; any added prefix silently breaks all dApp +// verification. + +void hive_signMessage(const HDNode* node, const HiveSignMessage* msg, + HiveSignedMessage* resp) { + if (!msg->has_message || msg->message.size > HIVE_MAX_MESSAGE_LEN) return; + + uint8_t digest[32]; + sha256_Raw(msg->message.bytes, msg->message.size, digest); + + uint8_t sig[65]; + if (!hive_sign_raw_digest(node, digest, sig)) { memzero(digest, sizeof(digest)); - return false; + memzero(sig, sizeof(sig)); + return; } - // Compact signature header: 27 + recovery_id + 4 (compressed key flag) - sig[0] = 27 + pby + 4; + + resp->has_signature = true; + resp->signature.size = 65; + memcpy(resp->signature.bytes, sig, 65); + + // Caller must have run hdnode_fill_public_key(node). Returned so the host + // can build Keychain's publicKey response field without a second call. + resp->has_public_key = true; + resp->public_key.size = 33; + memcpy(resp->public_key.bytes, node->public_key, 33); + memzero(digest, sizeof(digest)); - return true; + memzero(sig, sizeof(sig)); } // ── Transfer (op type 2) ────────────────────────────────────────────────── diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index df3a6128d..18514f5ee 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -195,12 +195,14 @@ MSG_IN(MessageType_MessageType_HiveSignTx, HiveSignTx, fsm_msgHiveSignTx) 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_OUT(MessageType_MessageType_HivePublicKey, HivePublicKey, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_HivePublicKeys, HivePublicKeys, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_HiveSignedTx, HiveSignedTx, NO_PROCESS_FUNC) 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) #endif // !BITCOIN_ONLY #if DEBUG_LINK