1
0
Fork 0
mirror of https://github.com/imjasonh/esp32 synced 2026-07-12 18:18:30 +00:00

Auto-publish from GHA on push to main; sign by immutable digest

GitHub Actions workflow at .github/workflows/publish.yml builds the
firmware on every push to main, pushes it to ghcr.io/imjasonh/esp32,
and cosign-signs it keylessly using the workflow's ambient OIDC token.
Required repo secrets: WIFI_SSID, WIFI_PASS (the GITHUB_TOKEN is
injected automatically).

Trust changes:
- src/trust.rs adds the GHA workflow identity to TRUSTED_IDENTITIES,
  alongside the existing email entry. The SAN URI pins the exact
  workflow file at refs/heads/main; a malicious commit that adds a
  different workflow won't match.
- src/sig.rs's SAN extractor now also handles GeneralName::Uniform-
  ResourceIdentifier (workflow URIs), not just Rfc822Name (emails).

Sign-by-digest:
- tools/publisher prints `digest: sha256:...` on stdout (tracing logs
  redirected to stderr) so downstream tooling can target the immutable
  manifest digest.
- Makefile captures that digest and runs `cosign sign ... $REPO@DIGEST`
  instead of `:latest`, eliminating the tag-race window between push
  and sign.

Bootstrap: the device's currently-running firmware doesn't know the
GHA identity yet, so the first GHA-signed publish will be rejected.
A manual `make publish` after this lands rolls out the new
TRUSTED_IDENTITIES, after which GHA-signed updates verify cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Hall 2026-05-02 13:58:44 -04:00
parent 25ac144222
commit 6cc6f38ce0
5 changed files with 170 additions and 18 deletions

91
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,91 @@
name: Publish OTA
# Build the firmware on every push to main, push it as an OCI artifact
# to ghcr.io/imjasonh/esp32, and sign it keylessly with cosign using
# the workflow's ambient OIDC token. The on-device verifier in
# src/trust.rs trusts this workflow's identity for OTA updates.
#
# Required repo secrets:
# WIFI_SSID, WIFI_PASS — embedded into the firmware at compile time
# (see src/main.rs env!()). The GITHUB_TOKEN is auto-provided.
on:
push:
branches: [main]
workflow_dispatch:
# Don't run two publish jobs at once -- :latest would race. The newer
# commit wins.
concurrency:
group: publish
cancel-in-progress: true
permissions:
contents: read
packages: write
id-token: write # cosign keyless OIDC
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install cmake + ninja
run: sudo apt-get update && sudo apt-get install -y cmake ninja-build
- name: Setup uv (for python-shim)
uses: astral-sh/setup-uv@v3
- name: Setup Xtensa toolchain (espup)
uses: esp-rs/xtensa-toolchain@v1.5
with:
default: true
buildtargets: esp32
ldproxy: true
- name: Install espflash
run: cargo install espflash@3 --locked
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Cache .embuild (ESP-IDF + tools)
uses: actions/cache@v4
with:
path: .embuild
key: embuild-v5.2.2-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'rust-toolchain.toml', '.cargo/config.toml') }}
restore-keys: |
embuild-v5.2.2-${{ runner.os }}-
- name: Cache cargo build
uses: Swatinem/rust-cache@v2
with:
workspaces: |
.
tools/publisher
shared-key: publish
- name: Write env files from secrets
env:
WIFI_SSID: ${{ secrets.WIFI_SSID }}
WIFI_PASS: ${{ secrets.WIFI_PASS }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# %q escapes for safe sourcing regardless of special chars.
{
printf 'export WIFI_SSID=%q\n' "$WIFI_SSID"
printf 'export WIFI_PASS=%q\n' "$WIFI_PASS"
} > wifi.env
printf 'export GH_TOKEN=%q\n' "$GH_TOKEN" > gh.env
chmod 600 wifi.env gh.env
- name: Build, push, and sign
env:
# cosign uses GH_TOKEN for registry auth (push the sig
# artifact to GHCR). The OIDC identity for keyless signing
# is the workflow's ambient token, picked up automatically
# because we set id-token: write at the job level.
COSIGN_REGISTRY_USERNAME: ${{ github.actor }}
run: make publish