From af96b6fa88c782b7bd4a3d6e56b5a336009a4bb9 Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 13 Jul 2026 17:16:31 -0300 Subject: [PATCH 1/2] =?UTF-8?q?chore(deps):=20bump=20hdwallet=20to=20maste?= =?UTF-8?q?r=20(async=20HID=20read=20=E2=86=92=20device=20Cancel=20works)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin modules/hdwallet to keepkey/hdwallet master 089e4ba0 (PR #54), which makes nodehid reads non-blocking so an on-device Cancel can dispatch during signing — fixes the swap flow Cancel button not aborting the device. --- modules/hdwallet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/hdwallet b/modules/hdwallet index eccc3fa6..089e4ba0 160000 --- a/modules/hdwallet +++ b/modules/hdwallet @@ -1 +1 @@ -Subproject commit eccc3fa680d6ad79d1f4799f3860679697631551 +Subproject commit 089e4ba0258d6fc22226acfe7819e8956ebfc1a4 From 840a92dd7369f8b95d6d8b35911141ce538f8e21 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 15 Jul 2026 12:14:06 -0300 Subject: [PATCH 2/2] =?UTF-8?q?feat(hive):=20REST=20POST=20/hive/sign-mess?= =?UTF-8?q?age=20=E2=80=94=20Keychain=20signBuffer=20(fw=207.15.0+)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hdwallet pointer → hiveSignMessage shim (msg 1614/1615) - keepkey-firmware pointer → feat/hive-sign-message (HiveSignMessage handler: SHA256(message) digest, chain-id-collision guard, SLIP-48 path/role enforcement, owner' excluded) - endpoint gated like /addresses/hive (hive_enabled 403, fw<7.15.0 501), registered in SIGNING_ROUTES (approval overlay + probe rejection), default posting role, returns { signature: hex, public_key: STM } Verified live on the rebuilt emulator: recovered signer == posting key, low-S, negative tests (chain-id prefix, owner path) rejected by firmware, make test-sign-gating 75/75. Co-Authored-By: Claude Fable 5 --- modules/hdwallet | 2 +- modules/keepkey-firmware | 2 +- projects/keepkey-vault/src/bun/rest-api.ts | 38 +++++++++++++++++++ projects/keepkey-vault/src/bun/schemas.ts | 10 +++++ .../keepkey-vault/src/bun/signing-routes.ts | 3 +- 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/modules/hdwallet b/modules/hdwallet index 089e4ba0..a8551cce 160000 --- a/modules/hdwallet +++ b/modules/hdwallet @@ -1 +1 @@ -Subproject commit 089e4ba0258d6fc22226acfe7819e8956ebfc1a4 +Subproject commit a8551cce637a04e3a082403a77104cc0eac5b3c7 diff --git a/modules/keepkey-firmware b/modules/keepkey-firmware index f322fc40..ddade55d 160000 --- a/modules/keepkey-firmware +++ b/modules/keepkey-firmware @@ -1 +1 @@ -Subproject commit f322fc4093de1631b21d8698e575a99654c71e66 +Subproject commit ddade55d97a1c2252cd3e132d99459a1b19907bd diff --git a/projects/keepkey-vault/src/bun/rest-api.ts b/projects/keepkey-vault/src/bun/rest-api.ts index dd6c3635..952e20c4 100644 --- a/projects/keepkey-vault/src/bun/rest-api.ts +++ b/projects/keepkey-vault/src/bun/rest-api.ts @@ -2067,6 +2067,44 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 return json({ address }) } + if (path === '/hive/sign-message' && method === 'POST') { + auth.requireAuth(req) + // Same gates as /addresses/hive: feature flag + firmware ≥ 7.15.0 + if (getSetting('hive_enabled') !== '1') return json({ error: 'Hive is disabled' }, 403) + const fwBlock = requireChainSupport('hive') + if (fwBlock) return fwBlock + const wallet = requireWallet(engine) + const body = await parseRequest(req, S.HiveSignMessageRequest) + const messageBytes = body.is_text === false + ? Buffer.from(body.message.replace(/^0x/, ''), 'hex') + : Buffer.from(body.message, 'utf8') + if (messageBytes.length === 0 || messageBytes.length > 1024) { + throw new HttpError(400, 'Hive message must be 1–1024 bytes') + } + // Default to the posting role — Keychain signBuffer is overwhelmingly + // dApp login, which verifies against the account's posting authority. + const addressNList = body.addressNList || body.address_n || hiveRolePath('posting', 0) + const result = await emuWrap(() => (wallet as any).hiveSignMessage({ + addressNList, + message: new Uint8Array(messageBytes), + }), { operation: 'hiveSignMessage', chain: 'HIVE' }) + if (!result?.signature) throw new HttpError(500, 'Hive sign-message: device returned no signature') + const sigBytes = result.signature instanceof Uint8Array ? Buffer.from(result.signature) : Buffer.from(String(result.signature), 'hex') + const pubBytes = result.publicKey instanceof Uint8Array ? Buffer.from(result.publicKey) : Buffer.from(String(result.publicKey), 'hex') + // STM encoding: 'STM' + base58(pub33 || ripemd160(pub33)[0:4]) + let stm = '' + if (pubBytes.length === 33) { + const { ripemd160 } = await import('@noble/hashes/ripemd160') + const bs58 = (await import('bs58')).default + const checksum = Buffer.from(ripemd160(pubBytes)).subarray(0, 4) + stm = 'STM' + bs58.encode(Buffer.concat([pubBytes, checksum])) + } + return json({ + signature: sigBytes.toString('hex'), + public_key: stm, + }) + } + if (path === '/hive/sign-transfer' && method === 'POST') { auth.requireAuth(req) // Same gates as /addresses/hive: feature flag + firmware ≥ 7.15.0 diff --git a/projects/keepkey-vault/src/bun/schemas.ts b/projects/keepkey-vault/src/bun/schemas.ts index 3d572779..54f5ae77 100644 --- a/projects/keepkey-vault/src/bun/schemas.ts +++ b/projects/keepkey-vault/src/bun/schemas.ts @@ -184,6 +184,16 @@ export const TonSignRequest = z.object({ amount: z.string().optional(), // amount in nanoTON — enables clear-sign on device }).strip() +/** POST /hive/sign-message — Keychain signBuffer: sign SHA256(message bytes) on-device (fw 7.15.0+) */ +export const HiveSignMessageRequest = z.object({ + /** SLIP-0048 path; defaults to posting role m/48'/13'/4'/0'/0' (dApp login) */ + address_n: z.array(z.number().int()).optional(), + addressNList: z.array(z.number().int()).optional(), + /** Message payload. Default: UTF-8 text. If is_text=false, hex (optional 0x). */ + message: z.string().min(1), + is_text: z.boolean().optional(), +}).strip() + /** POST /hive/sign-transfer — Graphene transfer op, serialized + signed on-device (fw 7.15.0+) */ export const HiveSignTransferRequest = z.object({ /** SLIP-0048 path; defaults to active role m/48'/13'/1'/0'/0' */ diff --git a/projects/keepkey-vault/src/bun/signing-routes.ts b/projects/keepkey-vault/src/bun/signing-routes.ts index 279ffc64..0294be28 100644 --- a/projects/keepkey-vault/src/bun/signing-routes.ts +++ b/projects/keepkey-vault/src/bun/signing-routes.ts @@ -15,7 +15,7 @@ export const SIGNING_ROUTES = new Set([ '/eth/sign-transaction', '/eth/sign-typed-data', '/eth/sign', '/utxo/sign-transaction', '/xrp/sign-transaction', '/solana/sign-transaction', '/solana/sign-message', '/tron/sign-transaction', '/ton/sign-transaction', - '/hive/sign-transfer', + '/hive/sign-transfer', '/hive/sign-message', // Message / off-chain signing. The firmware always confirms these on its // OLED, but that is the device surface only — without these in the set a // REST caller reaches the device with no in-Vault review. Gate them so the @@ -69,6 +69,7 @@ export function requiredSigningFields(path: string): string[] | null { '/tron/sign-transaction': ['raw_tx', 'rawTx', 'to_address', 'amount'], '/ton/sign-transaction': ['raw_tx', 'rawTx', 'to_address', 'amount'], '/hive/sign-transfer': ['from', 'to', 'amount'], + '/hive/sign-message': ['message'], '/tron/sign-message': ['message'], '/tron/sign-typed-hash': ['domain_separator_hash'], '/ton/sign-message': ['message'],