Skip to content
Closed
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
313 changes: 234 additions & 79 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

186 changes: 148 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,83 @@ 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:
- 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 @@ -71,26 +120,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 +171,63 @@ 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"
VARIANT="${{ matrix.variant }}"
[ -f firmware.keepkey.bin ] && mv firmware.keepkey.bin "firmware.keepkey.v${VER}-${VARIANT}.bin"
[ -f firmware.keepkey.elf ] && mv firmware.keepkey.elf "firmware.keepkey.v${VER}-${VARIANT}.elf"
[ -f bootloader.bin ] && mv bootloader.bin "bootloader.v${VER}-${VARIANT}.bin"
mv HASHES.txt "HASHES-${VARIANT}.txt"
ls -lh

- name: Record build provenance
working-directory: release
run: |
VARIANT="${{ matrix.variant }}"
{
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-${VARIANT}.txt"
cat "PROVENANCE-${VARIANT}.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 +242,60 @@ 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/
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
- **full (regular)** — \`firmware.keepkey.v${VER}-full.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 the
# matching HASHES-<variant>.txt
\`\`\`

> **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 +304,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