Repository navigation
Merge pull request #216 from forgesworn/release/lud25-key-notes-beta24 #590
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # Gate every PR (and main) on: the banned-identity rule, the host-testable | |
| # `common` logic, and a real firmware build that must still fit the 2 MB | |
| # ota_0 slot. The firmware previously had NO automated safety net — a compile | |
| # break or a size overflow was only ever caught by building locally. | |
| on: | |
| # Manual dispatch is the clean retry path when GitHub marks a push run as a | |
| # runner startup failure before any job exists. | |
| workflow_dispatch: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| # Default to the least privilege any job here needs; no job in this workflow | |
| # writes to the repo (release.yml scopes its own `contents: write`). | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Server-side enforcement of the check-in identity rule (the local pre-commit | |
| # hook can be bypassed with --no-verify; this gate cannot). The banned string | |
| # is assembled at runtime so this file stays clean and never trips its own check. | |
| identity-guard: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Forbid the banned commit identity | |
| env: | |
| BASE: ${{ github.event.pull_request.base.sha }} | |
| HEAD: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -eu | |
| banned="$(printf 'd%s' 'bowles')" | |
| fail=0 | |
| if git grep -nIi "$banned" -- . ':(exclude).githooks/'; then | |
| echo "::error::tracked content contains the banned identity" | |
| fail=1 | |
| fi | |
| if [ -n "${BASE:-}" ] && [ -n "${HEAD:-}" ]; then | |
| range="$BASE..$HEAD" | |
| else | |
| range="HEAD~1..HEAD" | |
| fi | |
| if git log --format='%an <%ae>%n%cn <%ce>' "$range" | grep -i "$banned"; then | |
| echo "::error::a commit in $range is authored/committed by the banned identity" | |
| fail=1 | |
| fi | |
| [ "$fail" -eq 0 ] && echo "OK: banned identity absent ($range)" | |
| exit "$fail" | |
| # Server-side twin of scripts/git-hooks/pre-commit's secret scan: the local | |
| # hook only runs for contributors who installed it and is bypassable with | |
| # --no-verify; this gate is neither. Scans every line a PR adds for 64-hex | |
| # secrets, nsec keys, SECRET=<hex> assignments and the deny-listed | |
| # credentials leaked in 97d3f954. | |
| secret-scan: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Scan added lines for secrets | |
| env: | |
| BASE: ${{ github.event.pull_request.base.sha }} | |
| HEAD: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -eu | |
| if [ -n "${BASE:-}" ] && [ -n "${HEAD:-}" ]; then | |
| range="$BASE...$HEAD" | |
| else | |
| # Push to main: scan the tip commit (mirrors identity-guard). | |
| range="HEAD~1...HEAD" | |
| fi | |
| python3 scripts/git-hooks/pre-commit --range "$range" | |
| # Pure host-testable logic (auth/replay decisions, BIP-39 generation, policy, | |
| # validation). Fast — no Xtensa toolchain. `+stable` overrides the repo's | |
| # rust-toolchain.toml (channel = "esp") which would otherwise force the | |
| # cross-compiler the host crate doesn't need. | |
| common-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable | |
| # Pass 1 — secp256k1 backend (the firmware's curve) + BIP-39 mnemonic/derive, | |
| # and LUD-25's note derivation on that same curve. `cash` has to run under | |
| # BOTH backends: it is the only path here with an unhardened level, so it | |
| # is the only one that touches a compressed public key, and the two | |
| # backends compute that separately. A secret that differs between them is | |
| # a note one build of this firmware can spend and the other cannot. | |
| - run: cargo +stable test --manifest-path common/Cargo.toml --no-default-features --features mnemonic-gen,cash | |
| # Pass 2 — k256 backend with the full data-plane crypto. Without this the | |
| # nip44/nip46/nip04/backup modules stay feature-gated OFF, so their tests | |
| # (the official NIP-44 vectors, NIP-04 round-trips, the synthetic nonce) | |
| # never run anywhere in CI. ota-sign covers the OTA release-signature | |
| # scheme (the on-device verifier and the CI signer share this module). | |
| - run: cargo +stable test --manifest-path common/Cargo.toml --features nip44,nip46,nip04,ota-sign,device-identity,seed-encrypt,cash,compact-hashes | |
| # Supply-chain gate over the host-buildable crates: RUSTSEC advisories, | |
| # licence allow-list and source registries, enforced against the shared | |
| # deny.toml. There is no workspace root here (each crate is standalone), so | |
| # cargo-deny is pointed at each manifest with an explicit --config. The | |
| # firmware crate cross-compiles for Xtensa/RISC-V and is covered by the | |
| # per-board builds; its dependency surface is shared with `common`, which is | |
| # scanned. serialport (provision/heartwoodd) needs libudev to resolve. | |
| cargo-deny: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - run: sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config | |
| - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2 | |
| with: | |
| tool: cargo-deny | |
| - name: cargo-deny (host crates) | |
| run: | | |
| set -eu | |
| for crate in common heartwoodd provision ota ota-sign sign-test sprite-gen ui-preview; do | |
| echo "::group::cargo-deny $crate" | |
| cargo deny --manifest-path "$crate/Cargo.toml" \ | |
| check --config deny.toml advisories licenses sources bans | |
| echo "::endgroup::" | |
| done | |
| # The host-side provisioning CLI (provision/) — the offline, air-gapped key | |
| # tool (restore from phrase/nsec, generate, pair the bridge secret). Build + | |
| # test it on the host so this security-critical path can't silently break. | |
| # `+stable` (not the repo's `esp` channel); serialport needs libudev on Linux. | |
| provision-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable | |
| - run: sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config | |
| - run: cargo +stable test --manifest-path provision/Cargo.toml | |
| # Screen geometry (ui-preview/) — it includes firmware/src/{layout,palette, | |
| # bigtext}.rs verbatim via #[path], so its tests are the firmware's own layout | |
| # tests plus a scan of every show_error card in firmware/src. `show_error` | |
| # does not wrap: five cards were silently clipped on the 128x64 OLED, two of | |
| # them the ones that tell an owner whether their RNG is trustworthy. Nothing | |
| # ran these before — the crate appeared only under cargo-deny. | |
| ui-preview-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable | |
| - run: cargo +stable test --manifest-path ui-preview/Cargo.toml | |
| # Bench-script logic (scripts/**/*.test.mjs): the frame codec, bridge session | |
| # auth, the partition-table parser, and the RNG self-test log classifier — | |
| # which reads firmware/src/entropy.rs so a reworded verdict fails here rather | |
| # than turning a stuck RNG into "inconclusive" at the bench. node:test only; | |
| # none of these need serialport. | |
| script-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 24 | |
| - run: node --test $(find scripts -name '*.test.mjs' | sort) | |
| # The OTA release signing tool (ota-sign/) — keygen/sign/verify for the | |
| # ed25519 release signatures the firmware enforces. Kept green here so the | |
| # release pipeline's signing step can't be the first place it compiles. | |
| ota-sign-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable | |
| - run: cargo +stable test --manifest-path ota-sign/Cargo.toml | |
| # The Pi-side daemon. Nothing built it here, so it sat broken on main for a | |
| # while after a shared ConnectSlot field landed: the firmware and common crates | |
| # compiled, the daemon did not. Build plus test, so a struct change in common | |
| # cannot quietly break the Soft-mode backend again. | |
| heartwoodd-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable | |
| - run: sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config | |
| - run: cargo +stable build --manifest-path heartwoodd/Cargo.toml | |
| - run: cargo +stable test --manifest-path heartwoodd/Cargo.toml | |
| # Real firmware build for every Xtensa esp-idf board, plus the app-slot fit | |
| # check that has bitten us before (heavy crypto crates / opt level overflow the | |
| # slot). heltec-v3/v4 are ESP32-S3 (2 MB ota_0); the T-Display is the classic | |
| # ESP32 with a 4 MB single-OTA layout (3 MB factory slot). Building all of them | |
| # here catches a board-specific break at PR time, not only when the release | |
| # runs. The C6 (RISC-V) and ESP8266 (lx106) firmwares need different toolchains | |
| # and so have their own jobs below. | |
| firmware-build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - board: heltec-v4 | |
| target: xtensa-esp32s3-espidf | |
| chip: esp32s3 | |
| limit: 2097152 | |
| - board: heltec-v3 | |
| target: xtensa-esp32s3-espidf | |
| chip: esp32s3 | |
| limit: 2097152 | |
| - board: tdisplay | |
| target: xtensa-esp32-espidf | |
| chip: esp32 | |
| limit: 3145728 | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: esp-rs/xtensa-toolchain@b989c80ffd4be88d30c09606ee9b449bd0d41958 # v1.5 | |
| # Authenticate the action's "latest Xtensa Rust" GitHub API lookup so it | |
| # uses the 5000/hr limit, not the 60/hr unauthenticated one that has | |
| # 403'd under concurrent matrix jobs. | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| with: | |
| default: true | |
| buildtargets: ${{ matrix.chip }} | |
| ldproxy: true | |
| - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 | |
| with: | |
| workspaces: firmware | |
| # Per-board cache: the boards now span esp32s3 and esp32, and esp-idf-sys | |
| # keeps a STICKY generated sdkconfig keyed to the chip — a shared cache | |
| # would let one board's sdkconfig bleed into another board's build. | |
| key: ${{ matrix.board }} | |
| - uses: taiki-e/install-action@5b4d68e2e660441203ab128a23676f1e4faf1532 # v2 | |
| with: | |
| tool: espflash | |
| - name: Build firmware (${{ matrix.board }}, release) | |
| working-directory: firmware | |
| env: | |
| # MCU drives esp-idf-sys's chip selection; it must match the board's | |
| # target (the config.toml default is esp32s3, fine for heltec but wrong | |
| # for the T-Display's classic esp32). | |
| MCU: ${{ matrix.chip }} | |
| ESP_IDF_SDKCONFIG_DEFAULTS: "sdkconfig.defaults;sdkconfig.defaults.${{ matrix.board }}" | |
| run: cargo build --release --target ${{ matrix.target }} --no-default-features --features ${{ matrix.board }} | |
| - name: App image must fit the ${{ matrix.board }} app slot | |
| working-directory: firmware | |
| run: | | |
| set -eu | |
| espflash save-image --chip ${{ matrix.chip }} \ | |
| target/${{ matrix.target }}/release/heartwood-esp32 /tmp/app.bin | |
| size=$(stat -c%s /tmp/app.bin) | |
| limit=${{ matrix.limit }} | |
| echo "${{ matrix.board }} app image: $size / $limit bytes ($((size * 100 / limit))%)" | |
| if [ "$size" -gt "$limit" ]; then | |
| echo "::error::${{ matrix.board }} app image ($size B) exceeds its app slot ($limit B)" | |
| exit 1 | |
| fi | |
| # Kept for a week so a branch can be benched on a board this machine | |
| # cannot build for (the T-Display's classic ESP32): an app-only write at | |
| # the board's app offset, never a full flash over its NVS. | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: app-${{ matrix.board }} | |
| path: /tmp/app.bin | |
| retention-days: 7 | |
| # ESP32-C6 (RISC-V rv32imac) firmware. The Xtensa toolchain action above cannot | |
| # install a RISC-V toolchain, so this job uses espup to install the Espressif | |
| # Rust channel (which ships the riscv32imac-esp-espidf target) plus the matching | |
| # riscv32-esp-elf GCC for the C FFI (secp256k1-sys, esp-idf). The -fno-pic CFLAGS | |
| # fix lives in firmware/.cargo/config.toml. 4 MB single-OTA layout, 3 MB factory. | |
| firmware-build-c6: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Install the esp Rust toolchain (espup — nightly Rust + LLVM) | |
| run: | | |
| set -eu | |
| curl -fL -o espup \ | |
| https://github.com/esp-rs/espup/releases/latest/download/espup-x86_64-unknown-linux-gnu | |
| chmod +x espup | |
| # The xtensa chip (esp32) pulls the NIGHTLY-based "esp" Rust toolchain that | |
| # firmware/rust-toolchain.toml pins — build-std (-Zbuild-std) is nightly-only, | |
| # riscv-on-stable can't build `core` — plus the esp LLVM (libclang for bindgen). | |
| # espup does NOT install the riscv gcc (every CI run logged only "Installing | |
| # GCC (xtensa-esp-elf)"), so the C FFI cross-compiler is fetched in the next step. | |
| ./espup install --targets esp32 --export-file "$HOME/export-esp.sh" | |
| - name: Install the riscv32-esp-elf gcc (the C FFI cross-compiler) | |
| run: | | |
| set -eu | |
| # esp-idf v5.3.2's RISC-V cross-compiler (gcc 13.2.0). secp256k1-sys's cc build | |
| # needs `riscv32-esp-elf-gcc` on PATH; the Xtensa boards get theirs from the | |
| # toolchain action, but espup never installs the riscv one — fetch it directly | |
| # and add it to PATH for the build (mirrors the esp8266 lx106-gcc step). | |
| curl -fL -o /tmp/riscv-gcc.tar.xz \ | |
| https://github.com/espressif/crosstool-NG/releases/download/esp-13.2.0_20240530/riscv32-esp-elf-13.2.0_20240530-x86_64-linux-gnu.tar.xz | |
| mkdir -p "$HOME/.local" | |
| tar -xJf /tmp/riscv-gcc.tar.xz -C "$HOME/.local" | |
| echo "$HOME/.local/riscv32-esp-elf/bin" >> "$GITHUB_PATH" | |
| - name: Install ldproxy (esp-idf linker shim) | |
| run: cargo install ldproxy | |
| - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 | |
| with: | |
| workspaces: firmware | |
| key: c6 | |
| - uses: taiki-e/install-action@5b4d68e2e660441203ab128a23676f1e4faf1532 # v2 | |
| with: | |
| tool: espflash | |
| - name: Build firmware (c6, release) | |
| working-directory: firmware | |
| env: | |
| MCU: esp32c6 | |
| ESP_IDF_SDKCONFIG_DEFAULTS: "sdkconfig.defaults;sdkconfig.defaults.c6" | |
| run: | | |
| set -euo pipefail | |
| . "$HOME/export-esp.sh" | |
| # The riscv gcc is on PATH from the install step above (via $GITHUB_PATH); | |
| # confirm it before building so a PATH problem fails here, not deep in cc-rs. | |
| riscv32-esp-elf-gcc --version | head -1 | |
| cargo build --release --target riscv32imac-esp-espidf --no-default-features --features c6 | |
| - name: App image must fit the C6 3 MB app slot | |
| working-directory: firmware | |
| run: | | |
| . "$HOME/export-esp.sh" | |
| set -eu | |
| espflash save-image --chip esp32c6 \ | |
| target/riscv32imac-esp-espidf/release/heartwood-esp32 /tmp/app.bin | |
| size=$(stat -c%s /tmp/app.bin) | |
| limit=3145728 | |
| echo "c6 app image: $size / $limit bytes ($((size * 100 / limit))%)" | |
| if [ "$size" -gt "$limit" ]; then | |
| echo "::error::c6 app image ($size B) exceeds its 3 MB app slot ($limit B)" | |
| exit 1 | |
| fi | |
| # ESP8266 tethered-signer firmware — a SEPARATE crate (esp8266-firmware/), a | |
| # no_std lx106 binary with its own target, toolchain and linker. It shares the | |
| # `esp` Rust channel (which ships xtensa-esp8266-none-elf) but the final link | |
| # needs Espressif's prebuilt xtensa-lx106-elf-gcc, which no toolchain action | |
| # provides. This job proves the firmware still COMPILES AND LINKS into a | |
| # flashable ELF; the runtime (does k256 fault on lx106's unaligned access?) is | |
| # a separate on-hardware step the boot self-test reports on first flash. | |
| esp8266-build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: esp-rs/xtensa-toolchain@b989c80ffd4be88d30c09606ee9b449bd0d41958 # v1.5 | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| with: | |
| default: true | |
| ldproxy: false | |
| # Pin to the esp toolchain the firmware is known to build on. The latest | |
| # esp Rust (1.95.0.0) dropped the `compiler-builtins-no-f16-f128` build-std | |
| # feature that esp8266-firmware/.cargo/config.toml needs to stop the lx106 | |
| # backend choking on f16/f128 builtins ("cannot scavenge register"). 1.87.0.0 | |
| # is nightly-based, so it also accepts the vendored rt's #![feature(naked_functions)]. | |
| version: "1.87.0.0" | |
| - name: Install the xtensa-lx106-elf GNU toolchain (the lx106 linker) | |
| run: | | |
| set -eu | |
| curl -fL -o /tmp/lx106.tar.gz \ | |
| https://dl.espressif.com/dl/xtensa-lx106-elf-gcc8_4_0-esp-2020r3-linux-amd64.tar.gz | |
| mkdir -p "$HOME/.local" | |
| tar -xzf /tmp/lx106.tar.gz -C "$HOME/.local" | |
| echo "$HOME/.local/xtensa-lx106-elf/bin" >> "$GITHUB_PATH" | |
| - uses: Swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # v2 | |
| with: | |
| workspaces: esp8266-firmware | |
| key: esp8266 | |
| - name: Build esp8266 firmware (release — compile + link) | |
| working-directory: esp8266-firmware | |
| run: cargo build --release |