Skip to content
Merged
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
20 changes: 17 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
# CHANGELOG

## Unreleased
## 0.0.24

- Embedded firmware bumped to `v26.09.0` (`prebuild/fetch_firmware.sh` and
`FIRMWARE_VERSION` in `src/utils/constants/__init__.py`);
- Removed the entire runtime download pipeline left over from the online workflow, now that firmware is embedded at build time:
- download screens (`base_download_screen`, `download_stable_zip_screen`, `download_beta_screen`), version selection screens (`select_version_screen`, `select_old_version_screen`) and the `warning_beta` / `warning_already_downloaded` / `unzip_stable` screens`;
- download screens (`base_download_screen`, `download_stable_zip_screen`, `download_beta_screen`), version selection screens (`select_version_screen`, `select_old_version_screen`) and the `warning_beta` / `warning_already_downloaded` / `unzip_stable` screens;
- `downloader`, `selector` and `unzip` utils, plus the `check_verifyer`;
- the associated unit and E2E tests and their orphaned i18n strings;
- With this cleanup the installer makes **no network calls at runtime** and no longer checks the internet connection on startup — the user flow is now just: open the installer, select a device and flash.
- With this cleanup the installer makes **no network calls at runtime** and no longer checks the internet connection on startup — the user flow is now just: open the installer, select a device and flash;
- Added `SECURITY.md` with the supported versions and the vulnerability
reporting process;
- Added the mission-hardening disclaimer to the greetings screen;
- Added pre-commit hooks running the `poe` quality tasks, so the same checks
CI runs happen before a commit is made;
- Migrated dev dependencies to a `uv` dependency group and documented the
frozen `uv sync` commands;
- Fixed Dependabot picking a release that a later patch release had already
fixed;
- CI now retries the NSIS install after transient Chocolatey failures;
- Bumped `cryptography` to 50.0.1, `pylint` to 4.0.7, `pymarkdownlnt` to
0.9.39 and `actions/setup-python` to 7.

## 0.0.23

Expand Down
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Instead, each release ships with a single firmware version already embedded, ver
### Choosing a firmware version

Each release ships with a fixed firmware version embedded in the binary
(the current one is `v26.03.0`). To flash a different firmware version,
(the current one is `v26.09.0`). To flash a different firmware version,
download the installer release that bundles it, or build from source with
your desired version (see [Firmware embedding (for developers)](/#firmware-embedding-for-developers)).

Expand Down Expand Up @@ -290,10 +290,10 @@ prebuild/
└── fetch_firmware.sh # canonical script: download → verify → embed

.firmware_download/ # landing folder (gitignored)
├── krux-<version>.zip
├── krux-<version>.zip.sha256.txt
├── krux-<version>.zip.sig
└── selfcustody.pem
└── <version>/
├── krux-<version>.zip
├── SHA256SUMS
└── SHA256SUMS.sig

src/utils/firmware/ # packing folder (committed, embedded in the binary)
└── <version>/
Expand All @@ -309,9 +309,10 @@ src/utils/firmware/ # packing folder (committed, embedded in the binary)
└── yahboom.kfpkg
```

The `.firmware_download/` landing folder holds the raw assets from GitHub
(zip, checksum, signature and public key). It is gitignored and can be
deleted after a successful build.
The `.firmware_download/<version>/` landing folder holds the raw assets from
GitHub (zip, `SHA256SUMS` and its signature). It is gitignored and can be
deleted after a successful build. The public key is not downloaded: the
script uses the `selfcustody.pem` committed to this repository.

The `src/utils/firmware/<version>/` packing folder contains only the
extracted `.kfpkg` files — one per supported device. These are committed
Expand All @@ -327,16 +328,22 @@ uv run poe fetch-firmware

This runs `prebuild/fetch_firmware.sh`, which:

1. Downloads the release zip, SHA256 checksum, ECDSA signature and
`selfcustody.pem` from GitHub into `.firmware_download/`;
2. Verifies the SHA256 checksum (`sha256sum` on Linux, `shasum` on macOS);
3. Verifies the ECDSA signature with `openssl` (warns and continues if
`openssl` is not available);
1. Downloads the release zip, `SHA256SUMS` and `SHA256SUMS.sig` from GitHub
into `.firmware_download/<version>/`;
2. Verifies the ECDSA signature of `SHA256SUMS` with `openssl`, against the
committed `selfcustody.pem`;
3. Verifies the zip against its entry in `SHA256SUMS` (`sha256sum` on Linux,
`shasum` on macOS);
4. Extracts `kboot.kfpkg` for each supported device and saves it as
`src/utils/firmware/<version>/<device>.kfpkg`.
`src/utils/firmware/<version>/<device>.kfpkg`;
5. Writes `src/utils/firmware/<version>/SHA256SUMS` and
`src/utils/firmware_hashes.py`, the table the app checks before flashing.

Since v26.09.0 the signature covers `SHA256SUMS`, not the zip itself.
`SHA256SUMS.asc` (the release manager's GPG signature) is not used.

Required tools in `PATH`: `curl`, `unzip`.
Optional (for full verification): `sha256sum`/`shasum`, `openssl`.
Required tools in `PATH`: `curl`, `unzip`, `openssl`, `sha256sum`/`shasum`.
Verification fails closed if any of them is missing.

### Using locally built binaries (dev mode)

Expand Down
49 changes: 35 additions & 14 deletions prebuild/fetch_firmware.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ set -euo pipefail
# ── Configuration ────────────────────────────────────────────────────────────
ALLOW_UNVERIFIED=0
VERIFICATION_SKIPPED=0
FIRMWARE_VERSION="v26.08.0"
FIRMWARE_VERSION="v26.09.0"
BASE_URL="https://github.com/selfcustody/krux/releases/download/${FIRMWARE_VERSION}"
ZIP_NAME="krux-${FIRMWARE_VERSION}.zip"
SHA256_NAME="${ZIP_NAME}.sha256.txt"
SIG_NAME="${ZIP_NAME}.sig"

# Since v26.09.0 the zip is no longer signed directly and the release carries a
# SHA256SUMS listing the zip -- and ATTESTATION.txt -- signed with the selfcustody's
# maintainer key as SHA256SUMS.sig. SHA256SUMS.asc next to it is the release manager's
# personal GPG signature over the same file — not the pinned key, and not
# something openssl can verify — so it is deliberately not used here.
SHA256_NAME="SHA256SUMS"
SIG_NAME="SHA256SUMS.sig"
PEM_NAME="selfcustody.pem"
MANIFEST_NAME="SHA256SUMS"
HASHES_MODULE="src/utils/firmware_hashes.py"
Expand All @@ -63,7 +69,7 @@ VALID_DEVICES=(
# Paths relative to project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "${SCRIPT_DIR}")"
LANDING_DIR="${ROOT_DIR}/.firmware_download"
LANDING_DIR="${ROOT_DIR}/.firmware_download/${FIRMWARE_VERSION}"
PACKING_DIR="${ROOT_DIR}/src/utils/firmware/${FIRMWARE_VERSION}"

# selfcustody's public key, committed to this repository rather than fetched.
Expand Down Expand Up @@ -96,8 +102,9 @@ _download() {

# A file already in the landing directory is reused rather than fetched
# again, which only holds because every one of them is re-verified on each
# run against the committed public key: a zip and signature planted here
# cannot be signed for, so they are rejected before extraction. The key
# run against the committed public key. A SHA256SUMS planted here cannot
# be signed for, and a zip that does not match the signed SHA256SUMS is
# rejected, so neither survives to extraction. The key
# itself is never downloaded — see PINNED_PEM. Under --allow-unverified
# that guarantee is gone, along with every other one.
if [[ -f "${dest}" ]]; then
Expand Down Expand Up @@ -157,8 +164,18 @@ _verify_sha256() {
fi

step "verify SHA256 checksum"

# SHA256SUMS lists more than the zip, so only its line is taken and taking
# the first column of every line compares the zip against all of them
# joined together -- which can never match. Anything but exactly one
# well-formed entry for the zip is refused rather than guessed at.
local expected
expected=$(awk '{print $1}' "${sha256_path}" | tr '[:upper:]' '[:lower:]')
expected=$(awk -v f="${ZIP_NAME}" '
$2 == f || $2 == "*" f { n++; h = $1 }
END { if (n == 1) print h }' "${sha256_path}" | tr '[:upper:]' '[:lower:]')

[[ "${expected}" =~ ^[0-9a-f]{64}$ ]] \
|| die "expected exactly one SHA256 entry for ${ZIP_NAME} in ${SHA256_NAME}"

local actual
actual=$(_sha256_of "${zip_path}")
Expand All @@ -172,7 +189,7 @@ _verify_sha256() {
}

_verify_signature() {
local zip_path="$1"
local signed_path="$1"
local sig_path="$2"
local pem_path="$3"

Expand All @@ -182,7 +199,7 @@ _verify_signature() {
fi

step "verify ECDSA signature"
openssl dgst -sha256 -verify "${pem_path}" -signature "${sig_path}" "${zip_path}" \
openssl dgst -sha256 -verify "${pem_path}" -signature "${sig_path}" "${signed_path}" \
|| die "Signature verification failed!"
info "Signature is valid"
}
Expand Down Expand Up @@ -234,8 +251,9 @@ _kfpkg_hashes() {
return
fi

# Hashes are captured after the zip's SHA256 and ECDSA signature have been
# verified against the committed public key, from a packing directory that
# Hashes are captured after SHA256SUMS's ECDSA signature has been verified
# against the committed public key and the zip has matched its entry in
# that SHA256SUMS, from a packing directory that
# _extract_kfpkg emptied and repopulated from that same zip, so every value
# below descends from a release proven authentic at build time. Both halves
# matter: verifying the zip says nothing about a file that was never
Expand Down Expand Up @@ -287,8 +305,9 @@ _kfpkg_hashes() {
cat > "${module}" <<EOF
# Generated by prebuild/fetch_firmware.sh — do not edit by hand.
#
# SHA256 of every embedded <device>.kfpkg, captured after the release zip's
# SHA256 and ECDSA signature were verified against selfcustody's public key.
# SHA256 of every embedded <device>.kfpkg, captured after the release's
# SHA256SUMS was verified against selfcustody's public key and the zip matched
# its entry in it.
#
# This lives in Python source on purpose. With --onefile, pure-Python modules
# stay in the PYZ archive embedded in the executable and are never unpacked to
Expand Down Expand Up @@ -363,9 +382,11 @@ It is committed to this repository and must not be fetched at build time."
_download "${BASE_URL}/${SIG_NAME}" "${sig_path}"
info "using committed public key ${PINNED_PEM}"

# The signature covers SHA256SUMS, not the zip, so it is checked first:
# nothing is read out of SHA256SUMS before it is known to be authentic.
step "step 2 — verifying integrity"
_verify_signature "${sha256_path}" "${sig_path}" "${PINNED_PEM}"
_verify_sha256 "${zip_path}" "${sha256_path}"
_verify_signature "${zip_path}" "${sig_path}" "${PINNED_PEM}"

step "step 3 — extracting firmware"
_extract_kfpkg "${zip_path}"
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[project]
name = "krux-installer"
version = "0.0.23"
version = "0.0.24"
description = "A GUI based application to flash Krux firmware on K210 based devices"
authors = [
{name = "qlrd", email = "qlrddev@gmail.com"},
{name = "qlrd", email = "qlrddev@proton.me"},
{name = "joaozinhom", email = "joaomcr@proton.me"}
]
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

ROOT_DIRNAME = os.path.abspath(os.path.dirname(__file__))

FIRMWARE_VERSION = "v26.08.0"
FIRMWARE_VERSION = "v26.09.0"

FIRMWARE_DIR = os.path.abspath(
os.path.join(ROOT_DIRNAME, "..", "firmware", FIRMWARE_VERSION)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading