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. ![coldcard picture front](https://coldcardwallet.com/static/images/coldcard-front.png) ![coldcard picture back](https://coldcardwallet.com/static/images/coldcard-back.png) +## 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