Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
594 changes: 450 additions & 144 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

483 changes: 257 additions & 226 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ build
.DS_Store
.vscode/

build-btconly-check/
.claude/

# cppcheck output (static-analysis writes this at repo root in CI)
Expand Down
69 changes: 62 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ endif()

project(
KeepKeyFirmware
VERSION 7.14.3
VERSION 7.15.0
LANGUAGES C CXX ASM)

set(BOOTLOADER_MAJOR_VERSION 2)
Expand All @@ -22,6 +22,16 @@ option(KK_DEBUG_LINK "Build with debug-link enabled" OFF)
option(KK_BUILD_FUZZERS "Build the fuzzers?" OFF)
option(KK_BITCOIN_ONLY "Build Bitcoin-only firmware (strip all non-BTC coins)"
OFF)
# Zcash shielded/Orchard support is part of the regular firmware. It is an
# internal compile selection, not a third release variant: bitcoin-only strips
# the Zcash coin and privacy engine; every regular device/emulator build ships
# both. The open constant-time Pallas audit finding remains a release gate for
# RC18, but it must not silently change the product being audited.
if(KK_BITCOIN_ONLY)
set(KK_ZCASH_PRIVACY OFF)
else()
set(KK_ZCASH_PRIVACY ON)
endif()

# When building the dylib, every static lib it links (kkfirmware, kkboard,
# trezorcrypto, kkrand, kktransport, qrcodegenerator, SecAESSTM32, ...) must
Expand Down Expand Up @@ -64,7 +74,7 @@ endif()
if(NOT EXISTS ${CMAKE_SOURCE_DIR}/deps/crypto/trezor-firmware/crypto/Makefile)
message(
FATAL_ERROR
" trezor-crypto missing. Need to 'git submodule update --init --recursive"
"trezor-crypto fork missing. Run 'git submodule update --init deps/crypto/trezor-firmware'"
)
endif()

Expand All @@ -79,13 +89,20 @@ find_program(NANOPB_GENERATOR nanopb_generator.py)
if(${KK_EMULATOR} AND NOT NANOPB_GENERATOR)
message(
FATAL_ERROR
"Must install nanopb 0.3.9.4, and put nanopb-nanopb-0.3.9.4/generator on your PATH"
"Must install nanopb 0.3.9.8, and put nanopb-nanopb-0.3.9.8/generator on your PATH"
)
endif()

if(${KK_EMULATOR})
add_definitions(-DEMULATOR)
add_definitions(-DCONFIDENTIAL=)
# macOS/BSD declare strlcpy/strlcat in <string.h>; glibc (Linux) and MinGW
# (Windows) do not. Force-include the prototypes so the ~20 call sites build
# without -Werror=implicit-function-declaration (definitions come from
# lib/board/strlcpy.c + strlcat.c). Apple already has them in <string.h>.
if(NOT APPLE)
add_compile_options(-include ${CMAKE_SOURCE_DIR}/include/keepkey/board/bsd_compat.h)
endif()
else()
add_definitions(-DCONFIDENTIAL=__attribute__\(\(section\("confidential"\)\)\))
endif()
Expand Down Expand Up @@ -115,6 +132,13 @@ add_definitions(-DUSE_CARDANO=0)
add_definitions(-DUSE_MONERO=0)
add_definitions(-DUSE_NEM=0)

# NOT a USE_* style on/off toggle despite sitting next to them: trezor-crypto's
# rand.c tests this macro with #ifndef, so only its *definedness* matters. The
# old -D...=0 spelling read as "off" while actually meaning "on", and the
# insecure LCG random32() stayed out of the build purely by that double
# negation. Define it bare, matching upstream trezor-core's SConscript.firmware,
# so a future cleanup of an apparent "=0 means unused" define cannot silently
# compile in the LCG. lib/rand/rng.c #errors if this ever goes missing.
# trezor-crypto's rand.c tests only whether this macro is defined. A value of
# zero therefore did not disable anything; it excluded the library's insecure
# test LCG by definedness. Use the upstream spelling so that intent is clear,
Expand All @@ -137,22 +161,37 @@ add_definitions(-DBIP39_WORDLIST_PADDED=1)

add_definitions(-DAES_128=1)

# NOTE: AES table size is selected per release product below. The regular
# image includes Zcash and its Pallas curve arithmetic, so it uses the smaller
# AES tables to preserve flash headroom. Bitcoin-only keeps FOUR_TABLES AES.

if(${KK_DEBUG_LINK})
add_definitions(-DDEBUG_LINK=1)
else()
add_definitions(-DDEBUG_LINK=0)
endif()

