diff --git a/Makefile.dep b/Makefile.dep index 40836f1d6350..9efbd0302bbe 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -753,6 +753,27 @@ ifneq (,$(filter uuid,$(USEMODULE))) USEMODULE += random endif +ifneq (,$(RIOTBOOT_SLOT0_SIZE)) + FEATURES_PROVIDED += riotboot +endif + +ifneq (,$(filter ota_%,$(USEMODULE))) + USEMODULE += ota +endif + +ifneq (,$(filter ota_coap,$(USEMODULE))) + USEMODULE += firmware + USEMODULE += nanocoap +endif + +ifneq (,$(filter firmware,$(USEMODULE))) + USEMODULE += checksum + USEMODULE += hashes + USEPKG += tweetnacl + FEATURES_REQUIRED += riotboot + FEATURES_REQUIRED += periph_flashpage +endif + # always select gpio (until explicit dependencies are sorted out) FEATURES_OPTIONAL += periph_gpio diff --git a/Makefile.include b/Makefile.include index 7736b8dde52d..be53aca3889d 100644 --- a/Makefile.include +++ b/Makefile.include @@ -628,6 +628,9 @@ CFLAGS += -include '$(RIOTBUILD_CONFIG_HEADER_C)' # include mcuboot support include $(RIOTMAKE)/mcuboot.mk +# include riotboot support +include $(RIOTMAKE)/riotboot.mk + # include Murdock helpers include $(RIOTMAKE)/murdock.inc.mk diff --git a/boards/samr21-xpro/Makefile.include b/boards/samr21-xpro/Makefile.include index ba753cc0a92b..9b7e7924ba32 100644 --- a/boards/samr21-xpro/Makefile.include +++ b/boards/samr21-xpro/Makefile.include @@ -5,4 +5,9 @@ export CPU_MODEL = samr21g18a # set edbg device type EDBG_DEVICE_TYPE = atmel_cm0p +# configure riotboot slots +export RIOTBOOT_SLOT0_SIZE ?= 0x1000 +export RIOTBOOT_SLOT1_SIZE ?= 0x1f800 +export RIOTBOOT_SLOT2_SIZE ?= 0x1f800 + include $(RIOTMAKE)/boards/sam0.inc.mk diff --git a/cpu/cortexm_common/include/cpu.h b/cpu/cortexm_common/include/cpu.h index 48e355ea2e2c..5b45e7794731 100644 --- a/cpu/cortexm_common/include/cpu.h +++ b/cpu/cortexm_common/include/cpu.h @@ -126,6 +126,44 @@ static inline void cortexm_isr_end(void) } } +/** + * @brief Jumps to another image in flash + * + * This function is supposed to be called by a bootloader application. + * + * @param[in] image_address address in flash of other image + */ +static inline void cpu_jump_to_image(uint32_t image_address) +{ + /* Disable IRQ */ + __disable_irq(); + + /* set MSP */ + __set_MSP(*(uint32_t*)image_address); + + /* skip stack pointer */ + image_address += 4; + + /* load the images reset_vector address */ + uint32_t destination_address = *(uint32_t*)image_address; + + /* Make sure the Thumb State bit is set. */ + destination_address |= 0x1; + + /* Branch execution */ + __asm("BX %0" :: "r" (destination_address)); +} + +/* The following register is only present for Cortex-M0+, -M3, -M4 and -M7 CPUs */ +#if defined(CPU_ARCH_CORTEX_M0PLUS) || defined(CPU_ARCH_CORTEX_M3) || \ + defined(CPU_ARCH_CORTEX_M4) || defined(CPU_ARCH_CORTEX_M4F) || \ + defined(CPU_ARCH_CORTEX_M7) +static inline unsigned cpu_get_image_baseaddr(void) +{ + return (unsigned)SCB->VTOR; +} +#endif + #ifdef __cplusplus } #endif diff --git a/cpu/cortexm_common/ldscripts/cortexm.ld b/cpu/cortexm_common/ldscripts/cortexm.ld index cb19c10935b1..2a7d0be2af21 100644 --- a/cpu/cortexm_common/ldscripts/cortexm.ld +++ b/cpu/cortexm_common/ldscripts/cortexm.ld @@ -1,5 +1,7 @@ /* * Copyright (C) 2017 Inria + * 2018 Kaspar Schleiser + * * * This file is subject to the terms and conditions of the GNU Lesser * General Public License v2.1. See the file LICENSE in the top level @@ -14,16 +16,17 @@ * @brief Memory definitions for the Cortex-M family * * @author Francisco Acosta + * @author Kaspar Schleiser * * @} */ -_boot_offset = DEFINED( _rom_offset ) ? _rom_offset : 0x0 ; +_rom_offset = DEFINED( _rom_offset ) ? _rom_offset : 0x0 ; MEMORY { - rom (rx) : ORIGIN = _rom_start_addr + _boot_offset, LENGTH = _rom_length - _boot_offset - ram (w!rx) : ORIGIN = _ram_start_addr, LENGTH = _ram_length + rom (rx) : ORIGIN = _rom_start_addr + _rom_offset, LENGTH = _rom_length + ram (w!rx) : ORIGIN = _ram_start_addr, LENGTH = _ram_length } INCLUDE cortexm_base.ld diff --git a/dist/riotboot/Makefile b/dist/riotboot/Makefile new file mode 100644 index 000000000000..70d6ac8c3051 --- /dev/null +++ b/dist/riotboot/Makefile @@ -0,0 +1,12 @@ +APPLICATION = riotboot + +BOARD ?= samr21-xpro + +USEMODULE += firmware + +CFLAGS += -DNDEBUG -DLOG_LEVEL=LOG_NONE +DISABLE_MODULE += auto_init + +RIOTBASE ?= $(CURDIR)/../.. + +include $(RIOTBASE)/Makefile.include diff --git a/dist/riotboot/README.md b/dist/riotboot/README.md new file mode 100644 index 000000000000..cd6a0ca67ea3 --- /dev/null +++ b/dist/riotboot/README.md @@ -0,0 +1,6 @@ +# Overview + +This folder contains a simple bootloader that allows ping-pong style +over-the-air updates. + +Please see examples/ota as example. diff --git a/dist/riotboot/main.c b/dist/riotboot/main.c new file mode 100644 index 000000000000..c5e3bd478c6e --- /dev/null +++ b/dist/riotboot/main.c @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup bootloader + * @{ + * + * @file + * @brief RIOT Bootloader + * + * @author Kaspar Schleiser + * @author Francisco Acosta + * + * @} + */ + +#include "firmware.h" +#include "cpu.h" +#include "panic.h" + +void kernel_init(void) +{ + uint32_t version = 0; + uint32_t slot = 0; + + /* skip slot 0 (which points to the bootloader) */ + for (unsigned i = 1; i < firmware_num_slots; i++) { + firmware_metadata_t *slot_metadata = firmware_get_metadata(i); + if (firmware_validate_metadata_checksum(slot_metadata)) { + /* skip slot if metadata broken */ + continue; + } + if (slot_metadata->start_addr != firmware_get_image_startaddr(i)) { + continue; + } + if (!slot || slot_metadata->version > version) { + version = slot_metadata->version; + slot = i; + } + } + + if (slot) { + firmware_jump_to_slot(slot); + } + + /* serious trouble! */ + while (1) {} +} + +NORETURN void core_panic(core_panic_t crash_code, const char *message) +{ + (void)crash_code; + (void)message; + while (1) {} +} diff --git a/dist/tools/firmware/Makefile b/dist/tools/firmware/Makefile new file mode 100644 index 000000000000..27de609fb818 --- /dev/null +++ b/dist/tools/firmware/Makefile @@ -0,0 +1,46 @@ +PKG_NAME = tweetnacl +PKG_URL = https://github.com/RIOT-OS/tweetnacl +PKG_VERSION = 7ea05c7098a16c87fa66e9166ce301666f3f2623 +PKG_LICENSE = PD +PKG_BUILDDIR = bin/tweetnacl_src +GITCACHE = ../git/git-cache + +RIOTBASE := ../../.. +RIOT_INCLUDE := $(RIOTBASE)/sys/include +SHA256_DIR := $(RIOTBASE)/sys/hashes +SHA256_INCLUDE := $(RIOT_INCLUDE)/hashes +TWEETNACL_DIR := $(PKG_BUILDDIR) +TWEETNACL_SRC := $(TWEETNACL_DIR)/tweetnacl.c randombytes.c +TWEETNACL_HDR := $(TWEETNACL_DIR)/tweetnacl.h +COMMON_SRC := common.c +COMMON_HDR := common.h + +RIOT_FIRMWARE_SRC := \ + $(SHA256_DIR)/sha256.c \ + $(RIOTBASE)/sys/checksum/fletcher32.c \ + $(RIOTBASE)/sys/firmware/firmware.c \ + $(RIOTBASE)/sys/firmware/firmware_simple.c + +RIOT_FIRMWARE_HDR := $(RIOT_INCLUDE)/firmware.h \ + $(RIOT_INCLUDE)/hashes/sha256.h \ + $(RIOT_INCLUDE)/checksum/fletcher32.h + +FIRMWARE_SRC := $(COMMON_SRC) $(TWEETNACL_SRC) $(RIOT_FIRMWARE_SRC) \ + main.c verify.c genkeys.c sign.c + +FIRMWARE_HDR := $(COMMON_HDR) $(RIOT_FIRMWARE_HDR) + +CFLAGS += -g -I. -O3 -Wall -Wextra -pedantic -std=c99 + +all: bin/firmware + +bin/: + mkdir -p bin + +bin/firmware: git-download $(FIRMWARE_HDR) $(FIRMWARE_SRC) Makefile | bin/ + $(CC) $(CFLAGS) -I$(RIOT_INCLUDE) -I$(TWEETNACL_DIR) $(FIRMWARE_SRC) -o $@ + +clean:: + rm -rf bin/firmware + +include $(RIOTBASE)/pkg/pkg.mk diff --git a/dist/tools/firmware/common.c b/dist/tools/firmware/common.c new file mode 100644 index 000000000000..98b7ce0245f4 --- /dev/null +++ b/dist/tools/firmware/common.c @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU General Public + * License v2. See the file LICENSE for more details. + */ + +#include +#include +#include +#include + +#include "hashes/sha256.h" + +off_t fsize(const char *filename) +{ + struct stat st; + + if (stat(filename, &st) == 0) { + return st.st_size; + } + + return -1; +} + +int to_file(const char *filename, void *buf, size_t len) +{ + int fd; + + if (strcmp("-", filename)) { + fd = open(filename, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); + } + else { + fd = STDOUT_FILENO; + } + + if (fd > 0) { + ssize_t res = write(fd, buf, len); + close(fd); + return res == (ssize_t)len; + } + else { + return fd; + } +} + +int from_file(const char *filename, void *buf, size_t len) +{ + int fd = open(filename, O_RDONLY); + + if (fd > 0) { + ssize_t res = read(fd, buf, len); + close(fd); + return res == (ssize_t)len; + } + else { + return fd; + } +} + +ssize_t do_sha256(const char *filename, void *tgt, off_t offset) +{ + sha256_context_t sha256; + + sha256_init(&sha256); + + ssize_t bytes_read; + ssize_t total = 0; + char buf[1024]; + + int fd = open(filename, O_RDONLY); + if (!fd) { + return -1; + } + + if (offset) { + if (lseek(fd, offset, SEEK_SET) == -1) { + return -1; + } + } + + while ((bytes_read = read(fd, buf, sizeof(buf)))) { + sha256_update(&sha256, buf, bytes_read); + total += bytes_read; + } + + sha256_final(&sha256, tgt); + + close(fd); + + return total; +} diff --git a/dist/tools/firmware/common.h b/dist/tools/firmware/common.h new file mode 100644 index 000000000000..078b87d2f60d --- /dev/null +++ b/dist/tools/firmware/common.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU General Public + * License v2. See the file LICENSE for more details. + */ + +#ifndef COMMON_H +#define COMMON_H + +#include +#include + +#ifdef __cplusplus + extern "C" { +#endif + +off_t fsize(const char *filename); +int to_file(const char *filename, void *buf, size_t len); +int from_file(const char *filename, void *buf, size_t len); +int do_sha256(const char *filename, void *tgt, size_t offset); + +#ifdef __cplusplus +} /* end extern "C" */ +#endif + +#endif /* COMMON_H */ diff --git a/dist/tools/firmware/genkeys.c b/dist/tools/firmware/genkeys.c new file mode 100644 index 000000000000..368b159e9b1f --- /dev/null +++ b/dist/tools/firmware/genkeys.c @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU General Public + * License v2. See the file LICENSE for more details. + */ + +#include + +#include "common.h" +#include "tweetnacl.h" + +const char genkeys_usage[] = "firmware genkeys "; + +int genkeys(int argc, const char *argv[]) +{ + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + + if (argc < 3) { + fprintf(stderr, "usage: %s\n", genkeys_usage); + return 1; + } + + crypto_sign_keypair(pk, sk); + + const char err[] = "error writing to %s\n"; + if (!to_file(argv[1], sk, sizeof(sk))) { + fprintf(stderr, err, argv[1]); + return -1; + } + + if (!to_file(argv[2], pk, sizeof(pk))) { + fprintf(stderr, err, argv[2]); + return -1; + } + + return 0; +} diff --git a/dist/tools/firmware/main.c b/dist/tools/firmware/main.c new file mode 100644 index 000000000000..c5944b502990 --- /dev/null +++ b/dist/tools/firmware/main.c @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU General Public + * License v2. See the file LICENSE for more details. + */ + +#include +#include + +int verify(int argc, char *argv[]); +extern const char verify_usage[]; + +int genkeys(int argc, char *argv[]); +extern const char genkeys_usage[]; + +int sign(int argc, char *argv[]); +extern const char sign_usage[]; + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + goto usage; + } + if (!strcmp(argv[1], "verify")) { + return verify(argc, argv); + } + else if (!strcmp(argv[1], "genkeys")) { + return genkeys(argc - 1, &argv[1]); + } + else if (!strcmp(argv[1], "sign")) { + return sign(argc - 1, &argv[1]); + } + +usage: + fprintf(stderr, "usage: %s\n" + " %s\n" + " %s\n", + genkeys_usage, sign_usage, verify_usage); + return 1; +} diff --git a/dist/tools/firmware/randombytes.c b/dist/tools/firmware/randombytes.c new file mode 100644 index 000000000000..e9170bb9ae6a --- /dev/null +++ b/dist/tools/firmware/randombytes.c @@ -0,0 +1,62 @@ +/* + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * 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 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. + * + * randombytes/devurandom.h version 20080713 + * D. J. Bernstein + * Public domain. + */ + +#include +#include +#include +#include + +/* it's really stupid that there isn't a syscall for this */ + +static int fd = -1; + +void randombytes(unsigned char *x,unsigned long long xlen) +{ + int i; + + if (fd == -1) { + for (;;) { + fd = open("/dev/urandom",O_RDONLY); + if (fd != -1) break; + sleep(1); + } + } + + while (xlen > 0) { + if (xlen < 1048576) i = xlen; else i = 1048576; + + i = read(fd,x,i); + if (i < 1) { + sleep(1); + continue; + } + + x += i; + xlen -= i; + } +} diff --git a/dist/tools/firmware/sign.c b/dist/tools/firmware/sign.c new file mode 100644 index 000000000000..2614531b8b55 --- /dev/null +++ b/dist/tools/firmware/sign.c @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016 Mark Solters + * 2016 Inria + * 2017 Kaspar Schleiser + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ + +/** + * @ingroup tools + * @file + * @brief Meta-data generation and signing for FW images + * + * @author Mark Solters + * @author Francisco Acosta + * @author Kaspar Schleiser + */ + +#include +#include +#include +#include + +#include "firmware.h" +#include "firmware/simple.h" +#include "common.h" + +const char sign_usage[] = "firmware sign "; + +int sign(int argc, char *argv[]) +{ + firmware_simple_t metadata; + unsigned char sk[FIRMWARE_SECKEY_LEN]; + ssize_t firmware_size; + + memset(&metadata, '\0', sizeof(firmware_simple_t)); + + if (argc < 7) { + fprintf(stderr, "usage: %s\n", sign_usage); + return -1; + } + + if (!from_file(argv[5], sk, sizeof(sk))) { + fprintf(stderr, "Error: cannot read keyfile\n"); + return(1); + } + + firmware_size = do_sha256(argv[1], metadata.hash, 0); + if (firmware_size == -1) { + fprintf(stderr, "Error: cannot read firmware file\n"); + return(1); + } + + /* Generate FW image metadata */ + memcpy(&metadata.metadata.magic_number, "RIOT", 4); + metadata.size = firmware_size; + sscanf(argv[2], "%x", (unsigned int *)&(metadata.metadata.version)); + sscanf(argv[3], "%x", &(metadata.appid)); + sscanf(argv[4], "%u", &(metadata.metadata.start_addr)); + + /* calculate metadata checksum */ + metadata.metadata.chksum = firmware_metadata_checksum(&metadata.metadata); + + metadata.metadata_type = FIRMWARE_METADATA_RIOTBOOT; + + /* sign */ + firmware_simple_sign(&metadata, sk); + + /* talk to the user */ + if (strcmp(argv[6], "-")) { + firmware_simple_print(&metadata); + printf("Metadata size: %lu\n", sizeof(firmware_simple_t)); + } + + /* Write the metadata */ + if (!to_file(argv[6], &metadata, FIRMWARE_METADATA_SIZE)) { + fprintf(stderr, "Error: cannot write output\n"); + return(1); + } + + return 0; +} diff --git a/dist/tools/firmware/verify.c b/dist/tools/firmware/verify.c new file mode 100644 index 000000000000..62b5adda4fc4 --- /dev/null +++ b/dist/tools/firmware/verify.c @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU General Public + * License v2. See the file LICENSE for more details. + */ + +#include +#include +#include + +#include "firmware.h" +#include "firmware/simple.h" +#include "common.h" +#include "tweetnacl.h" + +const char verify_usage[] = "firmware verify "; + +int verify(int argc, char *argv[]) +{ + unsigned char pubkey[FIRMWARE_PUBKEY_LEN]; + firmware_simple_t metadata; + + if (argc < 4) { + fprintf(stderr, "usage: %s\n", verify_usage); + return 1; + } + + if (!from_file(argv[2], &metadata, sizeof(metadata))) { + fprintf(stderr, "error reading image file\n"); + return 1; + } + + if (!from_file(argv[3], pubkey, sizeof(pubkey))) { + fprintf(stderr, "error reading keyfile\n"); + return 1; + } + + int res = firmware_simple_validate_signature(&metadata, pubkey) ? 1 : 0; + if (res) { + printf("signature check failed\n"); + return -1; + } else { + printf("signature check passed\n"); + } + + off_t size = fsize(argv[2]); + if (size == sizeof(firmware_simple_t)) { + printf("hash check skipped\n"); + goto out; + } + + char hash[SHA256_DIGEST_LENGTH]; + do_sha256(argv[2], hash, sizeof(firmware_simple_t)); + res = memcmp(hash, metadata.hash, SHA256_DIGEST_LENGTH) ? 1 : 0; + if (res) { + printf("hash check failed\n"); + } else { + printf("hash check passed\n"); + } + +out: + return res; +} diff --git a/examples/ota/Makefile b/examples/ota/Makefile new file mode 100644 index 000000000000..577788ef6915 --- /dev/null +++ b/examples/ota/Makefile @@ -0,0 +1,48 @@ +# name of your application +APPLICATION = ota_example + +# If no BOARD is found in the environment, use this default: +BOARD ?= samr21-xpro + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +# Signature verification needs quite a lot of stack +CFLAGS += "-DTHREAD_STACKSIZE_MAIN=((6*THREAD_STACKSIZE_DEFAULT)+THREAD_EXTRA_STACKSIZE_PRINTF)" + +# include ota modules +USEMODULE += ota_coap +USEMODULE += firmware + +# include CoAP server +USEMODULE += nanocoap_sock + +# Include packages that pull up and auto-init the link layer. +# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present +USEMODULE += gnrc_netdev_default +USEMODULE += auto_init_gnrc_netif +# Specify the mandatory networking modules for IPv6 and UDP +USEMODULE += gnrc_ipv6_default +USEMODULE += gnrc_udp +USEMODULE += gnrc_sock_udp +# Additional networking modules that can be dropped if not needed +USEMODULE += gnrc_icmpv6_echo + +# include this for printing IP addresses +USEMODULE += shell_commands + +include $(RIOTBASE)/Makefile.include + +# Set a custom channel if needed +ifneq (,$(filter cc110x,$(USEMODULE))) # radio is cc110x sub-GHz + DEFAULT_CHANNEL ?= 0 + CFLAGS += -DCC110X_DEFAULT_CHANNEL=$(DEFAULT_CHANNEL) +else + ifneq (,$(filter at86rf212b,$(USEMODULE))) # radio is IEEE 802.15.4 sub-GHz + DEFAULT_CHANNEL ?= 5 + CFLAGS += -DIEEE802154_DEFAULT_SUBGHZ_CHANNEL=$(DEFAULT_CHANNEL) + else # radio is IEEE 802.15.4 2.4 GHz + DEFAULT_CHANNEL ?= 26 + CFLAGS += -DIEEE802154_DEFAULT_CHANNEL=$(DEFAULT_CHANNEL) + endif +endif diff --git a/examples/ota/README.md b/examples/ota/README.md new file mode 100644 index 000000000000..c8dc357bc7b2 --- /dev/null +++ b/examples/ota/README.md @@ -0,0 +1,23 @@ +# Introduction + +This example intends to explain usage of the riotboot OTA update scheme using +CoAP. + +# Quickstart + +- run "make" in "dist/tools/firmware" + +- run "bin/firmware genkeys sec.key pub.key" in "dist/tools/firmware" to create + keypair + +- flash image and bootloader + + $ "BOARD=samr21-xpro APP_VER=$(date +%s) make -j4 riotboot/flash + +- recompile using + + $ BOARD=samr21-xpro APP_VER=$(date +%s) make -j4 clean riotboot + +- send upate via coap, e.g., + + $ coap-cli -m put coap://[ + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup examples + * @{ + * + * @file + * @brief CoAP / OTA example server application (using nanocoap) + * + * @author Kaspar Schleiser + * @} + */ + +#include + +#include "net/nanocoap.h" +#include "net/nanocoap_sock.h" +#include "net/ota/coap.h" +#include "firmware.h" +#include "firmware/simple.h" +#include "periph/pm.h" + +#include "xtimer.h" + +#define COAP_INBUF_SIZE (256U) + +#define MAIN_QUEUE_SIZE (8) +static msg_t _main_msg_queue[MAIN_QUEUE_SIZE]; + +/* import "ifconfig" shell command, used for printing addresses */ +extern int _gnrc_netif_config(int argc, char **argv); + +/* must be sorted by path (alphabetically) */ +const coap_resource_t coap_resources[] = { + COAP_WELL_KNOWN_CORE_DEFAULT_HANDLER, + COAP_OTA_HANDLER, +}; + +const unsigned coap_resources_numof = sizeof(coap_resources) / sizeof(coap_resources[0]); + +int main(void) +{ + printf("RIOT OTA update over CoAP example application\n"); + + /* print some information about the running image */ + unsigned current_slot = firmware_current_slot(); + firmware_metadata_t *metadata = firmware_get_metadata(current_slot); + printf("firmware: running from slot %u\n", current_slot); + firmware_simple_print((firmware_simple_t*)metadata); + + /* nanocoap_server uses gnrc sock which uses gnrc which needs a msg queue */ + msg_init_queue(_main_msg_queue, MAIN_QUEUE_SIZE); + + puts("Waiting for address autoconfiguration..."); + xtimer_sleep(3); + + /* print network addresses */ + puts("Configured network interfaces:"); + _gnrc_netif_config(0, NULL); + + /* initialize nanocoap server instance */ + uint8_t buf[COAP_INBUF_SIZE]; + sock_udp_ep_t local = { .port=COAP_PORT, .family=AF_INET6 }; + nanocoap_server(&local, buf, sizeof(buf)); + + /* should be never reached */ + return 0; +} diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index ebb54ee0a9cf..8ab5c45d10f7 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -60,6 +60,7 @@ PSEUDOMODULES += newlib PSEUDOMODULES += newlib_gnu_source PSEUDOMODULES += newlib_nano PSEUDOMODULES += openthread +PSEUDOMODULES += ota_% PSEUDOMODULES += pktqueue PSEUDOMODULES += printf_float PSEUDOMODULES += prng diff --git a/makefiles/riotboot.mk b/makefiles/riotboot.mk new file mode 100644 index 000000000000..400132a914cc --- /dev/null +++ b/makefiles/riotboot.mk @@ -0,0 +1,91 @@ +ifneq (,$(filter riotboot,$(FEATURES_USED))) + +.PHONY: riotboot/flash riotboot/flash-bootloader riotboot/flash-slot1 riotboot/flash-slot2 \ + riotboot/verify-image + +RIOTBOOT ?= $(RIOTBASE)/tools/riotboot/bin/$(BOARD)/riotboot.elf +CFLAGS += -I$(BINDIR)/riotbuild + +RIOTBOOT_SECKEY ?= $(RIOTBASE)/dist/tools/firmware/sec.key +RIOTBOOT_PUBKEY ?= $(RIOTBASE)/dist/tools/firmware/pub.key + +FIRMWARE_TOOL ?= $(RIOTBASE)/dist/tools/firmware/bin/firmware +RIOTBOOT_HDR_LEN := 256 + +APP_ID ?= 0 +APP_VER ?= 0 + +$(BINDIR)/$(APPLICATION)-%.elf: link + $(Q) \ + $(_LINK) \ + $(LINKFLAGPREFIX)--defsym=_rom_offset=$(OFFSET) \ + $(LINKFLAGPREFIX)--defsym=_rom_length=$(LENGTH) \ + -o $@ + +# slot 1 targets +SLOT1_OFFSET := $$(($(RIOTBOOT_SLOT0_SIZE) + $(RIOTBOOT_HDR_LEN))) +SLOT1_LENGTH := $$(($(RIOTBOOT_SLOT1_SIZE) - $(RIOTBOOT_HDR_LEN))) + +$(BINDIR)/$(APPLICATION)-slot1.elf: OFFSET=$(SLOT1_OFFSET) +$(BINDIR)/$(APPLICATION)-slot1.elf: LENGTH=$(SLOT1_LENGTH) + +# slot 2 targets +SLOT2_OFFSET := $$(($(RIOTBOOT_SLOT0_SIZE) + $(RIOTBOOT_SLOT2_SIZE) + $(RIOTBOOT_HDR_LEN))) +SLOT2_LENGTH := $$(($(RIOTBOOT_SLOT2_SIZE) - $(RIOTBOOT_HDR_LEN))) + +$(BINDIR)/$(APPLICATION)-slot2.elf: OFFSET=$(SLOT2_OFFSET) +$(BINDIR)/$(APPLICATION)-slot2.elf: LENGTH=$(SLOT2_LENGTH) + +# signing targets +$(BINDIR)/$(APPLICATION)-%.bin: + @echo "creating $@..." + $(Q) $(OBJCOPY) -Obinary $< $@.tmp + $(Q) $(FIRMWARE_TOOL) sign $@.tmp $(APP_VER) $(APP_ID) $(OFFSET) $(RIOTBOOT_SECKEY) - > $@ + $(Q) cat $@.tmp >> $@ + $(Q) rm $@.tmp + +$(BINDIR)/$(APPLICATION)-slot1.bin: OFFSET=$(SLOT1_OFFSET) +$(BINDIR)/$(APPLICATION)-slot1.bin: $(BINDIR)/$(APPLICATION)-slot1.elf + +$(BINDIR)/$(APPLICATION)-slot2.bin: OFFSET=$(SLOT2_OFFSET) +$(BINDIR)/$(APPLICATION)-slot2.bin: $(BINDIR)/$(APPLICATION)-slot2.elf + +# creating pubkey header file +$(RIOTBUILD_CONFIG_HEADER_C): $(BINDIR)/riotbuild/ota_pubkey.h +$(BINDIR)/riotbuild/ota_pubkey.h: $(RIOTBOOT_PUBKEY) + @mkdir -p $(@D) + @{ \ + echo "static const unsigned char ota_public_key[] = {"; \ + cat $< | xxd -i ; \ + echo "};"; \ + } > $@ + +riotboot: $(BINDIR)/$(APPLICATION)-slot1.bin $(BINDIR)/$(APPLICATION)-slot2.bin $(BINDIR)/riotbuild/ota_pubkey.h + +riotboot/verify-image: + $(FIRMWARE_TOOL) verify $(BINDIR)/$(APPLICATION)-slot1.bin $(RIOTBOOT_PUBKEY) + $(FIRMWARE_TOOL) verify $(BINDIR)/$(APPLICATION)-slot2.bin $(RIOTBOOT_PUBKEY) + +riotboot/flash-bootloader: + $(Q)/usr/bin/env -i \ + PATH=$(PATH) BOARD=$(BOARD) \ + make --no-print-directory -C $(RIOTBASE)/dist/riotboot flash + +riotboot/flash-slot1: FLASH_ADDR=$(RIOTBOOT_SLOT0_SIZE) +riotboot/flash-slot1: HEXFILE=$(BINDIR)/$(APPLICATION)-slot1.bin +riotboot/flash-slot1: $(BINDIR)/$(APPLICATION)-slot1.bin riotboot/flash-bootloader + $(FLASHER) $(FFLAGS) + +riotboot/flash-slot2: FLASH_ADDR=$$(($(RIOTBOOT_SLOT0_SIZE) + $(RIOTBOOT_SLOT1_SIZE))) +riotboot/flash-slot2: HEXFILE=$(BINDIR)/$(APPLICATION)-slot2.bin +riotboot/flash-slot2: $(BINDIR)/$(APPLICATION)-slot2.bin riotboot/flash-bootloader + $(FLASHER) $(FFLAGS) + +riotboot/flash: riotboot/flash-slot1 + +else +riotboot: + $(Q)echo "error: riotboot feature not selected! (try FEATURES_REQUIRED += riotboot)" + $(Q)false + +endif # RIOTBOOT_SLOT0_SIZE diff --git a/makefiles/tools/edbg.inc.mk b/makefiles/tools/edbg.inc.mk index 5738b49cd0a3..dc322653ac10 100644 --- a/makefiles/tools/edbg.inc.mk +++ b/makefiles/tools/edbg.inc.mk @@ -10,7 +10,10 @@ HEXFILE = $(BINFILE) ifneq (,$(DEBUG_ADAPTER_ID)) EDBG_ARGS += --serial $(DEBUG_ADAPTER_ID) endif -FFLAGS ?= $(EDBG_ARGS) -t $(EDBG_DEVICE_TYPE) -b -e -v -p -f $(HEXFILE) + +EDBG_ARGS += -o $(FLASH_ADDR) + +FFLAGS ?= $(EDBG_ARGS) -t $(EDBG_DEVICE_TYPE) -b -v -p -f $(HEXFILE) ifeq ($(RIOT_EDBG),$(FLASHER)) FLASHDEPS += $(RIOT_EDBG) diff --git a/sys/Makefile b/sys/Makefile index f08ec774aed1..e3a0a5e44b5b 100644 --- a/sys/Makefile +++ b/sys/Makefile @@ -133,6 +133,9 @@ endif ifneq (,$(filter rdcli_simple,$(USEMODULE))) DIRS += net/application_layer/rdcli_simple endif +ifneq (,$(filter ota_%,$(USEMODULE))) + DIRS += net/application_layer/ota +endif DIRS += $(dir $(wildcard $(addsuffix /Makefile, $(USEMODULE)))) diff --git a/sys/firmware/Makefile b/sys/firmware/Makefile new file mode 100644 index 000000000000..b59eb29ea1a6 --- /dev/null +++ b/sys/firmware/Makefile @@ -0,0 +1,5 @@ +CFLAGS += -DSLOT0_SIZE=$(RIOTBOOT_SLOT0_SIZE) +CFLAGS += -DSLOT1_SIZE=$(RIOTBOOT_SLOT1_SIZE) +CFLAGS += -DSLOT2_SIZE=$(RIOTBOOT_SLOT2_SIZE) + +include $(RIOTBASE)/Makefile.base diff --git a/sys/firmware/firmware.c b/sys/firmware/firmware.c new file mode 100644 index 000000000000..ce4e737928e2 --- /dev/null +++ b/sys/firmware/firmware.c @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_firmware + * @{ + * + * @file + * @brief Firmware module + * + * @author Kaspar Schleiser + * @author Francisco Acosta + * + * @} + */ + +#include + +#ifdef RIOT_VERSION +#include "cpu.h" +#include "log.h" +#else +#include +#define LOG_INFO(...) printf(__VA_ARGS__) +#endif + +#include "firmware.h" +#include "checksum/fletcher32.h" + +int firmware_validate_metadata_checksum(firmware_metadata_t *metadata) +{ + if (memcmp(metadata, "RIOT", 4)) { + LOG_INFO("%s: metadata magic number invalid\n", __func__); + return -1; + } + + int res = firmware_metadata_checksum(metadata) == metadata->chksum ? 0 : -1; + if (res) { + LOG_INFO("%s: metadata checksum invalid\n", __func__); + } + + return res; +} + +uint32_t firmware_metadata_checksum(firmware_metadata_t *metadata) +{ + return fletcher32((uint16_t *)metadata, FIRMWARE_CHECKSUM_LEN / 2); +} + +#ifdef RIOT_VERSION +#ifndef BOARD_NATIVE +static const unsigned _firmware_slot_start[] = { + CPU_FLASH_BASE, + CPU_FLASH_BASE + SLOT0_SIZE, + CPU_FLASH_BASE + SLOT0_SIZE + SLOT1_SIZE +}; + +const unsigned firmware_num_slots = sizeof(_firmware_slot_start) / sizeof(unsigned); + +void firmware_jump_to_image(firmware_metadata_t *metadata) +{ + cpu_jump_to_image(metadata->start_addr); +} + +int firmware_current_slot(void) +{ + unsigned base_addr = cpu_get_image_baseaddr() - FIRMWARE_METADATA_SIZE; + + /* don't consider bootloader slot, thus start at 1 */ + for (unsigned i = 1; i < firmware_num_slots; i++) { + if (base_addr == _firmware_slot_start[i]) { + return i; + } + } + + return -1; +} + +int firmware_target_slot(void) +{ + /* TODO: find logic for more than one slot */ + return firmware_current_slot() == 1 ? 2 : 1; +} + +firmware_metadata_t *firmware_get_metadata(unsigned slot) +{ + assert(slot < firmware_num_slots); + return (firmware_metadata_t *)_firmware_slot_start[slot]; +} + +unsigned firmware_get_image_startaddr(unsigned slot) +{ + assert(slot < firmware_num_slots); + return firmware_get_metadata(slot)->start_addr; +} + +void firmware_jump_to_slot(unsigned slot) +{ + firmware_jump_to_image(firmware_get_metadata(slot)); +} + +void firmware_dump_slot_addrs(void) +{ + for (unsigned i = 1; i < firmware_num_slots; i++) { + printf("slot %u: metadata: 0x%08x image: 0x%08x\n", i, + (unsigned)_firmware_slot_start[i], firmware_get_image_startaddr(i)); + } +} +#else /* BOARD_NATIVE */ +const unsigned firmware_num_slots = 1; + +void firmware_jump_to_image(firmware_metadata_t *metadata) +{ + (void)metadata; + printf("%s native stub\n", __func__); +} + +int firmware_current_slot(void) +{ + printf("%s native stub\n", __func__); + + return 1; +} + +firmware_metadata_t *firmware_get_metadata(unsigned slot) +{ + (void)slot; + printf("%s native stub\n", __func__); + return NULL; +} + +unsigned firmware_get_image_startaddr(unsigned slot) +{ + (void)slot; + printf("%s native stub\n", __func__); + return 0; +} + +void firmware_jump_to_slot(unsigned slot) +{ + (void)slot; + printf("%s native stub\n", __func__); +} + +void firmware_dump_slot_addrs(void) +{ + printf("%s native stub\n", __func__); +} +#endif +#endif /* RIOT_VERSION */ diff --git a/sys/firmware/firmware_flashwrite.c b/sys/firmware/firmware_flashwrite.c new file mode 100644 index 000000000000..55080b1a1d92 --- /dev/null +++ b/sys/firmware/firmware_flashwrite.c @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_firmware + * @{ + * + * @file + * @brief Firmware update helper functions + * + * @author Kaspar Schleiser + * + * @} + */ + +#include + +#include "firmware/flashwrite.h" +#include "od.h" + +#define LOG_PREFIX "firmware_flashwrite: " +#include "log.h" + +static inline size_t min(size_t a, size_t b) +{ + return a <= b ? a : b; +} + +size_t firmware_flashwrite_slotsize(const firmware_flashwrite_t *state) +{ + switch (state->target_slot) { + case 1: return SLOT1_SIZE; + case 2: return SLOT2_SIZE; + default: return 0; + } +} + +int firmware_flashwrite_init(firmware_flashwrite_t *state, int target_slot, + size_t offset) +{ + LOG_INFO(LOG_PREFIX "initializing update to target slot %i\n", + target_slot); + + memset(state, 0, sizeof(firmware_flashwrite_t)); + + state->offset = offset; + state->target_slot = target_slot; + state->flashpage = flashpage_page(firmware_get_metadata(target_slot)); + + return 0; +} + +int firmware_flashwrite_putbytes(firmware_flashwrite_t *state, + const uint8_t *bytes, size_t len, bool more) +{ + LOG_INFO(LOG_PREFIX "processing bytes %u-%u\n", state->offset, state->offset + len - 1); + + while (len) { + size_t flashpage_pos = state->offset % FLASHPAGE_SIZE; + size_t flashpage_avail = FLASHPAGE_SIZE - flashpage_pos; + + size_t to_copy = min(flashpage_avail, len); + + memcpy(state->flashpage_buf + flashpage_pos, bytes, to_copy); + flashpage_avail -= to_copy; + + state->offset += to_copy; + flashpage_pos += to_copy; + bytes += to_copy; + len -= to_copy; + if ((!flashpage_avail) || (!more)) { + if (flashpage_write_and_verify(state->flashpage, state->flashpage_buf) != FLASHPAGE_OK) { + LOG_WARNING(LOG_PREFIX "error writing flashpage %u!\n", state->flashpage); + return -1; + } + state->flashpage++; + } + } + + return 0; +} + +int firmware_flashwrite_finish(firmware_flashwrite_t *state, + firmware_metadata_t *metadata, size_t len) +{ + int res = -1; + + void *slot_start = firmware_get_metadata(state->target_slot); + size_t metadata_space = (void*)metadata->start_addr - slot_start; + + void *firstpage; + + if (len < FLASHPAGE_SIZE) { + firstpage = state->flashpage_buf; + memset(firstpage, 0, metadata_space); + memcpy(firstpage, metadata, len); + memcpy(firstpage + metadata_space, \ + slot_start + metadata_space, \ + FLASHPAGE_SIZE - metadata_space); + } + else { + firstpage = metadata; + } + + int flashpage = flashpage_page(slot_start); + if (flashpage_write_and_verify(flashpage, firstpage) != FLASHPAGE_OK) { + LOG_WARNING(LOG_PREFIX "re-flashing first block failed!\n"); + goto out; + } + + LOG_INFO(LOG_PREFIX "firmware flashing completed successfully\n"); + res = 0; + +out: + return res; +} diff --git a/sys/firmware/firmware_simple.c b/sys/firmware/firmware_simple.c new file mode 100644 index 000000000000..2207c3363a74 --- /dev/null +++ b/sys/firmware/firmware_simple.c @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2017 Kaspar Schleiser + * Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_firmware + * @{ + * + * @file + * @brief Simple firmware format update module + * + * @author Kaspar Schleiser + * @author Francisco Acosta + * + * @} + */ + +#include +#include + +#define LOG_PREFIX "firmware_simple: " + +#ifdef RIOT_VERSION +#include "log.h" +#include "ota_pubkey.h" +#else +#include +#define LOG_INFO(...) printf(__VA_ARGS__) +#endif + +#include "firmware.h" +#include "firmware/simple.h" + +static inline size_t min(size_t a, size_t b) +{ + return a <= b ? a : b; +} + +void firmware_simple_print(firmware_simple_t *simple) +{ + printf("Firmware magic_number: 0x%08x\n", (unsigned)simple->metadata.magic_number); + printf("Firmware Version: %#x\n", (unsigned)simple->metadata.version); + printf("Firmware start address: 0x%08x\n", (unsigned)simple->metadata.start_addr); + /* Only output full info if the metadata_type matches */ + if (simple->metadata_type == FIRMWARE_METADATA_RIOTBOOT) + { + printf("Firmware APPID: %#x\n", (unsigned)simple->appid); + printf("Firmware Size: %" PRIu32 "\n", simple->size); + printf("Firmware HASH: "); + for (unsigned long i = 0; i < sizeof(simple->hash); i++) { + printf("%02x ", simple->hash[i]); + } + printf("\n"); + printf("Firmware chksum: 0x%08x\n", (unsigned)simple->metadata.chksum); + printf("Firmware signature: "); + for (unsigned long i = 0; i < sizeof(simple->sig); i++) { + printf("%02x ", simple->sig[i]); + } + printf("\n"); + } + else { + printf("Firmware of unknown type\n"); + } +} + +int firmware_simple_validate_size(firmware_simple_update_t *simple) +{ + if (simple->m.metadata.size <= firmware_flashwrite_slotsize(&simple->writer)) { + return 0; + } + else { + return -1; + } +} + +int firmware_simple_validate(firmware_simple_t *simple, const unsigned char *pk) +{ + if (firmware_validate_metadata_checksum(&simple->metadata)) { + LOG_WARNING("%s: metadata chacksum invalid\n", __func__); + return -1; + } + + unsigned char sm[FIRMWARE_SIGN_BYTES + crypto_sign_BYTES]; + memcpy(sm, ((unsigned char *)simple) + FIRMWARE_SIGN_BYTES, crypto_sign_BYTES); + memcpy(sm + crypto_sign_BYTES, simple, FIRMWARE_SIGN_BYTES); + + unsigned char m[FIRMWARE_SIGN_BYTES + crypto_sign_BYTES]; + unsigned long long mlen; + int res = crypto_sign_open(m, &mlen, sm, FIRMWARE_SIGN_BYTES + crypto_sign_BYTES, pk); + if (res) { + LOG_WARNING("%s: RIOTboot metadata signature invalid\n", __func__); + } + return res; +} + +int firmware_simple_sign(firmware_simple_t *simple, unsigned char *sk) +{ + unsigned char sm[FIRMWARE_SIGN_BYTES + crypto_sign_BYTES]; + unsigned long long smlen; + + crypto_sign(sm, &smlen, (unsigned char *)simple, FIRMWARE_SIGN_BYTES, sk); + memcpy(simple->sig, sm, crypto_sign_BYTES); + return 0; +} + +#ifdef RIOT_VERSION +static ssize_t _simple_recv_metadata(firmware_simple_update_t *state, const uint8_t *buf, size_t len) +{ + size_t to_copy = min(len, FIRMWARE_METADATA_SIZE - state->writer.offset); + /* copy metadata from packet to the firmware update struct */ + memcpy(state->m.metadata_buf + state->writer.offset, + buf, + to_copy); + if ((state->writer.offset + to_copy) >= FIRMWARE_METADATA_SIZE - 1) { + /* Full metadata received */ + firmware_simple_t *metadata = &state->m.metadata; + LOG_INFO(LOG_PREFIX "verifying metadata ...\n"); + if (metadata->metadata.start_addr != firmware_get_image_startaddr(state->writer.target_slot)) { + LOG_WARNING(LOG_PREFIX "start address doesn't match selected slot. Aborting.\n"); + LOG_WARNING(LOG_PREFIX "(image start=%p slot start=%p)\n", (void *)metadata->metadata.start_addr, \ + (void *)firmware_get_image_startaddr(state->writer.target_slot)); + return -1; + } + + if (firmware_simple_validate_size(state)) { + LOG_WARNING("%s: firmware size invalid\n", __func__); + return -ENOSPC; + } + + /* check metadata magic nr, checksum, signature and general validity */ + if (firmware_simple_validate(metadata, ota_public_key)) { + LOG_WARNING(LOG_PREFIX "verification failed!\n"); + return -1; + } + else { + state->state = FIRMWARE_UPDATE_VERIFIED; + LOG_INFO(LOG_PREFIX "verification successful\n"); + } + state->writer.flashpage++; + } + return to_copy; +} + +int firmware_simple_init(firmware_simple_update_t *state) +{ + state->state = FIRMWARE_UPDATE_INITIALIZED; + return firmware_flashwrite_init(&state->writer, firmware_target_slot(), 0); +} + +int firmware_simple_putbytes(firmware_simple_update_t *state, const uint8_t *bytes, size_t len) +{ + /* metadata copy */ + if (state->state != FIRMWARE_UPDATE_VERIFIED) { + ssize_t res = _simple_recv_metadata(state, bytes, len); + if (res < 0 ) { + return res; + } + state->writer.offset += res; + bytes += res; + len -= res; + } + /* firmware copy */ + if (state->state == FIRMWARE_UPDATE_VERIFIED && len) { + /* Calculate if there are more bytes that should be written */ + size_t metadata_size = state->m.metadata.metadata.start_addr - + (uint32_t)firmware_get_metadata(state->writer.target_slot); + bool more = (state->m.metadata.size + metadata_size) - + (state->writer.offset + len); + return firmware_flashwrite_putbytes(&state->writer, bytes, len, more); + } + return 0; +} + +int firmware_simple_finish(firmware_simple_update_t *state) +{ + firmware_simple_t *metadata = &state->m.metadata; + + LOG_DEBUG(LOG_PREFIX "verifying hash...\n"); + sha256_init(&state->sha); + sha256_update(&state->sha, (void*)metadata->metadata.start_addr, + metadata->size); + + sha256_final(&state->sha, state->writer.flashpage_buf); + + if (memcmp(state->writer.flashpage_buf, metadata->hash, SHA256_DIGEST_LENGTH)) { + LOG_WARNING(LOG_PREFIX "image hash incorrect!\n"); + state->state = FIRMWARE_UPDATE_IDLE; + return -1; + } + + LOG_DEBUG(LOG_PREFIX "hash verified, re-flashing first block...\n"); + if (firmware_flashwrite_finish(&state->writer, + (firmware_metadata_t*)metadata, sizeof(firmware_simple_t)) != 0) { + return -1; + } + + LOG_INFO(LOG_PREFIX "firmware flashing completed successfully\n"); + return 0; + +} +#endif diff --git a/sys/include/firmware.h b/sys/include/firmware.h new file mode 100644 index 000000000000..72f0c3e2bb49 --- /dev/null +++ b/sys/include/firmware.h @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2018 Kaspar Schleiser + * Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_firmware Firmware Updates + * @ingroup sys + * @{ + * + * # riotboot minimal firmware update infrastructure + * + * ## Overview + * + * riotboot is the name of a minimal bootloader application and infrastructure. + * It consists of + * + * - the application "riotboot" in "dist/riotboot" which serves as + * minimal bootloader + * + * - the modules "firmware" and "firmware_update" that are used + * in an image to handle verification and flash-writing + * + * - the module "ota_coap" which serves as transport for the updates + * + * - a tool in dist/tools/firmware which handles key generation + * and firmware image signing. + * + * - a couple of make targets to glue everything together + * + * ## Concept + * + * riotboot expects the flash to be split in three parts: one for the + * bootloader itself, and two slots for images. + * Of the latter, one slot holds the running (or latest) image, while the other is + * used to write a newer image. + * + * The bootloader will, on reset, verify the checksum of the image slots, then + * choose the one with the highest version number that has a valid checksum. If + * all slots have the same version number, the first one will be booted. If none + * of the slots has a valid checksum, no image will be booted and the bootloader + * will enter "while(1);". + * + * Any image that the bootloader can load is supposed to be signed by the firmware + * tool, which will take an image, sign it using tweetnacl prefixes the image with + * corresponding metadata. + * + * The metadata contains + * + * - "RIOT" as magic number + * - an application version + * - Start address + * - a checksum + * + * The bootloader only cares about checksum and application version. It expects a + * running image to verify the metadata. + * + * All image transportation and verification is supposed to be done by a running + * image. + * + * The user must update using a binary that has been linked & signed for the + * desired target slot. + * The slot that contains the running image cannot be overwritten. + * + * The module "firmware_update" provides a simple API that can be fed image data + * in arbitrary block sizes. The API is similar to stream hashing. There's + * "firmware_update_init()", "firmware_update_put_bytes()" and + * "firmware_update_finish()". The module transparently handles image + * verification and flash writing. + * + * A tool in dist/tools/firmware can create the necessary keys using tweetnacl, + * and also sign/verify firmware images. + * + * A module in sys/firmware contains common code for firmware metadata handling. + * + * ## Threat vector + * + * If properly used, riotboot allows secure updates of running firmware. + * Only update requests that have a valid signature will cause a write to flash. + * Only updates with a higher version number will be considered, protecting from + * replay or downgrade attacks, given a node is running the latest version. + * Updates cannot put the node in undefined state if an upgrade is aborted at any + * time or otherwise invalid. + * Only correctly written and verified updates are considered by the bootloader. + * + * ## Usage + * + * riotboot needs a public key to be compiled in the image for verification purposes. + * Create a keypair using dist/tools/firmware. + * + * riotboot needs some board specific support, mainly slot size configuration, in + * order to work for a specific board. + * + * If that support is in place, it is easiest to use the riotboot specific make + * targets in order to get the support on the device. + * + * See README.md in examples/ota for examples. + * + * @file + * @brief Firmware Updates + * + * @author Kaspar Schleiser + * @author Francisco Acosta + * + * @} + */ + +#ifndef FIRMWARE_H +#define FIRMWARE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * @brief Number of metadata bytes that get checksummed + * + * Includes magic number, version, and start address. + */ +#define FIRMWARE_CHECKSUM_LEN (12) + +/** + * @brief Length of metadata prefixed to firmware binaries + */ +#ifndef FIRMWARE_METADATA_SIZE +#define FIRMWARE_METADATA_SIZE (256) +#endif + +/** + * @brief Real length of metadata without padding + */ +#define FIRMWARE_METADATA_REALLEN (sizeof(firmware_metadata_t) - FIRMWARE_PADDING) + +/** + * @brief Structure to store firmware metadata + * @{ + */ +typedef struct { + uint32_t magic_number; /**< metadata magic_number (always "RIOT") */ + uint32_t version; /**< Integer representing firmware version */ + uint32_t start_addr; /**< Start address in flash */ + uint32_t chksum; /**< checksum of metadata */ +} firmware_metadata_t; +/** @} */ + +/** + * @brief Validate FW image metadata + * + * @param[in] metadata ptr to firmware metadata + * + */ +int firmware_validate_metadata_checksum(firmware_metadata_t *metadata); + +/** + * @brief Calculate metadata checksum + * + * @param[in] metadata ptr to firmware metadata + * + */ +uint32_t firmware_metadata_checksum(firmware_metadata_t *metadata); + +/** + * @brief Jump to image + * + * @param[in] metadata ptr to firmware metadata + * + */ +void firmware_jump_to_image(firmware_metadata_t *metadata); + +/** + * @brief Get currently running image slot + * + * returns nr of currently active slot + */ +int firmware_current_slot(void); + +/** + * @brief Get next (to be empty) image slot + * + * returns free target slot + */ +int firmware_target_slot(void); + +/** + * @brief Get metadata of firmware slot + * + * @param[in] slot slot nr to work on + * + * returns metadata of image slot nr @p slot + */ +firmware_metadata_t *firmware_get_metadata(unsigned slot); + +/** + * @brief Get jump-to address of firmware slot + * + * @param[in] slot slot nr to work on + * + * @returns address of first byte of @p slot + */ +unsigned firmware_get_image_startaddr(unsigned slot); + +/** + * @brief Boot into image in slot @p slot + * + * @param[in] slot slot nr to jump to + */ +void firmware_jump_to_slot(unsigned slot); + +/** + * @brief Dump firmware slot addresses + */ +void firmware_dump_slot_addrs(void); + +/** + * @brief Number of configured firmware slots (incl. bootloader slot) + */ +extern const unsigned firmware_num_slots; + +#ifdef __cplusplus +} +#endif + +#endif /* FIRMWARE_H */ diff --git a/sys/include/firmware/flashwrite.h b/sys/include/firmware/flashwrite.h new file mode 100644 index 000000000000..591acc043da6 --- /dev/null +++ b/sys/include/firmware/flashwrite.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_firmware + * @{ + * + * @file + * @brief Over the air (OTA) update module + * + * This module provides an API to reliably write a firmware image to flash. + * + * The API is similar to stream hashing functions: + * + * 1. initialize state structure using firmware_flashwrite_init() + * 2. put some data using firmware_flashwrite_putbytes() + * 3. repeat 2. until all data has been received + * 4. finish update using firmware_flashwrite_finish() + * + * The module will *not* automatically reboot after an image has been + * successfully written. + * + * Under the hood, the module tries to abstract page sizes for writing the image + * to flash. Verification of the image is left to the caller. + * If the data is not correctly written, firmware_put_bytes() will + * return -1. + * + * The module makes sure that at no point in time an invalid image is bootable. + * The algorithm for that makes use of the bootloader verifying checksum and + * works as follows: + * + * 1. erase first block (making its checksum invalid) + * 2. write image + * 3. write first block + * + * @author Kaspar Schleiser + * @author Koen Zandberg + * + * @} + */ + +#ifndef FIRMWARE_FLASHWRITE_H +#define FIRMWARE_FLASHWRITE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "firmware.h" +#include "periph/flashpage.h" + +/** + * @brief firmware update state structure + */ +typedef struct { + int target_slot; /**< update targets this slot */ + size_t offset; /**< update is at this position */ + unsigned flashpage; /**< update is at this flashpage */ + uint8_t flashpage_buf[FLASHPAGE_SIZE]; /**< flash writing buffer */ +} firmware_flashwrite_t; + +/** + * @brief Initialize firmware update + * + * @param[in/out] state ptr to preallocated state structure + * @param[in] target_slot slot to write update into + * @param[in] offset Bytes offset to start write at + * + * @returns 0 on success, <0 otherwise + */ +int firmware_flashwrite_init(firmware_flashwrite_t *state, int target_slot, size_t offset); + +/** + * @brief Feed bytes into the firmware writer + * + * @param[in/out] state ptr to previously used update state + * @param[in] offset offset of @p bytes (from image start) + * @param[in] bytes ptr to data + * @param[in] len len of data + * @param[in] more Whether more data is comming + * + * @returns 0 on success, <0 otherwise + */ +int firmware_flashwrite_putbytes(firmware_flashwrite_t *state, + const uint8_t *bytes, size_t len, bool more); + +/** + * @brief Finish a firmware update + * + * @param[in] state ptr to previously used state structure + * @param[in] metadata Metadata to write on the first page + * @param[in] len Size of the metadata in bytes + * + * @returns 0 on success, <0 otherwise + */ +int firmware_flashwrite_finish(firmware_flashwrite_t *state, + firmware_metadata_t *metadata, size_t len); + +size_t firmware_flashwrite_slotsize(const firmware_flashwrite_t *state); + +#ifdef __cplusplus +} +#endif + +#endif /* FIRMWARE_FLASHWRITE_H */ diff --git a/sys/include/firmware/simple.h b/sys/include/firmware/simple.h new file mode 100644 index 000000000000..eeaa4bef776b --- /dev/null +++ b/sys/include/firmware/simple.h @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 Kaspar Schleiser + * Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#ifndef FIRMWARE_SIMPLE_H +#define FIRMWARE_SIMPLE_H + +#include "firmware.h" +#include "hashes/sha256.h" +#ifdef RIOT_VERSION +#include "firmware/flashwrite.h" +#endif + +#include "tweetnacl.h" + +/** + * @brief Provisional length for signed hash + */ +#define FIRMWARE_SIG_LEN (crypto_sign_BYTES) + +/** + * @brief Length of secret Ed25519 key + */ +#define FIRMWARE_SECKEY_LEN (crypto_sign_SECRETKEYBYTES) + +/** + * @brief Length of public Ed25519 key + */ +#define FIRMWARE_PUBKEY_LEN (crypto_sign_PUBLICKEYBYTES) + +#define FIRMWARE_METADATA_RIOTBOOT (0x0100) + +/** + * @brief Length of bytes to be signed + */ +#define FIRMWARE_SIGN_BYTES (sizeof(firmware_simple_t) - FIRMWARE_SIG_LEN) + +typedef struct { + firmware_metadata_t metadata; /**< generic bootloader specific firmware info */ + uint32_t metadata_type; /**< Type of metadata, 16 bits type, + 16 bit version */ + uint32_t appid; /**< Application type ID */ + uint32_t size; /**< Size of firmware image */ + uint8_t hash[SHA256_DIGEST_LENGTH]; /**< SHA256 Hash of firmware image */ + uint8_t sig[FIRMWARE_SIG_LEN]; /**< Firmware Signature */ +} firmware_simple_t; + +/** + * @brief Possible firmware update states + */ +enum { + FIRMWARE_UPDATE_IDLE, /**< no firmware update in progress */ + FIRMWARE_UPDATE_INITIALIZED, /**< firmware update in progress, awaiting + verification */ + FIRMWARE_UPDATE_VERIFIED, /**< firmware update in progress & verified */ +}; + +#ifdef RIOT_VERSION + +/** + * @brief Firmware update state struct + */ +typedef struct { + firmware_flashwrite_t writer; /**< flashwrite state struct */ + unsigned state; /**< state (see above enum) */ + union { + uint8_t metadata_buf[FIRMWARE_METADATA_SIZE]; + firmware_simple_t metadata; + } m; + sha256_context_t sha; /**< SHA256 digest state struct */ +} firmware_simple_update_t; + +int firmware_simple_init(firmware_simple_update_t *state); + +int firmware_simple_putbytes(firmware_simple_update_t *state, const uint8_t *bytes, size_t len); + +int firmware_simple_finish(firmware_simple_update_t *state); + +#endif /* RIOT_VERSION */ + +/** + * @brief Print formatted FW image metadata to STDIO + * + * @param[in] metadata ptr to firmware metadata + * + */ +void firmware_simple_print(firmware_simple_t *riotboot); + +/** + * @brief Sign metadata + * + * @param[in] metadata ptr to firmware metadata + * @param[in] sk NaCL secret signing key to use + * + */ +int firmware_simple_sign(firmware_simple_t *riotboot, unsigned char *sk); + +/** + * @brief Validate FW metadata + * + * @param[in] metadata ptr to firmware metadata + * @param[in] pk NaCL public signing key to use + * + */ +int firmware_simple_validate(firmware_simple_t *riotboot, const unsigned char *pk); + +#endif /* FIRMWARE_SIMPLE_H */ diff --git a/sys/include/hashes/sha256.h b/sys/include/hashes/sha256.h index 0682c7e57b36..adb315238210 100644 --- a/sys/include/hashes/sha256.h +++ b/sys/include/hashes/sha256.h @@ -47,6 +47,7 @@ #define HASHES_SHA256_H #include +#include #ifdef __cplusplus extern "C" { diff --git a/sys/include/net/ota/coap.h b/sys/include/net/ota/coap.h new file mode 100644 index 000000000000..a491e09f6dc6 --- /dev/null +++ b/sys/include/net/ota/coap.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_firmware + * @{ + * + * @file + * @brief Firmware update via CoAP API + * + * See examples/ota for example usage. + * + * @author Kaspar Schleiser + * + */ + +#ifndef OTA_COAP_H +#define OTA_COAP_H + +#include + +#include "net/nanocoap.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CoAP OTA handler function + * + * @param[in] pkt CoAP pkt to handle + * @param[out] buf reply buffer + * @param[in] len reply buffer length + * @param[in] context unused + * + * @returns length of reply packet + */ +ssize_t ota_coap_handler(coap_pkt_t* pkt, uint8_t *buf, size_t len, void *context); + +/** + * @brief Macro for integrating handler into main CoAP resource list + */ +#define COAP_OTA_HANDLER \ + { "/firmware", COAP_PUT | COAP_POST, ota_coap_handler, NULL } + +#ifdef __cplusplus +} +#endif + +#endif /* OTA_COAP_H */ + +/** @} */ diff --git a/sys/net/application_layer/ota/Makefile b/sys/net/application_layer/ota/Makefile new file mode 100644 index 000000000000..fa2f1007c91e --- /dev/null +++ b/sys/net/application_layer/ota/Makefile @@ -0,0 +1,2 @@ +SUBMODULES:=1 +include $(RIOTBASE)/Makefile.base diff --git a/sys/net/application_layer/ota/coap.c b/sys/net/application_layer/ota/coap.c new file mode 100644 index 000000000000..18a26f5cc435 --- /dev/null +++ b/sys/net/application_layer/ota/coap.c @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_firmware + * @{ + * + * @file + * @brief Firmware update via CoAP implementation + * + * @author Kaspar Schleiser + * + * @} + */ + +#include "firmware/simple.h" +#include "log.h" +#include "net/nanocoap.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +static firmware_simple_update_t _state; + +ssize_t ota_coap_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, void *context) +{ + (void)context; + + uint32_t result = COAP_CODE_204; + int res; + + coap_block1_t block1; + int blockwise = coap_get_block1(pkt, &block1); + + LOG_INFO("ota: received bytes %u-%u", + (unsigned)block1.offset, (unsigned)block1.offset + pkt->payload_len); + + if (_state.state == FIRMWARE_UPDATE_VERIFIED) { + unsigned total = _state.m.metadata.size + FIRMWARE_METADATA_SIZE; + LOG_INFO(" of %u (left=%u)\n", total, + total - block1.offset - pkt->payload_len); + } + else { + LOG_INFO("\n"); + } + + if (block1.offset == 0) { + /* initialize firmware upgrade state struct */ + firmware_simple_init(&_state); + } + + if (_state.state == FIRMWARE_UPDATE_IDLE) { + res = -1; + } + else { + if (block1.offset == _state.writer.offset) { + res = firmware_simple_putbytes(&_state, + pkt->payload, pkt->payload_len); + } + else { + LOG_INFO("coap_ota_handler(): ignoring already received block\n"); + res = 0; + } + } + + if (res) { + result = COAP_CODE_NOT_ACCEPTABLE; + _state.state = FIRMWARE_UPDATE_IDLE; + LOG_INFO("coap_ota_handler(): unexpected packet\n"); + } + else if (block1.more == 1) { + result = COAP_CODE_231; + } + + if (!res && (!blockwise || !block1.more)) { + if (firmware_simple_finish(&_state) != 0) { + result = COAP_CODE_BAD_OPTION; + } + } + + ssize_t reply_len = coap_build_reply(pkt, result, buf, len, 0); + uint8_t *pkt_pos = (uint8_t *)pkt->hdr + reply_len; + if (!res) { + pkt_pos += coap_put_block1_ok(pkt_pos, &block1, 0); + } + + return pkt_pos - (uint8_t *)pkt->hdr; +}