cryptoadvance / cryptoadvance/specter-diy

Automate the release signing pipeline (reproducible build + signature finalization)

Open
#415 1 comment 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.