Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f75dd42
feat(emulator): libkkemu shared library + native macOS build
BitHighlander Apr 27, 2026
bab2262
fix(emulator): clear canvas semantics for DebugLinkGetState screensho…
BitHighlander Apr 27, 2026
9c48feb
fix(emulator): size libkkemu ring buffers for the largest synchronous…
BitHighlander Apr 27, 2026
21fb5fc
fix(emulator): set CMAKE_POSITION_INDEPENDENT_CODE globally for KK_BU…
BitHighlander Apr 27, 2026
46f5330
fix(emulator): surface mlock failure + zero sensitive buffers on shut…
BitHighlander Apr 27, 2026
5694566
ci(emulator): pin python-keepkey + add native-dylib screenshot test job
BitHighlander Apr 27, 2026
860cb8a
fix(ci): correct protoc 3.21 download URL (version reset)
BitHighlander Apr 27, 2026
f92fd20
fix(ci): init googletest submodule for python-dylib-tests
BitHighlander Apr 27, 2026
7ad7501
fix(ci): generate nanopb's own .pb2 files with pinned protoc 3.21
BitHighlander Apr 27, 2026
2980aa8
fix(ci): pip-install requests + use bin/protoc-gen-nanopb (not genera…
BitHighlander Apr 27, 2026
9481ff1
fix(ci): append nanopb generator/ to PATH (don't prepend)
BitHighlander Apr 27, 2026
e001a7f
fix(ci): scope python-dylib-tests to macos-latest (Linux link error)
BitHighlander Apr 27, 2026
4333ca0
fix(ci): pip install pytest in dylib job
BitHighlander Apr 27, 2026
ea2e423
ci(emulator): upload libkkemu.dylib as a downloadable artifact
BitHighlander Apr 27, 2026
413899f
Merge pull request #218 from BitHighlander/fix/eip1559-chunked-data-a…
BitHighlander Apr 29, 2026
326ea18
chore(deps): pin python-keepkey to fork master (d4eda86)
BitHighlander Apr 29, 2026
e2424f2
Merge pull request #219 from BitHighlander/chore/pin-python-keepkey-fork
BitHighlander Apr 29, 2026
a813587
chore(deps): pin device-protocol to fork branch with TRON SignMessage…
BitHighlander Apr 29, 2026
f157952
feat(tron): TIP-191 SignMessage and VerifyMessage
BitHighlander Apr 29, 2026
dc1b5f4
chore(deps): re-pin device-protocol to fork master (post-merge)
BitHighlander Apr 29, 2026
ae4075b
fix(tron): tron_is_canonic must accept recovery IDs 0/1 (review fix)
BitHighlander Apr 29, 2026
21f8a4f
chore(deps): bump python-keepkey to fork branch with TRON message-sig…
BitHighlander Apr 29, 2026
4ba312d
style(tron): apply clang-format
BitHighlander Apr 29, 2026
9ac8e25
chore(deps): pin device-protocol to fork master for TIP-712 protos
BitHighlander Apr 29, 2026
5b39ce4
feat(tron): TIP-712 SignTypedHash (typed-data hash mode)
BitHighlander Apr 29, 2026
152f53d
chore(deps): pin device-protocol to fork master for TonSignMessage proto
BitHighlander Apr 29, 2026
f27990a
feat(ton): Ed25519 SignMessage with AdvancedMode policy gate
BitHighlander Apr 29, 2026
8eb33b5
chore(deps): pin device-protocol to fork master for SolanaSignOffchai…
BitHighlander Apr 29, 2026
a5224da
feat(solana): SignOffchainMessage with domain-separated envelope
BitHighlander Apr 29, 2026
d55377a
fix(transport): add nanopb options for new message-signing fields
BitHighlander Apr 29, 2026
8772395
fix(transport): add nanopb options for new message-signing fields
BitHighlander Apr 29, 2026
f6c82ab
fix(transport): add nanopb options for new message-signing fields
BitHighlander Apr 29, 2026
84b7f22
fix(transport): add nanopb options for new message-signing fields
BitHighlander Apr 29, 2026
b38faab
fix(fsm): forward-declare TronSignMessage / TronVerifyMessage handlers
BitHighlander Apr 29, 2026
f48230d
fix(fsm): forward-declare TronSignTypedHash handler
BitHighlander Apr 29, 2026
e6d9a64
fix(fsm): forward-declare TonSignMessage handler
BitHighlander Apr 29, 2026
ab7d9fa
fix(fsm): forward-declare SolanaSignOffchainMessage handler
BitHighlander Apr 29, 2026
ff3c243
chore(deps,ci): bump python-keepkey to 4da59c8 + add KK_BUILD_LABEL
BitHighlander Apr 29, 2026
614d4ca
chore(deps,ci): bump python-keepkey to 4da59c8 + add KK_BUILD_LABEL
BitHighlander Apr 29, 2026
0353709
chore(deps,ci): bump python-keepkey to 4da59c8 + add KK_BUILD_LABEL
BitHighlander Apr 29, 2026
2c97fa9
chore(deps,ci): bump python-keepkey to 4da59c8 + add KK_BUILD_LABEL
BitHighlander Apr 29, 2026
be79b8d
Merge PR #217: feat(emulator): libkkemu shared library + native macOS…
BitHighlander Apr 29, 2026
de6a288
Merge PR #221: feat(tron): TIP-191 SignMessage + VerifyMessage
BitHighlander Apr 29, 2026
0ebdfae
Merge PR #222: feat(tron): TIP-712 SignTypedHash
BitHighlander Apr 29, 2026
6cdeea7
Merge PR #223: feat(ton): Ed25519 SignMessage (AdvancedMode-gated)
BitHighlander Apr 29, 2026
5db2e20
Merge PR #224: feat(solana): SignOffchainMessage with domain-separate…
BitHighlander Apr 29, 2026
4e4c42b
chore(release): bump version to 7.14.1
BitHighlander Apr 29, 2026
8082861
style: apply clang-format to libkkemu, fsm_msg_tron, zxappliquid
BitHighlander Apr 29, 2026
de297b7
style(eth): apply clang-format to zxappliquid.c (pointer-asterisk side)
BitHighlander Apr 30, 2026
a82357e
merge: sync 7.14.1 release branch with upstream develop
BitHighlander Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 204 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# `<pyenv>/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
# `<site-packages>/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
# ═══════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand Down
114 changes: 114 additions & 0 deletions include/keepkey/emulator/libkkemu.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <stddef.h>

#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 */
Loading
Loading