diff --git a/.gitignore b/.gitignore index d61f598..3604adb 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ fuzz/timeout-* fuzz/oom-* fuzz/slow-unit-* fuzz/corpus/*/[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] +fuzz/build/ + +output/ diff --git a/Dockerfile.reproducible b/Dockerfile.reproducible new file mode 100644 index 0000000..42d2348 --- /dev/null +++ b/Dockerfile.reproducible @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: © 2026 PrivKey LLC +# SPDX-License-Identifier: AGPL-3.0-or-later + +FROM espressif/idf:v5.4.1@sha256:6b4adab7b282e9261a154d7130fb4945a3c28bd35c2e914357c2f522643cb92a AS builder + +ARG SOURCE_DATE_EPOCH=0 +ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} +ENV IDF_CCACHE_ENABLE=0 +ENV IDF_PATH=/opt/esp/idf +ENV IDF_PATH_FORCE=1 + +WORKDIR /build + +COPY . keep-esp32/ + +RUN for repo in \ + "privkeyio/secp256k1-frost 832bac2dbfaf4c35058cd5dadb0f8fe093cb6c65" \ + "privkeyio/libnostr-c 4e8d01b8d20680204429aaf3680d069ec222fdaa" \ + "privkeyio/noscrypt 45fcbef32f958145d6797f384a225c0d43616886" \ + "ElementsProject/libwally-core 242f841af6819b5d5174da5a9593907f56f1bc89"; do \ + set -- $repo; \ + git clone "https://github.com/$1.git" && \ + git -C "$(basename $1)" checkout "$2"; \ + done + +RUN . $IDF_PATH/export.sh && \ + cd keep-esp32 && \ + idf.py set-target esp32s3 && \ + idf.py build && \ + mkdir -p /out && \ + cp build/keep.bin /out/ && \ + cp build/bootloader/bootloader.bin /out/ && \ + cp build/partition_table/partition-table.bin /out/ && \ + cp build/ota_data_initial.bin /out/ + +RUN . $IDF_PATH/export.sh && \ + cd /out && \ + esptool.py --chip esp32s3 merge_bin \ + --flash_mode dio \ + --flash_freq 80m \ + --flash_size 8MB \ + -o keep-merged.bin \ + 0x0 bootloader.bin \ + 0x8000 partition-table.bin \ + 0xd000 ota_data_initial.bin \ + 0x10000 keep.bin && \ + sha256sum keep.bin > keep.bin.sha256 && \ + sha256sum keep-merged.bin > keep-merged.bin.sha256 + +FROM scratch AS export +COPY --from=builder /out/* / diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..c950f2d --- /dev/null +++ b/Justfile @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: © 2026 PrivKey LLC +# SPDX-License-Identifier: AGPL-3.0-or-later + +set shell := ["bash", "-uc"] + +docker_cmd := env("DOCKER_CMD", "docker") +_valid_docker := if docker_cmd == "docker" { "ok" } else { if docker_cmd == "podman" { "ok" } else { error("DOCKER_CMD must be 'docker' or 'podman'") } } +port_raw := env("PORT", "/dev/ttyACM0") +port := if port_raw =~ '^/dev/tty[A-Za-z0-9]+$' { port_raw } else { error("PORT must match /dev/tty* pattern") } + +default: + @just --list + +build: + idf.py build + +flash: + idf.py -p {{port}} flash + +monitor: + idf.py -p {{port}} monitor + +flash-monitor: + idf.py -p {{port}} flash monitor + +test: + #!/usr/bin/env bash + set -euo pipefail + cd test/native + mkdir -p build && cd build + cmake .. + make -j$(nproc) + for t in test_frost test_session test_storage test_secure_element test_secresult \ + test_integration test_self_test test_hw_entropy test_anti_glitch test_psbt_fraud \ + test_frost_signer_core test_protocol test_integration_full test_psbt_fraud_integration; do + [ ! -f "./$t" ] || ./$t + done + +fuzz target="" duration="30": + #!/usr/bin/env bash + set -euo pipefail + if [ -n "{{target}}" ] && ! [[ "{{target}}" =~ ^[a-zA-Z0-9_]+$ ]]; then + echo "Error: target must be alphanumeric (with underscores)" >&2 + exit 1 + fi + if ! [[ "{{duration}}" =~ ^[0-9]+$ ]]; then + echo "Error: duration must be a positive integer" >&2 + exit 1 + fi + cd fuzz + mkdir -p build && cd build + CC=clang cmake .. + make -j$(nproc) + if [ -n "{{target}}" ]; then + ./fuzz_{{target}} ../corpus/{{target}} -max_total_time={{duration}} + else + for fuzzer in fuzz_hex fuzz_protocol fuzz_dkg fuzz_policy fuzz_nostr; do + if [ -f "./$fuzzer" ]; then + corpus="${fuzzer#fuzz_}" + echo "Running $fuzzer..." + timeout {{duration}} ./$fuzzer ../corpus/$corpus -max_total_time={{duration}} || [ $? -eq 124 ] + fi + done + fi + +docs: + doxygen Doxyfile + +clean: + rm -rf build test/native/build fuzz/build docs/html output + +docker-build: + #!/usr/bin/env bash + set -euo pipefail + mkdir -p output + {{docker_cmd}} build -f Dockerfile.reproducible -o output . + sha256sum output/keep.bin + sha256sum output/keep-merged.bin + +verify-sha expected="": + #!/usr/bin/env bash + set -euo pipefail + [ -f output/keep.bin ] || { echo "Run 'just docker-build' first"; exit 1; } + BUILT_HASH=$(sha256sum output/keep.bin | cut -d' ' -f1) + echo "Build hash: $BUILT_HASH" + if [ -n "{{expected}}" ]; then + if ! [[ "{{expected}}" =~ ^[a-f0-9]{64}$ ]]; then + echo "Error: expected must be a valid SHA256 hash (64 hex chars)" >&2 + exit 1 + fi + [ "$BUILT_HASH" = "{{expected}}" ] || { echo "Mismatch: expected {{expected}}"; exit 1; } + echo "Match" + fi + +verify-release version: + #!/usr/bin/env bash + set -euo pipefail + if ! [[ "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: version must be in semver format (vX.Y.Z)" >&2 + exit 1 + fi + [ -f output/keep.bin ] || { echo "Run 'just docker-build' first"; exit 1; } + BUILT_HASH=$(sha256sum output/keep.bin | cut -d' ' -f1) + TMP=$(mktemp -d) + trap "rm -rf $TMP" EXIT + curl -fSL "https://github.com/privkeyio/keep-esp32/releases/download/{{version}}/keep-esp32-firmware-{{version}}.tar.gz" | tar -xz -C "$TMP" + RELEASE_HASH=$(sha256sum "$TMP/keep.bin" | cut -d' ' -f1) + echo "Built: $BUILT_HASH" + echo "Release: $RELEASE_HASH" + [ "$BUILT_HASH" = "$RELEASE_HASH" ] || { echo "Mismatch"; exit 1; } + echo "Match" + +verify-device: + #!/usr/bin/env bash + set -euo pipefail + [ -f output/keep.bin ] || { echo "Run 'just docker-build' first"; exit 1; } + SIZE=$(wc -c < output/keep.bin | tr -d '[:space:]') + TMP=$(mktemp) + trap "rm -f $TMP" EXIT + esptool.py --port {{port}} read_flash 0x10000 "$SIZE" "$TMP" + DEVICE_HASH=$(sha256sum "$TMP" | cut -d' ' -f1) + BUILT_HASH=$(sha256sum output/keep.bin | cut -d' ' -f1) + echo "Device: $DEVICE_HASH" + echo "Built: $BUILT_HASH" + [ "$DEVICE_HASH" = "$BUILT_HASH" ] || { echo "Mismatch"; exit 1; } + echo "Match" + +ci: test docs diff --git a/REPRODUCIBILITY.md b/REPRODUCIBILITY.md new file mode 100644 index 0000000..e17d2aa --- /dev/null +++ b/REPRODUCIBILITY.md @@ -0,0 +1,92 @@ +# Reproducible Builds + +Verify that firmware binaries match the source code. + +## Quick Start + +```bash +# Build in Docker +just docker-build + +# Compare with release +just verify-release v1.0.0 +``` + +## Requirements + +- Docker or Podman +- [just](https://github.com/casey/just) +- esptool (for device verification) + +## Build + +```bash +# Using Docker (default) +just docker-build + +# Using Podman +DOCKER_CMD=podman just docker-build +``` + +Output files in `output/`: +- `keep.bin` - Application firmware +- `keep-merged.bin` - Full flash image +- `bootloader.bin` - Bootloader +- `partition-table.bin` - Partition table +- `ota_data_initial.bin` - OTA data + +## Verify Release + +Compare your build against a published release: + +```bash +just docker-build +just verify-release v1.0.0 +``` + +## Verify Device + +Compare firmware on a connected device: + +```bash +just docker-build +just verify-device + +# Custom port +PORT=/dev/ttyUSB0 just verify-device +``` + +## Manual Verification + +```bash +# Build +just docker-build + +# Check hash +sha256sum output/keep.bin + +# Compare with expected +just verify-sha abc123... +``` + +## Justfile Commands + +| Command | Description | +|---------|-------------| +| `just build` | Build with local ESP-IDF | +| `just flash` | Flash to device | +| `just test` | Run native tests | +| `just fuzz` | Run fuzz tests | +| `just docs` | Generate documentation | +| `just clean` | Remove build artifacts | +| `just docker-build` | Reproducible build in Docker | +| `just verify-sha` | Compare build hash | +| `just verify-release VERSION` | Compare with release | +| `just verify-device` | Compare with device firmware | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DOCKER_CMD` | `docker` | Container runtime (`docker` or `podman`) | +| `PORT` | `/dev/ttyACM0` | Serial port for device operations | diff --git a/REUSE.toml b/REUSE.toml index 864fa45..1684a0f 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -163,4 +163,16 @@ SPDX-License-Identifier = "AGPL-3.0-or-later" path = "**.html" precedence = "aggregate" SPDX-FileCopyrightText = "© 2026 PrivKey LLC" -SPDX-License-Identifier = "AGPL-3.0-or-later" \ No newline at end of file +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "Justfile" +precedence = "aggregate" +SPDX-FileCopyrightText = "© 2026 PrivKey LLC" +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "Dockerfile*" +precedence = "aggregate" +SPDX-FileCopyrightText = "© 2026 PrivKey LLC" +SPDX-License-Identifier = "AGPL-3.0-or-later"