diff --git a/CHANGELOG.md b/CHANGELOG.md index 725b515b..79af4198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index f15469bc..275785e6 100644 --- a/README.md +++ b/README.md @@ -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)). @@ -290,10 +290,10 @@ prebuild/ └── fetch_firmware.sh # canonical script: download → verify → embed .firmware_download/ # landing folder (gitignored) -├── krux-.zip -├── krux-.zip.sha256.txt -├── krux-.zip.sig -└── selfcustody.pem +└── / + ├── krux-.zip + ├── SHA256SUMS + └── SHA256SUMS.sig src/utils/firmware/ # packing folder (committed, embedded in the binary) └── / @@ -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//` 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//` packing folder contains only the extracted `.kfpkg` files — one per supported device. These are committed @@ -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//`; +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//.kfpkg`. + `src/utils/firmware//.kfpkg`; +5. Writes `src/utils/firmware//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) diff --git a/prebuild/fetch_firmware.sh b/prebuild/fetch_firmware.sh index 4c5cd421..e85c1343 100755 --- a/prebuild/fetch_firmware.sh +++ b/prebuild/fetch_firmware.sh @@ -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" @@ -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. @@ -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 @@ -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}") @@ -172,7 +189,7 @@ _verify_sha256() { } _verify_signature() { - local zip_path="$1" + local signed_path="$1" local sig_path="$2" local pem_path="$3" @@ -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" } @@ -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 @@ -287,8 +305,9 @@ _kfpkg_hashes() { cat > "${module}" <.kfpkg, captured after the release zip's -# SHA256 and ECDSA signature were verified against selfcustody's public key. +# SHA256 of every embedded .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 @@ -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}" diff --git a/pyproject.toml b/pyproject.toml index 10670ae5..e6db193e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/src/utils/constants/__init__.py b/src/utils/constants/__init__.py index 26398be9..d827a7ab 100644 --- a/src/utils/constants/__init__.py +++ b/src/utils/constants/__init__.py @@ -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) diff --git a/uv.lock b/uv.lock index 93654a05..bf3f9566 100644 --- a/uv.lock +++ b/uv.lock @@ -725,7 +725,7 @@ wheels = [ [[package]] name = "krux-installer" -version = "0.0.23" +version = "0.0.24" source = { editable = "." } dependencies = [ { name = "easy-i18n" },