Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5a40246
build(7.15): release infrastructure, CI gates and dependency pins
BitHighlander Aug 6, 2026
ea20eaa
ci: satisfy workflow shellcheck
BitHighlander Aug 6, 2026
6af73e9
fix(board): show whole addresses, and drop the float printf engine
BitHighlander Aug 6, 2026
7dc66f7
fix(transport): share one frame arena instead of a 12 KB stack frame
BitHighlander Aug 6, 2026
c74233f
docs: map release dependency review ranges
BitHighlander Aug 6, 2026
ef822cf
ci: gate the stacked release train, and don't assert a fix two PRs early
BitHighlander Aug 8, 2026
31cda98
feat(clearsign): verify signed transaction metadata on device
BitHighlander Aug 6, 2026
9f625f2
feat(solana): clear-sign v0 transactions with reusable instruction sc…
BitHighlander Aug 6, 2026
d54e8fc
feat(clearsign): describe EVM transactions the device can verify
BitHighlander Aug 6, 2026
9b9faac
feat(clearsign): KeepKey as the schema attestor
BitHighlander Aug 6, 2026
6c27e11
fix(eip712): reject overflow widths and wrong array sizes
BitHighlander Aug 6, 2026
e109404
feat(storage): PIN KDF hardening, seed lock, BIP-85 and recovery fixes
BitHighlander Aug 6, 2026
320f0eb
feat(rng): auditable entropy source, on-device dice, and no entropy d…
BitHighlander Aug 6, 2026
09c417f
fix: clear dice digest on PIN cancellation
BitHighlander Aug 6, 2026
39555bd
ci: assert the emulator RNG source alongside the change that provides it
BitHighlander Aug 8, 2026
9bf3d57
feat(zcash): Orchard shielded signing engine
BitHighlander Aug 6, 2026
161a38f
feat(zcash): shielded message handlers, tests and constant-time gates
BitHighlander Aug 6, 2026
5312b29
feat(chains): Tendermint-family signing policy and custom denoms
BitHighlander Aug 6, 2026
a42d01a
feat(tron,ton): sign-message support and honest TRON fees
BitHighlander Aug 6, 2026
8f091e8
feat(hive): SLIP-0048 keys and operation signing
BitHighlander Aug 6, 2026
be1e210
feat(taproot): hold and spend P2TR (BIP-340/341/86)
BitHighlander Aug 6, 2026
e0e9a9b
build(variants): gate the coin engines behind the bitcoin-only build
BitHighlander Aug 6, 2026
cc0f2ab
docs(security): record the 7.15 hardening waves and release shape
BitHighlander Aug 6, 2026
51f045d
build: pin release client for final integration
BitHighlander Aug 6, 2026
5544bef
fix(rng): enforce RAND_PLATFORM_INDEPENDENT instead of relying on it
BitHighlander Aug 8, 2026
27970b0
ci: stop qualifying the regular build as "-full"
BitHighlander Aug 8, 2026
4dd5ae5
ci(report): validate the catalog against the same evidence it renders
BitHighlander Aug 9, 2026
60e299a
ci(dylib): run the one confirm-flow test that cannot hang
BitHighlander Aug 9, 2026
5f848f9
fix(release): don't rename HASHES.txt onto itself for the regular var…
BitHighlander Aug 9, 2026
940b701
ci(sram): put the budget numbers where a reviewer will see them
BitHighlander Aug 9, 2026
56807a7
fix(zcash): show the shielded output amount, which never reached the …
BitHighlander Aug 9, 2026
9f31519
chore(deps): pin python-keepkey with the on-device shielded signing test
BitHighlander Aug 9, 2026
f37d24e
ci: keep the evidence when the tests fail, and validate against all o…
BitHighlander Aug 9, 2026
b21f52c
fix(zcash): bound the transparent input index before indexing with it
BitHighlander Aug 9, 2026
774b5ab
fix(solana): never sign a transaction the parser judged malformed
BitHighlander Aug 9, 2026
ab2c978
fix(clearsign): don't let the tx<->metadata binding be disarmed mid-s…
BitHighlander Aug 9, 2026
f1e3d7a
fix(taproot): initialise the BIP-341 hash contexts on the taproot con…
BitHighlander Aug 9, 2026
79b7a3c
fix: restore the shielded two-screen confirm, bound the outputs array…
BitHighlander Aug 9, 2026
b024f8b
fix(confirm): page confirm bodies instead of dropping their tails
BitHighlander Aug 9, 2026
6b6f331
test(memo): count the screens confirm() now pages, and fail fast when…
BitHighlander Aug 9, 2026
4a54ca5
Merge PR #365 (confirm-body paging + release/report fixes) into relea…
BitHighlander Aug 9, 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
373 changes: 291 additions & 82 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

200 changes: 162 additions & 38 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# KeepKey Firmware Release Pipeline
#
# Triggered by version tags (v*) on master.
# Triggered by version tags (v*).
# Builds firmware, computes reproducible hashes,
# and creates a draft GitHub Release with all artifacts.
#
# Git-flow: tag master after merging a release/* or hotfix/* branch.
# Tag a commit only after its CI workflow has completed successfully.
#
# Usage:
# git tag v7.11.0
# git push origin v7.11.0
# git tag -a v7.15.0-rc17 -m "KeepKey firmware 7.15.0 RC17 test candidate"
# git push origin v7.15.0-rc17

name: Release

Expand All @@ -18,7 +18,7 @@ on:
- 'v*'

env:
BASE_IMAGE: kktech/firmware:v15
BASE_IMAGE: kktech/firmware@sha256:7438e53933d47d53157ed6d96d864cb208597e62dce26235ace09d1063427fa2

permissions:
contents: write
Expand All @@ -29,34 +29,89 @@ jobs:
timeout-minutes: 3
outputs:
fw_version: ${{ steps.version.outputs.fw_version }}
tag_name: ${{ steps.version.outputs.tag_name }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Extract and verify version
id: version
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
FW_VERSION=$(sed -n '/^project/,/)/p' CMakeLists.txt | grep -oP '\d+\.\d+\.\d+')
echo "fw_version=${FW_VERSION}" >> "$GITHUB_OUTPUT"
IS_PRERELEASE=false
RC_PREFIX="${FW_VERSION}-rc"

if [ "$TAG_VERSION" != "$FW_VERSION" ]; then
echo "::error::Tag (${TAG_VERSION}) != CMakeLists.txt (${FW_VERSION})"
if [[ "$TAG_VERSION" != "${RC_PREFIX}"* ]]; then
echo "::error::Tag (${TAG_VERSION}) must be ${FW_VERSION} or ${RC_PREFIX}<number>"
exit 1
fi

RC_NUMBER="${TAG_VERSION#"${RC_PREFIX}"}"
if ! [[ "$RC_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid release-candidate tag (${TAG_VERSION}); expected ${RC_PREFIX}<number>"
exit 1
fi
IS_PRERELEASE=true
fi

{
echo "fw_version=${FW_VERSION}"
echo "tag_name=${GITHUB_REF_NAME}"
echo "is_prerelease=${IS_PRERELEASE}"
} >> "$GITHUB_OUTPUT"

- name: Require green CI on the tagged commit
env:
GH_TOKEN: ${{ github.token }}
run: |
# A tag on a red (or untested) commit must not produce release
# artifacts. The tagged SHA already ran the CI workflow on its
# branch push; require that run to exist and have succeeded.
CONCLUSION=$(gh run list --repo "$GITHUB_REPOSITORY" \
--workflow CI --commit "$GITHUB_SHA" \
--json status,conclusion \
--jq '[.[] | select(.status == "completed")] | map(.conclusion) | first')
echo "CI conclusion for $GITHUB_SHA: ${CONCLUSION:-none}"
if [ "$CONCLUSION" != "success" ]; then
echo "::error::No successful CI run found for ${GITHUB_SHA} — refusing to release."
exit 1
fi

build-firmware:
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
# 'suffix' names the published files; 'variant' stays the internal build
# selector. The default build takes an EMPTY suffix so its assets keep the
# names every previous release used (v7.14.x shipped firmware.keepkey.bin).
# Only the bitcoin-only build is qualified, because it is the unusual one.
- variant: full
suffix: ""
cmake_flags: ""
- variant: bitcoin-only
suffix: "-bitcoin-only"
cmake_flags: "-DKK_BITCOIN_ONLY=ON"
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Init required 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/googletest
git submodule update --init deps/qrenc/QR-Code-generator
git submodule update --init deps/sca-hardening/SecAESSTM32

- name: Cache base image
id: cache-base
uses: actions/cache@v5
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: /tmp/base-image.tar
key: base-image-${{ env.BASE_IMAGE }}
Expand All @@ -71,26 +126,39 @@ jobs:
if: steps.cache-base.outputs.cache-hit == 'true'
run: docker load -i /tmp/base-image.tar

- name: Cross-compile firmware
- name: Cross-compile firmware (${{ matrix.variant }})
run: |
docker run --rm \
-v ${{ github.workspace }}:/root/keepkey-firmware:z \
${{ env.BASE_IMAGE }} /bin/sh -c "\
mkdir /root/build && cd /root/build && \
cmake -C /root/keepkey-firmware/cmake/caches/device.cmake /root/keepkey-firmware \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DCMAKE_COLOR_MAKEFILE=ON && \
-DCMAKE_COLOR_MAKEFILE=ON \
${{ matrix.cmake_flags }} && \
make && \
mkdir -p /root/keepkey-firmware/release && \
cp bin/firmware.keepkey.bin /root/keepkey-firmware/release/ && \
cp bin/firmware.keepkey.elf /root/keepkey-firmware/release/ && \
cp bin/bootloader.bin /root/keepkey-firmware/release/ 2>/dev/null || true && \
find . -name '*.su' -print0 | tar czf /root/keepkey-firmware/release/stack-usage.tgz --null -T - && \
chmod -R a+rw /root/keepkey-firmware/release"

# Same SRAM budget gate CI enforces (rc8 boot-fault class): release
# artifacts must clear it too, not just the 16 KiB linker ASSERT.
- name: SRAM budget gate (${{ matrix.variant }})
run: |
pip install --quiet pyelftools
python3 tools/check_sram_budget.py \
--elf release/firmware.keepkey.elf \
--su-tar release/stack-usage.tgz \
--budgets tools/sram-budgets.json \
--variant "${{ matrix.variant }}"

- name: Compute hashes
working-directory: release
run: |
echo "# KeepKey Firmware v${{ needs.validate.outputs.fw_version }} — Hash Manifest" > HASHES.txt
echo "# KeepKey Firmware v${{ needs.validate.outputs.fw_version }} (${{ matrix.variant }}) — Hash Manifest" > HASHES.txt
echo "" >> HASHES.txt
for f in *.bin; do
[ -f "$f" ] || continue
Expand All @@ -109,30 +177,69 @@ jobs:
working-directory: release
run: |
VER="${{ needs.validate.outputs.fw_version }}"
[ -f firmware.keepkey.bin ] && mv firmware.keepkey.bin "firmware.keepkey.v${VER}.bin"
[ -f firmware.keepkey.elf ] && mv firmware.keepkey.elf "firmware.keepkey.v${VER}.elf"
[ -f bootloader.bin ] && mv bootloader.bin "bootloader.v${VER}.bin"
SUFFIX="${{ matrix.suffix }}"
[ -f firmware.keepkey.bin ] && mv firmware.keepkey.bin "firmware.keepkey.v${VER}${SUFFIX}.bin"
[ -f firmware.keepkey.elf ] && mv firmware.keepkey.elf "firmware.keepkey.v${VER}${SUFFIX}.elf"
[ -f bootloader.bin ] && mv bootloader.bin "bootloader.v${VER}${SUFFIX}.bin"
# The regular variant carries an EMPTY suffix, so this rename is a
# no-op for it -- and `mv HASHES.txt HASHES.txt` is not a harmless
# no-op to GNU coreutils, it is "are the same file" and exit 1, which
# the step's default `bash -e` turns into a failed release. The three
# renames above are safe because each one also inserts the version.
[ -z "$SUFFIX" ] || mv HASHES.txt "HASHES${SUFFIX}.txt"
ls -lh

- name: Record build provenance
working-directory: release
run: |
VARIANT="${{ matrix.variant }}"
SUFFIX="${{ matrix.suffix }}"
{
echo "firmware_commit=${GITHUB_SHA}"
echo "source_repository=${GITHUB_REPOSITORY}"
echo "source_ref=${GITHUB_REF}"
echo "workflow_ref=${GITHUB_WORKFLOW_REF}"
echo "builder_image=${BASE_IMAGE}"
echo "runner_image=${ImageOS:-unknown}"
echo "variant=${VARIANT}"
echo "cmake_flags=${{ matrix.cmake_flags }}"
} > "PROVENANCE${SUFFIX}.txt"
cat "PROVENANCE${SUFFIX}.txt"

- name: Upload release artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-firmware
name: release-firmware-${{ matrix.variant }}
path: release/*
retention-days: 90

test:
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- variant: full
cmake_flags: ""
- variant: bitcoin-only
cmake_flags: "-DKK_BITCOIN_ONLY=ON"
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Init required 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/googletest
git submodule update --init deps/qrenc/QR-Code-generator
git submodule update --init deps/sca-hardening/SecAESSTM32

- name: Cache base image
id: cache-base
uses: actions/cache@v5
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: /tmp/base-image.tar
key: base-image-${{ env.BASE_IMAGE }}
Expand All @@ -147,46 +254,62 @@ jobs:
if: steps.cache-base.outputs.cache-hit == 'true'
run: docker load -i /tmp/base-image.tar

- name: Build and test emulator
- name: Build and test emulator (${{ matrix.variant }})
run: |
docker build -t kkemu-release -f scripts/emulator/Dockerfile .
docker run --rm --entrypoint /bin/sh kkemu-release \
docker build -t kkemu-release-${{ matrix.variant }} \
--build-arg coinsupport="${{ matrix.cmake_flags }}" \
-f scripts/emulator/Dockerfile .
docker run --rm --entrypoint /bin/sh kkemu-release-${{ matrix.variant }} \
-c "make xunit; RC=\$?; exit \$RC"

create-release:
needs: [validate, build-firmware, test]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Download firmware artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-firmware
pattern: release-firmware-*
path: artifacts
merge-multiple: true

- name: Prepare release assets
run: |
mkdir -p release-assets
cp artifacts/*.bin artifacts/*.elf artifacts/HASHES.txt release-assets/
# No dash in the glob: the default variant's files are HASHES.txt and
# PROVENANCE.txt, and a 'HASHES-*' pattern would silently drop them.
cp artifacts/*.bin artifacts/*.elf artifacts/HASHES*.txt \
artifacts/PROVENANCE*.txt release-assets/
ls -lh release-assets/

- name: Generate release body
run: |
VER="${{ needs.validate.outputs.fw_version }}"
cat > release-body.md <<EOF
## KeepKey Firmware v${VER}
## KeepKey Firmware ${{ needs.validate.outputs.tag_name }}

### Variants
- **regular** — \`firmware.keepkey.v${VER}.bin\` — all supported chains, including Zcash shielded/Orchard
- **bitcoin-only** — \`firmware.keepkey.v${VER}-bitcoin-only.bin\` — BTC-only, non-BTC coins stripped

### Reproducible Build Verification
\`\`\`bash
./scripts/build/docker/device/release.sh
tail -c +257 bin/firmware.keepkey.bin | shasum -a 256
# bitcoin-only:
# ./scripts/build/docker/device/release.sh -DKK_BITCOIN_ONLY=ON
# (extra args pass through to cmake) and compare against
# HASHES.txt, or HASHES-bitcoin-only.txt for that variant
\`\`\`

> **DRAFT** — firmware must be signed by 3/5 key holders before publishing.
> **DRAFT TEST CANDIDATE** — RC artifacts are unsigned and intended for
> release-candidate testing. Firmware must be signed by 3/5 key holders
> before publishing a production release.

### Signing Checklist
### Signing Checklist (per variant)
- [ ] Built on multiple machines, hashes match
- [ ] Signed on air-gapped machine (3/5 signers)
- [ ] Storage upgrade tested on production device
Expand All @@ -195,10 +318,11 @@ jobs:
EOF

- name: Create draft release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
draft: true
name: "Firmware v${{ needs.validate.outputs.fw_version }}"
prerelease: ${{ needs.validate.outputs.is_prerelease }}
name: "Firmware ${{ needs.validate.outputs.tag_name }}"
body_path: release-body.md
files: release-assets/*
fail_on_unmatched_files: true
36 changes: 36 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
title = "KeepKey firmware Gitleaks configuration"

[extend]
useDefault = true

[[allowlists]]
description = "Published AES test vectors in current and historical trezor-crypto layouts"
targetRules = ["generic-api-key"]
condition = "AND"
regexTarget = "line"
paths = [
'''^deps/crypto/trezor-firmware/crypto/aes/aestst\.c$''',
'''^deps/crypto/trezor-crypto/aes/aestst\.c$''',
]
regexes = ['''(?i)^[[:space:]]*//[[:space:]]*key[[:space:]]*=[[:space:]]*[0-9a-f]+[[:space:]]*$''']

[[allowlists]]
description = "Ed25519 C type names in current and historical trezor-crypto layouts"
targetRules = ["generic-api-key"]
condition = "AND"
regexTarget = "line"
paths = [
'''^deps/crypto/trezor-firmware/crypto/ed25519-donna/ed25519-(blake2b|keccak|sha3)\.h$''',
'''^deps/crypto/trezor-firmware/crypto/ed25519-donna/ed25519\.[ch]$''',
'''^deps/crypto/trezor-crypto/ed25519-donna/ed25519-(blake2b|keccak|sha3)\.h$''',
'''^deps/crypto/trezor-crypto/ed25519-donna/ed25519\.[ch]$''',
]
regexes = ['''ed25519.*secret_key.*signature''']

[[allowlists]]
description = "RC21 release provenance names an exact public python-keepkey git commit"
targetRules = ["generic-api-key"]
condition = "AND"
regexTarget = "line"
paths = ['''^docs/security/7\.15\.0-rc21-clearsign-release-control\.md$''']
regexes = ['''^[[:space:]]*-[[:space:]]*python-keepkey:[[:space:]]*`c406a1ba9120da410c356dbff7f4d4bd1e1758fa`\.[[:space:]]*$''']
10 changes: 5 additions & 5 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[submodule "deps/device-protocol"]
path = deps/device-protocol
url = https://github.com/keepkey/device-protocol.git
branch = master
url = https://github.com/BitHighlander/device-protocol.git
branch = up/release-protocol
[submodule "deps/trezor-firmware"]
path = deps/crypto/trezor-firmware
url = https://github.com/keepkey/trezor-firmware.git
url = https://github.com/BitHighlander/trezor-firmware.git
[submodule "googletest"]
path = deps/googletest
url = https://github.com/google/googletest.git
Expand All @@ -13,8 +13,8 @@ path = code-signing-keys
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
url = https://github.com/BitHighlander/python-keepkey.git
branch = reconcile/upstream-sync
[submodule "deps/qrenc/QR-Code-generator"]
path = deps/qrenc/QR-Code-generator
url = https://github.com/keepkey/QR-Code-generator.git
Expand Down
Loading
Loading