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 @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions include/keepkey/firmware/hive.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────
/**
Expand Down Expand Up @@ -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.
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 @@ -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
133 changes: 133 additions & 0 deletions lib/firmware/fsm_msg_hive.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
61 changes: 53 additions & 8 deletions lib/firmware/hive.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) ──────────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions lib/firmware/messagemap.def
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading