Reproducible Firmware Builds¶
Dotty's firmware build is designed to be reproducible: the same source commit should always produce byte-identical binaries. This page explains the mechanism and how to verify a published release.
What "reproducible" means here¶
Two engineers, starting from the same git commit, running make verify-firmware
on different machines, should get the same stack-chan.bin SHA256 checksum.
The GitHub Actions release workflow records that checksum in SHA256SUMS.txt
and attaches it to every fw-v* release.
Toolchain pinning¶
| Layer | Pinned to | How |
|---|---|---|
| IDF version | espressif/idf:v5.5.4 |
container.image in firmware-release.yml |
| Managed components | idf_component.yml + dependencies.lock |
Committed in firmware submodule |
| Upstream firmware | v2.2.4 tag + patches/xiaozhi-esp32.patch |
fetch_repos.py in firmware build |
Adding a SHA256 digest to the IDF image (optional, maximum trust): Run
docker pull espressif/idf:v5.5.4 && docker inspect espressif/idf:v5.5.4 --format '{{index .RepoDigests 0}}'on a trusted machine and updatefirmware-release.ymlimage:toespressif/idf:v5.5.4@sha256:<digest>. This prevents a tag-rewrite attack on DockerHub from silently changing your toolchain.
Verifying a release locally¶
# 1. Initialise the firmware submodule (once)
git submodule update --init --recursive
# 2. Build and checksum locally
make verify-firmware
# 3. Download the published SHA256SUMS.txt for the release you're verifying
RELEASE=fw-v0.1.0 # or whichever tag
curl -L "https://github.com/BrettKinny/dotty-stackchan/releases/download/${RELEASE}/SHA256SUMS.txt" \
-o firmware/firmware/build/SHA256SUMS.published
# 4. Re-run to compare
make verify-firmware
# Expect: PASS Build is reproducible.
Pinning managed components¶
IDF managed components are fetched at build time unless locked. To generate a lock file:
cd firmware/firmware
docker run --rm -v "$PWD:/project" -w /project \
espressif/idf:v5.5.4 \
bash -lc 'git config --global --add safe.directory "*" && python fetch_repos.py && idf.py reconfigure'
# Commit dependencies.lock alongside idf_component.yml
git add main/idf_component.yml dependencies.lock
The lock file pins exact component versions; without it, a new component release could silently change the binary.
CI workflow¶
The firmware-release.yml workflow fires on fw-v* tags and:
- Checks out with
submodules: recursive - Fetches upstream dependencies via
fetch_repos.py - Builds with
idf.py buildinsideespressif/idf:v5.5.4 - Generates
SHA256SUMS.txtover the three release binaries - Attaches binaries + checksums to the GitHub Release
GPG signing of release artifacts is scaffolded (see docs/signed-releases.md)
and enabled once GPG_PRIVATE_KEY / GPG_PASSPHRASE repo secrets are set.
Known non-determinism risks¶
| Risk | Status |
|---|---|
| Timestamp embedded in binary | Mitigated — ESP-IDF uses SOURCE_DATE_EPOCH when set |
| Managed component version drift | Mitigated once dependencies.lock is committed |
| IDF tag re-point on DockerHub | Low risk; pin digest for maximum confidence |
fetch_repos.py fetching HEAD |
Fixed — script pins to v2.2.4 tag |