diff --git a/README.md b/README.md
index 49f9cef1..70f1cfbe 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,23 @@ with the latest updates and security alerts.


+## Reproducable Builds
+
+To have confidence this source code tree is the same as the binary on your device,
+you can rebuild it from source and get **exactly the same bytes**. This process
+has been automated using Docker. Steps are as follows:
+
+1. Install Docker
+2. You'll need [GNUMake](https://www.gnu.org/software/make/) but you probably already have it.
+3. Checkout the code, and start the process.
+
+ git clone --recursive https://github.com/Coldcard/firmware.git
+ cd firmware/stm32
+ make repro
+
+4. Maek a coffee, drink it.
+5. At the end the process, the differences, if any are shown and/or a clear confirmation message.
+
## Check-out and Setup
Do a checkout, recursively to get all the submodules:
diff --git a/docker/Dockerfile b/docker/Dockerfile
deleted file mode 100644
index 207c2cc4..00000000
--- a/docker/Dockerfile
+++ /dev/null
@@ -1,35 +0,0 @@
-# Dockerfile to build firmware binary
-#
-# based on
-# and
-#
-FROM alpine:3.13.2
-
-WORKDIR /work
-
-#ADD . /work
-
-RUN apk add --no-cache git python3 py-pip musl-dev make && \
- apk add gcc-arm-none-eabi newlib-arm-none-eabi --update-cache \
- --repository http://dl-3.alpinelinux.org/alpine/edge/testing/
-
-RUN ln -s /usr/bin/python3 /usr/bin/python
-
-RUN git clone --recursive https://github.com/Coldcard/firmware.git
-
-WORKDIR /work/firmware
-
-RUN git submodule update --init
-
-WORKDIR /work/firmware/stm32
-
-RUN make setup
-
-# TODO ... more
-#RUN make
-
-#RUN sed -i '29 s/^/ #/' /home/project/firmware/unix/frozen-modules/pyb.py \
-#&& sed -i "31,32 s/# *//" /home/project/firmware/unix/frozen-modules/pyb.py
-
-
-#COPY docker_init.sh /home/project/docker_init.sh
diff --git a/docker/Makefile b/docker/Makefile
deleted file mode 100644
index a2214dc5..00000000
--- a/docker/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-
-all:
- @echo no default
-
-build:
- docker build -t coldcard-build .
-
-shell:
- docker run -it coldcard-build sh
-
-
-copy:
- (cd .. ; git archive --format tar HEAD) | \
- docker run -i coldcard-build tar x -C /work -f -
- (cd .. ; git archive -o docker/snapshot.tar HEAD)
diff --git a/docker/README.md b/docker/README.md
deleted file mode 100644
index 11eda886..00000000
--- a/docker/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Docker Build
-
-For deterministic builds, we are using Docker.
-
-Thanks to for inspiration.
-
-## Background
-
-- Alpine base image
-- files in this directory will be visible in container at /work
-- no need for git to be pushed, because we clone into container from current checkout
-
-
diff --git a/external/libngu b/external/libngu
index 2fbfefb1..88a61084 160000
--- a/external/libngu
+++ b/external/libngu
@@ -1 +1 @@
-Subproject commit 2fbfefb11b2a7bd4d1abe0b26bb99a6fea050b8d
+Subproject commit 88a61084bd483948d02876ffa12065ea89a28506
diff --git a/releases/ChangeLog.md b/releases/ChangeLog.md
index e2951eb7..5f1be924 100644
--- a/releases/ChangeLog.md
+++ b/releases/ChangeLog.md
@@ -17,6 +17,7 @@
was used, did not reflect the (non-zero) account number.
- HSM/CKBunker mode:
- IMPORTANT: users with passwords will have to be reconstructed as hash algo has changed
+- Enhancement: Reproducable builds! Checkout code, "cd stm32; make repro" should do it all.
- Enhancement: Paper wallet feature restored as it was previously. Same cautions apply.
- Enhancement: Show a progress bar during slow parts of the login process.
- Remaining GPL code has been removed, so licence is now MIT+CC on everything.
diff --git a/stm32/.gitignore b/stm32/.gitignore
index e0df7530..c13ba302 100644
--- a/stm32/.gitignore
+++ b/stm32/.gitignore
@@ -14,3 +14,9 @@ firmware-signed.dfu
dev.bin
dev.dfu
+
+# byproducts of check-repro target
+check-fw.bin
+check-bootrom.bin
+repro-got.txt
+repro-want.txt
diff --git a/stm32/COLDCARD/mpconfigboard.mk b/stm32/COLDCARD/mpconfigboard.mk
index f94232ae..884aa19b 100644
--- a/stm32/COLDCARD/mpconfigboard.mk
+++ b/stm32/COLDCARD/mpconfigboard.mk
@@ -26,11 +26,9 @@ TEXT0_ADDR = 0x08008000
TEXT1_ADDR = 0x0800C000
# don't want any of these: soft_spi, soft_qspi, dht
-DRIVERS_SRC_C -= \
- drivers/bus/softspi.c \
- drivers/bus/softqspi.c \
- drivers/memory/spiflash.c \
- drivers/dht/dht.c
+#DRIVERS_SRC_C -= drivers/bus/softspi.c \
+# drivers/bus/softqspi.c drivers/memory/spiflash.c \
+# drivers/dht/dht.c
# Approximately all the source code files?
ALL_SRC = $(SRC_LIB) $(SRC_LIBM) $(EXTMOD_SRC_C) $(DRIVERS_SRC_C) \
diff --git a/stm32/Makefile b/stm32/Makefile
index 96714063..b3b1c440 100644
--- a/stm32/Makefile
+++ b/stm32/Makefile
@@ -16,12 +16,14 @@ MAKE_ARGS = BOARD=COLDCARD -j 4 EXCLUDE_NGU_TESTS=1
all: COLDCARD/file_time.c
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS)
-clean-mpy:
- rm -rf build
-
-clean: clean-mpy
+clean:
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS) clean
+# These trigger the 'all' target when we haven't completed a successful build yet
+l-port/build-COLDCARD/firmware.elf: all
+l-port/build-COLDCARD/firmware0.bin: all
+l-port/build-COLDCARD/firmware1.bin: all
+
# These values used to make .DFU files. Flash memory locations.
FIRMWARE_BASE = 0x08008000
BOOTLOADER_BASE = 0x08000000
@@ -33,8 +35,8 @@ VERSION_STRING = 4.0.0
#
# Sign and merge various parts
#
-firmware-signed.bin: l-port/build-COLDCARD/firmware?.bin
- $(SIGNIT) sign $(VERSION_STRING)
+firmware-signed.bin: l-port/build-COLDCARD/firmware0.bin l-port/build-COLDCARD/firmware1.bin
+ $(SIGNIT) sign $(VERSION_STRING) -o $@
firmware-signed.dfu: firmware-signed.bin Makefile
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):$< $@
@@ -42,7 +44,7 @@ firmware-signed.dfu: firmware-signed.bin Makefile
dfu: firmware-signed.dfu
# Build a binary, signed w/ production key
-# - always rebuild binary
+# - always rebuild binary for this one
.PHONY: dev.dfu
dev.dfu: l-port/build-COLDCARD/firmware?.bin
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS)
@@ -66,7 +68,7 @@ COLDCARD/file_time.c: Makefile make_filetime.py
# Make a factory release: using key #1
production.bin: firmware-signed.bin Makefile
- $(SIGNIT) sign $(VERSION_STRING) -r firmware-signed.bin -k 1 -o production.bin
+ $(SIGNIT) sign $(VERSION_STRING) -r firmware-signed.bin -k 1 -o $@
# This is release of the bootloader that will be built into the release firmware.
BOOTLOADER_VERSION = 2.0.1
@@ -74,17 +76,21 @@ BOOTLOADER_VERSION = 2.0.1
# This target just combines latest version of production firmware with bootrom into a DFU
# file, stored in ../releases with appropriately dated file name.
.PHONY: release
-release: NEW_VERSION = $(shell $(SIGNIT) version production.bin)
+release: NEW_VERSION = $(shell $(SIGNIT) version built/production.bin)
release: RELEASE_FNAME = ../releases/$(NEW_VERSION)-coldcard.dfu
-release: production.bin
+release: built/production.bin
test ! -f $(RELEASE_FNAME)
- $(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):production.bin \
+ $(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin \
-b $(BOOTLOADER_BASE):bootloader/releases/$(BOOTLOADER_VERSION)/bootloader.bin \
$(RELEASE_FNAME)
@echo
@echo 'Made release: ' $(RELEASE_FNAME)
@echo
+built/production.bin:
+ @echo "To make production build, must run docker code"
+ @false
+
# Use DFU to install the latest production version you have on hand
dfu-latest:
$(PYTHON_DO_DFU) -u `ls -t1 ../releases/*.dfu | head -1`
@@ -103,6 +109,7 @@ code-committed:
.PHONY: sign-release
sign-release: PUBLIC_VERSION = $(shell $(SIGNIT) version production.bin)
sign-release:
+ test -f ../releases/$(PBULIC_VERSION)-coldcard.dfu # need to copy built=>releases
(cd ../releases; shasum -a 256 *.dfu *.md | sort -rk 2 | \
gpg --clearsign -u A3A31BAD5A2A5B10 --digest-algo SHA256 --output signatures.txt --yes - )
git commit -m "Signed for release: "$(PUBLIC_VERSION) ../releases/signatures.txt
@@ -165,11 +172,6 @@ PY_FILES = $(shell find ../shared -name \*.py)
ALL_MPY_FILES = $(addprefix build/, $(PY_FILES:../shared/%.py=%.mpy))
MPY_FILES = $(filter-out build/obsolete/%, $(ALL_MPY_FILES))
-build/%.mpy: ../shared/%.py Makefile
- mkdir -p $(dir $@)
- $(MPY_CROSS) -o $@ -s $*.py $<
-
-
# In another window:
#
# openocd -f openocd_stm32l4x6.cfg
@@ -182,11 +184,11 @@ build/%.mpy: ../shared/%.py Makefile
# - and so on
#
debug:
- arm-none-eabi-gdb $(PORT_TOP)/build-COLDCARD/firmware.elf -x gogo.gdb
+ arm-none-eabi-gdb l-port/build-COLDCARD/firmware.elf -x gogo.gdb
# detailed listing, very handy
OBJDUMP = arm-none-eabi-objdump
-firmware.lss: $(PORT_TOP)/build-COLDCARD/firmware.elf
+firmware.lss: l-port/build-COLDCARD/firmware.elf
$(OBJDUMP) -h -S $< > $@
# Dump sizes of all frozen py files; requires recent build.
@@ -207,6 +209,50 @@ setup:
cd $(MPY_TOP)/mpy-cross ; make
-ln -s $(PORT_TOP) l-port
-ln -s $(MPY_TOP) l-mpy
- -cd $(PORT_TOP)/boards ; ln -s ../../../../../stm32/COLDCARD COLDCARD
+ #-ln -s ../../../../../stm32/COLDCARD $(PORT_TOP)/boards/COLDCARD
+
+# Caution: docker container has write access to your source tree
+# - a readonly copy of source tree, and one output directory
+# - build products are copied to there, see repro-build.sh
+DOCK_RUN_ARGS = -v $(realpath ..):/work/src:ro \
+ -v $(realpath built):/work/built:rw \
+ --privileged coldcard-build
+repro:
+ docker build -t coldcard-build - < dockerfile.build
+ (cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh)
+
+# debug: shell into docker container
+shell:
+ docker run -it $(DOCK_RUN_ARGS) sh
+
+# debug: allow docker to write into source tree
+#DOCK_RUN_ARGS := -v $(realpath ..):/work/src:rw --privileged coldcard-build
+
+PUBLISHED_BIN = $(wildcard ../releases/*-v$(VERSION_STRING)-coldcard.dfu)
+
+# final step in repro-building: check you got the right bytes
+# - but you don't have the production signing key, so that section is removed
+check-repro: TRIM_SIG = sed -e 's/^00003f[89abcdef]0 .*/(firmware signature here)/'
+check-repro: firmware-signed.bin
+ifeq ($(PUBLISHED_BIN),)
+ @echo ""
+ @echo "No binary published yet for: $(VERSION_STRING)"
+ @echo ""
+else
+ @echo Comparing against: $(PUBLISHED_BIN)
+ test -n "$(PUBLISHED_BIN)" -a -f $(PUBLISHED_BIN)
+ $(RM) -f check-fw.bin check-bootrom.bin
+ $(SIGNIT) split $(PUBLISHED_BIN) check-fw.bin check-bootrom.bin
+ $(SIGNIT) check check-fw.bin
+ $(SIGNIT) check firmware-signed.bin
+ hexdump -C firmware-signed.bin | $(TRIM_SIG) > repro-got.txt
+ hexdump -C check-fw.bin | $(TRIM_SIG) > repro-want.txt
+ diff repro-got.txt repro-want.txt
+ @echo ""
+ @echo "SUCCESS. "
+ @echo ""
+ @echo "You have built a bit-for-bit identical copy of Coldcard firmware for v$(VERSION_STRING)"
+endif
+
# EOF
diff --git a/stm32/built/.gitignore b/stm32/built/.gitignore
new file mode 100644
index 00000000..c228bb25
--- /dev/null
+++ b/stm32/built/.gitignore
@@ -0,0 +1,3 @@
+*
+!README.md
+!.gitignore
diff --git a/stm32/built/README.md b/stm32/built/README.md
new file mode 100644
index 00000000..5f5d2ccc
--- /dev/null
+++ b/stm32/built/README.md
@@ -0,0 +1,7 @@
+
+# Built
+
+Output files will be saved into this directory after they are made inside the Docker container.
+
+Everything but this README is in the .gitignore
+
diff --git a/stm32/dockerfile.build b/stm32/dockerfile.build
new file mode 100644
index 00000000..2b310100
--- /dev/null
+++ b/stm32/dockerfile.build
@@ -0,0 +1,17 @@
+# Dockerfile to build tools for building firmware binary
+#
+# Thanks to for inspiration.
+#
+# Also somewhat based on
+#
+#
+FROM alpine:3.13.2
+
+WORKDIR /work
+
+RUN apk add --no-cache git python3 py-pip musl-dev make rsync && \
+ apk add gcc-arm-none-eabi newlib-arm-none-eabi --update-cache \
+ --repository http://dl-3.alpinelinux.org/alpine/edge/testing/
+
+RUN ln -s /usr/bin/python3 /usr/bin/python
+
diff --git a/stm32/repro-build.sh b/stm32/repro-build.sh
new file mode 100644
index 00000000..946c2fa7
--- /dev/null
+++ b/stm32/repro-build.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+#
+# Executes inside the docker container... but works on your files here!
+#
+set -ex
+
+TARGETS="firmware-signed.bin firmware-signed.dfu production.bin dev.dfu"
+
+cd /work/src/stm32
+
+if ! touch repro-build.sh ; then
+ # If we seem to be on a R/O filesystem:
+ # - create a writable overlay on top of read-only source tree
+ # from
+ # - copy certain files (build products) back to /work/built
+
+ mkdir /tmp/overlay
+ mount -t tmpfs tmpfs /tmp/overlay
+ mkdir -p /tmp/overlay/upper /tmp/overlay/work /work/tmp
+ mount -t overlay overlay -o lowerdir=/work/src,upperdir=/tmp/overlay/upper,workdir=/tmp/overlay/work /work/tmp
+
+ cd /work/tmp/stm32
+fi
+
+# need signit.py in path
+cd ../cli
+python -m pip install -r requirements.txt
+python -m pip install --editable .
+cd ../stm32
+
+make setup
+#make clean
+make all
+make $TARGETS
+
+if [ $PWD == '/work/tmp/stm32' ]; then
+ # Copy back build products.
+ rsync -av --ignore-missing-args $TARGETS /work/built
+fi
+
+make check-repro