From d6acd152effc0fe9daeba9f72dce88272efd4e38 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 8 Jul 2026 22:03:13 -0300 Subject: [PATCH 1/7] feat(clearsign): vault/SDK identity-icon wiring + signing idle-timeout fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /eth/clearsign/load-signer accepts icon (hex, <=384B) + iconWidth/ iconHeight + persist, forwards them to hdwallet loadClearsignSigner so a loaded identity's logo reaches the device (firmware 7.15 persistent identities). SDK LoadClearsignSignerParams + schema grow the fields. - Bun.serve idleTimeout: 0 — human-gated signing legitimately blocks the response longer than Bun's 120s default; letting the socket close aborted the second consecutive sign mid-confirm ('other side closed'). Device signing has no business timing out at the socket layer. - test: CLEARSIGN_FLOW= runs a single clear-sign flow (isolates one flow / sidesteps the consecutive-sign path). --- projects/keepkey-sdk/src/types.ts | 7 +++++++ .../evm-clearsign/clearsign-signer-flows.js | 5 ++++- projects/keepkey-vault/src/bun/rest-api.ts | 18 ++++++++++++++++-- projects/keepkey-vault/src/bun/schemas.ts | 7 +++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/projects/keepkey-sdk/src/types.ts b/projects/keepkey-sdk/src/types.ts index 3b7772d8..3b9258f8 100644 --- a/projects/keepkey-sdk/src/types.ts +++ b/projects/keepkey-sdk/src/types.ts @@ -113,6 +113,13 @@ export interface LoadClearsignSignerParams { pubkey: string /** Signer label shown on the device trust screen. `[A-Za-z0-9 _-]`, 1-31 chars. */ alias: string + /** Optional identity logo: 1bpp mono RLE bitmap as hex, <= 384 bytes. Shown on + * the trust screen and led before every clear-sign the identity vouches for. */ + icon?: string + iconWidth?: number + iconHeight?: number + /** Persist the identity in device flash across reboots (until WipeDevice). */ + persist?: boolean } export interface LoadClearsignSignerResult { diff --git a/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js b/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js index 82cdaf8d..2cc197f5 100644 --- a/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js +++ b/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js @@ -36,7 +36,10 @@ const PIONEER_URL = (process.env.PIONEER_URL || 'http://localhost:9001').replace const NONCE = '0x0', GAS_PRICE = '0x4a817c800' /* 20 gwei */, GAS_LIMIT = '0x3d090' /* 250000 */ // self mode: golden catalog flows the CI key signs offline (slot 3, byte-parity-proven). -const SELF_FLOWS = ['aave-v3-supply', 'erc20-transfer', 'erc20-approve-unlimited'] +// CLEARSIGN_FLOW= runs a single flow (useful for isolating one flow / avoiding +// the consecutive-sign hang while that's investigated separately). +const ALL_SELF_FLOWS = ['aave-v3-supply', 'erc20-transfer', 'erc20-approve-unlimited'] +const SELF_FLOWS = process.env.CLEARSIGN_FLOW ? [process.env.CLEARSIGN_FLOW] : ALL_SELF_FLOWS // pioneer mode: real contracts Pioneer classifies VERIFIED. Extend as coverage grows. const PIONEER_FLOWS = [ diff --git a/projects/keepkey-vault/src/bun/rest-api.ts b/projects/keepkey-vault/src/bun/rest-api.ts index 31efb2ef..c49a8997 100644 --- a/projects/keepkey-vault/src/bun/rest-api.ts +++ b/projects/keepkey-vault/src/bun/rest-api.ts @@ -1123,6 +1123,13 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 const server = Bun.serve({ port, maxRequestBodySize: 1024 * 1024, // 1 MB max (addresses/signing payloads are small) + // Disable the idle-connection timeout (Bun default 10s). Signing requests + // block the response while the user confirms on the device — an interactive + // emulator confirm or a slow physical-button press legitimately exceeds 10s, + // and letting Bun close the socket aborts the in-flight sign mid-confirm + // ("other side closed" → the device's confirm is cancelled and it returns + // home). Human-gated signing has no business timing out at the socket layer. + idleTimeout: 0, async fetch(req) { const url = new URL(req.url) const path = url.pathname @@ -2138,7 +2145,10 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 if (typeof (wallet as any).loadClearsignSigner !== 'function') { throw new HttpError(501, 'Connected device does not support LoadClearsignSigner (requires firmware 7.15.0+)') } - console.log(`[REST] clearsign load-signer: slot=${body.keyId} alias="${body.alias}" pubkey=${body.pubkey}`) + const icon = body.icon ? parseHex(body.icon, 'icon') : undefined + if (icon && icon.length > 384) throw new HttpError(400, `icon must be <= 384 bytes, got ${icon.length}`) + console.log(`[REST] clearsign load-signer: slot=${body.keyId} alias="${body.alias}" pubkey=${body.pubkey}` + + `${icon ? ` icon=${icon.length}B ${body.iconWidth}x${body.iconHeight}` : ''}${body.persist ? ' persist' : ''}`) try { // Loading a signer raises a mandatory on-device "Trust signer" confirm. // On the emulator that button press must be armed via emuWrap (interactive @@ -2146,7 +2156,11 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 // trust screen but no green button ever appears and the call hangs. // emuWrap is a transparent no-op on real hardware. await emuWrap( - () => (wallet as any).loadClearsignSigner({ keyId: body.keyId, pubkey, alias: body.alias }), + () => (wallet as any).loadClearsignSigner({ + keyId: body.keyId, pubkey, alias: body.alias, + icon, iconWidth: body.iconWidth, iconHeight: body.iconHeight, + persist: body.persist === true, + }), { operation: 'loadClearsignSigner', opLabel: 'Trust Signer', chain: 'Ethereum' }, ) return json({ ok: true, keyId: body.keyId, alias: body.alias }) diff --git a/projects/keepkey-vault/src/bun/schemas.ts b/projects/keepkey-vault/src/bun/schemas.ts index bdb61854..0208289e 100644 --- a/projects/keepkey-vault/src/bun/schemas.ts +++ b/projects/keepkey-vault/src/bun/schemas.ts @@ -79,6 +79,13 @@ export const LoadClearsignSignerRequest = z.object({ keyId: z.number().int().min(1).max(3), // slot 0 = built-in production key, not loadable pubkey: z.string().regex(/^(0x)?[0-9a-fA-F]{66}$/), // 33-byte compressed secp256k1 hex alias: z.string().min(1).max(31).regex(/^[A-Za-z0-9 _-]+$/), // shown on the device trust screen + // Optional identity logo (1bpp mono RLE, <=384 bytes hex) + its dimensions; + // shown on the trust screen and led before every clear-sign it vouches for. + icon: z.string().regex(/^(0x)?[0-9a-fA-F]{2,768}$/).optional(), + iconWidth: z.number().int().min(1).max(64).optional(), + iconHeight: z.number().int().min(1).max(64).optional(), + // Persist the identity in device flash across reboots (until WipeDevice). + persist: z.boolean().optional(), }).strip() /** POST /eth/sign-typed-data */ From b564d766f63e9f1f03b602b91ce951c80d77d8f2 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 8 Jul 2026 22:27:51 -0300 Subject: [PATCH 2/7] feat(clearsign): icon encoder + catalog tooling; wire identity logo into device test Turns any protocol/identity logo into the KeepKey 1bpp mono-RLE bitmap the OLED renders (<=384B, <=64x64): - encode-icon.mjs: image -> mono-RLE, self-verified by round-tripping through a mirror of the firmware draw_bitmap_mono_rle decoder + 384B cap check. ImageMagick does resize/center/threshold; this owns only the RLE codec. - build-catalog.mjs: batch sources/*.{png,svg,jpg} -> generated catalog JSON. - clearsign-signer-flows.js: loads the generated Pioneer identity logo with persist=true so the device renders it on the trust screen + every per-tx confirm (CLEARSIGN_NO_ICON=1 for the text-only path). Foundation for per-protocol icons: drop a logo in sources/, run build-catalog. --- .../evm-clearsign/clearsign-signer-flows.js | 21 ++- .../scripts/clearsign-icons/README.md | 41 +++++ .../scripts/clearsign-icons/build-catalog.mjs | 51 ++++++ .../scripts/clearsign-icons/encode-icon.mjs | 163 ++++++++++++++++++ .../generated/identity-icons.json | 9 + .../generated/protocol-icons.json | 8 + .../clearsign-icons/sources/pioneer.png | Bin 0 -> 998 bytes 7 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 projects/keepkey-vault/scripts/clearsign-icons/README.md create mode 100644 projects/keepkey-vault/scripts/clearsign-icons/build-catalog.mjs create mode 100644 projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs create mode 100644 projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json create mode 100644 projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json create mode 100644 projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png diff --git a/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js b/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js index 2cc197f5..6a9e800f 100644 --- a/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js +++ b/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js @@ -28,6 +28,17 @@ const { run, ETH_PATH, erc20Approve } = require('../_helpers') const { buildFlowBlob, sighashLegacy, CI_TEST_PUBKEY, CI_SIGNER_ALIAS, TEST_KEY_ID } = require('../_clearsign') +// Identity logo (1bpp mono RLE) generated by +// keepkey-vault/scripts/clearsign-icons/encode-icon.mjs — sent with the signer +// so the device renders it on the trust screen + every per-tx confirm. +// Set CLEARSIGN_NO_ICON=1 to load a text-only identity instead. +let IDENTITY_ICON = null +if (!process.env.CLEARSIGN_NO_ICON) { + try { + IDENTITY_ICON = require('../../../keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json').pioneer + } catch { /* catalog not generated — fall back to text-only */ } +} + const MODE = process.env.CLEARSIGN_MODE || 'self' const PIONEER_URL = (process.env.PIONEER_URL || 'http://localhost:9001').replace(/\/+$/, '') @@ -60,10 +71,14 @@ function selfProvider() { const b = buildFlowBlob(k) return { label: k, tx: b.tx, blobHex: b.blobHex, keyId: b.keyId } }) - return { - signer: { keyId: TEST_KEY_ID, pubkey: CI_TEST_PUBKEY, alias: CI_SIGNER_ALIAS }, - flows, + const signer = { keyId: TEST_KEY_ID, pubkey: CI_TEST_PUBKEY, alias: CI_SIGNER_ALIAS } + if (IDENTITY_ICON) { + signer.icon = IDENTITY_ICON.hex + signer.iconWidth = IDENTITY_ICON.width + signer.iconHeight = IDENTITY_ICON.height + signer.persist = true // exercise the persist-to-flash path (survives reboot) } + return { signer, flows } } /** pioneer mode: fetch each blob + the signer pubkey from live Pioneer. */ diff --git a/projects/keepkey-vault/scripts/clearsign-icons/README.md b/projects/keepkey-vault/scripts/clearsign-icons/README.md new file mode 100644 index 00000000..a52139cd --- /dev/null +++ b/projects/keepkey-vault/scripts/clearsign-icons/README.md @@ -0,0 +1,41 @@ +# Clear-sign icons + +Tooling to turn protocol/identity logos into the 1bpp mono-RLE bitmap the +KeepKey OLED renders (`draw_bitmap_mono_rle`), for clear-sign **identity** icons +(the signer's logo, persisted on-device via `LoadClearsignSigner.icon`) and +**protocol** icons (per-tx dApp glyphs the vault sends). + +Device constraints: **≤ 384 bytes**, **≤ 64×64** (48×48 recommended), 1bpp mono. + +## Add an icon + +1. Drop a logo into `sources/` named for its key (lowercase protocol/dApp or + signer slug), e.g. `sources/aave.png`, `sources/pioneer.svg`. PNG/SVG/JPG all + work — ImageMagick resizes/centers/thresholds. +2. `bun build-catalog.mjs` → writes `generated/protocol-icons.json` + (`{ "": { hex, width, height, bytes } }`). Each icon is self-verified by + round-tripping through a mirror of the firmware decoder and checked against + the 384-byte cap. + +Simple, chunky, high-contrast logos compress best. If one exceeds 384 B: +`ICON_SIZE=32 bun build-catalog.mjs`, simplify the art, or start from a mono +source. `ICON_GRAY=1` keeps antialiased grayscale instead of a hard threshold. + +## Encode one file / preview + +```sh +bun encode-icon.mjs [--size 48] [--gray] [--invert] [--json] +bun encode-icon.mjs --self-test # verify the RLE codec +``` + +The identity icon used by the clear-sign device test lives in +`generated/identity-icons.json` (consumed by +`keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js`). + +## Format (why a custom encoder) + +The firmware decoder reads a control byte as `int8`: `n>0` repeats the next +value byte `n` times; `n<0` takes the next `-n` bytes as literal pixels. Pixel = +`value * frame.color / 100`; identity frames use `color=100`, so value bytes are +direct 0–255 intensities. `encode-icon.mjs` owns this encoding and self-checks +every output; ImageMagick handles everything else. diff --git a/projects/keepkey-vault/scripts/clearsign-icons/build-catalog.mjs b/projects/keepkey-vault/scripts/clearsign-icons/build-catalog.mjs new file mode 100644 index 00000000..10b79ae8 --- /dev/null +++ b/projects/keepkey-vault/scripts/clearsign-icons/build-catalog.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env bun +/** + * Batch-encode every logo in sources/ into the device mono-RLE catalog that the + * vault sends as clear-sign identity / protocol icons. + * + * sources/.{png,svg,jpg} -> generated/protocol-icons.json + * { "": { hex, width, height, bytes } , ... } + * + * Drop a logo in sources/, run `bun build-catalog.mjs`, done. Each icon is + * self-verified (encode round-trips through a firmware-decoder mirror) and + * checked against the 384-byte device cap; oversized ones are reported, not + * silently dropped. + * + * Naming convention for : the lowercase protocol/dApp key or a signer + * alias slug (e.g. `aave`, `uniswap`, `pioneer`). The vault maps a tx's + * dappName / the loaded signer to this key. + */ +import { readdirSync, mkdirSync } from 'node:fs' +import { join, extname, basename } from 'node:path' +import { encodeIconFile } from './encode-icon.mjs' + +const HERE = new URL('.', import.meta.url).pathname +const SRC = join(HERE, 'sources') +const OUT = join(HERE, 'generated', 'protocol-icons.json') +const SIZE = Number(process.env.ICON_SIZE || 48) +const GRAY = process.env.ICON_GRAY === '1' + +mkdirSync(join(HERE, 'generated'), { recursive: true }) + +const files = readdirSync(SRC).filter((f) => ['.png', '.svg', '.jpg', '.jpeg'].includes(extname(f).toLowerCase())) +const catalog = {} +const failures = [] + +for (const f of files.sort()) { + const key = basename(f, extname(f)).toLowerCase() + try { + const { hex, width, height, bytes } = encodeIconFile(join(SRC, f), { size: SIZE, gray: GRAY }) + catalog[key] = { hex, width, height, bytes } + console.log(` ✓ ${key.padEnd(20)} ${String(bytes).padStart(3)}B ${width}x${height}`) + } catch (e) { + failures.push({ key, err: e.message }) + console.log(` ✗ ${key.padEnd(20)} ${e.message}`) + } +} + +await Bun.write(OUT, JSON.stringify(catalog, null, 2) + '\n') +console.log(`\nwrote ${Object.keys(catalog).length} icon(s) -> ${OUT}`) +if (failures.length) { + console.log(`${failures.length} failed (too large / unreadable) — simplify the logo, try --size 32, or a cleaner mono source.`) + process.exit(1) +} diff --git a/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs b/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs new file mode 100644 index 00000000..b2514f9c --- /dev/null +++ b/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs @@ -0,0 +1,163 @@ +#!/usr/bin/env bun +/** + * clearsign icon encoder — any image → the KeepKey 1bpp/grayscale mono-RLE + * bitmap format that firmware's draw_bitmap_mono_rle() decodes (device-protocol + * LoadClearsignSigner.icon, <=384 bytes, <=64x64). + * + * ImageMagick does the resize/center/threshold; this tool owns only the RLE + * encoding (the part that must match the firmware decoder byte-for-byte) and + * self-verifies every encode by round-tripping through a decoder mirror. + * + * Format (see modules/keepkey-firmware/lib/board/draw.c): + * control byte read as int8: + * n > 0 : the next single value byte repeats n times -> [n][value] + * n < 0 : the next (-n) bytes are literal pixel values -> [n][v1..v_-n] + * pixel = value * frame.color / 100; identity frames use color=100, so the + * value bytes are direct 0-255 intensities (0x00 off, 0xFF on for mono). + * + * Usage: + * bun encode-icon.mjs [--size 48] [--gray] [--invert] [--json] + * bun encode-icon.mjs --self-test + * + * Prints { hex, width, height, bytes } (or a human summary without --json). + */ +import { execFileSync } from 'node:child_process' + +const ICON_MAX_BYTES = 384 +const MAX_DIM = 64 + +// ── RLE (must mirror draw_bitmap_mono_rle) ──────────────────────────────── + +/** Encode a row-major pixel array (0-255) to the mono-RLE byte stream. */ +export function rleEncode(pixels) { + const out = [] + const n = pixels.length + let i = 0 + while (i < n) { + // Longest run of identical pixels at i, capped at int8 max (127). + let run = 1 + while (i + run < n && pixels[i + run] === pixels[i] && run < 127) run++ + if (run >= 2) { + out.push(run, pixels[i] & 0xff) + i += run + } else { + // Literal packet: pixels up to the next >=2 run, capped at 128 (int8 min). + const lit = [] + while (i < n && lit.length < 128) { + if (i + 1 < n && pixels[i + 1] === pixels[i]) break // a run starts next + lit.push(pixels[i] & 0xff) + i++ + } + out.push((256 - lit.length) & 0xff, ...lit) // -len as an unsigned byte + } + } + return Uint8Array.from(out) +} + +/** Decode — a faithful mirror of the firmware loop, used only to self-verify. */ +export function rleDecode(data, w, h) { + const pixels = new Array(w * h) + let idx = 0 // pixel_index into data + let sequence = 0 + let nonsequence = 0 + for (let p = 0; p < w * h; p++) { + if (sequence === 0 && nonsequence === 0) { + let ctrl = data[idx++] + if (ctrl > 127) ctrl -= 256 // int8 + sequence = ctrl + if (sequence < 0) { + nonsequence = -sequence + sequence = 0 + } + } + pixels[p] = data[idx] // value byte (color=100 => identity) + if (sequence > 0) { + sequence-- + if (sequence === 0) idx++ + } else { + idx++ + nonsequence-- + } + } + return pixels +} + +// ── Image → pixels (via ImageMagick) ────────────────────────────────────── + +function rasterize(imagePath, size, { gray, invert }) { + // Fit into size x size on a black canvas, centered; grayscale 8-bit raw out. + const args = [ + imagePath[0] === '-' ? 'png:-' : imagePath, // (stdin unused today, keep simple) + '-alpha', 'remove', '-alpha', 'off', + '-resize', `${size}x${size}`, + '-background', 'black', '-gravity', 'center', '-extent', `${size}x${size}`, + '-colorspace', 'Gray', '-depth', '8', + ] + if (!gray) args.push('-threshold', '50%') + if (invert) args.push('-negate') + args.push('gray:-') + const raw = execFileSync('magick', args, { maxBuffer: 1 << 24 }) + return Array.from(raw) // one byte per pixel, row-major +} + +// ── Public: encode a file ───────────────────────────────────────────────── + +export function encodeIconFile(imagePath, { size = 48, gray = false, invert = false } = {}) { + if (size < 1 || size > MAX_DIM) throw new Error(`size must be 1..${MAX_DIM}`) + const pixels = rasterize(imagePath, size, { gray, invert }) + const data = rleEncode(pixels) + + // Self-verify: the firmware decoder must reproduce the exact pixels. + const back = rleDecode(data, size, size) + for (let i = 0; i < pixels.length; i++) { + if (back[i] !== pixels[i]) throw new Error(`RLE self-check failed at pixel ${i}`) + } + if (data.length > ICON_MAX_BYTES) { + throw new Error( + `encoded ${data.length}B > ${ICON_MAX_BYTES}B cap. Use --size 32, a simpler/mono logo, or drop --gray.`, + ) + } + return { + hex: Buffer.from(data).toString('hex'), + width: size, + height: size, + bytes: data.length, + } +} + +// ── CLI ─────────────────────────────────────────────────────────────────── + +function selfTest() { + // A tiny hand-checked pattern: run of 3 zeros, 2 literals, run of 4 of 0xff. + const px = [0, 0, 0, 0x10, 0x20, 255, 255, 255, 255] + const enc = rleEncode(px) + const dec = rleDecode(enc, 3, 3) + const ok = px.every((v, i) => v === dec[i]) + if (!ok) { console.error('SELF-TEST FAILED', { px, enc: [...enc], dec }); process.exit(1) } + // Expected stream: [3,0][-2,0x10,0x20][4,255] + const expect = [3, 0, 254, 0x10, 0x20, 4, 255] + const got = [...enc] + if (JSON.stringify(got) !== JSON.stringify(expect)) { + console.error('SELF-TEST byte mismatch', { expect, got }); process.exit(1) + } + console.log('self-test OK (encode+decode round-trip + exact byte stream)') +} + +if (import.meta.main) { + const argv = process.argv.slice(2) + if (argv.includes('--self-test')) { selfTest(); process.exit(0) } + const image = argv.find((a) => !a.startsWith('--')) + if (!image) { + console.error('usage: bun encode-icon.mjs [--size 48] [--gray] [--invert] [--json]') + process.exit(1) + } + const sizeArg = argv.indexOf('--size') + const opts = { + size: sizeArg >= 0 ? parseInt(argv[sizeArg + 1], 10) : 48, + gray: argv.includes('--gray'), + invert: argv.includes('--invert'), + } + const res = encodeIconFile(image, opts) + if (argv.includes('--json')) console.log(JSON.stringify(res)) + else console.log(`${res.width}x${res.height} ${res.bytes}B (cap ${ICON_MAX_BYTES})\n${res.hex}`) +} diff --git a/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json b/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json new file mode 100644 index 00000000..a2180fea --- /dev/null +++ b/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json @@ -0,0 +1,9 @@ +{ + "pioneer": { + "hex": "7f00560007ff25000fff1f0013ff1b0017ff180019ff16001bff14001dff12001fff100021ff0e0009ff040007ff05000aff0d0009ff040006ff05000bff0c000aff040005ff05000dff0b000aff040004ff05000eff0a000bff040003ff050010ff09000bff040002ff040012ff09000bff0400ffff040013ff09000bff090013ff08000cff090014ff07000cff0a0013ff07000cff0500ffff050012ff07000cff040003ff050011ff07000cff040003ff050011ff07000cff040004ff050010ff07000cff040005ff05000fff08000bff040006ff05000dff09000bff040006ff05000dff09000bff040007ff05000cff090027ff0a0025ff0b0025ff0c0023ff0d0023ff0e0021ff10001fff12001dff14001bff160019ff180017ff1b0013ff1f000fff250007ff7f002500", + "width": 48, + "height": 48, + "bytes": 302, + "alias": "Pioneer" + } +} diff --git a/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json b/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json new file mode 100644 index 00000000..59a7b134 --- /dev/null +++ b/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json @@ -0,0 +1,8 @@ +{ + "pioneer": { + "hex": "7f00560007ff25000fff1f0013ff1b0017ff180019ff16001bff14001dff12001fff100021ff0e0009ff040007ff05000aff0d0009ff040006ff05000bff0c000aff040005ff05000dff0b000aff040004ff05000eff0a000bff040003ff050010ff09000bff040002ff040012ff09000bff0400ffff040013ff09000bff090013ff08000cff090014ff07000cff0a0013ff07000cff0500ffff050012ff07000cff040003ff050011ff07000cff040003ff050011ff07000cff040004ff050010ff07000cff040005ff05000fff08000bff040006ff05000dff09000bff040006ff05000dff09000bff040007ff05000cff090027ff0a0025ff0b0025ff0c0023ff0d0023ff0e0021ff10001fff12001dff14001bff160019ff180017ff1b0013ff1f000fff250007ff7f002500", + "width": 48, + "height": 48, + "bytes": 302 + } +} diff --git a/projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png b/projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png new file mode 100644 index 0000000000000000000000000000000000000000..4fb10c552612b1854afb08eededaec72ae81ac08 GIT binary patch literal 998 zcmeAS@N?(olHy`uVBq!ia0vp^1|Tc|Bp8%_R!IOUg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6a&zUu3o6Ss;(S#M9T6{S`YWqlAE#@8c^# z`BR=Qjv*0;--e&}mUa}WoBZ|qu{nEgx~xdj(OFuxA|#^ddrXnQsas)}qZf$3biM5@ zxUt)P|>Nxhg($#PG+rBsY>%M)`Y|i)BRzKGN>E0Lq;QnI4EiAo1 zzRPrIOYf53dBJ*$i|C_GZp(i@nr=OF(pE0!=I4(-b#zobyDvU1+*!0M-l6uJ|5HK1 zs{-~GzptKFRw`mrwLiGUnAzuaf46AtQnuvp2ZF60eH3u&I3I2!*1IdF1;||X^mChO z)iEpG>+5SY;scVE85*uvn;Mxv-xU$2G3EU1T&J5G3@%vzVC(LlbmYdv4?A5oUzeY` zJljZlx|ELo^(zM7E=Rqo64Lgaix>e6k~3fCnzg1T2G5SL#NT^jlTlm7w&_ioaa{iT4x2^S(k<8<^UK%`rk_r`8E&(0 z3k#FRi&&`=dx4p33s7>R)zgnn8xgs zmL?D$&YfP7&@i91+3}a+dLKc-_IB<+HzTWmZu!`DyHYQyplsQyp7&+TgUZ;f^?58k z>V!NusOajLm@+b?Y(AKvmVDMHPl`uqyDJYvRMeutZ>0qSn|AIOvOC;RAFkrU^ROWC z+LqMkf!FjsKR;qyzC1QICoJ~vw{1W7Y^oPznEPbBWc#m5Kk1xOITytXOsoA@PZxjTJ2!&kR6NTo|JGM8 zx$d#+-2Jk4hgI*CZF`ymuDSNjyO$XJo$27OtnI>9nYUEL9N)~fxnH*Zx$lGO?|;9^ z&-*^}pK|=C#ef&SvTTb4il-X{l4=&B{I5hW>!C8<`)MX5lF!N|bK zOxM6%*T6Ewz|hLb#LCc2+rYrez~Gd#Pc4du-29Zxv`X9>cFTP^2-Khfx1l66H?_DV YF}DD>o;TcAL9Sx(boFyt=akR{00GLM&j0`b literal 0 HcmV?d00001 From 65267450b16950562b91dbdf8fadb1fa0b1564ee Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 8 Jul 2026 22:47:47 -0300 Subject: [PATCH 3/7] feat(clearsign): Pioneer compass identity icon (drop the KeepKey K) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The clearsigner is a third-party provider, not KeepKey — a KeepKey mark would conflate the two and undermine the trust model. Replace the test K glyph with a Pioneer compass rose (48x48, 316B). Test loads it under alias 'Pioneer'. --- .../evm-clearsign/clearsign-signer-flows.js | 1 + .../generated/identity-icons.json | 4 ++-- .../generated/protocol-icons.json | 4 ++-- .../clearsign-icons/sources/pioneer.png | Bin 998 -> 1084 bytes 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js b/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js index 6a9e800f..30399462 100644 --- a/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js +++ b/projects/keepkey-sdk/tests/evm-clearsign/clearsign-signer-flows.js @@ -73,6 +73,7 @@ function selfProvider() { }) const signer = { keyId: TEST_KEY_ID, pubkey: CI_TEST_PUBKEY, alias: CI_SIGNER_ALIAS } if (IDENTITY_ICON) { + signer.alias = IDENTITY_ICON.alias || signer.alias // e.g. "Pioneer" (compass logo) signer.icon = IDENTITY_ICON.hex signer.iconWidth = IDENTITY_ICON.width signer.iconHeight = IDENTITY_ICON.height diff --git a/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json b/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json index a2180fea..a5399058 100644 --- a/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json +++ b/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json @@ -1,9 +1,9 @@ { "pioneer": { - "hex": "7f00560007ff25000fff1f0013ff1b0017ff180019ff16001bff14001dff12001fff100021ff0e0009ff040007ff05000aff0d0009ff040006ff05000bff0c000aff040005ff05000dff0b000aff040004ff05000eff0a000bff040003ff050010ff09000bff040002ff040012ff09000bff0400ffff040013ff09000bff090013ff08000cff090014ff07000cff0a0013ff07000cff0500ffff050012ff07000cff040003ff050011ff07000cff040003ff050011ff07000cff040004ff050010ff07000cff040005ff05000fff08000bff040006ff05000dff09000bff040006ff05000dff09000bff040007ff05000cff090027ff0a0025ff0b0025ff0c0023ff0d0023ff0e0021ff10001fff12001dff14001bff160019ff180017ff1b0013ff1f000fff250007ff7f002500", + "hex": "7f00260007ff25000fff1f0013ff1b0017ff18000cffff000cff15000effff000eff12000fffff000fff100010ffff0010ff0f000fff03000fff0e0010ff030010ff0c0011ff030011ff0b0011ff030011ff0a0012ff030012ff090012ff030012ff080012ff050012ff070012ff050012ff070012ff050012ff070012ff050012ff060013ff050013ff05000eff0f000eff050008ff1b0008ff050004ff230004ff050008ff1b0008ff05000eff0f000eff050013ff050013ff060012ff050012ff070012ff050012ff070012ff050012ff070012ff050012ff080012ff030012ff090012ff030012ff0a0011ff030011ff0b0011ff030011ff0c0010ff030010ff0e000fff03000fff0f0010ffff0010ff10000fffff000fff12000effff000eff15000cffff000cff180017ff1b0013ff1f000fff250007ff7400", "width": 48, "height": 48, - "bytes": 302, + "bytes": 316, "alias": "Pioneer" } } diff --git a/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json b/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json index 59a7b134..a3db16ae 100644 --- a/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json +++ b/projects/keepkey-vault/scripts/clearsign-icons/generated/protocol-icons.json @@ -1,8 +1,8 @@ { "pioneer": { - "hex": "7f00560007ff25000fff1f0013ff1b0017ff180019ff16001bff14001dff12001fff100021ff0e0009ff040007ff05000aff0d0009ff040006ff05000bff0c000aff040005ff05000dff0b000aff040004ff05000eff0a000bff040003ff050010ff09000bff040002ff040012ff09000bff0400ffff040013ff09000bff090013ff08000cff090014ff07000cff0a0013ff07000cff0500ffff050012ff07000cff040003ff050011ff07000cff040003ff050011ff07000cff040004ff050010ff07000cff040005ff05000fff08000bff040006ff05000dff09000bff040006ff05000dff09000bff040007ff05000cff090027ff0a0025ff0b0025ff0c0023ff0d0023ff0e0021ff10001fff12001dff14001bff160019ff180017ff1b0013ff1f000fff250007ff7f002500", + "hex": "7f00260007ff25000fff1f0013ff1b0017ff18000cffff000cff15000effff000eff12000fffff000fff100010ffff0010ff0f000fff03000fff0e0010ff030010ff0c0011ff030011ff0b0011ff030011ff0a0012ff030012ff090012ff030012ff080012ff050012ff070012ff050012ff070012ff050012ff070012ff050012ff060013ff050013ff05000eff0f000eff050008ff1b0008ff050004ff230004ff050008ff1b0008ff05000eff0f000eff050013ff050013ff060012ff050012ff070012ff050012ff070012ff050012ff070012ff050012ff080012ff030012ff090012ff030012ff0a0011ff030011ff0b0011ff030011ff0c0010ff030010ff0e000fff03000fff0f0010ffff0010ff10000fffff000fff12000effff000eff15000cffff000cff180017ff1b0013ff1f000fff250007ff7400", "width": 48, "height": 48, - "bytes": 302 + "bytes": 316 } } diff --git a/projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png b/projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png index 4fb10c552612b1854afb08eededaec72ae81ac08..3ad69dce5c57db66f4dd8efb30b08687bd560e8b 100644 GIT binary patch delta 903 zcmV;219<%A2fPT7Xn!g*Wm?A90009ZNkl;KwvRKvQ9d>hksxASXV!D*Va<5=H6z6 z)%R=9Hn;YkoYue7L%8ci7hIR8W^tZWJ<2YWiUdXJ7t2A^v^q1&&*9<<_<1I5NWC1E!z1CQ(fqYn;@rU|DkqZFi0{ZRo2dIl!D{ZXPet@)} z#)w^{kNpNcnST=kz=S~O1h@2OPM9F@<^jBUjK$p3)}P{+=!Ug~Ospm7Jbq4dU9gBe zAyP2qVL58D-Y=2#WiiA6<7 zSd_V85$AF@8sx1J+=LMOVN~{v5w>+wHAD^u>omZ@OMlTNe~GSxmpI?it@;V|;t>_x zb$-E(^Fsrw9{BEH(QkW}ckBPlyW1YD7rxu7A6{eW)DluiNG(RfqCKN6Vb6rc#fXrS zLRw3YkE^Jq?}xQgC7TPR{S$&WkB_n%-u#4+_U8hXta4tIimGN(l`GAO>OtuP0Khr9 zs$8+12Y&z{QngqZ+xWc}U1|T&ZAR3aXQkmsx0Kx3UK@QaWQGA>}<@RR_ z?wOuCf2OD4-k+u0#K%+)LVahj4tiA%`jfc9{%oC(MNbGJ{fjCG)+laZdM}DJB#i6A zO-${k9WwXmJeV#5ijmiiB@NR*jyYH#ETb+4`hOZreUx|B2mh*M(L|d#laq>SQ{BOe zD7>P_)i%;t>On}Zh?)(|eJrxm-HQMq_?2#I=w|nY+P&Cinz4bqsSU}$#Tg&dH%D*R z=H6xa)i-NTH}iXsPZ;?>gg>~N z=`TK>Wjm8Z10oMJGCDOeD=;uRFfeWf#toBo10oMJGCDOeD=;uRFfbojezB9L10@?Y dGCDOeD=;uRFfd3>Uv2;Z002ovPDHLkV1fu;v)upy delta 816 zcmV-01JC@t2<8WnXnzNS*T$T@*+i&fEG(sZ zs8m>T_gr!i%Gy@bTc8)|McYdev4=QSj}mMpil8SiIVf1Of;ALdqXkiHDQ*7%lO`UF zuZJ<3Y<71hSxYZ|r)Bu&`)1yIv-1Yv2LBsGge1kX#_Z)~T8@eC4&^wew72>i+r}^TSJDHNT3Il`Z;_`@yc;??1 zmRCa+ieEr|_D|&z5!DbsIQ!MhBO-DFB|pKmGXqS^kBb#ur3Pd7z*9Qn@eo3Y&sRJa zjk;Wf5JHHi<$v>rHG0N6E!VGnC|^)xBLINcdo?mQ=ek%{D3s5eZ9>_wFwi^r28)Z5 z#Mt5RxkV}Kcg)k9G9#B7EHBs8F!s=pZK`)o4CoZkU}dGYmT^Z$>@!U#%jBh)IrYjQ z9`WFG=&_2B)W8U2Ile-S)Qmyt0200G;HQo+xOf#sbHK&f+iI&D+svW6Ll`gu+pY;_()Az{!0t52(!7wAo$SP05r1nN(P>gfvJ z2Q1z6t++akl(xKrP}W6`p59?o_X5HCYPSP2hX?{q`T8_nH19 zU-Dz3b?fEv=WBQ;-{3E!owTRr_l_Qungb#aGBi3dHY+eNIxsNGLri^>%L5`0GBi3d uHY+eNIxsN19`wPJ`~xK$GBi3dHY+eNIxsNo3)LO~0000 Date: Wed, 8 Jul 2026 23:18:51 -0300 Subject: [PATCH 4/7] chore(clearsign): compass identity icon at 40px (fits confirm column) --- .../generated/identity-icons.json | 8 ++++---- .../clearsign-icons/sources/pioneer.png | Bin 1084 -> 876 bytes 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json b/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json index a5399058..6e451583 100644 --- a/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json +++ b/projects/keepkey-vault/scripts/clearsign-icons/generated/identity-icons.json @@ -1,9 +1,9 @@ { "pioneer": { - "hex": "7f00260007ff25000fff1f0013ff1b0017ff18000cffff000cff15000effff000eff12000fffff000fff100010ffff0010ff0f000fff03000fff0e0010ff030010ff0c0011ff030011ff0b0011ff030011ff0a0012ff030012ff090012ff030012ff080012ff050012ff070012ff050012ff070012ff050012ff070012ff050012ff060013ff050013ff05000eff0f000eff050008ff1b0008ff050004ff230004ff050008ff1b0008ff05000eff0f000eff050013ff050013ff060012ff050012ff070012ff050012ff070012ff050012ff070012ff050012ff080012ff030012ff090012ff030012ff0a0011ff030011ff0b0011ff030011ff0c0010ff030010ff0e000fff03000fff0f0010ffff0010ff10000fffff000fff12000effff000eff15000cffff000cff180017ff1b0013ff1f000fff250007ff7400", - "width": 48, - "height": 48, - "bytes": 316, + "hex": "7f000a0007ff1e000dff190011ff15000affff000aff12000bffff000bff10000cffff000cff0e000dffff000dff0c000dff03000dff0b000dff03000dff0a000eff03000eff09000eff03000eff08000eff05000eff07000eff05000eff07000eff05000eff06000fff05000fff05000bff0d000bff050007ff150007ff050003ff1d0003ff050007ff150007ff05000bff0d000bff05000fff05000fff06000eff05000eff07000eff05000eff07000eff05000eff08000eff03000eff09000eff03000eff0a000dff03000dff0b000dff03000dff0c000dffff000dff0e000cffff000cff10000bffff000bff12000affff000aff150011ff19000dff1e0007ff6000", + "width": 40, + "height": 40, + "bytes": 260, "alias": "Pioneer" } } diff --git a/projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png b/projects/keepkey-vault/scripts/clearsign-icons/sources/pioneer.png index 3ad69dce5c57db66f4dd8efb30b08687bd560e8b..5e28dfcb6ef94b03ecd008e4c7458f9a03ce34e8 100644 GIT binary patch delta 728 zcmV;}0w?{v2WD< zGJcEz00K}+L_t(Y$L&@@Yt%p#9t;!~)+mxgE{1N<%|X|HP}twU(|z8*Ns?n zT-ZyFEwb0#1&=wkw{1Kvi>62gyDF7N5OlI$QYhnLtUF2OWz)I`4}LI&nfd0+n>X*x z8-)HN4dps+yGG!UI!#DDuw5N@e-!yrQc?rk;RzKY9OLKGy$i{iBl?pTe3z4x8Z?3Ef055Pkq-t@ zBHhhwxz$1o&`}*cOn(JfXx-0@gkRwpc77+DBg35VV@ggbpi-)L8B6aLv6Lz&h&wPb zS)THeWtfMbaXz$s$T_DggUPfvn4GRaOzS1r3G8g3ME>rgBcE@Uhu(GM-Nney22rrd zkw}eFO6fRm4vp;{tA|R#f9h@T42`(St_^aNn_`z$IT;a{jq|qf4)4RqbMWC7M{;X$ z(CjTud3#H6;JCLqLHu+MarlH2RH{WeP%=}lr92ZJ!PHW@{F!n7+%|T<;52WfzarQ# zG6PbB66o(G?n5G|(|>hikA!A>-=GCn41;kXDyhfMrCiYl!p2T2o_0Uw@_@XDTF=5VC$Bo38>BVu&&wI-Fdr;heE&0 z?9P1edvE5=Pk{eGU@=0nPCB}We_!}mS3h&t)>5wK-e!c=_iN8KxAvZ#*1yw3xa&k0 zT$iV2ah_B?$}W_O1V!l=%R$t%Iy1`8;o=JTc_wU1KKM;XNdh2}c2jV}@VLj?^&+ig z0V-L&)>x~7d{~_Ehx$X23jpK-`t9)tsEb%DZLHINfV7{+h+U+Q{RTame-i@0gh1y6 zxAbRDm>}@x0layP#oW`@pW>J3hP8xDtR?6?eok{;u!uY%Qb=Y>A&ki`Y0RF|Ps~h6 z$y_0YF-tqjY-&;*xIfz3uqpiMb7QFw+ZA`_SQXcaMMX$hl(}IM=W;h1iLQf}IN#B&`U&;o5f$8Ze!-3NLj$TF`0ilQZ+n(^ z>;KEU+a9bJzT2uFUSsLh5>iM=Ek?qkJ)(&(T1$_QtEi>#hqY2An+v4< z6M{F7kFpxx{DhG9=K__ia$b~*s%BD^E6s`OLFoekz&W|9T(O-8e*hp-wOAP2_`Mcg zY5&k|M%0^UrQt`nnyⅇP}Dly}w#|EgrsM4LF1lZt9n-NA||yrRd|Hquz?K}fEMnhnf- zEV9$xivS?_m2PV2X7`2Kz1U@%v4Oj(4avX786VR(M{n2ThA!~23wi>``{0t{_nPi4 zTcW&hvIfNuXH*!w3ah~E*zcJ6qJf=@&vC;I3*uLz;)=0bDcxY^l>-# z(vLW94nOz#U4u-()>^LS-evgJH)~Hf^Lvj^82LYhKe(FdFFu}SJCn)+A`dh&IyEsX zFfckWFm46L4U_u Date: Wed, 8 Jul 2026 23:34:30 -0300 Subject: [PATCH 5/7] docs: per-feature test handoffs for the 2026-07-08 session One handoff per NEW feature/fix (persistent identities, clearsign allowlist, emulator dead-click/capture/confirm-button, idle-timeout, icon tooling, SDK tests, perf telemetry, RUJI icon, rujira failover, emu-ABI graft) + an INDEX with test order, env prereqs, and open items. --- .../01-persistent-identity-icons.md | 43 +++++++++++++++++++ .../02-clearsign-allowlist-loadsigner.md | 31 +++++++++++++ docs/handoff-testing/03-emulator-deadclick.md | 27 ++++++++++++ .../04-emulator-capture-seedreveal.md | 33 ++++++++++++++ .../05-loadsigner-confirm-button.md | 25 +++++++++++ .../06-idletimeout-consecutive-sign.md | 30 +++++++++++++ .../07-icon-encoder-tooling.md | 32 ++++++++++++++ .../handoff-testing/08-sdk-clearsign-tests.md | 28 ++++++++++++ docs/handoff-testing/09-perf-telemetry.md | 32 ++++++++++++++ .../handoff-testing/10-ruji-dashboard-icon.md | 24 +++++++++++ .../11-rujira-thornode-failover.md | 30 +++++++++++++ docs/handoff-testing/12-emulator-abi-graft.md | 34 +++++++++++++++ docs/handoff-testing/INDEX.md | 38 ++++++++++++++++ 13 files changed, 407 insertions(+) create mode 100644 docs/handoff-testing/01-persistent-identity-icons.md create mode 100644 docs/handoff-testing/02-clearsign-allowlist-loadsigner.md create mode 100644 docs/handoff-testing/03-emulator-deadclick.md create mode 100644 docs/handoff-testing/04-emulator-capture-seedreveal.md create mode 100644 docs/handoff-testing/05-loadsigner-confirm-button.md create mode 100644 docs/handoff-testing/06-idletimeout-consecutive-sign.md create mode 100644 docs/handoff-testing/07-icon-encoder-tooling.md create mode 100644 docs/handoff-testing/08-sdk-clearsign-tests.md create mode 100644 docs/handoff-testing/09-perf-telemetry.md create mode 100644 docs/handoff-testing/10-ruji-dashboard-icon.md create mode 100644 docs/handoff-testing/11-rujira-thornode-failover.md create mode 100644 docs/handoff-testing/12-emulator-abi-graft.md create mode 100644 docs/handoff-testing/INDEX.md diff --git a/docs/handoff-testing/01-persistent-identity-icons.md b/docs/handoff-testing/01-persistent-identity-icons.md new file mode 100644 index 00000000..f13780c6 --- /dev/null +++ b/docs/handoff-testing/01-persistent-identity-icons.md @@ -0,0 +1,43 @@ +# 01 — Persistent clear-sign identities + logo render + +**What:** the "KeepKey + identity" model. A clear-sign signer loaded with +`persist=true` survives reboot (flash storage, `STORAGE_VERSION 17→18`), and +every clear-sign now **leads with the identity's logo + alias + fingerprint** +instead of the "NOT verified by KeepKey" banner — on both the load-consent +screen and every per-tx confirm. + +**Where:** +- firmware `release/7.15.0-rc7` (`BitHighlander/keepkey-firmware#299`, OPEN) — + `lib/firmware/storage.{c,h}`, `signed_metadata.c`, `fsm_msg_ethereum.h`, + `lib/board/layout.c`, `include/keepkey/board/layout.h`. +- hdwallet `feat/clearsign-signer-icon` (`keepkey/hdwallet#53`, OPEN). +- vault `feat/clearsign-identity-icons-vault` (`keepkey/keepkey-vault#342`, OPEN). + +## Test (emulator, fastest) +1. Emulator dylib from rc7 (handoff 12), reloaded in Vault; Vault has #342. +2. `cd /Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-sdk` + `KEEPKEY_API_KEY=… node tests/evm-clearsign/clearsign-signer-flows.js` + (sends the Pioneer compass icon + `persist:true`). + +## Verify +- [ ] **Load screen** shows the compass logo, full "LOAD CLEARSIGNER" title, + unclipped "Trust 'Pioneer' (fp)…" body (no first-letter clipping). +- [ ] **Per-tx confirm** leads with the same logo + "Identity" + alias + fp, + then decoded pages, **no raw hex**, no "NOT verified by KeepKey" scare. +- [ ] **Persistence:** load with `persist:true` → reboot the emulator → drive a + clear-sign for the SAME `key_id` **without** re-loading → it still verifies + and renders (reads the flash slot). WipeDevice clears it. +- [ ] **Migration (brick-risk):** seed an emulator on the OLD (v17) dylib, then + swap to rc7 → wallet loads, keys/label/policies intact, identities empty + (no data loss, no reset). +- [ ] Load an icon > 384 B or dims > 64 → device rejects at load (validated). +- [ ] Load `persist:true` into all persistent slots, then one more → honest + "No free persistent identity slot" failure (not silent RAM-only). + +## Status / gotchas +- Firmware unit tests 75/75 pass (storage round-trip + migration + clamp). +- Icon column is 40px; identity icons generated at 40px + centered (handoff 07). +- device-protocol pinned to fork `33521a8` (icon fields) — **upstream-pin swap + is the final-release gate**, not the rc. +- Boot click-through review of trusted identities = deferred (Stage 3). + diff --git a/docs/handoff-testing/02-clearsign-allowlist-loadsigner.md b/docs/handoff-testing/02-clearsign-allowlist-loadsigner.md new file mode 100644 index 00000000..344441c5 --- /dev/null +++ b/docs/handoff-testing/02-clearsign-allowlist-loadsigner.md @@ -0,0 +1,31 @@ +# 02 — Clearsign vendored allowlist + LoadClearsignSigner endpoint + +**What:** Pioneer's `/descriptors/decode` + `/descriptors/sign` are 404 in prod, +so the vault stopped calling them. `needsBlindSigning` is now keyed off a +**vendored `firmwareClearSigns` allowlist** that mirrors the device's own +`ethereum_contractHandled` pins (THOR/Maya router deposit, 0x proxy, standard +ERC-20 transfer/approve, addLiquidityETH). Also adds the +`POST /eth/clearsign/load-signer` REST route. + +**Where:** vault `#337` (MERGED, develop) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/src/bun/calldata-decoder.ts`, +`rest-api.ts`, `SigningApproval.tsx`, `engine-controller.ts`, `FirmwareDropZone.tsx`. + +## Test +- Unit: `cd /Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault && bun test __tests__/firmware-clearsign-gate.test.ts` (7/7 — spoof guard, wrong-address rejection, ERC-20 path). +- Device (rc7): drive real EVM txs and watch the trust badge / blind-sign gate. + +## Verify +- [ ] THOR/Maya router **deposit** → clear-signs (trust badge "known"). +- [ ] Same deposit selector to a **different** address → blind-signs ("unknown"). +- [ ] Standard 68-byte ERC-20 transfer/approve → clear-signs. +- [ ] Uniswap/1inch/relay (firmware-unknown) → blind-signs, **not** falsely "known". +- [ ] `FirmwareDropZone`: flashing against a **wallet-mode** device is blocked + until it reports `bootloaderMode=true` (no stalled HID freeze). + +## Status / gotchas +- MERGED to develop but **allowlist was never device-verified** — confirm on rc7 + that the vendored pins actually match what the firmware clear-signs. +- Supersedes the old `EVM_INSIGHT` flag-gated Pioneer path (removed in the #342 + merge reconciliation). + diff --git a/docs/handoff-testing/03-emulator-deadclick.md b/docs/handoff-testing/03-emulator-deadclick.md new file mode 100644 index 00000000..6f1ece60 --- /dev/null +++ b/docs/handoff-testing/03-emulator-deadclick.md @@ -0,0 +1,27 @@ +# 03 — Emulator never-dead-click + +**What:** `initEmulator()` caught every dylib-load failure and returned a normal +`EmulatorStatus{state:'error'}` object; the RPC handlers forwarded that as a +*successful* response, so the UI silently reverted (a dead click, real error +stranded in console). Now the handlers throw and the UI surfaces the error. + +**Where:** vault `#338` (MERGED, develop) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/src/bun/index.ts` +(`emulatorInit`/`emulatorSwitchWallet`/`emulatorImportWallet` throw), +`src/mainview/components/DeviceGrid.tsx` (`handleStartEmu` surfaces any +non-running result). + +## Test (needs a FAILING emulator to see the value) +1. Install a **stale/incompatible** dylib (e.g. one missing `kkemu_start`) at + `~/.keepkey/emulator/libkkemu.dylib`. +2. Click **Start** on the emulator card in Vault. + +## Verify +- [ ] A red error banner appears with the real reason (e.g. `Symbol "kkemu_start" + not found` / `kkemu_init returned N`) — **not** a silent revert. +- [ ] With a good dylib, Start still boots the emulator normally. + +## Status / gotchas +- This is exactly the bug that masked the earlier `kkemu_start` mismatch — + before the fix it just dead-clicked. + diff --git a/docs/handoff-testing/04-emulator-capture-seedreveal.md b/docs/handoff-testing/04-emulator-capture-seedreveal.md new file mode 100644 index 00000000..9d82e4c1 --- /dev/null +++ b/docs/handoff-testing/04-emulator-capture-seedreveal.md @@ -0,0 +1,33 @@ +# 04 — Emulator capture-frame + seed-reveal/version tooling + +**What:** emulator-only dev tooling — grab the live OLED as a PNG (visual proof +for automated test drivers) and reveal the active flash's saved seed / show +version controls. + +**Where:** vault `#339` (MERGED, develop) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/src/bun/emulator-window.ts` +(`captureCurrentFrame` + `/_emu/capture` bridge), `index.ts` +(`emulatorCaptureFrame`, `emulatorRevealSeed` RPCs), `rest-api.ts` +(`POST /emulator/capture`, auth'd, `engine.isEmulator`-gated), +`DeviceSettingsDrawer.tsx`. + +## Test +- Capture (used already this session to verify the identity logo): + `POST http://localhost:1646/emulator/capture` with a bearer token → returns + `{ dataUrl }` (PNG). Example driver: + `projects/keepkey-sdk/tests/evm-clearsign/_capture-load.js` pattern (removed; + reconstruct from git if needed). +- Seed reveal: DeviceSettingsDrawer → reveal seed (emulator wallet only). + +## Verify +- [ ] `POST /emulator/capture` returns a valid PNG data URL of the current OLED. +- [ ] `emulatorRevealSeed` shows the active flash's saved mnemonic; **rejects on + a real device** (`engine.isEmulator` gate). +- [ ] Version/seed controls in DeviceSettingsDrawer work; the two prior + race-condition fixes hold (stale revealed seed on wallet-switch/wipe; + concurrent mutating actions mutually exclusive). + +## Status / gotchas +- REST capture route is registered manually (not in tsoa swagger) — probe it, + don't expect it in `/spec/swagger.json`. + diff --git a/docs/handoff-testing/05-loadsigner-confirm-button.md b/docs/handoff-testing/05-loadsigner-confirm-button.md new file mode 100644 index 00000000..fca75f21 --- /dev/null +++ b/docs/handoff-testing/05-loadsigner-confirm-button.md @@ -0,0 +1,25 @@ +# 05 — LoadClearsignSigner emulator confirm button + +**What:** the `/eth/clearsign/load-signer` route called `loadClearsignSigner()` +directly, unlike every other signing route which wraps in `emuWrap()`. Loading a +signer raises a mandatory on-device "Trust identity" confirm — on the emulator +that button press must be armed via `emuWrap`, or the OLED shows the trust screen +but **no green approve button ever appears and the call hangs**. Now wrapped. +`emuWrap` is a no-op on real hardware. + +**Where:** vault `#341` (MERGED, develop) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/src/bun/rest-api.ts` +(the `/eth/clearsign/load-signer` handler now `emuWrap(() => wallet.loadClearsignSigner(...))`). + +## Test +- Emulator + rc7: run `clearsign-signer-flows.js` (handoff 01/08). The load step + hits this path. + +## Verify +- [ ] On the emulator, the "Trust identity 'Pioneer'" load screen shows a + clickable **green approve button** (armed) — the load does not hang. +- [ ] Real device (rc7): physical button confirms the load as before. + +## Status / gotchas +- Prerequisite for testing handoff 01 on the emulator (without it the load hangs). + diff --git a/docs/handoff-testing/06-idletimeout-consecutive-sign.md b/docs/handoff-testing/06-idletimeout-consecutive-sign.md new file mode 100644 index 00000000..b30bb9cf --- /dev/null +++ b/docs/handoff-testing/06-idletimeout-consecutive-sign.md @@ -0,0 +1,30 @@ +# 06 — Signing idle-timeout fix (consecutive signs) + +**What:** `Bun.serve` had no `idleTimeout` → default 120s. A human-gated sign +blocks the response while the user confirms; on the SECOND consecutive sign the +wait exceeded the socket idle window, Bun closed the connection +(`other side closed`), aborting the in-flight sign mid-confirm (device returned +home). Set `idleTimeout: 0` — device signing must not time out at the socket. + +**Where:** vault `#342` (OPEN) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/src/bun/rest-api.ts` +(`Bun.serve({ idleTimeout: 0, … })`). Test helper: `CLEARSIGN_FLOW=` runs a +single flow in `clearsign-signer-flows.js`. + +## Test +- Emulator + rc7, Vault with #342: run the FULL 3-flow suite (no `CLEARSIGN_FLOW`): + `node tests/evm-clearsign/clearsign-signer-flows.js`. + +## Verify +- [ ] All **3 consecutive** flows (aave-supply, erc20-transfer, + erc20-approve-unlimited) sign in one run — the 2nd no longer hangs / + `other side closed`. +- [ ] Each single flow via `CLEARSIGN_FLOW=` also passes (already confirmed + erc20-transfer + approve-unlimited individually this session). + +## Status / gotchas +- **Root cause vs symptom:** idle-timeout removes the socket-close *symptom*. If + the 2nd sign STILL hangs (no close, just stuck), there's a deeper emulator + poll-thread/transport state bug — that was the original "busy/queue" report and + is NOT yet root-caused. Confirm 3-in-a-row genuinely completes before closing. + diff --git a/docs/handoff-testing/07-icon-encoder-tooling.md b/docs/handoff-testing/07-icon-encoder-tooling.md new file mode 100644 index 00000000..7a920c5d --- /dev/null +++ b/docs/handoff-testing/07-icon-encoder-tooling.md @@ -0,0 +1,32 @@ +# 07 — Clear-sign icon encoder + catalog tooling + +**What:** turn any protocol/identity logo into the KeepKey 1bpp mono-RLE bitmap +the OLED renders (`draw_bitmap_mono_rle`), ≤384 B, ≤64×64. The encoder owns only +the RLE codec (must match the firmware decoder byte-for-byte) and self-verifies +every output by round-tripping through a decoder mirror; ImageMagick does the +resize/center/threshold. + +**Where:** vault `#342` (OPEN) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/scripts/clearsign-icons/` +(`encode-icon.mjs`, `build-catalog.mjs`, `README.md`, `sources/`, `generated/`). + +## Test +- Codec self-test: `cd .../scripts/clearsign-icons && bun encode-icon.mjs --self-test`. +- One image: `bun encode-icon.mjs sources/pioneer.png --json`. +- Batch: `bun build-catalog.mjs` (encodes `sources/*` → `generated/protocol-icons.json`). +- ASCII-preview to eyeball a shape: see the `rleDecode` one-liner in the README. + +## Verify +- [ ] `--self-test` prints the exact expected byte stream + round-trip OK. +- [ ] Pioneer compass encodes ≤384 B at 40×40 and previews as a clean compass. +- [ ] End-to-end: the generated `identity-icons.json` icon renders correctly on + the device (covered by handoff 01) — proves the encoder matches firmware. +- [ ] An oversized/detailed logo fails with a clear ">384B" message (not silent). + +## Status / gotchas +- Confirm-column width is 40px → generate confirm icons at **40px** (not the + handoff's aspirational 48). Boot-review (bigger) can use larger later. +- To add a real protocol logo: drop `sources/.png`, run `build-catalog.mjs`. +- Per-tx **protocol** glyphs (Aave/Uniswap beside the method) are a separate + transient path — tooling produces them, wiring is a follow-up. + diff --git a/docs/handoff-testing/08-sdk-clearsign-tests.md b/docs/handoff-testing/08-sdk-clearsign-tests.md new file mode 100644 index 00000000..884e94d9 --- /dev/null +++ b/docs/handoff-testing/08-sdk-clearsign-tests.md @@ -0,0 +1,28 @@ +# 08 — keepkey-sdk clear-sign test train + +**What:** the runtime-signer clear-sign test suite (recovered from untracked +files): shared blob builder + offline parity gate + on-device flows. + +**Where:** vault `#340` (MERGED, develop) — +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-sdk/tests/` +(`_clearsign.js`, `clearsign-offline-parity.js`, +`evm-clearsign/{clearsign-signer-flows,loadsigner-sign-flows,relay-v2-schema-flow}.js`, +`fixtures/clearsign-golden.json`). + +## Test +- Offline (no device): `cd .../projects/keepkey-sdk && node tests/clearsign-offline-parity.js`. +- On-device (rc7): `KEEPKEY_API_KEY=… node tests/evm-clearsign/clearsign-signer-flows.js` + (self mode; `CLEARSIGN_MODE=pioneer` mode is dead until Pioneer /sign returns). + +## Verify +- [ ] Offline parity: **51/51** catalog flows match python-keepkey @1545299 + (blob sha256+len + JS sighash) — already green this session. +- [ ] On-device self mode: load signer → 3 flows sign, no raw hex (ties into + handoffs 01/06). + +## Status / gotchas +- `CLEARSIGN_MODE=pioneer` calls `/api/v1/descriptors/sign` which is 404 in prod + — only `self` mode is viable now. +- The test now sends the Pioneer identity icon + `persist:true` (handoff 01/07); + `CLEARSIGN_NO_ICON=1` for the text-only path. + diff --git a/docs/handoff-testing/09-perf-telemetry.md b/docs/handoff-testing/09-perf-telemetry.md new file mode 100644 index 00000000..0c4dc049 --- /dev/null +++ b/docs/handoff-testing/09-perf-telemetry.md @@ -0,0 +1,32 @@ +# 09 — Vault↔API perf telemetry + +**What:** split perceived-slow portfolio loads into API vs network/client time. +Server stamps `serverMs`/`traceId` on `POST /portfolio` and exposes +`POST /api/v1/telemetry/vault`; the vault times every `GetPortfolioBalances` +(wrapped once on the pioneer client singleton) and posts +`{traceId, serverMs, clientTotalMs, outcome, …}`; backend computes +`networkMs = clientTotalMs − serverMs`. + +**Where:** +- pioneer `#164` (MERGED) — server stamp + ingest. +- vault `#334` (MERGED, develop) — + `/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/projects/keepkey-vault/src/bun/perf-telemetry.ts` + + `pioneer.ts` hook; tests `__tests__/perf-telemetry.test.ts`. + +## Test +- Unit: `cd .../projects/keepkey-vault && bun test __tests__/perf-telemetry.test.ts` (11/11). +- Data-flow: run a Vault build with #334 against a pioneer that has #164 deployed + with the `/api/v1/telemetry/vault` route live; load a portfolio a few times. + +## Verify +- [ ] Records batch + flush to `POST /api/v1/telemetry/vault` (fire-and-forget — + a failed flush must never block/error a load). +- [ ] Backend joins on `traceId` and reports `networkMs`/`serverMs` split. +- [ ] Outcome classification: ok / slow (>3s) / degraded / timeout / error. +- [ ] `appVersion` matches the shipped build; endpoint no-ops gracefully until + the pioneer ingest is deployed. + +## Status / gotchas +- Both halves merged; the **pioneer prod deploy of the ingest endpoint** + a + dashboard read endpoint are the remaining pieces. + diff --git a/docs/handoff-testing/10-ruji-dashboard-icon.md b/docs/handoff-testing/10-ruji-dashboard-icon.md new file mode 100644 index 00000000..b633641f --- /dev/null +++ b/docs/handoff-testing/10-ruji-dashboard-icon.md @@ -0,0 +1,24 @@ +# 10 — RUJI dashboard icon (CDN) + +**What:** RUJI showed a broken "?" icon on the dashboard. Two causes: (1) the +icon was never uploaded to the coin-icon CDN, and (2) `assetData.json` keyed RUJI +under the old CAIP (`cosmos:thorchain-1/slip44:ruji`) while the runtime CAIP is +`cosmos:thorchain-mainnet-v1/denom:x/ruji`. Uploaded `ruji.png` to DO Spaces +under **both** base64 keys (200 now). + +**Where:** CDN only (no code change). Source PNG: +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-website-v7/public/images/chains/ruji.png`. +CDN keys: `coins/Y29zbW9zOnRob3JjaGFpbi1tYWlubmV0LXYxL2Rlbm9tOngvcnVqaQ.png` + +`coins/Y29zbW9zOnRob3JjaGFpbi0xL3NsaXA0NDpydWpp.png`. + +## Test / Verify +- [ ] `curl -sIL https://api.keepkey.info/coins/Y29zbW9zOnRob3JjaGFpbi1tYWlubmV0LXYxL2Rlbm9tOngvcnVqaQ.png` + → 200 image/png (both keys). +- [ ] RUJI shows its real logo on the Vault dashboard (no "?" box) — next load, + no rebuild needed. + +## Status / gotchas +- Dashboard `` tags have no `onError` fallback (unlike `AssetIcon.tsx`) → + a missing CDN icon renders the browser's raw broken-image glyph. Separate + follow-up if you want a graceful letter-bubble there too. + diff --git a/docs/handoff-testing/11-rujira-thornode-failover.md b/docs/handoff-testing/11-rujira-thornode-failover.md new file mode 100644 index 00000000..ff0c223d --- /dev/null +++ b/docs/handoff-testing/11-rujira-thornode-failover.md @@ -0,0 +1,30 @@ +# 11 — Rujira THORNode failover + +**What:** `intergrations/rujira/src/index.ts` hardcoded a single THORNode host +(`thornode.thorchain.liquify.com`) for FIN contract smart-queries. That host is +dead (connection refused) with no fallback → **every** Rujira FIN quote (RUJI, +TCY swaps) failed. Added the same 2-host failover the main `thorchain` +integration already uses. + +**Where:** pioneer `#165` (MERGED) — +`.../projects/pioneer/modules/intergrations/rujira/src/index.ts` +(`THORNODE_HOSTS = [thornode.thorchain.network, rest.cosmos.directory/thorchain]`, +`smartQuery` loops hosts). + +## Test +- Needs a pioneer build/deploy with #165 (`make start` in the pioneer monorepo), + then a RUJI or TCY swap quote through the vault. + +## Verify +- [ ] RUJI → (USDC/BTC) and TCY swap **quotes return** (no "Unable to fetch + pools" / empty quote from the FIN path). +- [ ] Both failover hosts serve the cosmwasm smart-query (verified live this + session: `thornode.thorchain.network` returns out-of-gas simulate, + `rest.cosmos.directory/thorchain` returns `{returned,fee}`). + +## Status / gotchas +- The "thorchain: Unable to fetch pools" you saw may have been this FIN-path + failure surfacing under a generic label (prod's main `/thorchain/pools` tested + healthy). Retry a RUJI swap after the pioneer deploy to confirm. +- Rujira FIN is gated behind `FEATURE_RUJIRA_SWAPS` (api-blue only). + diff --git a/docs/handoff-testing/12-emulator-abi-graft.md b/docs/handoff-testing/12-emulator-abi-graft.md new file mode 100644 index 00000000..669de278 --- /dev/null +++ b/docs/handoff-testing/12-emulator-abi-graft.md @@ -0,0 +1,34 @@ +# 12 — Firmware emulator-ABI graft (testable dylib) + +**What:** the clear-sign firmware line (RC6) and the emulator-dylib ABI line +(`kkemu_start`, poll thread) had **diverged** — neither branch had both, so an +emulator built from the clear-sign branch was missing `kkemu_start` (dead click, +handoff 03). Cherry-picked the 4 emulator-ABI commits (#249–252) onto the +clear-sign branch so the feature is emulator-testable. Those commits touch +`lib/emulator/*` only (no-op for device firmware). + +**Where:** firmware `feat/clearsign-persistent-identity-icons` / +`release/7.15.0-rc7` (`BitHighlander/keepkey-firmware`). Submodule at +`/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/modules/keepkey-firmware`. + +## Test / build +``` +cd /Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11/modules/keepkey-firmware +git checkout release/7.15.0-rc7 # (or feat/clearsign-persistent-identity-icons) +cd /Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11 +make build-emulator # installs ~/.keepkey/emulator/libkkemu.dylib +``` +then **reload the emulator in Vault**. + +## Verify +- [ ] `nm -gU ~/.keepkey/emulator/libkkemu.dylib | grep kkemu_start` → present + (all 12 `kkemu_*` FFI symbols the vault requires). +- [ ] Emulator boots in Vault; clear-sign firmware (`signed_metadata.c`) is + compiled in (handoffs 01/05 exercise it). + +## Status / gotchas +- This is the **prerequisite** for emulator-testing handoffs 01/03/04/05. +- The device firmware build ignores these commits (emulator-only files); they're + cleanly separable if the eventual PR wants the feature without the emu graft. +- device-protocol pinned to fork `33521a8` (msg-117 icon fields). + diff --git a/docs/handoff-testing/INDEX.md b/docs/handoff-testing/INDEX.md new file mode 100644 index 00000000..b3439a8a --- /dev/null +++ b/docs/handoff-testing/INDEX.md @@ -0,0 +1,38 @@ +# New-feature test handoffs (session 2026-07-08) + +Every NEW feature/fix from this session, each with its own handoff below. Test +in roughly this order (device-independent first, then emulator, then swaps). +Full paths per repo convention. + +| # | Feature | Repo / PR | Merged? | Needs | +|---|---------|-----------|---------|-------| +| 01 | Persistent clear-sign identities + logo render | firmware #299, hdwallet #53, vault #342 | all OPEN (rc7) | emulator + device | +| 02 | Clearsign vendored allowlist + LoadClearsignSigner endpoint | vault #337 | merged | device (blind-sign gating) | +| 03 | Emulator never-dead-click | vault #338 | merged | emulator (failure path) | +| 04 | Emulator capture-frame + seed-reveal/version tooling | vault #339 | merged | emulator | +| 05 | LoadClearsignSigner emulator confirm button | vault #341 | merged | emulator | +| 06 | Signing idle-timeout fix (consecutive signs) | vault #342 | OPEN | emulator (3 consecutive signs) | +| 07 | Clear-sign icon encoder + catalog tooling | vault #342 | OPEN | local (self-test) + emulator | +| 08 | keepkey-sdk clear-sign test train | vault #340 | merged | local (offline) + device | +| 09 | Vault↔API perf telemetry | vault #334, pioneer #164 | merged | deploy + data-flow | +| 10 | RUJI dashboard icon (CDN) | CDN upload | live | dashboard eyeball | +| 11 | Rujira THORNode failover | pioneer #165 | merged | RUJI/TCY swap quote | +| 12 | Firmware emulator-ABI graft (testable dylib) | firmware feat/clearsign-plus-emu-dylib | rc7 branch | prerequisite for 01/03/04/05 | + +## Environment prerequisites +- Emulator dylib: `~/.keepkey/emulator/libkkemu.dylib` must be built from a + firmware branch that has **both** clear-sign AND the emu ABI (`kkemu_start`). + That is `feat/clearsign-persistent-identity-icons` / `release/7.15.0-rc7` + (handoff 12). Rebuild: `cd /Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11 && make build-emulator`, then **reload the emulator in Vault**. +- Vault on :1646 built from a checkout that has vault #342 (icon route + + idle-timeout). `make vault` from + `/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-vault-v11`. +- Real device: a `release/7.15.0-rc7` DEBUG_LINK build for the automated + python/SDK suites (they wipe + load the public test seed — never real funds). + +## Known open items (not per-feature) +- Consecutive-sign root cause: idle-timeout (06) removes the socket-close + symptom; confirm 3 signs in a row actually complete before calling it done. +- Firmware #299 SOP gate: CI green + device-protocol **upstream**-pin swap + before the final release (rc is fork-pinned). + From 1640b147577e52770e4d32d76a65f5f71dadd70e Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 9 Jul 2026 00:45:41 -0300 Subject: [PATCH 6/7] chore(hdwallet): pin to master eccc3fa6 (LoadClearsignSigner icon #53); add fw clearsign-gate test --- modules/hdwallet | 2 +- .../__tests__/firmware-clearsign-gate.test.ts | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 projects/keepkey-vault/__tests__/firmware-clearsign-gate.test.ts diff --git a/modules/hdwallet b/modules/hdwallet index 1ae8ea07..eccc3fa6 160000 --- a/modules/hdwallet +++ b/modules/hdwallet @@ -1 +1 @@ -Subproject commit 1ae8ea0724719e34611f57583b103f2a3ac73a73 +Subproject commit eccc3fa680d6ad79d1f4799f3860679697631551 diff --git a/projects/keepkey-vault/__tests__/firmware-clearsign-gate.test.ts b/projects/keepkey-vault/__tests__/firmware-clearsign-gate.test.ts new file mode 100644 index 00000000..27531eb6 --- /dev/null +++ b/projects/keepkey-vault/__tests__/firmware-clearsign-gate.test.ts @@ -0,0 +1,64 @@ +/** + * firmwareClearSigns allowlist guard (pure unit — no device, no network). + * + * `firmwareClearSigns` decides whether the Vault overlay forces AdvancedMode: + * it must mirror the rc3 device's `ethereum_contractHandled` + native ERC-20 + * path EXACTLY. Two failure directions this locks down: + * - under-warn: claiming a tx is clear-signed when the device blind-signs it + * - over-force: forcing global AdvancedMode on a tx the device clear-signs + * natively (re-opens the drain vector PR #261/#303 closed). + * + * Run: cd projects/keepkey-vault && bun test __tests__/firmware-clearsign-gate.test.ts + */ +import { describe, test, expect } from 'bun:test' +import { firmwareClearSigns } from '../src/bun/calldata-decoder' + +const ZX = '0xdef1c0ded9bec7f1a1670819833240f027b25eff' +const UNIV2 = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d' +const THOR = '0xd37bbe5744d730a1d98d8dc97c42f0ca46ad7146' +const MAYA = '0xd89dce570de35a6f42d3bca7dba50a6d89bfc2a2' +const RANDOM = '0x1111111111111111111111111111111111111111' +const word = '00'.repeat(32) +const erc20Transfer = '0xa9059cbb' + word + word // 68-byte standard transfer +const erc20Approve = '0x095ea7b3' + word + word + +describe('firmwareClearSigns mirrors the rc3 native clear-sign allowlist', () => { + test('THORChain/Maya deposit to the pinned router → clear-signs', () => { + expect(firmwareClearSigns(THOR, '0x1fece7b4' + word)).toBe(true) + expect(firmwareClearSigns(THOR, '0x44bc937b' + word)).toBe(true) + expect(firmwareClearSigns(MAYA, '0x1fece7b4' + word)).toBe(true) + }) + + test('deposit selector to a DIFFERENT address → blind (spoof guard)', () => { + expect(firmwareClearSigns(RANDOM, '0x1fece7b4' + word)).toBe(false) + }) + + test('0x proxy methods only at the pinned ExchangeProxy/router', () => { + expect(firmwareClearSigns(ZX, '0x415565b0' + word)).toBe(true) // transformERC20 + expect(firmwareClearSigns(ZX, '0xd9627aa4' + word)).toBe(true) // sellToUniswap + expect(firmwareClearSigns(UNIV2, '0xf305d719' + word)).toBe(true) // addLiquidityETH + expect(firmwareClearSigns(RANDOM, '0x415565b0' + word)).toBe(false) + expect(firmwareClearSigns(UNIV2, '0x415565b0' + word)).toBe(false) // right selector, wrong pin + }) + + test('standard 68-byte ERC-20 transfer/approve → clear-signs (any token addr)', () => { + expect(firmwareClearSigns(RANDOM, erc20Transfer)).toBe(true) + expect(firmwareClearSigns(RANDOM, erc20Approve)).toBe(true) + }) + + test('non-standard-length transfer selector → blind (not the 68-byte path)', () => { + expect(firmwareClearSigns(RANDOM, '0xa9059cbb' + word)).toBe(false) // 36-byte, malformed + }) + + test('firmware-unknown contracts → blind (Uniswap / 1inch / relay)', () => { + expect(firmwareClearSigns(UNIV2, '0x38ed1739' + word)).toBe(false) // swapExactTokensForTokens + expect(firmwareClearSigns(RANDOM, '0x12aa3caf' + word)).toBe(false) // 1inch + expect(firmwareClearSigns(RANDOM, '0xdeadbeef' + word)).toBe(false) // relay/opaque + }) + + test('empty / missing calldata → blind', () => { + expect(firmwareClearSigns(THOR, '0x')).toBe(false) + expect(firmwareClearSigns(undefined, erc20Transfer)).toBe(false) + expect(firmwareClearSigns(THOR, undefined)).toBe(false) + }) +}) From b4e611347999d852c0e31f0cb317bf6590e7ddf0 Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 11 Jul 2026 23:09:48 -0300 Subject: [PATCH 7/7] =?UTF-8?q?fix(clearsign):=20address=20#342=20review?= =?UTF-8?q?=20=E2=80=94=20encoder=200x80=20overflow,=20scoped=20idle-timeo?= =?UTF-8?q?ut,=20icon=20coupling,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - encode-icon: cap literal packets at 127 (128 emits control 0x80/-128 which overflows firmware int8 count and trips draw.c assertion). +boundary self-test. - rest-api: drop server-wide idleTimeout:0; disable idle timeout per-request only for signing + clear-sign trust routes via server.timeout(req, 0). - schemas: refine LoadClearsignSignerRequest so icon/iconWidth/iconHeight are all-present-or-all-absent; SDK type documents the coupling. - docs: SDK loadClearsignSigner notes persist/icon; handoff #53 marked MERGED; drop stray in INDEX.md. --- .../01-persistent-identity-icons.md | 2 +- docs/handoff-testing/INDEX.md | 1 - projects/keepkey-sdk/src/index.ts | 9 +++++--- projects/keepkey-sdk/src/types.ts | 4 +++- .../scripts/clearsign-icons/encode-icon.mjs | 22 ++++++++++++++++--- projects/keepkey-vault/src/bun/rest-api.ts | 22 ++++++++++++------- projects/keepkey-vault/src/bun/schemas.ts | 7 +++++- 7 files changed, 49 insertions(+), 18 deletions(-) diff --git a/docs/handoff-testing/01-persistent-identity-icons.md b/docs/handoff-testing/01-persistent-identity-icons.md index f13780c6..03dba5fa 100644 --- a/docs/handoff-testing/01-persistent-identity-icons.md +++ b/docs/handoff-testing/01-persistent-identity-icons.md @@ -10,7 +10,7 @@ screen and every per-tx confirm. - firmware `release/7.15.0-rc7` (`BitHighlander/keepkey-firmware#299`, OPEN) — `lib/firmware/storage.{c,h}`, `signed_metadata.c`, `fsm_msg_ethereum.h`, `lib/board/layout.c`, `include/keepkey/board/layout.h`. -- hdwallet `feat/clearsign-signer-icon` (`keepkey/hdwallet#53`, OPEN). +- hdwallet `keepkey/hdwallet#53` **MERGED** → pinned at `modules/hdwallet` `eccc3fa6`. - vault `feat/clearsign-identity-icons-vault` (`keepkey/keepkey-vault#342`, OPEN). ## Test (emulator, fastest) diff --git a/docs/handoff-testing/INDEX.md b/docs/handoff-testing/INDEX.md index b3439a8a..1544df56 100644 --- a/docs/handoff-testing/INDEX.md +++ b/docs/handoff-testing/INDEX.md @@ -35,4 +35,3 @@ Full paths per repo convention. symptom; confirm 3 signs in a row actually complete before calling it done. - Firmware #299 SOP gate: CI green + device-protocol **upstream**-pin swap before the final release (rc is fork-pinned). - diff --git a/projects/keepkey-sdk/src/index.ts b/projects/keepkey-sdk/src/index.ts index 76fdac7f..51ae9f77 100644 --- a/projects/keepkey-sdk/src/index.ts +++ b/projects/keepkey-sdk/src/index.ts @@ -333,9 +333,12 @@ export class KeepKeySdk { this.client.post('/eth/sign-transaction', params), /** - * Load a runtime EVM clear-sign signer into a device key slot (RAM-only, - * user-confirmed on device). The device shows a trust screen naming the - * alias + pubkey fingerprint. Dropped on reboot/wipe — reload per session. + * Load an EVM clear-sign signer into a device key slot (user-confirmed on + * device). The device shows a trust screen naming the alias + pubkey + * fingerprint. Default is RAM-only (dropped on reboot — reload per session); + * pass `persist: true` to keep it in device flash across reboots (until + * WipeDevice). An optional `icon` (+ `iconWidth`/`iconHeight`) renders the + * identity's logo on the trust screen and every clear-sign it vouches for. * Firmware 7.15.0+. Used to trust a metadata-signing key (e.g. a CI test * key in slot 3) so `ethSignTransaction`'s `txMetadata` blobs verify. */ diff --git a/projects/keepkey-sdk/src/types.ts b/projects/keepkey-sdk/src/types.ts index 3b9258f8..ed57ca6c 100644 --- a/projects/keepkey-sdk/src/types.ts +++ b/projects/keepkey-sdk/src/types.ts @@ -114,7 +114,9 @@ export interface LoadClearsignSignerParams { /** Signer label shown on the device trust screen. `[A-Za-z0-9 _-]`, 1-31 chars. */ alias: string /** Optional identity logo: 1bpp mono RLE bitmap as hex, <= 384 bytes. Shown on - * the trust screen and led before every clear-sign the identity vouches for. */ + * the trust screen and led before every clear-sign the identity vouches for. + * `icon`, `iconWidth`, and `iconHeight` must be supplied together — the REST + * layer rejects a half-specified icon. */ icon?: string iconWidth?: number iconHeight?: number diff --git a/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs b/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs index b2514f9c..4d8bec66 100644 --- a/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs +++ b/projects/keepkey-vault/scripts/clearsign-icons/encode-icon.mjs @@ -41,9 +41,11 @@ export function rleEncode(pixels) { out.push(run, pixels[i] & 0xff) i += run } else { - // Literal packet: pixels up to the next >=2 run, capped at 128 (int8 min). + // Literal packet: pixels up to the next >=2 run, capped at 127. The + // control byte carries -len as int8; -128 (from len 128) overflows the + // firmware's int8 count and trips the draw.c assertion, so 127 is the max. const lit = [] - while (i < n && lit.length < 128) { + while (i < n && lit.length < 127) { if (i + 1 < n && pixels[i + 1] === pixels[i]) break // a run starts next lit.push(pixels[i] & 0xff) i++ @@ -140,7 +142,21 @@ function selfTest() { if (JSON.stringify(got) !== JSON.stringify(expect)) { console.error('SELF-TEST byte mismatch', { expect, got }); process.exit(1) } - console.log('self-test OK (encode+decode round-trip + exact byte stream)') + // Boundary: a long literal stretch (alternating pixels → no run ≥2) must + // split into ≤127-length packets and never emit control 0x80 (int8 -128), + // which overflows the firmware's int8 negated count. Values are mono + // (0x00/0xFF) so a value byte can't be mistaken for a 0x80 control byte. + const alt = Array.from({ length: 300 }, (_, k) => (k % 2) * 0xff) + const altEnc = rleEncode(alt) + for (let k = 0; k < altEnc.length;) { + let c = altEnc[k]; if (c > 127) c -= 256 + if (c === -128) { console.error('SELF-TEST: emitted 0x80/-128 literal control byte'); process.exit(1) } + k += c >= 2 ? 2 : 1 + (-c) // run packet [n][v] : literal packet [n][v..] + } + if (!alt.every((v, k) => v === rleDecode(altEnc, 30, 10)[k])) { + console.error('SELF-TEST: alternating round-trip failed'); process.exit(1) + } + console.log('self-test OK (round-trip + exact byte stream + 127-literal boundary)') } if (import.meta.main) { diff --git a/projects/keepkey-vault/src/bun/rest-api.ts b/projects/keepkey-vault/src/bun/rest-api.ts index c49a8997..ec28d6d9 100644 --- a/projects/keepkey-vault/src/bun/rest-api.ts +++ b/projects/keepkey-vault/src/bun/rest-api.ts @@ -1123,19 +1123,25 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 const server = Bun.serve({ port, maxRequestBodySize: 1024 * 1024, // 1 MB max (addresses/signing payloads are small) - // Disable the idle-connection timeout (Bun default 10s). Signing requests - // block the response while the user confirms on the device — an interactive - // emulator confirm or a slow physical-button press legitimately exceeds 10s, - // and letting Bun close the socket aborts the in-flight sign mid-confirm - // ("other side closed" → the device's confirm is cancelled and it returns - // home). Human-gated signing has no business timing out at the socket layer. - idleTimeout: 0, - async fetch(req) { + // Keep Bun's default idle timeout for the server as a whole (so idle/ + // unauthenticated connections still get reaped) — but lift it per-request + // for human-gated device operations below, where the response legitimately + // blocks while the user confirms on the device. Without that, Bun closes + // the socket mid-confirm ("other side closed" → the device cancels the + // confirm and returns home), which killed the second consecutive sign. + async fetch(req, server) { const url = new URL(req.url) const path = url.pathname const method = req.method const requestStart = Date.now() + // Human-gated device ops (signing + the clear-sign trust confirm) block + // the socket while the user presses the physical button. Disable the idle + // timeout for just these requests, not the whole server. + if (method === 'POST' && (SIGNING_ROUTES.has(path) || path === '/eth/clearsign/load-signer')) { + server.timeout(req, 0) + } + // Resolve app info from bearer token (or 'public') const resolveAppInfo = (): { appName: string; imageUrl: string } => { const token = auth.extractBearerToken(req) diff --git a/projects/keepkey-vault/src/bun/schemas.ts b/projects/keepkey-vault/src/bun/schemas.ts index 0208289e..e831ac85 100644 --- a/projects/keepkey-vault/src/bun/schemas.ts +++ b/projects/keepkey-vault/src/bun/schemas.ts @@ -86,7 +86,12 @@ export const LoadClearsignSignerRequest = z.object({ iconHeight: z.number().int().min(1).max(64).optional(), // Persist the identity in device flash across reboots (until WipeDevice). persist: z.boolean().optional(), -}).strip() +}).strip().refine( + // icon + its dimensions travel together: firmware rejects an icon without + // dimensions, and dimensions without an icon are silently dropped. + (v) => [v.icon, v.iconWidth, v.iconHeight].filter((x) => x !== undefined).length % 3 === 0, + { message: 'icon, iconWidth, and iconHeight must all be present or all absent' }, +) /** POST /eth/sign-typed-data */ export const EthSignTypedDataRequest = z.object({