# Always defined, 0 or 1, and always tested with `#if BITCOIN_ONLY`. Device
# builds compile with -Wundef -Werror, so an undefined identifier inside `#if`
# is a hard error rather than a silent zero -- which is what we want, because
# a silently-zero guard would ship the coin engines into the stripped image.
# Value macros: always defined, 0 or 1, and always tested with `#if FLAG`.
# Device builds compile with -Wundef -Werror, so an undefined identifier inside
# `#if` is a hard error rather than a silent zero -- which is what we want,
# because a silently-zero guard would ship the coin engines into the stripped
# image.
if(${KK_BITCOIN_ONLY})
add_definitions(-DBITCOIN_ONLY=1)
else()
add_definitions(-DBITCOIN_ONLY=0)
endif()

if(${KK_ZCASH_PRIVACY})
add_definitions(-DZCASH_PRIVACY=1)
# The Orchard engine leaves the regular image tightest on flash; shrink the
# Gladman AES lookup tables from 4KB to 1KB each (-15,360 bytes ROM,
# slightly slower AES). Bitcoin-only keeps the fast FOUR_TABLES.
add_definitions(-DAES_SMALL_TABLES)
else()
add_definitions(-DZCASH_PRIVACY=0)
endif()

if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
add_definitions(-DDEBUG_ON)
add_definitions(-DMEMORY_PROTECT=0)
Expand Down Expand Up @@ -185,6 +224,12 @@ if(NOT ${KK_EMULATOR})
link_directories(${LIBOPENCM3_PATH}/lib)
include_directories(${LIBOPENCM3_PATH}/include)

# Emit per-function stack-frame sizes (.su files) on device builds. CI's
# SRAM budget gate (tools/check_sram_budget.py) reports the largest frames
# and fails when the linker-asserted stack reserve minus the largest frame
# leaves less than the configured margin. See tools/firmware/keepkey.ld.
add_compile_options(-fstack-usage)

# Dummy empty libraries for stack smashing protection support, since we
# implement __stack_chk_guard and __stack_chk_fail ourselves.
file(WRITE ${CMAKE_BINARY_DIR}/ssp.c "")
Expand Down Expand Up @@ -213,6 +258,10 @@ if(${KK_EMULATOR})
add_test(test-firmware ${CMAKE_BINARY_DIR}/bin/firmware-unit)
add_test(test-board ${CMAKE_BINARY_DIR}/bin/board-unit)
add_test(test-crypto ${CMAKE_BINARY_DIR}/bin/crypto-unit)
if(${KK_ZCASH_PRIVACY})
add_test(test-pallas-ct ${CMAKE_BINARY_DIR}/bin/pallas-ct-unit)
add_test(test-zcash-crypto ${CMAKE_BINARY_DIR}/bin/zcash-crypto-unit)
endif()

add_custom_target(
xunit
Expand All @@ -223,4 +272,10 @@ if(${KK_EMULATOR})
COMMAND ${CMAKE_BINARY_DIR}/bin/crypto-unit
--gtest_output=xml:${CMAKE_BINARY_DIR}/unittests/crypto.xml)

if(${KK_ZCASH_PRIVACY})
add_custom_command(TARGET xunit POST_BUILD
COMMAND ${CMAKE_BINARY_DIR}/bin/pallas-ct-unit
--gtest_output=xml:${CMAKE_BINARY_DIR}/unittests/pallas-ct.xml)
endif()

endif()
42 changes: 42 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Convenience targets — mirrors CI jobs so failures are caught locally.
#
# CI pins clang-format-20. Use that version if available, otherwise fall back.
# To install: brew install llvm@20 or apt-get install clang-format-20
CLANG_FORMAT ?= $(shell command -v clang-format-20 2>/dev/null || echo clang-format)

# Directories and exclusions must match .github/workflows/ci.yml lint-format job.
LINT_DIRS := include/keepkey lib/firmware lib/board lib/transport/src
LINT_SOURCES := $(shell find $(LINT_DIRS) -name '*.c' -o -name '*.h' 2>/dev/null \
| grep -v generated | grep -v '\.pb\.')

.PHONY: lint format help

## lint: Check formatting (same rules as CI). Exits non-zero on any violation.
lint:
@echo "clang-format version: $$($(CLANG_FORMAT) --version)"
@FAILED=0; \
for f in $(LINT_SOURCES); do \
if ! $(CLANG_FORMAT) --style=file --dry-run --Werror "$$f" 2>/dev/null; then \
echo " NEEDS FORMAT: $$f"; \
FAILED=1; \
fi; \
done; \
if [ "$$FAILED" = "1" ]; then \
echo ""; \
echo "Run 'make format' to fix all files."; \
exit 1; \
else \
echo "All files pass clang-format check."; \
fi

