diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index a9dac03d2c..d8c9772b26 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -30,7 +30,7 @@ FROZEN_MANIFEST ?= boards/manifest.py # include py core make definitions include $(TOP)/py/py.mk -GIT_SUBMODULES = lib/lwip lib/mbedtls lib/mynewt-nimble lib/stm32lib +GIT_SUBMODULES = lib/libhydrogen lib/lwip lib/mbedtls lib/mynewt-nimble lib/stm32lib MCU_SERIES_UPPER = $(shell echo $(MCU_SERIES) | tr '[:lower:]' '[:upper:]') CMSIS_MCU_LOWER = $(shell echo $(CMSIS_MCU) | tr '[:upper:]' '[:lower:]') @@ -41,6 +41,7 @@ HAL_DIR=lib/stm32lib/STM32$(MCU_SERIES_UPPER)xx_HAL_Driver USBDEV_DIR=usbdev #USBHOST_DIR=usbhost DFU=$(TOP)/tools/dfu.py +MBOOT_PACK_DFU = mboot/mboot_pack_dfu.py # may need to prefix dfu-util with sudo USE_PYDFU ?= 1 PYDFU ?= $(TOP)/tools/pydfu.py @@ -546,7 +547,13 @@ $(PY_BUILD)/formatfloat.o: COPT += -Os $(PY_BUILD)/parsenum.o: COPT += -Os $(PY_BUILD)/mpprint.o: COPT += -Os -all: $(TOP)/lib/stm32lib/README.md $(BUILD)/firmware.dfu $(BUILD)/firmware.hex +all: $(TOP)/lib/stm32lib/README.md all_main $(BUILD)/firmware.hex + +ifeq ($(MBOOT_ENABLE_PACKING),1) +all_main: $(BUILD)/firmware.pack.dfu +else +all_main: $(BUILD)/firmware.dfu +endif # For convenience, automatically fetch required submodules if they don't exist $(TOP)/lib/stm32lib/README.md: @@ -607,6 +614,11 @@ define GENERATE_DFU $(1) endef +define GENERATE_PACK_DFU + $(ECHO) "GEN $(1)" + $(Q)$(PYTHON) $(MBOOT_PACK_DFU) --keys $(MBOOT_PACK_KEYS_FILE) pack-dfu --gzip $(MBOOT_PACK_CHUNKSIZE) $(2) $(1) +endef + define GENERATE_HEX $(ECHO) "GEN $(1)" $(Q)$(OBJCOPY) -O ihex $(2) $(1) @@ -614,8 +626,13 @@ endef .PHONY: deploy deploy-stlink deploy-openocd +ifeq ($(MBOOT_ENABLE_PACKING),1) +deploy: $(BUILD)/firmware.pack.dfu + $(call RUN_DFU,$^) +else deploy: $(BUILD)/firmware.dfu $(call RUN_DFU,$^) +endif # A board should specify TEXT0_ADDR if to use a different location than the # default for the firmware memory location. A board can also optionally define @@ -662,6 +679,9 @@ $(BUILD)/firmware.dfu: $(BUILD)/firmware0.bin $(BUILD)/firmware1.bin $(call GENERATE_DFU,$@,$(word 1,$^),$(TEXT0_ADDR),$(word 2,$^),$(TEXT1_ADDR)) endif +$(BUILD)/firmware.pack.dfu: $(BUILD)/firmware.dfu $(BOARD_DIR)/mboot_keys.h + $(call GENERATE_PACK_DFU,$@,$<) + $(BUILD)/firmware.hex: $(BUILD)/firmware.elf $(call GENERATE_HEX,$@,$^) diff --git a/ports/stm32/mboot/Makefile b/ports/stm32/mboot/Makefile index 44372b0e8a..ad2fda3eee 100755 --- a/ports/stm32/mboot/Makefile +++ b/ports/stm32/mboot/Makefile @@ -12,6 +12,12 @@ BOARD_DIR ?= $(abspath ../boards/$(BOARD)) # that can be built with or without mboot. USE_MBOOT ?= 1 +# Set MBOOT_ENABLE_PACKING to 1 to enable DFU packing with encryption and signing. +# Ensure the MBOOT_PACK_xxx values match stm32/Makefile, to build matching application firmware. +MBOOT_ENABLE_PACKING ?= 0 +MBOOT_PACK_CHUNKSIZE ?= 16384 +MBOOT_PACK_KEYS_FILE ?= $(BOARD_DIR)/mboot_keys.h + # Sanity check that the board configuration directory exists ifeq ($(wildcard $(BOARD_DIR)/.),) $(error Invalid BOARD specified: $(BOARD_DIR)) @@ -110,6 +116,7 @@ SRC_C = \ elem.c \ fsload.c \ gzstream.c \ + pack.c \ vfs_fat.c \ vfs_lfs.c \ drivers/bus/softspi.c \ @@ -129,6 +136,15 @@ SRC_O = \ $(SYSTEM_FILE) \ ports/stm32/resethandler.o \ +ifeq ($(MBOOT_ENABLE_PACKING), 1) + +SRC_C += lib/libhydrogen/hydrogen.c + +CFLAGS += -DMBOOT_ENABLE_PACKING=1 -DPARTICLE -DPLATFORM_ID=3 +CFLAGS += -DMBOOT_PACK_CHUNKSIZE=$(MBOOT_PACK_CHUNKSIZE) +CFLAGS += -DMBOOT_PACK_KEYS_FILE=\"$(MBOOT_PACK_KEYS_FILE)\" +endif + $(BUILD)/$(HAL_DIR)/Src/stm32$(MCU_SERIES)xx_ll_usb.o: CFLAGS += -Wno-attributes SRC_HAL = $(addprefix $(HAL_DIR)/Src/stm32$(MCU_SERIES)xx_,\ hal_cortex.c \ diff --git a/ports/stm32/mboot/Particle.h b/ports/stm32/mboot/Particle.h new file mode 100644 index 0000000000..5e0953739a --- /dev/null +++ b/ports/stm32/mboot/Particle.h @@ -0,0 +1,10 @@ +// Header for libhydrogen use only. Act like a Particle board for the random +// implementation. This code is not actually called when just decrypting and +// verifying a signature, but a correct implementation is provided anyway. + +#include "py/mphal.h" +#include "rng.h" + +static inline uint32_t HAL_RNG_GetRandomNumber(void) { + return rng_get(); +} diff --git a/ports/stm32/mboot/README.md b/ports/stm32/mboot/README.md index d8aa6d456f..59213eb615 100644 --- a/ports/stm32/mboot/README.md +++ b/ports/stm32/mboot/README.md @@ -155,6 +155,34 @@ firmware.dfu.gz stored on the default FAT filesystem: The 0x80000000 value is the address understood by Mboot as the location of the external SPI flash, configured via `MBOOT_SPIFLASH_ADDR`. +Signed and encrypted DFU support +-------------------------------- + +Mboot optionally supports signing and encrypting the binary firmware in the DFU file. +In general this is refered to as a packed DFU file. This requires additional settings +in the board config and requires the `pyhy` Python module to be installed for `python3` +to be used when building packed firmware, eg: + + $ pip3 install pyhy + +In addition to the changes made to mpconfigboard.mk earlier, for encrypted +support you also need to add: + + MBOOT_ENABLE_PACKING = 1 + +You will also need to generate signing and encryption keys which will be built into +mboot and used for all subsequent installations of firmware. This can be done via: + + $ python3 ports/stm32/mboot/mboot_pack_dfu.py generate-keys + +This command generates a `mboot_keys.h` file which should be stored in the board +definition folder (next to mpconfigboard.mk). + +Once you build the firmware, the `firmware.pack.dfu` file will contain the encrypted +and signed firmware, and can be deployed via USB DFU, or by copying it to the device's +internal filesystem (if `MBOOT_FSLOAD` is enabled). `firmware.dfu` is still unencrypted +and can be directly flashed with jtag etc. + Example: Mboot on PYBv1.x ------------------------- diff --git a/ports/stm32/mboot/dfu.h b/ports/stm32/mboot/dfu.h index a1d4d10d0e..73db3545d8 100644 --- a/ports/stm32/mboot/dfu.h +++ b/ports/stm32/mboot/dfu.h @@ -39,6 +39,14 @@ #define MBOOT_ERROR_STR_INVALID_ADDRESS_IDX 0x11 #define MBOOT_ERROR_STR_INVALID_ADDRESS "Address out of range" +#if MBOOT_ENABLE_PACKING +#define MBOOT_ERROR_STR_INVALID_SIG_IDX 0x12 +#define MBOOT_ERROR_STR_INVALID_SIG "Invalid signature in file" + +#define MBOOT_ERROR_STR_INVALID_READ_IDX 0x13 +#define MBOOT_ERROR_STR_INVALID_READ "Read support disabled on encrypted bootloader" +#endif + // DFU class requests enum { DFU_DETACH = 0, @@ -104,6 +112,6 @@ typedef struct _dfu_state_t { uint8_t buf[DFU_XFER_SIZE] __attribute__((aligned(4))); } dfu_context_t; -static dfu_context_t dfu_context SECTION_NOZERO_BSS; +extern dfu_context_t dfu_context; #endif // MICROPY_INCLUDED_STM32_MBOOT_DFU_H diff --git a/ports/stm32/mboot/fsload.c b/ports/stm32/mboot/fsload.c index 1e1ad7a04d..591b670aa0 100644 --- a/ports/stm32/mboot/fsload.c +++ b/ports/stm32/mboot/fsload.c @@ -28,6 +28,7 @@ #include "py/mphal.h" #include "mboot.h" +#include "pack.h" #include "vfs.h" #if MBOOT_FSLOAD @@ -36,13 +37,43 @@ #error Must enable at least one VFS component #endif +#if MBOOT_ENABLE_PACKING +// Packed DFU files are gzip'd internally, not on the outside, so reads of the file +// just read the file directly. + +static void *input_stream_data; +static stream_read_t input_stream_read_meth; + +static inline int input_stream_init(void *stream_data, stream_read_t stream_read) { + input_stream_data = stream_data; + input_stream_read_meth = stream_read; + return 0; +} + +static inline int input_stream_read(size_t len, uint8_t *buf) { + return input_stream_read_meth(input_stream_data, buf, len); +} + +#else +// Standard (non-packed) DFU files must be gzip'd externally / on the outside, so +// reads of the file go through gz_stream. + +static inline int input_stream_init(void *stream_data, stream_read_t stream_read) { + return gz_stream_init_from_stream(stream_data, stream_read); +} + +static inline int input_stream_read(size_t len, uint8_t *buf) { + return gz_stream_read(len, buf); +} +#endif + static int fsload_program_file(bool write_to_flash) { // Parse DFU uint8_t buf[512]; size_t file_offset; // Read file header, <5sBIB - int res = gz_stream_read(11, buf); + int res = input_stream_read(11, buf); if (res != 11) { return -1; } @@ -62,7 +93,7 @@ static int fsload_program_file(bool write_to_flash) { uint32_t total_size = get_le32(buf + 6); // Read target header, <6sBi255sII - res = gz_stream_read(274, buf); + res = input_stream_read(274, buf); if (res != 274) { return -1; } @@ -82,7 +113,7 @@ static int fsload_program_file(bool write_to_flash) { // Parse each element for (size_t elem = 0; elem < num_elems; ++elem) { // Read element header, sizeof(buf)) { l = sizeof(buf); } - res = gz_stream_read(l, buf); + res = input_stream_read(l, buf); if (res != l) { return -1; } @@ -135,7 +168,7 @@ static int fsload_program_file(bool write_to_flash) { } // Read trailing info - res = gz_stream_read(16, buf); + res = input_stream_read(16, buf); if (res != 16) { return -1; } @@ -151,7 +184,7 @@ static int fsload_validate_and_program_file(void *stream, const stream_methods_t led_state_all(pass == 0 ? 2 : 4); int res = meth->open(stream, fname); if (res == 0) { - res = gz_stream_init(stream, meth->read); + res = input_stream_init(stream, meth->read); if (res == 0) { res = fsload_program_file(pass == 0 ? false : true); } diff --git a/ports/stm32/mboot/fwupdate.py b/ports/stm32/mboot/fwupdate.py index dab5fa6632..65284a6ac0 100644 --- a/ports/stm32/mboot/fwupdate.py +++ b/ports/stm32/mboot/fwupdate.py @@ -157,14 +157,15 @@ def update_mboot(filename): def update_mpy(filename, fs_base, fs_len, fs_type=VFS_FAT): - # Check firmware is of .dfu.gz type + # Check firmware is of .dfu or .dfu.gz type try: with open(filename, "rb") as f: hdr = uzlib.DecompIO(f, 16 + 15).read(6) except Exception: - hdr = None + with open(filename, "rb") as f: + hdr = f.read(6) if hdr != b"DfuSe\x01": - print("Firmware must be a .dfu.gz file.") + print("Firmware must be a .dfu(.gz) file.") return ELEM_TYPE_END = 1 diff --git a/ports/stm32/mboot/gzstream.c b/ports/stm32/mboot/gzstream.c index 4f8a4a40cc..6530539f40 100644 --- a/ports/stm32/mboot/gzstream.c +++ b/ports/stm32/mboot/gzstream.c @@ -31,7 +31,7 @@ #include "gzstream.h" #include "mboot.h" -#if MBOOT_FSLOAD +#if MBOOT_FSLOAD || MBOOT_ENABLE_PACKING #define DICT_SIZE (1 << 15) @@ -61,7 +61,17 @@ static int gz_stream_read_src(TINF_DATA *tinf) { return gz_stream.buf[0]; } -int gz_stream_init(void *stream_data, stream_read_t stream_read) { +int gz_stream_init_from_raw_data(const uint8_t *data, size_t len) { + memset(&gz_stream.tinf, 0, sizeof(gz_stream.tinf)); + gz_stream.tinf.source = data; + gz_stream.tinf.source_limit = data + len; + + uzlib_uncompress_init(&gz_stream.tinf, gz_stream.dict, DICT_SIZE); + + return 0; +} + +int gz_stream_init_from_stream(void *stream_data, stream_read_t stream_read) { gz_stream.stream_data = stream_data; gz_stream.stream_read = stream_read; @@ -97,4 +107,4 @@ int gz_stream_read(size_t len, uint8_t *buf) { return gz_stream.tinf.dest - buf; } -#endif // MBOOT_FSLOAD +#endif // MBOOT_FSLOAD || MBOOT_ENABLE_PACKING diff --git a/ports/stm32/mboot/gzstream.h b/ports/stm32/mboot/gzstream.h index ec11ba79bb..d3d93aa657 100644 --- a/ports/stm32/mboot/gzstream.h +++ b/ports/stm32/mboot/gzstream.h @@ -39,7 +39,8 @@ typedef struct _stream_methods_t { stream_read_t read; } stream_methods_t; -int gz_stream_init(void *stream_data, stream_read_t stream_read); +int gz_stream_init_from_raw_data(const uint8_t *data, size_t len); +int gz_stream_init_from_stream(void *stream_data, stream_read_t stream_read); int gz_stream_read(size_t len, uint8_t *buf); #endif // MICROPY_INCLUDED_STM32_MBOOT_GZSTREAM_H diff --git a/ports/stm32/mboot/main.c b/ports/stm32/mboot/main.c index 99187e3ef8..0846d97cf3 100644 --- a/ports/stm32/mboot/main.c +++ b/ports/stm32/mboot/main.c @@ -37,6 +37,7 @@ #include "mboot.h" #include "powerctrl.h" #include "dfu.h" +#include "pack.h" // This option selects whether to use explicit polling or IRQs for USB events. // In some test cases polling mode can run slightly faster, but it uses more power. @@ -56,11 +57,17 @@ // Configure PLL to give the desired CPU freq #undef MICROPY_HW_FLASH_LATENCY #if defined(STM32F4) || defined(STM32F7) -#define CORE_PLL_FREQ (48000000) -#define MICROPY_HW_FLASH_LATENCY FLASH_LATENCY_1 + #if MBOOT_ENABLE_PACKING + // With encryption/signing/compression, a faster CPU makes processing much faster. + #define CORE_PLL_FREQ (96000000) + #define MICROPY_HW_FLASH_LATENCY FLASH_LATENCY_3 + #else + #define CORE_PLL_FREQ (48000000) + #define MICROPY_HW_FLASH_LATENCY FLASH_LATENCY_1 + #endif #elif defined(STM32H7) -#define CORE_PLL_FREQ (96000000) -#define MICROPY_HW_FLASH_LATENCY FLASH_LATENCY_2 + #define CORE_PLL_FREQ (96000000) + #define MICROPY_HW_FLASH_LATENCY FLASH_LATENCY_2 #endif #undef MICROPY_HW_CLK_PLLM #undef MICROPY_HW_CLK_PLLN @@ -87,7 +94,8 @@ // These bits are used to detect valid application firmware at APPLICATION_ADDR #define APP_VALIDITY_BITS (0x00000003) -#define MP_ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +// Global dfu state +dfu_context_t dfu_context SECTION_NOZERO_BSS; static void do_reset(void); @@ -140,7 +148,7 @@ void HAL_Delay(uint32_t ms) { mp_hal_delay_ms(ms); } -static void __fatal_error(const char *msg) { +NORETURN static void __fatal_error(const char *msg) { NVIC_SystemReset(); for (;;) { } @@ -547,7 +555,7 @@ static int spiflash_page_erase(mp_spiflash_t *spif, uint32_t addr, uint32_t n_bl } #endif -int do_page_erase(uint32_t addr, uint32_t *next_addr) { +int hw_page_erase(uint32_t addr, uint32_t *next_addr) { int ret = -1; led0_state(LED0_STATE_ON); @@ -573,7 +581,7 @@ int do_page_erase(uint32_t addr, uint32_t *next_addr) { return ret; } -void do_read(uint32_t addr, int len, uint8_t *buf) { +void hw_read(uint32_t addr, int len, uint8_t *buf) { led0_state(LED0_STATE_FAST_FLASH); #if defined(MBOOT_SPIFLASH_ADDR) if (MBOOT_SPIFLASH_ADDR <= addr && addr < MBOOT_SPIFLASH_ADDR + MBOOT_SPIFLASH_BYTE_SIZE) { @@ -592,7 +600,7 @@ void do_read(uint32_t addr, int len, uint8_t *buf) { led0_state(LED0_STATE_SLOW_FLASH); } -int do_write(uint32_t addr, const uint8_t *src8, size_t len) { +int hw_write(uint32_t addr, const uint8_t *src8, size_t len) { int ret = -1; led0_state(LED0_STATE_FAST_FLASH); #if defined(MBOOT_SPIFLASH_ADDR) @@ -616,6 +624,34 @@ int do_write(uint32_t addr, const uint8_t *src8, size_t len) { return ret; } +int do_page_erase(uint32_t addr, uint32_t *next_addr) { + #if MBOOT_ENABLE_PACKING + // Erase handled automatically for packed mode. + return 0; + #else + return hw_page_erase(addr, next_addr); + #endif +} + +void do_read(uint32_t addr, int len, uint8_t *buf) { + #if MBOOT_ENABLE_PACKING + // Read disabled on packed (encrypted) mode. + dfu_context.status = DFU_STATUS_ERROR_FILE; + dfu_context.error = MBOOT_ERROR_STR_INVALID_READ_IDX; + led0_state(LED0_STATE_SLOW_INVERTED_FLASH); + #else + hw_read(addr, len, buf); + #endif +} + +int do_write(uint32_t addr, const uint8_t *src8, size_t len) { + #if MBOOT_ENABLE_PACKING + return mboot_pack_write(addr, src8, len); + #else + return hw_write(addr, src8, len); + #endif +} + /******************************************************************************/ // I2C slave interface @@ -1068,6 +1104,16 @@ static uint8_t *pyb_usbdd_StrDescriptor(USBD_HandleTypeDef *pdev, uint8_t idx, u USBD_GetString((uint8_t*)MBOOT_ERROR_STR_INVALID_ADDRESS, str_desc, length); return str_desc; + #if MBOOT_ENABLE_PACKING + case MBOOT_ERROR_STR_INVALID_SIG_IDX: + USBD_GetString((uint8_t*)MBOOT_ERROR_STR_INVALID_SIG, str_desc, length); + return str_desc; + + case MBOOT_ERROR_STR_INVALID_READ_IDX: + USBD_GetString((uint8_t*)MBOOT_ERROR_STR_INVALID_READ, str_desc, length); + return str_desc; + #endif + default: return NULL; } @@ -1388,6 +1434,10 @@ enter_bootloader: mp_spiflash_init(MBOOT_SPIFLASH2_SPIFLASH); #endif + #if MBOOT_ENABLE_PACKING + mboot_pack_init(); + #endif + #if MBOOT_FSLOAD if ((initial_r0 & 0xffffff80) == 0x70ad0080) { // Application passed through elements, validate then process them diff --git a/ports/stm32/mboot/mboot.h b/ports/stm32/mboot/mboot.h index 37929665eb..e4ed3cecc7 100644 --- a/ports/stm32/mboot/mboot.h +++ b/ports/stm32/mboot/mboot.h @@ -36,6 +36,9 @@ #define ELEM_DATA_START (&_estack[0]) #define ELEM_DATA_MAX (&_estack[ELEM_DATA_SIZE]) +#define NORETURN __attribute__((noreturn)) +#define MP_ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + enum { ELEM_TYPE_END = 1, ELEM_TYPE_MOUNT, @@ -53,6 +56,10 @@ extern uint8_t _estack[ELEM_DATA_SIZE]; uint32_t get_le32(const uint8_t *b); void led_state_all(unsigned int mask); +int hw_page_erase(uint32_t addr, uint32_t *next_addr); +void hw_read(uint32_t addr, int len, uint8_t *buf); +int hw_write(uint32_t addr, const uint8_t *src8, size_t len); + int do_page_erase(uint32_t addr, uint32_t *next_addr); void do_read(uint32_t addr, int len, uint8_t *buf); int do_write(uint32_t addr, const uint8_t *src8, size_t len); diff --git a/ports/stm32/mboot/mboot_pack_dfu.py b/ports/stm32/mboot/mboot_pack_dfu.py new file mode 100644 index 0000000000..47382f5910 --- /dev/null +++ b/ports/stm32/mboot/mboot_pack_dfu.py @@ -0,0 +1,260 @@ +# This file is part of the MicroPython project, http://micropython.org/ +# +# The MIT License (MIT) +# +# Copyright (c) 2020-2021 Damien P. George +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +Utility to create compressed, encrypted and signed DFU files. +""" + +import argparse +import os +import re +import struct +import sys +import zlib + +sys.path.append(os.path.dirname(__file__) + "/../../../tools") +import dfu + +try: + import pyhy +except ImportError: + raise SystemExit( + "ERROR: pyhy not found. Please install python pyhy for encrypted mboot support: pip3 install pyhy" + ) + + +# Currenty supported version of a packed DFU file. +MBOOT_PACK_HEADER_VERSION = 1 + +# Must match MBOOT_PACK_HYDRO_CONTEXT in mboot/pack.h +MBOOT_PACK_HYDRO_CONTEXT = "mbootenc" + +# Must match enum in mboot/pack.h. +MBOOT_PACK_CHUNK_META = 0 +MBOOT_PACK_CHUNK_FULL_SIG = 1 +MBOOT_PACK_CHUNK_FW_RAW = 2 +MBOOT_PACK_CHUNK_FW_GZIP = 3 + + +class Keys: + def __init__(self, filename): + self.filename = filename + + def generate(self): + kp = pyhy.hydro_sign_keygen() + self.sign_sk = kp.sk + self.sign_pk = kp.pk + self.secretbox = pyhy.hydro_secretbox_keygen() + + def _save_data(self, name, data, file_, hide=False): + prefix = "//" if hide else "" + data = ",".join("0x{:02x}".format(b) for b in data) + file_.write("{}const uint8_t {}[] = {{{}}};\n".format(prefix, name, data)) + + def _load_data(self, name, line): + line = line.split(name + "[] = ") + if len(line) != 2: + raise Exception("malformed input keys: {}".format(line)) + data = line[1].strip() + return bytes(int(value, 16) for value in data[1:-2].split(",")) + + def save(self): + with open(self.filename, "w") as f: + self._save_data("mboot_pack_sign_secret_key", self.sign_sk, f, hide=True) + self._save_data("mboot_pack_sign_public_key", self.sign_pk, f) + self._save_data("mboot_pack_secretbox_key", self.secretbox, f) + + def load(self): + with open(self.filename) as f: + self.sign_sk = self._load_data("mboot_pack_sign_secret_key", f.readline()) + self.sign_pk = self._load_data("mboot_pack_sign_public_key", f.readline()) + self.secretbox = self._load_data("mboot_pack_secretbox_key", f.readline()) + + +def dfu_read(filename): + elems = [] + + with open(filename, "rb") as f: + hdr = f.read(11) + sig, ver, size, num_targ = struct.unpack("<5sBIB", hdr) + file_offset = 11 + + for i in range(num_targ): + hdr = f.read(274) + sig, alt, has_name, name, t_size, num_elem = struct.unpack("<6sBi255sII", hdr) + + file_offset += 274 + file_offset_t = file_offset + for j in range(num_elem): + hdr = f.read(8) + addr, e_size = struct.unpack(" + +#include "dfu.h" +#include "gzstream.h" +#include "mboot.h" +#include "pack.h" + +#if MBOOT_ENABLE_PACKING + +// Keys provided externally by the board, will be built into mboot flash. +#include MBOOT_PACK_KEYS_FILE + +// Encrypted dfu files using gzip require a decompress buffer. Larger can be faster. +// This setting is independent to the incoming encrypted/signed/compressed DFU file. +#ifndef MBOOT_PACK_GZIP_BUFFER_SIZE +#define MBOOT_PACK_GZIP_BUFFER_SIZE (2048) +#endif + +// State to manage automatic flash erasure. +static uint32_t erased_base_addr; +static uint32_t erased_top_addr; + +// DFU chunk buffer, used to cache incoming blocks of data from USB. +static uint32_t firmware_chunk_base_addr; +static mboot_pack_chunk_buf_t firmware_chunk_buf; + +// Temporary buffer for decrypted data. +static uint8_t decrypted_buf[MBOOT_PACK_DFU_CHUNK_BUF_SIZE] __attribute__((aligned(8))); + +// Temporary buffer for uncompressing. +static uint8_t uncompressed_buf[MBOOT_PACK_GZIP_BUFFER_SIZE] __attribute__((aligned(8))); + +// Buffer to hold the start of the firmware, which is only written once the +// entire firmware is validated. This is 8 bytes due to STM32WB MCUs requiring +// that a double-word write to flash can only be done once (due to ECC). +static uint8_t firmware_head[8]; + +void mboot_pack_init(void) { + erased_base_addr = 0; + erased_top_addr = 0; + firmware_chunk_base_addr = 0; +} + +// In encrypted mode the erase is automatically managed. +// Note: this scheme requires blocks be written in sequence, which is the case. +static int mboot_pack_erase(uint32_t addr, size_t len) { + while (!(erased_base_addr <= addr && addr + len <= erased_top_addr)) { + uint32_t erase; + if (erased_base_addr <= addr && addr < erased_top_addr) { + erase = erased_top_addr; + } else { + erase = addr; + erased_base_addr = addr; + } + uint32_t next_addr; + int ret = hw_page_erase(erase, &next_addr); + if (ret != 0) { + return ret; + } + erased_top_addr = next_addr; + } + return 0; +} + +// Commit an unencrypted and uncompressed chunk of firmware to the flash. +static int mboot_pack_commit_chunk(uint32_t addr, uint8_t *data, size_t len) { + // Erase any required sectors before writing. + int ret = mboot_pack_erase(addr, len); + if (ret != 0) { + return ret; + } + + if (addr == APPLICATION_ADDR) { + // Don't write the very start of the firmware, just copy it into a temporary buffer. + // It will be written only if the full firmware passes the checksum/signature. + memcpy(firmware_head, data, sizeof(firmware_head)); + addr += sizeof(firmware_head); + data += sizeof(firmware_head); + len -= sizeof(firmware_head); + } + + // Commit this piece of the firmware. + return hw_write(addr, data, len); +} + +// Handle a chunk with the full firmware signature. +static int mboot_pack_handle_full_sig(void) { + if (firmware_chunk_buf.header.length < hydro_sign_BYTES) { + return -1; + } + + uint8_t *full_sig = &firmware_chunk_buf.data[firmware_chunk_buf.header.length - hydro_sign_BYTES]; + uint32_t *region_data = (uint32_t *)&firmware_chunk_buf.data[0]; + size_t num_regions = (full_sig - (uint8_t *)region_data) / sizeof(uint32_t) / 2; + + uint8_t *buf = decrypted_buf; + const size_t buf_alloc = sizeof(decrypted_buf); + + // Compute the signature of the full firmware. + hydro_sign_state sign_state; + hydro_sign_init(&sign_state, MBOOT_PACK_HYDRO_CONTEXT); + for (size_t region = 0; region < num_regions; ++region) { + uint32_t addr = region_data[2 * region]; + uint32_t len = region_data[2 * region + 1]; + while (len) { + uint32_t l = len <= buf_alloc ? len : buf_alloc; + hw_read(addr, l, buf); + if (addr == APPLICATION_ADDR) { + // The start of the firmware was not yet written to flash so copy + // it out of the temporary buffer to compute the full signature. + memcpy(buf, firmware_head, sizeof(firmware_head)); + } + int ret = hydro_sign_update(&sign_state, buf, l); + if (ret != 0) { + return -1; + } + addr += l; + len -= l; + } + } + + // Verify the signature of the full firmware. + int ret = hydro_sign_final_verify(&sign_state, full_sig, mboot_pack_sign_public_key); + if (ret != 0) { + dfu_context.status = DFU_STATUS_ERROR_VERIFY; + dfu_context.error = MBOOT_ERROR_STR_INVALID_SIG_IDX; + return -1; + } + + // Full firmware passed the signature check. + // Write the start of the firmware so it boots. + return hw_write(APPLICATION_ADDR, firmware_head, sizeof(firmware_head)); +} + +// Handle a chunk with firmware data. +static int mboot_pack_handle_firmware(void) { + const uint8_t *fw_data = &firmware_chunk_buf.data[0]; + const size_t fw_len = firmware_chunk_buf.header.length; + + // Decrypt the chunk. + if (hydro_secretbox_decrypt(decrypted_buf, fw_data, fw_len, 0, MBOOT_PACK_HYDRO_CONTEXT, mboot_pack_secretbox_key) != 0) { + dfu_context.status = DFU_STATUS_ERROR_VERIFY; + dfu_context.error = MBOOT_ERROR_STR_INVALID_SIG_IDX; + return -1; + } + + // Use the decrypted message contents going formward. + size_t len = fw_len - hydro_secretbox_HEADERBYTES; + uint32_t addr = firmware_chunk_buf.header.address; + + if (firmware_chunk_buf.header.format == MBOOT_PACK_CHUNK_FW_GZIP) { + // Decompress chunk data. + gz_stream_init_from_raw_data(decrypted_buf, len); + for (;;) { + int read = gz_stream_read(sizeof(uncompressed_buf), uncompressed_buf); + if (read == 0) { + return 0; // finished decompressing + } else if (read < 0) { + return -1; // error reading + } + int ret = mboot_pack_commit_chunk(addr, uncompressed_buf, read); + if (ret != 0) { + return ret; + } + addr += read; + } + } else { + // Commit chunk data directly. + return mboot_pack_commit_chunk(addr, decrypted_buf, len); + } +} + +int mboot_pack_write(uint32_t addr, const uint8_t *src8, size_t len) { + if (addr == APPLICATION_ADDR) { + // Base address of main firmware, reset any previous state + firmware_chunk_base_addr = 0; + } + + if (firmware_chunk_base_addr == 0) { + // First piece of data starting a new chunk, so set the base address. + firmware_chunk_base_addr = addr; + } + + if (addr < firmware_chunk_base_addr) { + // Address out of range. + firmware_chunk_base_addr = 0; + return -1; + } + + size_t offset = addr - firmware_chunk_base_addr; + if (offset + len > sizeof(firmware_chunk_buf)) { + // Address/length out of range. + firmware_chunk_base_addr = 0; + return -1; + } + + // Copy in the new data piece into the chunk buffer. + memcpy((uint8_t *)&firmware_chunk_buf + offset, src8, len); + + if (offset + len < sizeof(firmware_chunk_buf.header)) { + // Don't have the header yet. + return 0; + } + + if (firmware_chunk_buf.header.header_vers != MBOOT_PACK_HEADER_VERSION) { + // Chunk header has the wrong version. + dfu_context.status = DFU_STATUS_ERROR_FILE; + dfu_context.error = MBOOT_ERROR_STR_INVALID_SIG_IDX; + return -1; + } + + if (firmware_chunk_buf.header.address != firmware_chunk_base_addr) { + // Chunk address doesn't agree with dfu address, abort. + dfu_context.status = DFU_STATUS_ERROR_ADDRESS; + dfu_context.error = MBOOT_ERROR_STR_INVALID_SIG_IDX; + return -1; + } + + if (offset + len < sizeof(firmware_chunk_buf.header) + firmware_chunk_buf.header.length + sizeof(firmware_chunk_buf.signature)) { + // Don't have the full chunk yet. + return 0; + } + + // Have the full chunk in firmware_chunk_buf, process it now. + + // Reset the chunk base address for the next chunk that comes in. + firmware_chunk_base_addr = 0; + + // Verify the signature of the chunk. + const size_t fw_len = firmware_chunk_buf.header.length; + const uint8_t *sig = &firmware_chunk_buf.data[0] + fw_len; + if (hydro_sign_verify(sig, &firmware_chunk_buf, sizeof(firmware_chunk_buf.header) + fw_len, + MBOOT_PACK_HYDRO_CONTEXT, mboot_pack_sign_public_key) != 0) { + // Signature failed + dfu_context.status = DFU_STATUS_ERROR_VERIFY; + dfu_context.error = MBOOT_ERROR_STR_INVALID_SIG_IDX; + return -1; + } + + // Signature passed, we have valid chunk. + + if (firmware_chunk_buf.header.format == MBOOT_PACK_CHUNK_META) { + // Ignore META chunks. + return 0; + } else if (firmware_chunk_buf.header.format == MBOOT_PACK_CHUNK_FULL_SIG) { + return mboot_pack_handle_full_sig(); + } else if (firmware_chunk_buf.header.format == MBOOT_PACK_CHUNK_FW_RAW + || firmware_chunk_buf.header.format == MBOOT_PACK_CHUNK_FW_GZIP) { + return mboot_pack_handle_firmware(); + } else { + // Unsupported contents. + return -1; + } +} + +#endif // MBOOT_ENABLE_PACKING diff --git a/ports/stm32/mboot/pack.h b/ports/stm32/mboot/pack.h new file mode 100644 index 0000000000..195f297ca1 --- /dev/null +++ b/ports/stm32/mboot/pack.h @@ -0,0 +1,82 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2017-2019 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_STM32_MBOOT_PACK_H +#define MICROPY_INCLUDED_STM32_MBOOT_PACK_H + +#include +#include "py/mphal.h" + +#if MBOOT_ENABLE_PACKING + +#include "lib/libhydrogen/hydrogen.h" + +// Encrypted & signed bootloader support + +/******************************************************************************/ +// Interface + +#define MBOOT_PACK_HEADER_VERSION (1) + +// Used by libhydrogen for signing and secretbox context. +#define MBOOT_PACK_HYDRO_CONTEXT "mbootenc" + +// Maximum size of the firmware payload. +#define MBOOT_PACK_DFU_CHUNK_BUF_SIZE (MBOOT_PACK_CHUNKSIZE + hydro_secretbox_HEADERBYTES) + +enum mboot_pack_chunk_format { + MBOOT_PACK_CHUNK_META = 0, + MBOOT_PACK_CHUNK_FULL_SIG = 1, + MBOOT_PACK_CHUNK_FW_RAW = 2, + MBOOT_PACK_CHUNK_FW_GZIP = 3, +}; + +// Each DFU chunk transfered has this header to validate it. + +typedef struct _mboot_pack_chunk_buf_t { + struct { + uint8_t header_vers; + uint8_t format; // enum mboot_pack_chunk_format + uint8_t _pad[2]; + uint32_t address; + uint32_t length; // number of bytes in following "data" payload, excluding "signature" + } header; + uint8_t data[MBOOT_PACK_DFU_CHUNK_BUF_SIZE]; + uint8_t signature[hydro_sign_BYTES]; +} mboot_pack_chunk_buf_t; + +// Signing and encryption keys, stored in mboot flash, provided externally. +extern const uint8_t mboot_pack_sign_public_key[hydro_sign_PUBLICKEYBYTES]; +extern const uint8_t mboot_pack_secretbox_key[hydro_secretbox_KEYBYTES]; + +/******************************************************************************/ +// Implementation + +void mboot_pack_init(void); +int mboot_pack_write(uint32_t addr, const uint8_t *src8, size_t len); + +#endif // MBOOT_ENABLE_PACKING + +#endif // MICROPY_INCLUDED_STM32_MBOOT_PACK_H diff --git a/ports/stm32/mboot/stm32_generic.ld b/ports/stm32/mboot/stm32_generic.ld index 0504d6d6ae..ade4349674 100644 --- a/ports/stm32/mboot/stm32_generic.ld +++ b/ports/stm32/mboot/stm32_generic.ld @@ -6,11 +6,11 @@ MEMORY { FLASH_BL (rx) : ORIGIN = 0x08000000, LENGTH = 32K /* sector 0 (can be 32K) */ - RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K + RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 120K } /* produce a link error if there is not this amount of RAM for these sections */ -_minimum_stack_size = 2K; +_minimum_stack_size = 8K; /* Define tho top end of the stack. The stack is full descending so begins just above last byte of RAM. Note that EABI requires the stack to be 8-byte diff --git a/ports/stm32/mboot/vfs_fat.c b/ports/stm32/mboot/vfs_fat.c index 20de074f0d..5120bdb104 100644 --- a/ports/stm32/mboot/vfs_fat.c +++ b/ports/stm32/mboot/vfs_fat.c @@ -42,7 +42,7 @@ DRESULT disk_read(void *pdrv, BYTE *buf, DWORD sector, UINT count) { vfs_fat_context_t *ctx = pdrv; if (0 <= sector && sector < ctx->bdev_byte_len / 512) { - do_read(ctx->bdev_base_addr + sector * SECSIZE, count * SECSIZE, buf); + hw_read(ctx->bdev_base_addr + sector * SECSIZE, count * SECSIZE, buf); return RES_OK; } diff --git a/ports/stm32/mboot/vfs_lfs.c b/ports/stm32/mboot/vfs_lfs.c index dec7c015fc..e4dc511db5 100644 --- a/ports/stm32/mboot/vfs_lfs.c +++ b/ports/stm32/mboot/vfs_lfs.c @@ -72,7 +72,7 @@ static uint8_t lfs_lookahead_buffer[LFS_LOOKAHEAD_SIZE]; static int dev_read(const struct LFSx_API (config) * c, LFSx_API(block_t) block, LFSx_API(off_t) off, void *buffer, LFSx_API(size_t) size) { VFS_LFSx_CONTEXT_T *ctx = c->context; if (0 <= block && block < ctx->config.block_count) { - do_read(ctx->bdev_base_addr + block * ctx->config.block_size + off, size, buffer); + hw_read(ctx->bdev_base_addr + block * ctx->config.block_size + off, size, buffer); return LFSx_MACRO(_ERR_OK); } return LFSx_MACRO(_ERR_IO); @@ -93,7 +93,7 @@ static int dev_sync(const struct LFSx_API (config) * c) { int VFS_LFSx_MOUNT(VFS_LFSx_CONTEXT_T *ctx, uint32_t base_addr, uint32_t byte_len) { // Read start of superblock. uint8_t buf[48]; - do_read(base_addr, sizeof(buf), buf); + hw_read(base_addr, sizeof(buf), buf); // Verify littlefs and detect block size. if (memcmp(&buf[SUPERBLOCK_MAGIC_OFFSET], "littlefs", 8) != 0) { diff --git a/ports/stm32/mpconfigport.mk b/ports/stm32/mpconfigport.mk index c6b3ddc74f..98ae93d20b 100644 --- a/ports/stm32/mpconfigport.mk +++ b/ports/stm32/mpconfigport.mk @@ -1,4 +1,4 @@ -# Enable/disable extra modules +# Enable/disable extra modules and features # wiznet5k module for ethernet support; valid values are: # 0 : no Wiznet support @@ -11,3 +11,8 @@ MICROPY_PY_CC3K ?= 0 # VFS FAT FS support MICROPY_VFS_FAT ?= 1 + +# Encrypted/signed bootloader support (ensure the MBOOT_PACK_xxx values match stm32/mboot/Makefile) +MBOOT_ENABLE_PACKING ?= 0 +MBOOT_PACK_CHUNKSIZE ?= 16384 +MBOOT_PACK_KEYS_FILE ?= $(BOARD_DIR)/mboot_keys.h