cryptoadvance / cryptoadvance/specter-diy
Automate the release signing pipeline (reproducible build + signature finalization)
- Dominant language
- Python
- Stars
- 585
- Forks
- 108
- Avg merge
- 6d 1h
- Merged PRs (30d)
- 5
Description
## Problem
The Specter DIY release process today is almost entirely manual and undocumented in an executable form. Producing a signed release means, by hand:
1. Building the firmware reproducibly in the Docker environment.
2. Reading the bech32 signing message out of the build log.
3. Getting the authorized vendor signers to sign that message offline.
4. Importing the base64 signatures back into `specter_upgrade.bin` with `upgrade-generator.py import-sig`.
5. Re-hashing the now-signed binaries into `release/sha256.txt`.
6. GPG-signing that manifest offline into `sha256.signed.txt`.
Nothing enforces the ordering or checks the results. This has a few concrete failure modes:
- **Pre-sign vs. final hashes get confused.** `build_firmware.sh hash` writes `release/sha256.txt` *before* any signatures are imported. If that file is published as the release manifest, the hashes describe an unsigned intermediate, not what users actually flash.
- **No automated threshold / signer check.** The production bootloader key set currently requires `bootloader_sig_threshold = 2` and `main_fw_sig_threshold = 2`. Whether the collected signatures actually satisfy that, are cryptographically valid, map to *authorized production keys*, and contain no duplicate signer, is only ever checked by eye.
- **No check that the signed payload is the reviewed one.** Nothing verifies that the tag being released matches the version in the firmware signing message, or that importing signatures didn't change the payload.
- **Release-window friction.** Signers (e.g. for firmware signatures and for the final GPG manifest) are only intermittently available. Because the steps aren't packaged, each one is a fresh manual effort and the whole thing can't be done in a single sitting when a signer *is* available.
`upgrade-generator.py` (message generation + external signature import) and `introspect-binary.py` (cryptographic signature verification, authorized-key mapping, threshold enforcement, embedded-key inspection) already provide the primitives. They just aren't wired into CI.
## Optimization
Wrap the process in **two separate GitHub Actions workflows**, keeping the three trust layers cleanly separated:
```
reproducibility -> firmware vendor signatures -> final GPG-signed hash manifest
```
**Important constraint: no private signing keys in GitHub Actions.** Firmware signatures stay external/offline, produced by the authorized signers. CI only receives the finished public base64 signatures and verifies/imports them. The final `sha256.txt` is still GPG-signed offline.
### Workflow 1 - Reproducible Firmware Build
Answers exactly one question: *which reproducible build belongs to this release, and what message must the authorized vendor signers sign?*
- Runs automatically when `boot/main/boot.py` changes on `master` (version bump), and manually for an exact commit/tag.
- Checks out the exact source + recursive submodules, does the reproducible Docker build.
- Surfaces the firmware signing message prominently, reads the production thresholds straight from `pubkeys.c`, records the production key list + key-set SHA-256 and exact submodule SHAs.
- Emits the pre-sign hashes only as `sha256.presign.txt`, so they can't be mistaken for the final `sha256.txt`.
- Does **not** produce the final release manifest.
### Workflow 2 - Release Finalization & Verification
Runs only after the signers have delivered their base64 firmware signatures. It rebuilds from the exact tag (not an uploaded binary), imports the external signatures, and then automatically checks:
- tag exists and the checked-out commit matches it;
- firmware signing message version matches the tag;
- thresholds read from `pubkeys.c`, at least the required number of signatures supplied;
- duplicate signer -> error; unauthorized signer doesn't count; every counted signature cryptographically valid; threshold met;
- signing message byte-identical before and after import (payload untouched);
- `introspect-binary.py` on the final firmware; all expected production public keys present in `initial_firmware.bin`;
- only now is the final `sha256.txt` generated, with versioned filenames;
- summary states clearly that `sha256.txt` must still be GPG-signed offline.
## PRs
- Workflow 1: #414
- Workflow 2: (separate PR, linked below)
The two are intentionally split so they can be reviewed and merged independently rather than landing a large release-automation change on `master` in one shot.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with upgrade-generator.py, introspect-binary.py, build_firmware.sh, boot/main/boot.py, and pubkeys.c, then inspect workflow PR #414 and the existing Docker build. Implement the two independently reviewable workflows with external firmware signatures and offline GPG signing preserved. Done means the final workflow rebuilds from the exact tag, verifies thresholds, keys, payload identity, and final hashes before clearly requiring offline GPG signing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, github-actions, python, shell
- Domain
- ci-cd, devops, release, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100