## format: Auto-fix formatting in-place for all source files.
format:
@echo "Formatting $(LINT_DIRS)..."
@for f in $(LINT_SOURCES); do \
$(CLANG_FORMAT) --style=file -i "$$f"; \
done
@echo "Done. Review changes with: git diff"

## help: List available targets.
help:
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / make /'
9 changes: 8 additions & 1 deletion cmake/caches/device.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ set(WARN_FLAGS
-Werror")


set(KK_C_FLAGS "${ARCH_FLAGS} -std=gnu99 ${WARN_FLAGS}" CACHE STRING "")
# Newlib's snprintf unconditionally links the float engine (_svfprintf_r,
# _dtoa_r, soft-double libgcc, malloc) — ~22 KB of ROM with zero %f users in
# the firmware. Route all callers to the integer-only siprintf family instead.
# %lld/%llu still work (this toolchain's libc.a compiles the integer engine
# with long-long support). Device builds only; host/emulator keep libc printf.
set(PRINTF_FLAGS "-Dsnprintf=sniprintf -Dvsnprintf=vsniprintf")

set(KK_C_FLAGS "${ARCH_FLAGS} -std=gnu99 ${WARN_FLAGS} ${PRINTF_FLAGS}" CACHE STRING "")
set(KK_CXX_FLAGS "${ARCH_FLAGS} -std=gnu++11 ${WARN_FLAGS} \
-fno-exceptions \
-fno-rtti \
Expand Down
40 changes: 40 additions & 0 deletions cmake/toolchains/mingw-w64-x86_64.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# MinGW-w64 cross-compile toolchain for the Windows emulator DLL (libkkemu.dll,
# x86_64). Lets us cross-build the Windows DLL from the existing macOS/Linux
# emulator build host — no Windows runner required.
#
# Usage:
# cmake -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake \
# -DKK_EMULATOR=ON -DKK_BUILD_DYLIB=ON -DKK_DEBUG_LINK=ON ...
# cmake --build <dir> --target kkemulator_dylib
#
# Install MinGW: `brew install mingw-w64` (macOS) / `apt-get install mingw-w64`.
#
# Only the kkemulator_dylib target is meant to cross-compile. The standalone
# UDP `kkemu` binary is gated out on Windows (tools/emulator/CMakeLists.txt).

set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)
find_program(CMAKE_C_COMPILER NAMES ${TOOLCHAIN_PREFIX}-gcc)
find_program(CMAKE_CXX_COMPILER NAMES ${TOOLCHAIN_PREFIX}-g++)
find_program(CMAKE_RC_COMPILER NAMES ${TOOLCHAIN_PREFIX}-windres)

if(NOT CMAKE_C_COMPILER)
message(FATAL_ERROR
"${TOOLCHAIN_PREFIX}-gcc not found. Install MinGW-w64 "
"(brew install mingw-w64 / apt-get install mingw-w64).")
endif()

# Derive the target sysroot from the compiler location so this works across
# Homebrew versions and Linux package layouts.
get_filename_component(_kk_cc "${CMAKE_C_COMPILER}" REALPATH)
get_filename_component(_kk_bin "${_kk_cc}" DIRECTORY)
get_filename_component(_kk_root "${_kk_bin}/.." ABSOLUTE)
set(CMAKE_FIND_ROOT_PATH "${_kk_root}/${TOOLCHAIN_PREFIX}")

# Find host programs on the host; libraries/headers in the target sysroot.
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
13 changes: 12 additions & 1 deletion deps/crypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ set(sources
#trezor-firmware/crypto/aes/aestst.c
trezor-firmware/crypto/aes/aestab.c)

# Pallas/Orchard curve arithmetic (~2.4k LOC) -- only the Zcash shielded engine
# uses it. Excluded from the default and bitcoin-only images.
if(${KK_ZCASH_PRIVACY})
list(APPEND sources
trezor-firmware/crypto/pallas.c
trezor-firmware/crypto/pallas_ct.c
trezor-firmware/crypto/pallas_sinsemilla.c
trezor-firmware/crypto/pallas_swu.c
trezor-firmware/crypto/redpallas.c
trezor-firmware/crypto/zcash_zip316.c)
endif()

# Clang 5.0 in the docker image (kktech/firmware:v7) is missing
# <xmmintrin.h>, which breaks these. Until they're needed, we'll just elide
# them.
Expand All @@ -69,7 +81,6 @@ set(sources

include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/trezor-firmware/crypto
${CMAKE_CURRENT_SOURCE_DIR}/trezor-firmware/
${CMAKE_CURRENT_SOURCE_DIR}/trezor-firmware/crypto/ed25519-donna
${OPENSSL_INCLUDE_DIR})

Expand Down
26 changes: 24 additions & 2 deletions docs/Build.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
Prerequisites
-------------

Install nanopb-0.3.9.4 from:
Install nanopb-0.3.9.8 from:

`https://github.com/nanopb/nanopb/releases/tag/nanopb-0.3.9.4`
`https://github.com/nanopb/nanopb/releases/tag/nanopb-0.3.9.8`

This must match the version baked into the pinned builder image
(`Dockerfile`, `git clone --branch nanopb-0.3.9.8`). Generated headers
differ between nanopb majors, so a mismatch means a local build and a CI
build are not the same product even from the same source. See GH #425.

Install the python-protobuf dependency:

Expand Down Expand Up @@ -31,3 +36,20 @@ Running the tests
$ cd build
$ make all test
```

Release products
-----------------

Two release products, no separate `zcash-privacy` artifact:

| Product | Contents |
| --- | --- |
| Regular (`full`) | Every supported chain, including Zcash shielded/Orchard |
| Bitcoin-only | Bitcoin only; all non-Bitcoin coins and Zcash privacy code removed |

An unflagged CMake build is the regular product (`BITCOIN_ONLY=0`,
`ZCASH_PRIVACY=1`). `-DKK_BITCOIN_ONLY=ON` sets `BITCOIN_ONLY=1` and
`ZCASH_PRIVACY=0`. Zcash privacy is part of the regular firmware and cannot be
disabled as a release choice; the internal `ZCASH_PRIVACY` value exists only so
bitcoin-only can compile the privacy sources out. Device, emulator, unit-test,
SRAM, and tagged-release CI matrices cover only these two products.
12 changes: 2 additions & 10 deletions docs/DiceEntropy.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
# Dice Entropy

On-device dice rolls, folded into the seed at creation time. Available from
firmware v7.14.3 (bitcoin-only line) and v7.15.0 (`ResetDevice.dice_entropy`).

One difference from 7.15 in this line: the legacy `display_random` entropy
screen still exists here, because already-shipped 7.14 hosts request it. The
two are mutually exclusive — `ResetDevice` with both `display_random` and
`dice_entropy` set is refused with a SyntaxError, since the screen shows the
POST-mix internal entropy and honoring both would hand a host the seed
pre-image and make the dice fold-in worthless.
firmware v7.15.0 (`ResetDevice.dice_entropy`).

## What happens

Expand Down Expand Up @@ -53,8 +46,7 @@ described it as a verifiable commitment; that was strictly worse. A host that
supplies `ext_entropy` and reads that screen once computes
`SHA256(shown || ext_entropy)` — the seed pre-image. Dice change nothing about
that attack, because the displayed value is already post-mix. Unverifiable
mixing beats a verifiable seed pre-image. See the comment above the
`dice_entropy` block in `reset.c:reset_init()`.
mixing beats a verifiable seed pre-image. See the comment at `reset.c:136`.

The roll digest is safe by contrast because it hashes the user's own input, not
seed material.
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* [How to Build](Build.md)
* [Storage Layout](Storage.md)
* [Storage Version Gate — audit SOP](StorageVersionGate.md)
* [Supported Coins](Coins.md)
* [Host Communications](Host.md)
* [Release Process](Release.md)
* [Dice Entropy](DiceEntropy.md)
6 changes: 6 additions & 0 deletions docs/Release.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Release Process
1. We stay in compliance with the GPL license.
1. Build a release build of the firmware on multiple different machines, and compare firmware hashes.
1. Sign it on the airgapped machine with 3/5 signers.
1. Verify the signatures before publishing: `scripts/release/verify-signatures.py <image>`.
* `hash-manifest.sh --require-signed` is structural only — a signature region holding one non-zero byte passes it. This does the real 3-of-5 ECDSA check the device does.
* It parses the keys from `include/keepkey/board/pubkeys.h`, so a rotation cannot leave it checking a stale set.
1. Double check that storage upgrade preserves keys on a production device.
* A signed upgrade must NEVER wipe; a downgrade wiping is correct and expected.
* If `STORAGE_VERSION` was bumped, set `STORAGE_VERSION_LAST_SHIPPED` to match in this release commit. See [Storage Version Gate](StorageVersionGate.md).
* Unsigned RC/dev builds cannot prove this: the bootloader wipes storage for unsigned images by design. Use a signed build.
1. Upload the signed firmware to github.
1. Publish release notes on github.
Loading
Loading