From 81bdf19e4263c0bbbc930a1dffe518d75788562d Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 30 Apr 2026 16:50:35 -0500 Subject: [PATCH 01/13] release: integrate 7.14.1 firmware updates --- .github/workflows/ci.yml | 205 ++++++++++- .gitmodules | 4 +- CMakeLists.txt | 19 ++ deps/device-protocol | 2 +- deps/python-keepkey | 2 +- include/keepkey/emulator/libkkemu.h | 114 +++++++ include/keepkey/emulator/setup.h | 1 + include/keepkey/firmware/fsm.h | 5 + include/keepkey/firmware/solana.h | 13 + include/keepkey/firmware/ton.h | 16 + include/keepkey/firmware/tron.h | 32 ++ .../keepkey/transport/messages-solana.options | 7 + .../keepkey/transport/messages-ton.options | 7 + .../keepkey/transport/messages-tron.options | 19 ++ lib/emulator/CMakeLists.txt | 27 ++ lib/emulator/libkkemu.c | 322 ++++++++++++++++++ lib/emulator/ringbuf.c | 39 +++ lib/emulator/ringbuf.h | 43 +++ lib/emulator/setup.c | 5 +- lib/emulator/udp.c | 51 ++- lib/firmware/fsm_msg_debug.h | 14 +- lib/firmware/fsm_msg_solana.h | 111 ++++++ lib/firmware/fsm_msg_ton.h | 92 +++++ lib/firmware/fsm_msg_tron.h | 222 ++++++++++++ lib/firmware/messagemap.def | 9 + lib/firmware/solana.c | 79 +++++ lib/firmware/ton.c | 34 ++ lib/firmware/tron.c | 214 ++++++++++++ tools/emulator/CMakeLists.txt | 30 +- 29 files changed, 1705 insertions(+), 33 deletions(-) create mode 100644 include/keepkey/emulator/libkkemu.h create mode 100644 lib/emulator/libkkemu.c create mode 100644 lib/emulator/ringbuf.c create mode 100644 lib/emulator/ringbuf.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f358ddc5..784473e79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -463,6 +463,208 @@ jobs: working-directory: scripts/emulator run: docker compose down -v || true + # ═══════════════════════════════════════════════════════════ + # STAGE 3a-bis: DYLIB TESTS — libkkemu shared lib via python-keepkey + # ═══════════════════════════════════════════════════════════ + # Builds the firmware emulator as a shared library (libkkemu.dylib on + # macOS / .so on Linux when supported) and runs the dylib-specific + # screenshot regression tests in python-keepkey. + # + # macOS-only for now. The Linux build hits a duplicate-symbol link + # error: __stack_chk_guard is defined by both lib/board/keepkey_board.c + # and lib/emulator/setup.c. The Apple linker silently picks one + # (matching local macos arm64 builds); GNU ld is strict and fails. + # Fixing requires deduping the symbol — out of scope for this PR. + # TODO: enable ubuntu-latest in a follow-up after the symbol cleanup. + # + # Why a separate job from python-integration-tests: + # - Different artifact: .dylib (in-process FFI), not the kkemu + # UDP binary the existing job builds. + # - Different transport in tests: KK_TRANSPORT=dylib instead of UDP. + # - Catches a class of bugs the UDP path hides — the standalone + # kkemu binary has its own poll thread; the dylib does not, so + # caller-driven polling correctness only surfaces here. + # + # Toolchain pinning rationale: + # - protoc 3.21.x (matches protobuf 3.20 wire format the firmware + # pb2 files expect; newer protoc generates Python that requires + # newer protobuf runtime, which breaks the python-keepkey suite). + # - protobuf 3.20.3 (Python runtime — strict pin). + # - nanopb 0.3.9.4.post3 (the proto generator the firmware build uses). + # - KK_DEBUG_LINK=ON (default OFF; without it, + # fsm_msgDebugLinkGetState is excluded from the build and any + # read_layout() call hangs the test). + python-dylib-tests: + needs: [lint-format, static-analysis, check-submodules, secret-scan] + runs-on: macos-latest + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Init submodules + run: | + git submodule update --init deps/crypto/trezor-firmware + git submodule update --init deps/device-protocol + git submodule update --init --recursive deps/python-keepkey + git submodule update --init deps/qrenc/QR-Code-generator + git submodule update --init deps/sca-hardening/SecAESSTM32 + # CMakeLists.txt requires googletest unconditionally even when + # we only build the dylib target — the project's add_subdirectory + # for it runs at configure time, before any target selection. + git submodule update --init deps/googletest + + - name: Setup Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install pinned Python deps + run: | + python -m pip install --upgrade pip + # Strict version pins — see job-level comment above. `requests` + # is needed by lib/firmware's ethereum_tokens.def build step + # (it fetches token-list JSON at compile time). + pip install "protobuf==3.20.3" "nanopb==0.3.9.4.post3" requests + + - name: Install pinned protoc 3.21 + run: | + # macos-latest is currently arm64. Pin to that explicitly so a + # future runner image swap doesn't silently switch to x86 and + # break the wire-format expectation downstream. + # Protobuf renamed releases from v3.21.x → v21.x at this point + # (the protoc "version reset"), so the tag and the file name + # both drop the leading 3. + PROTOC_VERSION=21.12 + PROTOC_ASSET="protoc-${PROTOC_VERSION}-osx-aarch_64.zip" + curl -sSL -fL -o /tmp/protoc.zip \ + "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ASSET}" + # `-f` makes curl fail-fast on HTTP errors so we don't unzip + # a 404 HTML page. + file /tmp/protoc.zip + sudo unzip -o /tmp/protoc.zip -d /usr/local + sudo chmod +x /usr/local/bin/protoc + protoc --version + + - name: Build nanopb generator's own .pb2 files + run: | + # The pip-installed nanopb 0.3.9.4.post3 ships nanopb.proto + + # plugin.proto but NOT the generated *_pb2.py files. Without + # them, `nanopb_generator.py` blows up at import time with + # "ImportError: attempted relative import with no known parent + # package" the first time the firmware build invokes it. + # + # We MUST regenerate with the pinned protoc 3.21 — the system + # protoc on github-runners is too new and produces .pb2 files + # that require a newer protobuf runtime than the 3.20.3 we + # pinned (which would fail with "Descriptors cannot be created + # directly. ... please regenerate with protoc >= 3.19.0 ... + # OR downgrade protobuf to 3.20.x"). + NANOPB_PROTO_DIR="$(python -c 'import os, nanopb; print(os.path.dirname(nanopb.__file__))')/generator/proto" + cd "$NANOPB_PROTO_DIR" + /usr/local/bin/protoc --python_out=. nanopb.proto + /usr/local/bin/protoc --python_out=. plugin.proto + ls -la nanopb_pb2.py plugin_pb2.py + + - name: Verify build tools + run: | + # macos runners pre-install cmake + Xcode CLI tools (clang, + # ld, etc). Just sanity-check the versions; no install needed. + cmake --version + clang --version + + - name: Configure cmake (KK_EMULATOR + KK_BUILD_DYLIB + KK_DEBUG_LINK) + run: | + # Two PATH entries are at play, and order matters: + # + # 1. The pip install puts a proper console-script wrapper at + # `/bin/protoc-gen-nanopb` that loads nanopb_generator + # AS A MODULE so relative imports resolve. setup-python + # already adds this bin dir to PATH. + # + # 2. cmake's `find_program(NANOPB_GENERATOR nanopb_generator.py)` + # wants the .py extension, which only exists in + # `/nanopb/generator/`. We APPEND that dir + # to PATH (NOT prepend) so the bin/ wrapper still wins for + # the `protoc-gen-nanopb` name resolution. The generator/ + # dir contains its own `protoc-gen-nanopb` raw script that + # fails with a relative-import error if it wins. + # + # KK_DEBUG_LINK=ON is REQUIRED for screenshot tests — without + # it fsm_msgDebugLinkGetState is excluded from the build. + # CMAKE_POLICY_VERSION_MINIMUM works around vendored + # googletest's pre-3.5 policy declaration. + export PATH="$PATH:$(python -c 'import os, nanopb; print(os.path.dirname(nanopb.__file__))')/generator" + which protoc-gen-nanopb + which nanopb_generator.py + cmake \ + -DKK_EMULATOR=1 \ + -DKK_BUILD_DYLIB=1 \ + -DKK_DEBUG_LINK=ON \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -B build-emu . + + - name: Build kkemulator_dylib + run: | + export PATH="$PATH:$(python -c 'import os, nanopb; print(os.path.dirname(nanopb.__file__))')/generator" + cmake --build build-emu --target kkemulator_dylib -j$(sysctl -n hw.ncpu) + ls -la build-emu/lib/libkkemu* || ls -la build-emu/lib/emulator/libkkemu* || true + # Surface the resolved binary path for the run step. macOS + # produces .dylib; .so is preserved as a fallback for when this + # job goes cross-platform. + DYLIB=$(find build-emu -name 'libkkemu.dylib' -o -name 'libkkemu.so' | head -1) + test -f "$DYLIB" || (echo "::error::libkkemu artifact not found" && exit 1) + echo "DYLIB_PATH=$(pwd)/$DYLIB" >> $GITHUB_ENV + + - name: Upload libkkemu.dylib + # Always upload, even on later test failure — the binary is + # what vault and external auditors actually consume from this + # PR. Tagged with the short commit SHA so multiple PR pushes + # don't overwrite each other when a reviewer downloads them. + if: always() && env.DYLIB_PATH != '' + uses: actions/upload-artifact@v4 + with: + name: libkkemu-${{ github.sha }} + path: ${{ env.DYLIB_PATH }} + retention-days: 30 + if-no-files-found: error + + - name: Install python-keepkey + pytest + working-directory: deps/python-keepkey + run: | + pip install -e . + # python-keepkey's setup.py doesn't list pytest as a dep; + # the existing python-integration-tests job runs inside a + # Docker image that has it baked. We're running on a vanilla + # macos runner so we install it explicitly. Pin pytest-timeout + # too — even though pytest-timeout can't break the dylib's C + # busy-loop (documented at length in the test_dylib_confirm_flow + # skip rationale), it's a transitive dep some tests use. + pip install pytest pytest-timeout + + - name: Run dylib screenshot tests + working-directory: deps/python-keepkey/tests + env: + KK_TRANSPORT: dylib + KK_DYLIB: ${{ env.DYLIB_PATH }} + run: | + # `keepkeylib/` on PYTHONPATH so the package's relative-style + # imports inside generated *_pb2.py files resolve. + PYTHONPATH=../keepkeylib:.. python -m pytest \ + test_dylib_screenshot.py \ + -v --tb=short --junit-xml=../../../test-reports/dylib-junit.xml + + - name: Upload dylib test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: python-dylib-test-results + path: test-reports/dylib-junit.xml + retention-days: 30 + if-no-files-found: warn + # ═══════════════════════════════════════════════════════════ # STAGE 3b: TEST REPORT — generate PDF from test artifacts # ═══════════════════════════════════════════════════════════ @@ -511,6 +713,7 @@ jobs: - name: Generate test report PDF env: FW_VERSION: ${{ steps.version.outputs.fw_version }} + KK_BUILD_LABEL: ${{ github.head_ref || github.ref_name }}@${{ github.event.pull_request.head.sha || github.sha }} run: python3 scripts/generate-test-report.py - name: Upload test report @@ -526,7 +729,7 @@ jobs: # ═══════════════════════════════════════════════════════════ publish-emulator: - needs: [unit-tests, python-integration-tests, build-arm-firmware] + needs: [unit-tests, python-integration-tests, python-dylib-tests, build-arm-firmware] if: >- github.event_name == 'workflow_dispatch' && github.event.inputs.publish_emulator == 'true' diff --git a/.gitmodules b/.gitmodules index 2d6c4446a..25a7f2fe4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "deps/device-protocol"] path = deps/device-protocol url = https://github.com/keepkey/device-protocol.git -branch = master +branch = release/7.14.1-device-protocol [submodule "deps/trezor-firmware"] path = deps/crypto/trezor-firmware url = https://github.com/keepkey/trezor-firmware.git @@ -14,7 +14,7 @@ url = https://github.com/keepkey/code-signing-keys.git [submodule "deps/python-keepkey"] path = deps/python-keepkey url = https://github.com/keepkey/python-keepkey.git -branch = master +branch = release/7.14.1-python-keepkey [submodule "deps/qrenc/QR-Code-generator"] path = deps/qrenc/QR-Code-generator url = https://github.com/keepkey/QR-Code-generator.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 025216066..cee2e661e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,12 @@ cmake_minimum_required(VERSION 3.7.2) +# CMP0079: allow target_link_libraries() to reference targets defined in +# a different directory. Required so tools/emulator can add firmware libs +# to kkemulator_dylib (defined in lib/emulator). +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + project( KeepKeyFirmware VERSION 7.14.1 @@ -10,8 +17,20 @@ set(BOOTLOADER_MINOR_VERSION 1) set(BOOTLOADER_PATCH_VERSION 5) option(KK_EMULATOR "Build the emulator" OFF) +option(KK_BUILD_DYLIB "Build libkkemu shared library (.dylib/.so)" OFF) option(KK_DEBUG_LINK "Build with debug-link enabled" OFF) option(KK_BUILD_FUZZERS "Build the fuzzers?" OFF) + +# When building the dylib, every static lib it links (kkfirmware, kkboard, +# trezorcrypto, kkrand, kktransport, qrcodegenerator, SecAESSTM32, ...) must +# itself be compiled with -fPIC. macOS happens to be lenient and will produce +# a working .dylib without it; Linux's link step against a non-PIC archive +# fails with "recompile with -fPIC". Set this BEFORE add_subdirectory(lib) +# below so all targets pick it up at definition time. CMP0079 (already set +# above) lets us reach across directories to link them. +if(KK_BUILD_DYLIB) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() set(LIBOPENCM3_PATH /root/libopencm3 CACHE PATH "Path to an already-built libopencm3") diff --git a/deps/device-protocol b/deps/device-protocol index bf8646b81..73ca75f68 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit bf8646b817401d4623e0977ddb4c961b1101121f +Subproject commit 73ca75f6822ea7727861c122f2fd5b1a6bc67461 diff --git a/deps/python-keepkey b/deps/python-keepkey index a4e346e8d..7141dc81c 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit a4e346e8d4345b8e858df8c3073ebfe24e14b730 +Subproject commit 7141dc81c72c06327118ba00dbd06b4b44dda476 diff --git a/include/keepkey/emulator/libkkemu.h b/include/keepkey/emulator/libkkemu.h new file mode 100644 index 000000000..f0f0020d9 --- /dev/null +++ b/include/keepkey/emulator/libkkemu.h @@ -0,0 +1,114 @@ +/* + * libkkemu — KeepKey firmware emulator as a shared library. + * + * The host process provides a pre-allocated 1MB flash buffer. + * All I/O goes through ring buffers (no UDP sockets). + * Single-threaded: call kkemu_poll() from your event loop. + */ +#ifndef LIBKKEMU_H +#define LIBKKEMU_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define KKEMU_FLASH_SIZE (1024 * 1024) /* 1 MB */ +#define KKEMU_PACKET_SIZE 64 /* HID report size */ +#define KKEMU_IFACE_MAIN 0 +#define KKEMU_IFACE_DEBUG 1 + +/** + * Initialize the emulator with a host-provided flash buffer. + * + * @param flash_buf Pointer to 1MB buffer (host-owned, must remain valid). + * If contents are all 0xFF, treated as fresh/erased device. + * If contents are from a previous session, device state is + * restored. + * @param flash_len Must be KKEMU_FLASH_SIZE (1048576). + * @return 0 on success, -1 on error. + * + * After this call, the emulator is ready to process messages via + * kkemu_write() + kkemu_poll() + kkemu_read(). + */ +int kkemu_init(uint8_t* flash_buf, size_t flash_len); + +/** + * Shut down the emulator. Flushes pending storage writes to the + * flash buffer. After this call, the host should encrypt and persist + * the flash buffer, then zero it. + */ +void kkemu_shutdown(void); + +/** + * Write a 64-byte HID report into the emulator's input queue. + * + * @param data Exactly 64 bytes. + * @param len Must be 64. + * @param iface KKEMU_IFACE_MAIN (0) or KKEMU_IFACE_DEBUG (1). + * @return 0 on success, -1 if queue is full. + */ +int kkemu_write(const uint8_t* data, size_t len, int iface); + +/** + * Read a 64-byte HID report from the emulator's output queue. + * + * Non-blocking. Returns 0 immediately if no output is available. + * + * @param buf Buffer of at least 64 bytes. + * @param len Must be 64. + * @param iface KKEMU_IFACE_MAIN (0) or KKEMU_IFACE_DEBUG (1). + * @return Number of bytes read (64), or 0 if queue is empty. + */ +int kkemu_read(uint8_t* buf, size_t len, int iface); + +/** + * Run one iteration of the firmware event loop. + * + * Drains the input queue, dispatches messages through the FSM, + * queues output messages, and updates display/animations. + * + * Call this at 10-60 Hz from your event loop. + * + * @return Number of messages processed, or -1 on error. + */ +int kkemu_poll(void); + +/** + * Get the OLED framebuffer (256x64, 1-bit per pixel = 2048 bytes). + * + * @param width Receives 256. + * @param height Receives 64. + * @return Pointer to framebuffer (valid until next kkemu_poll). + * Returns NULL if emulator is not initialized. + */ +const uint8_t* kkemu_get_display(int* width, int* height); + +/** + * Pop the next captured framebuffer from the display capture ring. + * + * Every display_refresh() inside the firmware (including those that fire + * inside confirm_helper's busy loop within a single kkemu_poll() call) + * snapshots the canvas into a ring buffer. Adjacent identical frames + * are deduplicated. This lets the host see intermediate screen states + * (confirm dialogs, cipher prompts, recovery screens) that would + * otherwise be invisible — they exist only inside synchronous C calls. + * + * @param out_packed Buffer of at least 2048 bytes (256x64, 1-bit packed + * SSD1306 page format — same as kkemu_get_display). + * @return 1 if a frame was popped, 0 if the ring is empty. + */ +int kkemu_pop_frame(uint8_t* out_packed); + +/** + * Check if the emulator has been initialized. + */ +int kkemu_is_running(void); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBKKEMU_H */ diff --git a/include/keepkey/emulator/setup.h b/include/keepkey/emulator/setup.h index 57e0b949f..fadd9843c 100644 --- a/include/keepkey/emulator/setup.h +++ b/include/keepkey/emulator/setup.h @@ -2,5 +2,6 @@ #define KEEPKEY_EMULATOR_SETUP_H void setup(void); +void setup_urandom_only(void); /* For libkkemu: init RNG without flash mmap */ #endif diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index ca80eb7db..656f0c127 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -120,11 +120,16 @@ void fsm_msgMayachainMsgAck(const MayachainMsgAck* msg); void fsm_msgTronGetAddress(const TronGetAddress* msg); void fsm_msgTronSignTx(TronSignTx* msg); +void fsm_msgTronSignMessage(TronSignMessage* msg); +void fsm_msgTronVerifyMessage(const TronVerifyMessage* msg); +void fsm_msgTronSignTypedHash(const TronSignTypedHash* msg); void fsm_msgTonGetAddress(const TonGetAddress* msg); void fsm_msgTonSignTx(TonSignTx* msg); +void fsm_msgTonSignMessage(const TonSignMessage* msg); void fsm_msgSolanaGetAddress(const SolanaGetAddress* msg); void fsm_msgSolanaSignTx(const SolanaSignTx* msg); void fsm_msgSolanaSignMessage(const SolanaSignMessage* msg); +void fsm_msgSolanaSignOffchainMessage(const SolanaSignOffchainMessage* msg); #if DEBUG_LINK // void fsm_msgDebugLinkDecision(DebugLinkDecision *msg); diff --git a/include/keepkey/firmware/solana.h b/include/keepkey/firmware/solana.h index 042645e3e..a56611dad 100644 --- a/include/keepkey/firmware/solana.h +++ b/include/keepkey/firmware/solana.h @@ -199,4 +199,17 @@ const SolanaTokenInfo* solana_findTokenInfo( bool solana_signTx(const HDNode* node, const SolanaSignTx* msg, SolanaSignedTx* resp); +/* Sign a Solana off-chain message with domain separation. + * + * Builds the spec envelope (0xFF || "solana offchain" || version || format + * || length:u16 || message) and Ed25519-signs it. Format 2 (extended + * UTF-8) is rejected — only formats 0 (ASCII) and 1 (UTF-8 limited) are + * supported on this device. + * + * Caller must have populated node->public_key (hdnode_fill_public_key). + */ +bool solana_offchain_message_sign(const HDNode* node, + const SolanaSignOffchainMessage* msg, + SolanaOffchainMessageSignature* resp); + #endif /* KEEPKEY_FIRMWARE_SOLANA_H */ diff --git a/include/keepkey/firmware/ton.h b/include/keepkey/firmware/ton.h index 7b9d42870..77c8feb50 100644 --- a/include/keepkey/firmware/ton.h +++ b/include/keepkey/firmware/ton.h @@ -65,4 +65,20 @@ void ton_formatAmount(char* buf, size_t len, uint64_t amount); */ bool ton_signTx(const HDNode* node, const TonSignTx* msg, TonSignedTx* resp); +/** + * Sign an arbitrary message with raw Ed25519 (no domain separation). + * + * Caller MUST gate this behind the AdvancedMode policy — bare Ed25519 + * over message bytes is indistinguishable from signing a transaction. + * The proper domain-separated path is TON Connect's ton_proof envelope, + * which is not yet implemented. + * + * @param node HD node with public_key filled + * @param msg TonSignMessage request + * @param resp TonMessageSignature response (signature + Ed25519 public key) + * @return true on success + */ +bool ton_message_sign(const HDNode* node, const TonSignMessage* msg, + TonMessageSignature* resp); + #endif diff --git a/include/keepkey/firmware/tron.h b/include/keepkey/firmware/tron.h index 759680b85..6e51d8040 100644 --- a/include/keepkey/firmware/tron.h +++ b/include/keepkey/firmware/tron.h @@ -56,4 +56,36 @@ void tron_formatAmount(char* buf, size_t len, uint64_t amount); */ bool tron_signTx(const HDNode* node, const TronSignTx* msg, TronSignedTx* resp); +/** + * Sign an arbitrary message using TIP-191 personal_sign. + * Hash = keccak256("\x19TRON Signed Message:\n" + ASCII(len) + message) + * @param node HD node containing private key + * @param msg TronSignMessage request + * @param resp TronMessageSignature response (signature + Base58Check address) + * @return true on success + */ +bool tron_message_sign(const HDNode* node, const TronSignMessage* msg, + TronMessageSignature* resp); + +/** + * Verify a TIP-191 signature against the claimed Base58Check TRON address. + * @return 0 on success, 1 on malformed input, 2 on signature/address mismatch + */ +int tron_message_verify(const TronVerifyMessage* msg); + +/** + * Sign a TIP-712 typed-data digest in hash mode. + * Host pre-computes the domain separator hash + message hash per the + * TIP-712 spec; the device assembles + * keccak256("\x19\x01" || domain_separator_hash || message_hash) + * and signs with secp256k1. + * + * @param node HD node with public_key filled + * @param msg TronSignTypedHash request + * @param resp TronTypedDataSignature response (signature + Base58Check address) + * @return true on success + */ +bool tron_typed_hash_sign(const HDNode* node, const TronSignTypedHash* msg, + TronTypedDataSignature* resp); + #endif diff --git a/include/keepkey/transport/messages-solana.options b/include/keepkey/transport/messages-solana.options index 7c9e3978f..ded9d68c1 100644 --- a/include/keepkey/transport/messages-solana.options +++ b/include/keepkey/transport/messages-solana.options @@ -19,3 +19,10 @@ SolanaSignMessage.message max_size:1024 SolanaMessageSignature.public_key max_size:32 SolanaMessageSignature.signature max_size:64 + +SolanaSignOffchainMessage.address_n max_count:8 +SolanaSignOffchainMessage.coin_name max_size:21 +SolanaSignOffchainMessage.message max_size:1212 + +SolanaOffchainMessageSignature.public_key max_size:32 +SolanaOffchainMessageSignature.signature max_size:64 diff --git a/include/keepkey/transport/messages-ton.options b/include/keepkey/transport/messages-ton.options index 413a14939..adbf08a55 100644 --- a/include/keepkey/transport/messages-ton.options +++ b/include/keepkey/transport/messages-ton.options @@ -11,3 +11,10 @@ TonSignTx.to_address max_size:50 TonSignTx.memo max_size:121 TonSignedTx.signature max_size:64 + +TonSignMessage.address_n max_count:8 +TonSignMessage.coin_name max_size:21 +TonSignMessage.message max_size:1024 + +TonMessageSignature.public_key max_size:32 +TonMessageSignature.signature max_size:64 diff --git a/include/keepkey/transport/messages-tron.options b/include/keepkey/transport/messages-tron.options index 9cd09facb..464d3ba5d 100644 --- a/include/keepkey/transport/messages-tron.options +++ b/include/keepkey/transport/messages-tron.options @@ -19,3 +19,22 @@ TronSignTx.data max_size:256 TronSignedTx.signature max_size:65 TronSignedTx.serialized_tx max_size:1024 + +TronSignMessage.address_n max_count:8 +TronSignMessage.coin_name max_size:21 +TronSignMessage.message max_size:1024 + +TronMessageSignature.address max_size:35 +TronMessageSignature.signature max_size:65 + +TronVerifyMessage.address max_size:35 +TronVerifyMessage.signature max_size:65 +TronVerifyMessage.message max_size:1024 + +TronSignTypedHash.address_n max_count:8 +TronSignTypedHash.coin_name max_size:21 +TronSignTypedHash.domain_separator_hash max_size:32 +TronSignTypedHash.message_hash max_size:32 + +TronTypedDataSignature.address max_size:35 +TronTypedDataSignature.signature max_size:65 diff --git a/lib/emulator/CMakeLists.txt b/lib/emulator/CMakeLists.txt index feac9053d..fa5909a17 100644 --- a/lib/emulator/CMakeLists.txt +++ b/lib/emulator/CMakeLists.txt @@ -13,4 +13,31 @@ if(${KK_EMULATOR}) add_library(kkemulator ${sources}) + # ── Shared library target (libkkemu.dylib / libkkemu.so) ────────── + if(KK_BUILD_DYLIB) + set(dylib_sources + oled.c + udp.c + setup.c + ringbuf.c + libkkemu.c) + + if(NOT ${KK_HAVE_STRLCPY}) + set(dylib_sources ${dylib_sources} strlcpy.c) + endif() + if(NOT ${KK_HAVE_STRLCAT}) + set(dylib_sources ${dylib_sources} strlcat.c) + endif() + + add_library(kkemulator_dylib SHARED ${dylib_sources}) + target_compile_definitions(kkemulator_dylib PRIVATE KKEMU_DYLIB=1) + target_include_directories(kkemulator_dylib PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ${CMAKE_SOURCE_DIR}/deps/crypto/trezor-crypto) + set_target_properties(kkemulator_dylib PROPERTIES + OUTPUT_NAME "kkemu" + POSITION_INDEPENDENT_CODE ON) + endif() + endif() diff --git a/lib/emulator/libkkemu.c b/lib/emulator/libkkemu.c new file mode 100644 index 000000000..be45bf608 --- /dev/null +++ b/lib/emulator/libkkemu.c @@ -0,0 +1,322 @@ +/* + * libkkemu — KeepKey firmware emulator as a shared library. + * + * Replaces main() with kkemu_init/poll/shutdown. Uses ring buffers + * instead of UDP sockets for message I/O. + */ +#include "keepkey/emulator/libkkemu.h" +#include "keepkey/emulator/emulator.h" +#include "keepkey/emulator/setup.h" +#include "keepkey/board/canvas.h" +#include "keepkey/board/keepkey_board.h" +#include "keepkey/board/keepkey_display.h" +#include "keepkey/board/keepkey_flash.h" +#include "keepkey/board/layout.h" +#include "keepkey/board/usb.h" +#include "keepkey/board/memory.h" +#include "keepkey/board/timer.h" +#include "keepkey/firmware/home_sm.h" +#include "keepkey/firmware/storage.h" +#include "keepkey/rand/rng.h" +#include "ringbuf.h" +#include "trezor/crypto/memzero.h" + +#include +#include +#include +#include + +/* Defined in firmware — we just need the declaration */ +extern void fsm_init(void); + +/* ── Ring buffers (replace UDP sockets) ─────────────────────────────── */ + +static RingBuf rb_main_in; /* host → firmware (main interface) */ +static RingBuf rb_main_out; /* firmware → host (main interface) */ +static RingBuf rb_debug_in; /* host → firmware (debug link) */ +static RingBuf rb_debug_out; /* firmware → host (debug link) */ + +static int libkkemu_initialized = 0; + +/* ── Display capture ring ───────────────────────────────────────────── */ + +/* + * Captures every display_refresh() into a ring of 1-bit packed snapshots. + * The host drains via kkemu_pop_frame(). Adjacent identical frames are + * skipped so an idle firmware doesn't spam the ring. + * + * Sized for ~4 seconds at 16ms refresh; if the host falls behind the + * oldest frames are dropped (write advances past read). + */ +#define FRAME_PACKED_SIZE 2048 +#define FRAME_RING_SIZE 64 + +static uint8_t frame_ring[FRAME_RING_SIZE][FRAME_PACKED_SIZE]; +static uint8_t last_packed[FRAME_PACKED_SIZE]; +static int last_packed_valid = 0; +static uint32_t frame_write_idx = + 0; /* monotonic, mod FRAME_RING_SIZE for slot */ +static uint32_t frame_read_idx = 0; /* monotonic */ + +/* + * Scratch returned by kkemu_get_display(). File-scope (not function-static) + * so kkemu_shutdown() can zero it alongside the other display buffers. + */ +static uint8_t display_packed_scratch[FRAME_PACKED_SIZE]; + +/* ── Replacement I/O functions ──────────────────────────────────────── */ + +/* + * These replace the UDP socket functions in emulator/udp.c. + * When building as a shared library, we link against these instead. + */ + +void libkkemu_socketInit(void) { + ringbuf_init(&rb_main_in); + ringbuf_init(&rb_main_out); + ringbuf_init(&rb_debug_in); + ringbuf_init(&rb_debug_out); +} + +size_t libkkemu_socketRead(int* iface, void* buffer, size_t size) { + if (ringbuf_pop(&rb_main_in, (uint8_t*)buffer, size)) { + *iface = 0; + return size < RINGBUF_SLOT_SIZE ? size : RINGBUF_SLOT_SIZE; + } + if (ringbuf_pop(&rb_debug_in, (uint8_t*)buffer, size)) { + *iface = 1; + return size < RINGBUF_SLOT_SIZE ? size : RINGBUF_SLOT_SIZE; + } + return 0; +} + +size_t libkkemu_socketWrite(int iface, const void* buffer, size_t size) { + RingBuf* rb = (iface == 0) ? &rb_main_out : &rb_debug_out; + if (!ringbuf_push(rb, (const uint8_t*)buffer, size)) return 0; + return size; +} + +/* ── Display capture callback ───────────────────────────────────────── */ + +/* + * Pack the 8-bpp grayscale canvas (256x64 = 16384 bytes) into the + * 1-bit SSD1306 page format the host wants. Skip if identical to the + * last frame we captured. Called from display_refresh() on every poll + * and on every iteration of confirm_helper's busy loop. + */ +static void libkkemu_capture_frame(const uint8_t* canvas_buf) { + if (!canvas_buf) return; + + uint8_t* slot = frame_ring[frame_write_idx % FRAME_RING_SIZE]; + memset(slot, 0, FRAME_PACKED_SIZE); + for (int x = 0; x < 256; x++) { + for (int y = 0; y < 64; y++) { + if (canvas_buf[y * 256 + x] > 0) { + slot[x + (y / 8) * 256] |= (uint8_t)(1u << (y % 8)); + } + } + } + + /* Dedup: skip if identical to last captured */ + if (last_packed_valid && memcmp(slot, last_packed, FRAME_PACKED_SIZE) == 0) { + return; + } + memcpy(last_packed, slot, FRAME_PACKED_SIZE); + last_packed_valid = 1; + + frame_write_idx++; + /* Drop oldest if host fell behind */ + if (frame_write_idx - frame_read_idx > FRAME_RING_SIZE) { + frame_read_idx = frame_write_idx - FRAME_RING_SIZE; + } +} + +/* ── Public API ─────────────────────────────────────────────────────── */ + +int kkemu_init(uint8_t* flash_buf, size_t flash_len) { + if (flash_len != KKEMU_FLASH_SIZE) return -1; + if (!flash_buf) return -1; + if (libkkemu_initialized) return -1; + + /* Point firmware's flash pointer at the host-provided buffer */ + emulator_flash_base = flash_buf; + + /* + * Lock memory to prevent secrets in the flash buffer (seed, FVK, PIN + * derivation state) from being swapped out. Failure is non-fatal — many + * platforms cap unprivileged mlock at a few MB (RLIMIT_MEMLOCK), and a + * dev/CI environment that hits the cap shouldn't break emulator usage. + * We DO log to stderr so the host can decide to escalate (raise the + * rlimit, run with CAP_IPC_LOCK, etc.) before signing real material. + * Production hosts of libkkemu should treat a logged failure as a + * security warning and refuse to load secrets. + */ + if (mlock(flash_buf, flash_len) != 0) { + fprintf(stderr, + "[libkkemu] mlock(%zu bytes) failed: %s — flash buffer may be " + "swapped to disk; do not load production secrets\n", + flash_len, strerror(errno)); + } + + /* Initialize ring buffers (replaces UDP socket init) */ + libkkemu_socketInit(); + + /* Reset frame capture state */ + frame_write_idx = 0; + frame_read_idx = 0; + last_packed_valid = 0; + + /* Initialize /dev/urandom for RNG */ + setup_urandom_only(); + + /* Board init (timers, etc.) */ + kk_board_init(); + + /* Hook display_refresh() so every canvas update is captured into + * our ring buffer. Must be set before storage_init/fsm_init/ + * layoutHomeForced so the boot screens get captured too. */ + display_set_dump_callback(libkkemu_capture_frame); + + /* Load storage from flash buffer */ + storage_init(); + + /* Initialize message handler FSM */ + fsm_init(); + + /* Draw initial home screen */ + layoutHomeForced(); + + libkkemu_initialized = 1; + return 0; +} + +void kkemu_shutdown(void) { + if (!libkkemu_initialized) return; + + /* Flush any pending storage to the flash buffer */ + storage_commit(); + + /* + * Zero every static buffer that could hold sensitive material before + * we tear down. In dylib mode this library lives inside a long-running + * host process — the static rings, frame ring, and packed-display + * scratch can outlive the emulator session and be visible to the rest + * of the host's memory image (core dumps, ptrace, GC roots in a Bun + * runtime, etc.). Specifically: + * + * - rb_main_in / rb_main_out: PIN, passphrase, signing inputs/outputs + * - rb_debug_in / rb_debug_out: mnemonic + recovery state when + * KK_DEBUG_LINK builds are loaded + * - frame_ring / last_packed: rendered OLED bytes for every screen, + * including PIN matrix, recovery words, + * address confirms, signing summaries + * + * memzero() is the trezor-crypto helper that the compiler can't optimize + * out. Same primitive used throughout the firmware to clear key material. + */ + memzero(&rb_main_in, sizeof(rb_main_in)); + memzero(&rb_main_out, sizeof(rb_main_out)); + memzero(&rb_debug_in, sizeof(rb_debug_in)); + memzero(&rb_debug_out, sizeof(rb_debug_out)); + memzero(frame_ring, sizeof(frame_ring)); + memzero(last_packed, sizeof(last_packed)); + memzero(display_packed_scratch, sizeof(display_packed_scratch)); + last_packed_valid = 0; + frame_write_idx = 0; + frame_read_idx = 0; + + /* + * Unlock + caller is responsible for zeroing the host-owned flash buffer + * after this returns. We explicitly DO NOT zero it here — the host may + * want to inspect / persist post-mortem state. Documented contract. + */ + if (emulator_flash_base) { + munlock(emulator_flash_base, KKEMU_FLASH_SIZE); + emulator_flash_base = NULL; + } + + libkkemu_initialized = 0; +} + +int kkemu_write(const uint8_t* data, size_t len, int iface) { + if (!libkkemu_initialized) return -1; + if (len != KKEMU_PACKET_SIZE) return -1; + + RingBuf* rb = (iface == KKEMU_IFACE_MAIN) ? &rb_main_in : &rb_debug_in; + return ringbuf_push(rb, data, len) ? 0 : -1; +} + +int kkemu_read(uint8_t* buf, size_t len, int iface) { + if (!libkkemu_initialized) return 0; + if (len < KKEMU_PACKET_SIZE) return 0; + + RingBuf* rb = (iface == KKEMU_IFACE_MAIN) ? &rb_main_out : &rb_debug_out; + return ringbuf_pop(rb, buf, KKEMU_PACKET_SIZE) ? KKEMU_PACKET_SIZE : 0; +} + +int kkemu_poll(void) { + if (!libkkemu_initialized) return -1; + + /* + * This is the same as exec() in main.cpp: + * usbPoll() — reads input, dispatches through FSM + * animate() — updates screen animations + * display_refresh() — renders framebuffer + * + * usbPoll() internally calls emulatorSocketRead() which we've + * replaced with libkkemu_socketRead() via the ring buffers. + */ + usbPoll(); + animate(); + display_refresh(); + + return 0; +} + +const uint8_t* kkemu_get_display(int* width, int* height) { + /* + * Pack the firmware's 8-bpp grayscale canvas (256×64 = 16384 bytes) into + * the 1-bit packed layout vault expects (2048 bytes). Same format + * DebugLinkGetState.layout uses: byte index = x + (y/8)*256, + * bit within byte = y%8 (LSB = top row of the 8-pixel column). + * + * Output goes into the file-scope `display_packed_scratch` so + * kkemu_shutdown() can zero it on teardown alongside the frame ring. + */ + if (!libkkemu_initialized) { + if (width) *width = 0; + if (height) *height = 0; + return NULL; + } + + const Canvas* c = display_canvas(); + if (!c || !c->buffer) { + if (width) *width = 0; + if (height) *height = 0; + return NULL; + } + + memset(display_packed_scratch, 0, sizeof(display_packed_scratch)); + for (int x = 0; x < 256; x++) { + for (int y = 0; y < 64; y++) { + if (c->buffer[y * 256 + x] > 0) { + display_packed_scratch[x + (y / 8) * 256] |= (uint8_t)(1u << (y % 8)); + } + } + } + + if (width) *width = 256; + if (height) *height = 64; + return display_packed_scratch; +} + +int kkemu_pop_frame(uint8_t* out_packed) { + if (!libkkemu_initialized || !out_packed) return 0; + if (frame_read_idx == frame_write_idx) return 0; + const uint8_t* slot = frame_ring[frame_read_idx % FRAME_RING_SIZE]; + memcpy(out_packed, slot, FRAME_PACKED_SIZE); + frame_read_idx++; + return 1; +} + +int kkemu_is_running(void) { return libkkemu_initialized; } diff --git a/lib/emulator/ringbuf.c b/lib/emulator/ringbuf.c new file mode 100644 index 000000000..779a06e81 --- /dev/null +++ b/lib/emulator/ringbuf.c @@ -0,0 +1,39 @@ +/* + * Lock-free SPSC ring buffer for 64-byte HID reports. + */ +#include "ringbuf.h" +#include + +void ringbuf_init(RingBuf* rb) { memset(rb, 0, sizeof(*rb)); } + +bool ringbuf_push(RingBuf* rb, const uint8_t* msg, size_t len) { + if (len > RINGBUF_SLOT_SIZE) return false; + + uint32_t head = rb->head; + uint32_t next = (head + 1) % RINGBUF_CAPACITY; + + if (next == rb->tail) return false; /* full */ + + memcpy(rb->data[head], msg, len); + if (len < RINGBUF_SLOT_SIZE) + memset(rb->data[head] + len, 0, RINGBUF_SLOT_SIZE - len); + + __sync_synchronize(); /* memory barrier before publishing head */ + rb->head = next; + return true; +} + +bool ringbuf_pop(RingBuf* rb, uint8_t* msg, size_t len) { + uint32_t tail = rb->tail; + + if (tail == rb->head) return false; /* empty */ + + size_t copy = len < RINGBUF_SLOT_SIZE ? len : RINGBUF_SLOT_SIZE; + memcpy(msg, rb->data[tail], copy); + + __sync_synchronize(); /* memory barrier before advancing tail */ + rb->tail = (tail + 1) % RINGBUF_CAPACITY; + return true; +} + +bool ringbuf_empty(const RingBuf* rb) { return rb->head == rb->tail; } diff --git a/lib/emulator/ringbuf.h b/lib/emulator/ringbuf.h new file mode 100644 index 000000000..45cddad56 --- /dev/null +++ b/lib/emulator/ringbuf.h @@ -0,0 +1,43 @@ +/* + * Lock-free single-producer single-consumer ring buffer for HID reports. + * Used by libkkemu to pass 64-byte messages between host and firmware. + */ +#ifndef RINGBUF_H +#define RINGBUF_H + +#include +#include +#include + +#define RINGBUF_SLOT_SIZE 64 /* HID report size */ + +/* + * Capacity must hold the largest synchronous response the firmware emits in + * a single dispatch. The driver: DebugLinkGetState now serializes a 2048-byte + * `layout` field plus the rest of DebugLinkState (~2.7 KB total payload), and + * we want headroom for DebugLinkFlashDumpResponse (1024-byte chunks) and for + * any future field growth. With ~62 bytes of payload per HID report after the + * sync prefix + continuation byte, 2.7 KB is ~44 reports. The previous value + * of 32 left effective room for 31 reports, so DebugLinkGetState was being + * truncated mid-screenshot — emulatorSocketWrite() returned 0 but the + * upstream `msg_debug_write()` ignored the failure, so the host saw a + * silently-clipped response. + * + * 128 gives ~3x headroom on the worst current response, costs 4 * 8 KB = + * 32 KB of RAM across the four rings, and keeps the slot index a power of + * two so the modulo in ringbuf_push/pop remains a cheap mask. + */ +#define RINGBUF_CAPACITY 128 /* max queued messages */ + +typedef struct { + uint8_t data[RINGBUF_CAPACITY][RINGBUF_SLOT_SIZE]; + volatile uint32_t head; /* written by producer */ + volatile uint32_t tail; /* written by consumer */ +} RingBuf; + +void ringbuf_init(RingBuf* rb); +bool ringbuf_push(RingBuf* rb, const uint8_t* msg, size_t len); +bool ringbuf_pop(RingBuf* rb, uint8_t* msg, size_t len); +bool ringbuf_empty(const RingBuf* rb); + +#endif diff --git a/lib/emulator/setup.c b/lib/emulator/setup.c index 45f601c31..1b4d657d4 100644 --- a/lib/emulator/setup.c +++ b/lib/emulator/setup.c @@ -43,7 +43,10 @@ void setup(void) { setup_flash(); } -void emulatorRandom(void *buffer, size_t size) { +/* For libkkemu: init RNG only (flash buffer provided by host) */ +void setup_urandom_only(void) { setup_urandom(); } + +void emulatorRandom(void* buffer, size_t size) { ssize_t n = read(urandom, buffer, size); if (n < 0 || ((size_t)n) != size) { perror("Failed to read /dev/urandom"); diff --git a/lib/emulator/udp.c b/lib/emulator/udp.c index 7b61b6623..671c6437b 100644 --- a/lib/emulator/udp.c +++ b/lib/emulator/udp.c @@ -24,7 +24,9 @@ #include #include +#ifndef KEEPKEY_UDP_PORT #define KEEPKEY_UDP_PORT 11044 +#endif struct usb_socket { int fd; @@ -48,7 +50,7 @@ static int socket_setup(int port) { addr.sin_addr.s_addr = htonl(INADDR_ANY); // addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { perror("Failed to bind socket"); exit(1); } @@ -56,11 +58,11 @@ static int socket_setup(int port) { return fd; } -static size_t socket_write(struct usb_socket *sock, const void *buffer, +static size_t socket_write(struct usb_socket* sock, const void* buffer, size_t size) { if (sock->fromlen > 0) { ssize_t n = sendto(sock->fd, buffer, size, MSG_DONTWAIT, - (const struct sockaddr *)&sock->from, sock->fromlen); + (const struct sockaddr*)&sock->from, sock->fromlen); if (n < 0 || ((size_t)n) != size) { perror("Failed to write socket"); return 0; @@ -70,10 +72,10 @@ static size_t socket_write(struct usb_socket *sock, const void *buffer, return size; } -static size_t socket_read(struct usb_socket *sock, void *buffer, size_t size) { +static size_t socket_read(struct usb_socket* sock, void* buffer, size_t size) { sock->fromlen = sizeof(sock->from); ssize_t n = recvfrom(sock->fd, buffer, size, MSG_DONTWAIT, - (struct sockaddr *)&sock->from, &sock->fromlen); + (struct sockaddr*)&sock->from, &sock->fromlen); if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { @@ -94,14 +96,44 @@ static size_t socket_read(struct usb_socket *sock, void *buffer, size_t size) { return n; } +#ifdef KKEMU_DYLIB +/* + * Dylib mode: I/O goes through ring buffers managed by libkkemu.c. + * These are thin trampolines to the libkkemu_socket* functions. + */ +extern void libkkemu_socketInit(void); +extern size_t libkkemu_socketRead(int* iface, void* buffer, size_t size); +extern size_t libkkemu_socketWrite(int iface, const void* buffer, size_t size); + +void emulatorSocketInit(void) { libkkemu_socketInit(); } + +size_t emulatorSocketRead(int* iface, void* buffer, size_t size) { + return libkkemu_socketRead(iface, buffer, size); +} + +size_t emulatorSocketWrite(int iface, const void* buffer, size_t size) { + return libkkemu_socketWrite(iface, buffer, size); +} + +#else +/* Standard mode: UDP sockets (standalone kkemu binary) */ + void emulatorSocketInit(void) { - usb_main.fd = socket_setup(KEEPKEY_UDP_PORT); + int port = KEEPKEY_UDP_PORT; + const char* env_port = getenv("KEEPKEY_UDP_PORT"); + if (env_port) { + int p = atoi(env_port); + if (p > 0 && p < 65535) port = p; + } + fprintf(stderr, "Emulator listening on UDP ports %d (main) and %d (debug)\n", + port, port + 1); + usb_main.fd = socket_setup(port); usb_main.fromlen = 0; - usb_debug.fd = socket_setup(KEEPKEY_UDP_PORT + 1); + usb_debug.fd = socket_setup(port + 1); usb_debug.fromlen = 0; } -size_t emulatorSocketRead(int *iface, void *buffer, size_t size) { +size_t emulatorSocketRead(int* iface, void* buffer, size_t size) { size_t n = socket_read(&usb_main, buffer, size); if (n > 0) { *iface = 0; @@ -117,7 +149,7 @@ size_t emulatorSocketRead(int *iface, void *buffer, size_t size) { return 0; } -size_t emulatorSocketWrite(int iface, const void *buffer, size_t size) { +size_t emulatorSocketWrite(int iface, const void* buffer, size_t size) { if (iface == 0) { return socket_write(&usb_main, buffer, size); } @@ -126,3 +158,4 @@ size_t emulatorSocketWrite(int iface, const void *buffer, size_t size) { } return 0; } +#endif diff --git a/lib/firmware/fsm_msg_debug.h b/lib/firmware/fsm_msg_debug.h index 8eaa0b312..3a1635c9d 100644 --- a/lib/firmware/fsm_msg_debug.h +++ b/lib/firmware/fsm_msg_debug.h @@ -46,14 +46,12 @@ void fsm_msgDebugLinkGetState(DebugLinkGetState* msg) { resp->storage_hash.size = memory_storage_hash(resp->storage_hash.bytes, storage_getLocation()); - /* Render pending animations ONLY if the animation queue is active. - * Static layouts (warning screens, address displays) write directly - * to the canvas — calling animate() unconditionally overwrites them - * with stale animation frames. Only run animations when queued. */ - if (is_animating()) { - force_animation_start(); - animate(); - } + /* Just refresh the display — don't force animations. + * The confirm() loop already ran animate() before sending ButtonRequest, + * so the canvas has the correct content. Calling force_animation_start() + * + animate() here would either: (a) do nothing if the queue is empty, + * or (b) re-run an animation that overwrites static content. + * display_refresh() ensures the framebuffer is synced for reading. */ display_refresh(); /* Pack 256x64 grayscale canvas into 1bpp layout for screenshot capture. diff --git a/lib/firmware/fsm_msg_solana.h b/lib/firmware/fsm_msg_solana.h index bb00eb894..ea3072b07 100644 --- a/lib/firmware/fsm_msg_solana.h +++ b/lib/firmware/fsm_msg_solana.h @@ -546,3 +546,114 @@ void fsm_msgSolanaSignMessage(const SolanaSignMessage* msg) { msg_write(MessageType_MessageType_SolanaMessageSignature, resp); layoutHome(); } + +void fsm_msgSolanaSignOffchainMessage(const SolanaSignOffchainMessage* msg) { + RESP_INIT(SolanaOffchainMessageSignature); + + CHECK_INITIALIZED + CHECK_PIN + + if (!msg->has_message || msg->message.size == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, _("Missing message")); + layoutHome(); + return; + } + + /* Validate format upfront so the user sees a meaningful error rather + * than a generic signing failure. The envelope's 0xFF prefix provides + * the domain separation that bare SolanaSignMessage lacks, so NO + * AdvancedMode gate is required here — that fence was a band-aid for + * the missing envelope. */ + uint32_t format = msg->has_message_format ? msg->message_format : 0; + if (format != 0 && format != 1) { + fsm_sendFailure( + FailureType_Failure_Other, + _("Off-chain format 2 (extended UTF-8) not supported on device")); + layoutHome(); + return; + } + + uint32_t version = msg->has_version ? msg->version : 0; + if (version != 0) { + fsm_sendFailure(FailureType_Failure_Other, + _("Unsupported off-chain message version")); + layoutHome(); + return; + } + + if (msg->message.size > 1212) { + fsm_sendFailure(FailureType_Failure_Other, + _("Off-chain message exceeds 1212-byte limit")); + layoutHome(); + return; + } + + /* Path validation: warn on non-standard derivation, mirroring the + * existing SolanaSignMessage handler. */ + if (!solana_pathIsStandard(msg->address_n, msg->address_n_count)) { + if (!confirm(ButtonRequestType_ButtonRequest_Other, "WARNING", + "Non-standard Solana derivation path. Continue?")) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + } + + HDNode* node = fsm_getDerivedNode(ED25519_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + hdnode_fill_public_key(node); + + /* Confirm dialog. Format 0 (ASCII) is always renderable; format 1 + * (UTF-8 limited) we render as printable bytes only — non-printable + * sequences fall through to a hex preview to avoid encoding + * surprises on the OLED. */ + { + char msgBuf[129] = {0}; + const char* typeLabel; + bool printable = true; + for (unsigned i = 0; i < msg->message.size; i++) { + if (msg->message.bytes[i] < 0x20 || msg->message.bytes[i] > 0x7e) { + printable = false; + break; + } + } + if (printable && msg->message.size <= sizeof(msgBuf) - 1) { + typeLabel = "Off-chain Message"; + memcpy(msgBuf, msg->message.bytes, msg->message.size); + msgBuf[msg->message.size] = '\0'; + } else { + typeLabel = "Off-chain Bytes"; + unsigned show = msg->message.size; + if (show > 32) show = 32; + for (unsigned i = 0; i < show; i++) { + snprintf(&msgBuf[2 * i], 3, "%02x", msg->message.bytes[i]); + } + msgBuf[2 * show] = '\0'; + if (msg->message.size > 32) { + snprintf(&msgBuf[64], sizeof(msgBuf) - 64, "... (%u bytes)", + (unsigned)msg->message.size); + } + } + if (!confirm(ButtonRequestType_ButtonRequest_ProtectCall, _(typeLabel), + "%s", msgBuf)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + + if (!solana_offchain_message_sign(node, msg, resp)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("Off-chain message signing failed")); + layoutHome(); + return; + } + + memzero(node, sizeof(*node)); + msg_write(MessageType_MessageType_SolanaOffchainMessageSignature, resp); + layoutHome(); +} diff --git a/lib/firmware/fsm_msg_ton.h b/lib/firmware/fsm_msg_ton.h index a0384b4f0..5f4844896 100644 --- a/lib/firmware/fsm_msg_ton.h +++ b/lib/firmware/fsm_msg_ton.h @@ -148,3 +148,95 @@ void fsm_msgTonSignTx(TonSignTx* msg) { msg_write(MessageType_MessageType_TonSignedTx, resp); layoutHome(); } + +void fsm_msgTonSignMessage(const TonSignMessage* msg) { + RESP_INIT(TonMessageSignature); + + CHECK_INITIALIZED + CHECK_PIN + + if (!msg->has_message || msg->message.size == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, _("Missing message")); + layoutHome(); + return; + } + + // Validate path: m/44'/607'/... + if (msg->address_n_count < 2 || msg->address_n[0] != (0x80000000 | 44) || + msg->address_n[1] != (0x80000000 | 607)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid TON path (expected m/44'/607'/...)")); + layoutHome(); + return; + } + + /* AdvancedMode gate: TON message signing is bare Ed25519 over arbitrary + * bytes — no domain separation. A signed message is indistinguishable + * over the wire from a signed transaction. Same fence as SolanaSignMessage + * (see fsm_msg_solana.h:461) until TON Connect's ton_proof envelope is + * added as a separate handler. */ + if (!storage_isPolicyEnabled("AdvancedMode")) { + (void)review(ButtonRequestType_ButtonRequest_Other, "Blocked", + "TON message signing is experimental. " + "Enable AdvancedMode in device settings."); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Message signing disabled by policy")); + layoutHome(); + return; + } + + HDNode* node = fsm_getDerivedNode(ED25519_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + hdnode_fill_public_key(node); + + /* Always require on-device confirmation. Display message content if + * printable, hex preview otherwise. */ + { + char msgBuf[129] = {0}; + const char* typeLabel; + bool printable = true; + for (unsigned i = 0; i < msg->message.size; i++) { + if (msg->message.bytes[i] < 0x20 || msg->message.bytes[i] > 0x7e) { + printable = false; + break; + } + } + if (printable && msg->message.size <= sizeof(msgBuf) - 1) { + typeLabel = "Sign TON Message"; + memcpy(msgBuf, msg->message.bytes, msg->message.size); + msgBuf[msg->message.size] = '\0'; + } else { + typeLabel = "Sign TON Bytes"; + unsigned show = msg->message.size; + if (show > 32) show = 32; + for (unsigned i = 0; i < show; i++) { + snprintf(&msgBuf[2 * i], 3, "%02x", msg->message.bytes[i]); + } + msgBuf[2 * show] = '\0'; + if (msg->message.size > 32) { + snprintf(&msgBuf[64], sizeof(msgBuf) - 64, "... (%u bytes)", + (unsigned)msg->message.size); + } + } + if (!confirm(ButtonRequestType_ButtonRequest_ProtectCall, _(typeLabel), + "%s", msgBuf)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } + + if (!ton_message_sign(node, msg, resp)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, _("TON message signing failed")); + layoutHome(); + return; + } + + memzero(node, sizeof(*node)); + msg_write(MessageType_MessageType_TonMessageSignature, resp); + layoutHome(); +} diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index 849603225..aee20c602 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -138,3 +138,225 @@ void fsm_msgTronSignTx(TronSignTx* msg) { msg_write(MessageType_MessageType_TronSignedTx, resp); layoutHome(); } + +#ifndef TRON_MSG_DISPLAY_MAX +#define TRON_MSG_DISPLAY_MAX \ + (38 * 3) // mirrors ETH MSG_MAX (3 lines × 38 chars) +#endif + +void fsm_msgTronSignMessage(TronSignMessage* msg) { + RESP_INIT(TronMessageSignature); + + CHECK_INITIALIZED + + CHECK_PIN + + // Validate path: m/44'/195'/... + if (msg->address_n_count < 3 || msg->address_n[0] != (0x80000000 | 44) || + msg->address_n[1] != (0x80000000 | 195)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid TRON path (expected m/44'/195'/...)")); + layoutHome(); + return; + } + + char msgBuf[TRON_MSG_DISPLAY_MAX + 1] = {0}; + const char* typeIndicator; + bool canPrint = true; + unsigned ctr; + + for (ctr = 0; ctr < msg->message.size; ctr++) { + if (isprint(msg->message.bytes[ctr]) == false) { + canPrint = false; + break; + } + } + + if (canPrint) { + typeIndicator = "Sign TRON Message"; + unsigned copy = msg->message.size; + if (copy > TRON_MSG_DISPLAY_MAX) copy = TRON_MSG_DISPLAY_MAX; + memcpy(msgBuf, msg->message.bytes, copy); + msgBuf[copy] = '\0'; + } else { + typeIndicator = "Sign TRON Bytes"; + unsigned hexBytes = msg->message.size; + if (hexBytes * 2 > TRON_MSG_DISPLAY_MAX) { + hexBytes = TRON_MSG_DISPLAY_MAX / 2; + } + for (ctr = 0; ctr < hexBytes; ctr++) { + snprintf(&msgBuf[2 * ctr], 3, "%02x", msg->message.bytes[ctr]); + } + } + + if (!confirm(ButtonRequestType_ButtonRequest_ProtectCall, _(typeIndicator), + "%s", msgBuf)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + HDNode* node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + hdnode_fill_public_key(node); + + if (!tron_message_sign(node, msg, resp)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("TRON message signing failed")); + layoutHome(); + return; + } + + memzero(node, sizeof(*node)); + msg_write(MessageType_MessageType_TronMessageSignature, resp); + layoutHome(); +} + +void fsm_msgTronVerifyMessage(const TronVerifyMessage* msg) { + CHECK_PARAM(msg->has_address, _("No address provided")); + CHECK_PARAM(msg->has_message, _("No message provided")); + CHECK_PARAM(msg->has_signature, _("No signature provided")); + + if (tron_message_verify(msg) != 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, _("Invalid signature")); + return; + } + + if (!confirm_address(_("Confirm Signer"), msg->address)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + char msgBuf[TRON_MSG_DISPLAY_MAX + 1] = {0}; + const char* typeIndicator; + bool canPrint = true; + unsigned ctr; + + for (ctr = 0; ctr < msg->message.size; ctr++) { + if (isprint(msg->message.bytes[ctr]) == false) { + canPrint = false; + break; + } + } + + if (canPrint) { + typeIndicator = "Message Verified"; + unsigned copy = msg->message.size; + if (copy > TRON_MSG_DISPLAY_MAX) copy = TRON_MSG_DISPLAY_MAX; + memcpy(msgBuf, msg->message.bytes, copy); + msgBuf[copy] = '\0'; + } else { + typeIndicator = "Bytes Verified"; + unsigned hexBytes = msg->message.size; + if (hexBytes * 2 > TRON_MSG_DISPLAY_MAX) { + hexBytes = TRON_MSG_DISPLAY_MAX / 2; + } + for (ctr = 0; ctr < hexBytes; ctr++) { + snprintf(&msgBuf[2 * ctr], 3, "%02x", msg->message.bytes[ctr]); + } + } + + if (!confirm(ButtonRequestType_ButtonRequest_Other, _(typeIndicator), "%s", + msgBuf)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + fsm_sendSuccess(_("Message verified")); + layoutHome(); +} +void fsm_msgTronSignTypedHash(const TronSignTypedHash* msg) { + RESP_INIT(TronTypedDataSignature); + + CHECK_INITIALIZED + + CHECK_PIN + + // Validate path: m/44'/195'/... + if (msg->address_n_count < 3 || msg->address_n[0] != (0x80000000 | 44) || + msg->address_n[1] != (0x80000000 | 195)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid TRON path (expected m/44'/195'/...)")); + layoutHome(); + return; + } + + if (msg->domain_separator_hash.size != 32 || + (msg->has_message_hash && msg->message_hash.size != 32)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid TIP-712 hash length")); + layoutHome(); + return; + } + + HDNode* node = fsm_getDerivedNode(SECP256K1_NAME, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + hdnode_fill_public_key(node); + + // Derive Base58Check address for confirm dialog + response. + char address[TRON_ADDRESS_MAX_LEN]; + if (!tron_getAddress(node->public_key, address, sizeof(address))) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, _("Address derivation failed")); + layoutHome(); + return; + } + + if (!confirm(ButtonRequestType_ButtonRequest_Other, "Verify Address", + "Confirm address: %s", address)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + // Show domain separator hash as 64-char hex. + char str[64 + 1]; + for (int ctr = 0; ctr < 64 / 2; ctr++) { + snprintf(&str[2 * ctr], 3, "%02x", msg->domain_separator_hash.bytes[ctr]); + } + if (!confirm(ButtonRequestType_ButtonRequest_Other, "TIP-712 domain", + "Confirm hash digest: %s", str)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + if (msg->has_message_hash) { + for (int ctr = 0; ctr < 64 / 2; ctr++) { + snprintf(&str[2 * ctr], 3, "%02x", msg->message_hash.bytes[ctr]); + } + if (!confirm(ButtonRequestType_ButtonRequest_Other, "TIP-712 message", + "Confirm hash digest: %s", str)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + } else { + if (!confirm(ButtonRequestType_ButtonRequest_Other, "TIP-712 message", + "Confirm: No message")) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + } + + if (!tron_typed_hash_sign(node, msg, resp)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("TIP-712 hash signing failed")); + layoutHome(); + return; + } + + memzero(node, sizeof(*node)); + msg_write(MessageType_MessageType_TronTypedDataSignature, resp); + layoutHome(); +} diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index 18de20abf..ee82cc2bd 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -135,25 +135,34 @@ /* TRON */ MSG_IN(MessageType_MessageType_TronGetAddress, TronGetAddress, fsm_msgTronGetAddress) MSG_IN(MessageType_MessageType_TronSignTx, TronSignTx, fsm_msgTronSignTx) + MSG_IN(MessageType_MessageType_TronSignMessage, TronSignMessage, fsm_msgTronSignMessage) + MSG_IN(MessageType_MessageType_TronVerifyMessage, TronVerifyMessage, fsm_msgTronVerifyMessage) + MSG_IN(MessageType_MessageType_TronSignTypedHash, TronSignTypedHash, fsm_msgTronSignTypedHash) MSG_OUT(MessageType_MessageType_TronAddress, TronAddress, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_TronSignedTx, TronSignedTx, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_TronMessageSignature, TronMessageSignature, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_TronTypedDataSignature, TronTypedDataSignature, NO_PROCESS_FUNC) /* TON */ MSG_IN(MessageType_MessageType_TonGetAddress, TonGetAddress, fsm_msgTonGetAddress) MSG_IN(MessageType_MessageType_TonSignTx, TonSignTx, fsm_msgTonSignTx) + MSG_IN(MessageType_MessageType_TonSignMessage, TonSignMessage, fsm_msgTonSignMessage) MSG_OUT(MessageType_MessageType_TonAddress, TonAddress, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_TonSignedTx, TonSignedTx, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_TonMessageSignature, TonMessageSignature, NO_PROCESS_FUNC) /* Solana */ MSG_IN(MessageType_MessageType_SolanaGetAddress, SolanaGetAddress, fsm_msgSolanaGetAddress) MSG_IN(MessageType_MessageType_SolanaSignTx, SolanaSignTx, fsm_msgSolanaSignTx) MSG_IN(MessageType_MessageType_SolanaSignMessage, SolanaSignMessage, fsm_msgSolanaSignMessage) + MSG_IN(MessageType_MessageType_SolanaSignOffchainMessage, SolanaSignOffchainMessage, fsm_msgSolanaSignOffchainMessage) MSG_OUT(MessageType_MessageType_SolanaAddress, SolanaAddress, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_SolanaSignedTx, SolanaSignedTx, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_SolanaMessageSignature, SolanaMessageSignature, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_SolanaOffchainMessageSignature, SolanaOffchainMessageSignature, NO_PROCESS_FUNC) #if DEBUG_LINK /* Debug Messages */ diff --git a/lib/firmware/solana.c b/lib/firmware/solana.c index 4b8ee78c1..abadf4cfe 100644 --- a/lib/firmware/solana.c +++ b/lib/firmware/solana.c @@ -674,3 +674,82 @@ bool solana_signTx(const HDNode* node, const SolanaSignTx* msg, return true; } + +/* ------------------------------------------------------------------ */ +/* Off-chain message signing (domain-separated) */ +/* ------------------------------------------------------------------ */ + +/* Per the Solana off-chain message spec, the device signs over the + * envelope: + * + * "\xff" || "solana offchain" || version:u8 || format:u8 + * || length:u16 LE || message bytes + * + * The 0xff lead byte is invalid as a Solana transaction prefix, so a + * signed off-chain message can NEVER be replayed as a transaction — + * this is the domain separation that bare SolanaSignMessage lacks. */ + +#define SOL_OFFCHAIN_PREFIX_LEN 1 +#define SOL_OFFCHAIN_TAG "solana offchain" +#define SOL_OFFCHAIN_TAG_LEN 15 +#define SOL_OFFCHAIN_HEADER_LEN \ + (SOL_OFFCHAIN_PREFIX_LEN + SOL_OFFCHAIN_TAG_LEN + 1 /*version*/ \ + + 1 /*format*/ + 2 /*length*/) + +#define SOL_OFFCHAIN_FORMAT_ASCII 0 +#define SOL_OFFCHAIN_FORMAT_UTF8_LIMITED 1 +#define SOL_OFFCHAIN_FORMAT_UTF8_EXTENDED 2 /* not supported on this device */ +#define SOL_OFFCHAIN_MAX_MSG_LEN 1212 /* spec ceiling for fmt 0 / 1 */ + +#define SOL_OFFCHAIN_ENVELOPE_MAX \ + (SOL_OFFCHAIN_HEADER_LEN + SOL_OFFCHAIN_MAX_MSG_LEN) + +bool solana_offchain_message_sign(const HDNode* node, + const SolanaSignOffchainMessage* msg, + SolanaOffchainMessageSignature* resp) { + if (!node || !msg || !resp) return false; + if (!msg->has_message || msg->message.size == 0) return false; + if (msg->message.size > SOL_OFFCHAIN_MAX_MSG_LEN) return false; + + /* Reject format 2 — extended UTF-8 mode is Ledger-only blind-sign and + * the proto's max_size cap (1212) wouldn't permit its real ceiling + * anyway. Force callers onto format 0 or 1. */ + uint8_t format = msg->has_message_format + ? (uint8_t)(msg->message_format & 0xFF) + : SOL_OFFCHAIN_FORMAT_ASCII; + if (format != SOL_OFFCHAIN_FORMAT_ASCII && + format != SOL_OFFCHAIN_FORMAT_UTF8_LIMITED) { + return false; + } + + uint8_t version = msg->has_version ? (uint8_t)(msg->version & 0xFF) : 0; + if (version != 0) return false; /* spec: only version 0 defined */ + + uint8_t envelope[SOL_OFFCHAIN_ENVELOPE_MAX]; + size_t off = 0; + envelope[off++] = 0xFF; + memcpy(&envelope[off], SOL_OFFCHAIN_TAG, SOL_OFFCHAIN_TAG_LEN); + off += SOL_OFFCHAIN_TAG_LEN; + envelope[off++] = version; + envelope[off++] = format; + /* length is u16 little-endian */ + envelope[off++] = (uint8_t)(msg->message.size & 0xFF); + envelope[off++] = (uint8_t)((msg->message.size >> 8) & 0xFF); + memcpy(&envelope[off], msg->message.bytes, msg->message.size); + off += msg->message.size; + + uint8_t sig[SOL_SIG_SIZE]; + ed25519_sign(envelope, off, node->private_key, node->public_key + 1, sig); + + resp->has_public_key = true; + resp->public_key.size = SOL_PUBKEY_SIZE; + memcpy(resp->public_key.bytes, node->public_key + 1, SOL_PUBKEY_SIZE); + + resp->has_signature = true; + resp->signature.size = SOL_SIG_SIZE; + memcpy(resp->signature.bytes, sig, SOL_SIG_SIZE); + + memzero(envelope, sizeof(envelope)); + memzero(sig, sizeof(sig)); + return true; +} diff --git a/lib/firmware/ton.c b/lib/firmware/ton.c index 32354a71b..043e41c9d 100644 --- a/lib/firmware/ton.c +++ b/lib/firmware/ton.c @@ -252,3 +252,37 @@ bool ton_signTx(const HDNode* node, const TonSignTx* msg, TonSignedTx* resp) { return true; } + +/** + * Sign an arbitrary message with raw Ed25519. + * + * NOTE: this is a bare Ed25519 signature over message bytes, with NO + * domain separation. A signed "message" is indistinguishable from a + * signed transaction over the wire — TON Connect's `ton_proof` envelope + * (with its own prefix + workchain + address binding) is the proper + * domain-separated path and should be added as a separate proto+handler + * before this primitive is exposed without an AdvancedMode gate. + * + * Caller must have populated node->public_key via hdnode_fill_public_key. + */ +bool ton_message_sign(const HDNode* node, const TonSignMessage* msg, + TonMessageSignature* resp) { + if (!node || !msg || !resp) { + return false; + } + + ed25519_signature signature; + ed25519_sign(msg->message.bytes, msg->message.size, node->private_key, + &node->public_key[1], signature); + + resp->has_public_key = true; + resp->public_key.size = 32; + memcpy(resp->public_key.bytes, &node->public_key[1], 32); + + resp->has_signature = true; + resp->signature.size = 64; + memcpy(resp->signature.bytes, signature, 64); + + memzero(signature, sizeof(signature)); + return true; +} diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index d44974fe7..b8d3ec618 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -130,3 +130,217 @@ bool tron_signTx(const HDNode* node, const TronSignTx* msg, return true; } + +static int tron_is_canonic(uint8_t v, uint8_t signature[64]) { + // Match ethereum_is_canonic: accept only recovery IDs 0 and 1 (reject + // 2 and 3). Mirrors verifier expectations across the TRON/EVM ecosystem. + // Returning non-zero means "canonical, accept"; ecdsa_sign_digest retries + // when this returns 0, so a permanent 0 here causes signing to fail. + (void)signature; + return (v & 2) == 0; +} + +/** + * Compute the TIP-191 personal_sign hash: + * keccak256("\x19TRON Signed Message:\n" || ASCII(len) || message) + * + * Mirrors ethereum_message_hash() — only the prefix differs. + */ +static void tron_message_hash(const uint8_t* message, size_t message_len, + uint8_t hash[32]) { + struct SHA3_CTX ctx; + uint8_t c; + + sha3_256_Init(&ctx); + sha3_Update(&ctx, (const uint8_t*)"\x19" "TRON Signed Message:\n", 22); + if (message_len >= 1000000000) { + c = '0' + message_len / 1000000000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 100000000) { + c = '0' + message_len / 100000000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 10000000) { + c = '0' + message_len / 10000000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 1000000) { + c = '0' + message_len / 1000000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 100000) { + c = '0' + message_len / 100000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 10000) { + c = '0' + message_len / 10000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 1000) { + c = '0' + message_len / 1000 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 100) { + c = '0' + message_len / 100 % 10; + sha3_Update(&ctx, &c, 1); + } + if (message_len >= 10) { + c = '0' + message_len / 10 % 10; + sha3_Update(&ctx, &c, 1); + } + c = '0' + message_len % 10; + sha3_Update(&ctx, &c, 1); + sha3_Update(&ctx, message, message_len); + keccak_Final(&ctx, hash); +} + +/** + * Sign an arbitrary message under TIP-191 personal_sign. + * Output signature is 65 bytes (r || s || v) where v = 27 + recovery_id. + */ +bool tron_message_sign(const HDNode* node, const TronSignMessage* msg, + TronMessageSignature* resp) { + if (!node || !msg || !resp) { + return false; + } + + // Caller must have populated node->public_key (hdnode_fill_public_key). + char address[TRON_ADDRESS_MAX_LEN]; + if (!tron_getAddress(node->public_key, address, sizeof(address))) { + return false; + } + + uint8_t hash[32]; + tron_message_hash(msg->message.bytes, msg->message.size, hash); + + uint8_t v; + if (ecdsa_sign_digest(&secp256k1, node->private_key, hash, + resp->signature.bytes, &v, tron_is_canonic) != 0) { + memzero(hash, sizeof(hash)); + return false; + } + + resp->signature.bytes[64] = 27 + v; + resp->signature.size = 65; + resp->has_signature = true; + + strlcpy(resp->address, address, sizeof(resp->address)); + resp->has_address = true; + + memzero(hash, sizeof(hash)); + return true; +} + +/** + * Verify a TIP-191 signature against the claimed Base58Check TRON address. + * Returns 0 on success, non-zero on malformed input or signature mismatch. + */ +int tron_message_verify(const TronVerifyMessage* msg) { + if (!msg || msg->signature.size != 65) { + return 1; + } + + uint8_t pubkey[65]; + uint8_t hash[32]; + + tron_message_hash(msg->message.bytes, msg->message.size, hash); + + uint8_t v = msg->signature.bytes[64]; + if (v >= 27) { + v -= 27; + } + if (v >= 2 || ecdsa_recover_pub_from_sig( + &secp256k1, pubkey, msg->signature.bytes, hash, v) != 0) { + memzero(hash, sizeof(hash)); + return 2; + } + + uint8_t addr_hash[32]; + keccak_256(pubkey + 1, 64, addr_hash); + + uint8_t addr_bytes[21]; + addr_bytes[0] = TRON_ADDRESS_PREFIX; + memcpy(addr_bytes + 1, addr_hash + 12, 20); + + char recovered_addr[TRON_ADDRESS_MAX_LEN]; + if (!base58_encode_check(addr_bytes, 21, HASHER_SHA2D, recovered_addr, + sizeof(recovered_addr))) { + memzero(hash, sizeof(hash)); + memzero(addr_hash, sizeof(addr_hash)); + return 2; + } + + int rv = (strcmp(recovered_addr, msg->address) != 0) ? 2 : 0; + + memzero(hash, sizeof(hash)); + memzero(addr_hash, sizeof(addr_hash)); + return rv; +} + +/** + * Compute the TIP-712 typed-data digest: + * keccak256("\x19\x01" || domain_separator_hash || message_hash) + * + * If the typed-data primaryType is the EIP712Domain itself, message_hash + * is empty/absent and the digest folds in only the domain separator. + * + * Mirrors ethereum_typed_hash() — TIP-712 uses the same '\x19\x01' prefix + * as EIP-712. + */ +static void tron_typed_hash(const uint8_t domain_separator_hash[32], + const uint8_t message_hash[32], + bool has_message_hash, uint8_t hash[32]) { + struct SHA3_CTX ctx = {0}; + sha3_256_Init(&ctx); + sha3_Update(&ctx, (const uint8_t*)"\x19\x01", 2); + sha3_Update(&ctx, domain_separator_hash, 32); + if (has_message_hash) { + sha3_Update(&ctx, message_hash, 32); + } + keccak_Final(&ctx, hash); +} + +/** + * Sign a TIP-712 typed-data digest. Host pre-computes the domain + * separator hash and message hash per the TIP-712 spec; the device just + * signs the assembled digest with secp256k1 + recovery id. + * + * Reuses tron_is_canonic from the TIP-191 path — both impose the same + * v-in-{0,1} canonicality required by EVM-style verifiers. + * + * Caller must have populated node->public_key (hdnode_fill_public_key). + */ +bool tron_typed_hash_sign(const HDNode* node, const TronSignTypedHash* msg, + TronTypedDataSignature* resp) { + if (!node || !msg || !resp) { + return false; + } + if (msg->domain_separator_hash.size != 32 || + (msg->has_message_hash && msg->message_hash.size != 32)) { + return false; + } + + char address[TRON_ADDRESS_MAX_LEN]; + if (!tron_getAddress(node->public_key, address, sizeof(address))) { + return false; + } + + uint8_t hash[32] = {0}; + tron_typed_hash(msg->domain_separator_hash.bytes, msg->message_hash.bytes, + msg->has_message_hash, hash); + + uint8_t v = 0; + if (ecdsa_sign_digest(&secp256k1, node->private_key, hash, + resp->signature.bytes, &v, tron_is_canonic) != 0) { + memzero(hash, sizeof(hash)); + return false; + } + + resp->signature.bytes[64] = 27 + v; + resp->signature.size = 65; + strlcpy(resp->address, address, sizeof(resp->address)); + + memzero(hash, sizeof(hash)); + return true; +} diff --git a/tools/emulator/CMakeLists.txt b/tools/emulator/CMakeLists.txt index 16aff0c9a..63b80bd88 100644 --- a/tools/emulator/CMakeLists.txt +++ b/tools/emulator/CMakeLists.txt @@ -8,14 +8,7 @@ if(${KK_EMULATOR}) ${CMAKE_BINARY_DIR}/include ${CMAKE_SOURCE_DIR}/deps/crypto/trezor-crypto) - add_executable(kkemu ${sources}) - - # Add linker flags for ARM64 Mac compatibility - if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") - target_link_options(kkemu PRIVATE "-Wl,-no_fixup_chains") - endif() - - target_link_libraries(kkemu + set(FIRMWARE_LIBS kkfirmware kkfirmware.keepkey kkboard @@ -26,6 +19,23 @@ if(${KK_EMULATOR}) trezorcrypto qrcodegenerator SecAESSTM32 - kkrand - kkemulator) + kkrand) + + # Standalone emulator binary (uses UDP sockets) + add_executable(kkemu ${sources}) + + # Add linker flags for ARM64 Mac compatibility + if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") + target_link_options(kkemu PRIVATE "-Wl,-no_fixup_chains") + endif() + + target_link_libraries(kkemu ${FIRMWARE_LIBS} kkemulator) + + # Shared library (ring buffers, no sockets) for in-process FFI (vault) + if(KK_BUILD_DYLIB) + target_link_libraries(kkemulator_dylib ${FIRMWARE_LIBS}) + if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") + target_link_options(kkemulator_dylib PRIVATE "-Wl,-no_fixup_chains") + endif() + endif() endif() From 40eda376d7bad5a106225b5e6075a0193dd6cf18 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 30 Apr 2026 17:30:09 -0500 Subject: [PATCH 02/13] test(dylib): bump python-keepkey to add test_dylib_screenshot.py CI's python-dylib-tests job runs `pytest test_dylib_screenshot.py` from deps/python-keepkey/tests, but the previous submodule pin (7141dc8) did not contain that file, so pytest exited 4 ("file or directory not found") and the job failed. Bump python-keepkey to 571c829, which adds the regression test for the ringbuf capacity + DebugLinkGetState canvas-refresh fixes that landed on this release branch. --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index 7141dc81c..571c829ee 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 7141dc81c72c06327118ba00dbd06b4b44dda476 +Subproject commit 571c829eef38a160a20e1416a5166a005b2bcec1 From 51a63ee7dce3d43a3c091c95ee885312d92fd542 Mon Sep 17 00:00:00 2001 From: highlander Date: Sun, 3 May 2026 17:01:13 -0600 Subject: [PATCH 03/13] test(dylib): bump python-keepkey to wire DylibTransport for screenshot tests The screenshot regression added in 40eda37 hung CI for 25min because tests/config.py had no KK_TRANSPORT=dylib branch, so it fell through to UDPTransport against an emulator that the python-dylib-tests job doesn't start. Bumps python-keepkey to f1a77c3 which adds: - keepkeylib/transport_dylib.py: ctypes singleton over libkkemu, pumps kkemu_poll on every read/write - tests/config.py: KK_TRANSPORT=dylib KK_DYLIB=... branch + a _KNOWN_TRANSPORTS guard so a typo'd value errors at import instead of silently falling back to UDP - tests/test_dylib_confirm_flow.py: confirm-flow regression (skipped pending the firmware confirm-helper fix) Verified locally: 5 passed, 1 skipped in 0.22s. --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index 571c829ee..f1a77c3e0 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 571c829eef38a160a20e1416a5166a005b2bcec1 +Subproject commit f1a77c3e04b7c951e292e550bed5b20d675e0329 From 6a2c16e0338328b8c6765f62d5fc881ed2d6a77c Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 9 May 2026 17:55:23 -0300 Subject: [PATCH 04/13] chore(submodule): bump python-keepkey to 86e88b1 (latest release/7.14.1-python-keepkey) --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index f1a77c3e0..86e88b1db 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit f1a77c3e04b7c951e292e550bed5b20d675e0329 +Subproject commit 86e88b1dbaed74e056632c0531a9ae56e356ff6a From 58d0c7de99be5e67d5498253ec4a6cf8aa7046fb Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 9 May 2026 18:03:58 -0300 Subject: [PATCH 05/13] chore(submodule): bump python-keepkey + device-protocol to master tips --- deps/device-protocol | 2 +- deps/python-keepkey | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/device-protocol b/deps/device-protocol index 73ca75f68..d637b7829 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit 73ca75f6822ea7727861c122f2fd5b1a6bc67461 +Subproject commit d637b78291a423fd8119df9935a9365be8a7758e diff --git a/deps/python-keepkey b/deps/python-keepkey index 86e88b1db..fabd6c618 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 86e88b1dbaed74e056632c0531a9ae56e356ff6a +Subproject commit fabd6c6189b7f1b3ea7cbd1d372fc13729761178 From 763ed49f1d551dcfc6da70e3384bac8509781cbc Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 9 May 2026 18:17:27 -0300 Subject: [PATCH 06/13] chore(submodule): track master for device-protocol + python-keepkey Both release branches are now merged to master. --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 25a7f2fe4..2d6c4446a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "deps/device-protocol"] path = deps/device-protocol url = https://github.com/keepkey/device-protocol.git -branch = release/7.14.1-device-protocol +branch = master [submodule "deps/trezor-firmware"] path = deps/crypto/trezor-firmware url = https://github.com/keepkey/trezor-firmware.git @@ -14,7 +14,7 @@ url = https://github.com/keepkey/code-signing-keys.git [submodule "deps/python-keepkey"] path = deps/python-keepkey url = https://github.com/keepkey/python-keepkey.git -branch = release/7.14.1-python-keepkey +branch = master [submodule "deps/qrenc/QR-Code-generator"] path = deps/qrenc/QR-Code-generator url = https://github.com/keepkey/QR-Code-generator.git From 2746ed3ac6277054cfe94141f9f95e73bef8c4fd Mon Sep 17 00:00:00 2001 From: pastaghost <62026038+pastaghost@users.noreply.github.com> Date: Sat, 9 May 2026 15:23:50 -0600 Subject: [PATCH 07/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/firmware/tron.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index b8d3ec618..f8d3198c6 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -339,7 +339,9 @@ bool tron_typed_hash_sign(const HDNode* node, const TronSignTypedHash* msg, resp->signature.bytes[64] = 27 + v; resp->signature.size = 65; + resp->has_signature = true; strlcpy(resp->address, address, sizeof(resp->address)); + resp->has_address = true; memzero(hash, sizeof(hash)); return true; From e0bd9d105c2c3d8a7b9c914c6c5b772654e51ac0 Mon Sep 17 00:00:00 2001 From: Highlander Date: Sat, 9 May 2026 18:25:14 -0300 Subject: [PATCH 08/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- include/keepkey/emulator/libkkemu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/keepkey/emulator/libkkemu.h b/include/keepkey/emulator/libkkemu.h index f0f0020d9..269f8e471 100644 --- a/include/keepkey/emulator/libkkemu.h +++ b/include/keepkey/emulator/libkkemu.h @@ -72,7 +72,7 @@ int kkemu_read(uint8_t* buf, size_t len, int iface); * * Call this at 10-60 Hz from your event loop. * - * @return Number of messages processed, or -1 on error. + * @return 0 on success, or -1 if the emulator is not initialized. */ int kkemu_poll(void); From 51c47526c0ba2c4e9e01ad72230dd1a30e21f99b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 21:29:03 +0000 Subject: [PATCH 09/13] fix: replace volatile+__sync_synchronize with C11 atomics in ringbuf Agent-Logs-Url: https://github.com/keepkey/keepkey-firmware/sessions/b03afbba-5c03-46ee-8b5f-2901060e7b9e Co-authored-by: pastaghost <62026038+pastaghost@users.noreply.github.com> --- lib/emulator/ringbuf.c | 31 +++++++++++++++++++++---------- lib/emulator/ringbuf.h | 14 +++++++++----- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/emulator/ringbuf.c b/lib/emulator/ringbuf.c index 779a06e81..ecb32aa95 100644 --- a/lib/emulator/ringbuf.c +++ b/lib/emulator/ringbuf.c @@ -1,39 +1,50 @@ /* * Lock-free SPSC ring buffer for 64-byte HID reports. + * Uses C11 atomics with explicit acquire/release memory orders so that + * concurrent producer/consumer threads are well-defined (no UB). */ #include "ringbuf.h" #include -void ringbuf_init(RingBuf* rb) { memset(rb, 0, sizeof(*rb)); } +void ringbuf_init(RingBuf* rb) { + memset(rb->data, 0, sizeof(rb->data)); + atomic_init(&rb->head, 0); + atomic_init(&rb->tail, 0); +} bool ringbuf_push(RingBuf* rb, const uint8_t* msg, size_t len) { if (len > RINGBUF_SLOT_SIZE) return false; - uint32_t head = rb->head; + uint32_t head = atomic_load_explicit(&rb->head, memory_order_relaxed); uint32_t next = (head + 1) % RINGBUF_CAPACITY; - if (next == rb->tail) return false; /* full */ + if (next == atomic_load_explicit(&rb->tail, memory_order_acquire)) + return false; /* full */ memcpy(rb->data[head], msg, len); if (len < RINGBUF_SLOT_SIZE) memset(rb->data[head] + len, 0, RINGBUF_SLOT_SIZE - len); - __sync_synchronize(); /* memory barrier before publishing head */ - rb->head = next; + atomic_store_explicit(&rb->head, next, memory_order_release); return true; } bool ringbuf_pop(RingBuf* rb, uint8_t* msg, size_t len) { - uint32_t tail = rb->tail; + uint32_t tail = atomic_load_explicit(&rb->tail, memory_order_relaxed); - if (tail == rb->head) return false; /* empty */ + if (tail == atomic_load_explicit(&rb->head, memory_order_acquire)) + return false; /* empty */ size_t copy = len < RINGBUF_SLOT_SIZE ? len : RINGBUF_SLOT_SIZE; memcpy(msg, rb->data[tail], copy); - __sync_synchronize(); /* memory barrier before advancing tail */ - rb->tail = (tail + 1) % RINGBUF_CAPACITY; + atomic_store_explicit(&rb->tail, (tail + 1) % RINGBUF_CAPACITY, + memory_order_release); return true; } -bool ringbuf_empty(const RingBuf* rb) { return rb->head == rb->tail; } +bool ringbuf_empty(RingBuf* rb) { + uint32_t head = atomic_load_explicit(&rb->head, memory_order_relaxed); + uint32_t tail = atomic_load_explicit(&rb->tail, memory_order_relaxed); + return head == tail; +} diff --git a/lib/emulator/ringbuf.h b/lib/emulator/ringbuf.h index 45cddad56..2c6b610cd 100644 --- a/lib/emulator/ringbuf.h +++ b/lib/emulator/ringbuf.h @@ -1,13 +1,17 @@ /* * Lock-free single-producer single-consumer ring buffer for HID reports. + * head/tail use C11 _Atomic with explicit acquire/release memory orders so + * that concurrent access from separate producer and consumer threads is + * well-defined (no data race, no UB). * Used by libkkemu to pass 64-byte messages between host and firmware. */ #ifndef RINGBUF_H #define RINGBUF_H -#include -#include +#include #include +#include +#include #define RINGBUF_SLOT_SIZE 64 /* HID report size */ @@ -31,13 +35,13 @@ typedef struct { uint8_t data[RINGBUF_CAPACITY][RINGBUF_SLOT_SIZE]; - volatile uint32_t head; /* written by producer */ - volatile uint32_t tail; /* written by consumer */ + _Atomic uint32_t head; /* written by producer */ + _Atomic uint32_t tail; /* written by consumer */ } RingBuf; void ringbuf_init(RingBuf* rb); bool ringbuf_push(RingBuf* rb, const uint8_t* msg, size_t len); bool ringbuf_pop(RingBuf* rb, uint8_t* msg, size_t len); -bool ringbuf_empty(const RingBuf* rb); +bool ringbuf_empty(RingBuf* rb); #endif From 37a6fa94e3cb4870fb655c6781e63e651289f163 Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 9 May 2026 18:33:15 -0300 Subject: [PATCH 10/13] fix(tron): drop bogus has_signature/has_address on TronTypedDataSignature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot autofix in 2746ed3a copy-pasted the has_* pattern from the TronMessageSignature path (where fields are 'optional' so nanopb emits has_* flags) into tron_typed_hash_sign(), where the response type is TronTypedDataSignature with 'required' fields — no has_* exists, so the build broke on every target. --- lib/firmware/tron.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index f8d3198c6..b8d3ec618 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -339,9 +339,7 @@ bool tron_typed_hash_sign(const HDNode* node, const TronSignTypedHash* msg, resp->signature.bytes[64] = 27 + v; resp->signature.size = 65; - resp->has_signature = true; strlcpy(resp->address, address, sizeof(resp->address)); - resp->has_address = true; memzero(hash, sizeof(hash)); return true; From 0f7315820b64b20a33d4d4a9fad5db0df98652bf Mon Sep 17 00:00:00 2001 From: pastaghost <62026038+pastaghost@users.noreply.github.com> Date: Sat, 9 May 2026 15:44:19 -0600 Subject: [PATCH 11/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/firmware/tron.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index b8d3ec618..f8d3198c6 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -339,7 +339,9 @@ bool tron_typed_hash_sign(const HDNode* node, const TronSignTypedHash* msg, resp->signature.bytes[64] = 27 + v; resp->signature.size = 65; + resp->has_signature = true; strlcpy(resp->address, address, sizeof(resp->address)); + resp->has_address = true; memzero(hash, sizeof(hash)); return true; From 78314fd06ef1b837f45e845158634e80b67db1cd Mon Sep 17 00:00:00 2001 From: pastaghost <62026038+pastaghost@users.noreply.github.com> Date: Sat, 9 May 2026 15:46:22 -0600 Subject: [PATCH 12/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- include/keepkey/emulator/libkkemu.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/include/keepkey/emulator/libkkemu.h b/include/keepkey/emulator/libkkemu.h index 269f8e471..ec75ff957 100644 --- a/include/keepkey/emulator/libkkemu.h +++ b/include/keepkey/emulator/libkkemu.h @@ -79,10 +79,18 @@ int kkemu_poll(void); /** * Get the OLED framebuffer (256x64, 1-bit per pixel = 2048 bytes). * + * This returns a pointer to internal scratch storage containing a snapshot + * of the current display in packed SSD1306 page format. + * * @param width Receives 256. * @param height Receives 64. - * @return Pointer to framebuffer (valid until next kkemu_poll). - * Returns NULL if emulator is not initialized. + * @return Pointer to framebuffer data. The pointer remains valid only until + * the next call to kkemu_get_display(), which overwrites the same + * scratch buffer. Calling kkemu_poll() may update the emulator's + * display state, but it does not refresh previously returned data + * in place; call kkemu_get_display() again after kkemu_poll() to + * obtain an updated framebuffer snapshot. Returns NULL if emulator + * is not initialized. */ const uint8_t* kkemu_get_display(int* width, int* height); From 104a9cf4d98e3fe68cfe30a9ffb193056e046d50 Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 9 May 2026 18:51:49 -0300 Subject: [PATCH 13/13] fix(tron): re-revert bogus has_* on TronTypedDataSignature (re-introduced by 0f731582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Copilot Autofix suggestion that broke the build at 2746ed3a was re-applied via the GitHub UI in 0f731582 with the same payload. TronTypedDataSignature.{address,signature} are 'required' in the proto so nanopb does not emit has_* flags — the lines do not compile. Please dismiss the underlying code-scanning finding rather than re-applying the suggested fix, or this will recur. --- lib/firmware/tron.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/firmware/tron.c b/lib/firmware/tron.c index f8d3198c6..b8d3ec618 100644 --- a/lib/firmware/tron.c +++ b/lib/firmware/tron.c @@ -339,9 +339,7 @@ bool tron_typed_hash_sign(const HDNode* node, const TronSignTypedHash* msg, resp->signature.bytes[64] = 27 + v; resp->signature.size = 65; - resp->has_signature = true; strlcpy(resp->address, address, sizeof(resp->address)); - resp->has_address = true; memzero(hash, sizeof(hash)); return true;