Skip to content

Commit

Permalink
Merge pull request DotBots#283 from aabadie/ota_crypto
Browse files Browse the repository at this point in the history
OTAP: add basic support for crypto primitives (sha256 hash, ed25519 signature)
  • Loading branch information
aabadie authored Feb 13, 2024
2 parents 37f9ad2 + 2b21c7e commit 6b4c4dc
Show file tree
Hide file tree
Showing 44 changed files with 677 additions and 97 deletions.
24 changes: 15 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ SEGGER_DIR ?= /opt/segger
BUILD_CONFIG ?= Debug
BUILD_TARGET ?= dotbot-v1
PROJECT_FILE ?= $(BUILD_TARGET).emProject
BOOTLOADER ?= bootloader

ifeq (nrf5340dk-app,$(BUILD_TARGET))
PROJECTS ?= \
Expand Down Expand Up @@ -56,30 +57,30 @@ else ifeq (nrf5340dk-net,$(BUILD_TARGET))
03app_log_dump \
03app_nrf5340_net \
#
# Bootloader not supported on nrf5340 network core
BOOTLOADER :=
else
PROJECTS ?= $(shell find projects/ -maxdepth 1 -mindepth 1 -type d | tr -d "/" | sed -e s/projects// | sort)
endif

TESTBED_APPS ?= $(shell find testbed/ -maxdepth 1 -mindepth 1 -type d | tr -d "/" | sed -e s/testbed// | sort)
OTAP_APPS ?= $(shell find otap/ -maxdepth 1 -mindepth 1 -type d | tr -d "/" | sed -e s/otap// | sort)
OTAP_APPS := $(filter-out bootloader,$(OTAP_APPS))

# remove incompatible apps (nrf5340, sailbot gateway) for dotbot (v1, v2) builds
ifneq (,$(filter dotbot-v1,$(BUILD_TARGET)))
PROJECTS := $(filter-out 01bsp_qdec 01drv_lis3mdl 01drv_move 03app_dotbot_gateway 03app_dotbot_gateway_lr 03app_sailbot 03app_nrf5340_%,$(PROJECTS))
ARTIFACT_PROJECTS := 03app_dotbot
TESTBED_APPS := $(filter-out bootloader partition0 partition1,$(TESTBED_APPS))
endif

ifneq (,$(filter dotbot-v2,$(BUILD_TARGET)))
PROJECTS := $(filter-out 03app_dotbot_gateway 03app_dotbot_gateway_lr 03app_sailbot 03app_nrf5340_net,$(PROJECTS))
ARTIFACT_PROJECTS := 03app_dotbot
TESTBED_APPS := $(filter-out bootloader partition0 partition1,$(TESTBED_APPS))
endif

# remove incompatible apps (nrf5340, dotbot, gateway) for sailbot-v1 build
ifeq (sailbot-v1,$(BUILD_TARGET))
PROJECTS := $(filter-out 01bsp_qdec 01drv_lis3mdl 01drv_move 03app_dotbot_gateway 03app_dotbot_gateway_lr 03app_dotbot 03app_nrf5340_%,$(PROJECTS))
ARTIFACT_PROJECTS := 03app_sailbot
TESTBED_APPS := $(filter-out bootloader partition0 partition1,$(TESTBED_APPS))
endif

# remove incompatible apps (nrf5340) for nrf52833dk/nrf52840dk build
Expand All @@ -96,7 +97,7 @@ ifneq (,$(filter nrf5340dk-net,$(BUILD_TARGET)))
ARTIFACT_PROJECTS := 03app_nrf5340_net
endif

SRCS ?= $(shell find bsp/ -name "*.[c|h]") $(shell find crypto/ -name "*.[c|h]") $(shell find drv/ -name "*.[c|h]") $(shell find projects/ -name "*.[c|h]") $(shell find testbed/ -name "*.[c|h]")
SRCS ?= $(shell find bsp/ -name "*.[c|h]") $(shell find crypto/ -name "*.[c|h]") $(shell find drv/ -name "*.[c|h]") $(shell find projects/ -name "*.[c|h]") $(shell find otap/ -name "*.[c|h]")
CLANG_FORMAT ?= clang-format
CLANG_FORMAT_TYPE ?= file

Expand All @@ -107,16 +108,21 @@ ARTIFACTS = $(ARTIFACT_ELF) $(ARTIFACT_HEX)

.PHONY: $(PROJECTS) $(ARTIFACT_PROJECTS) artifacts docker docker-release format check-format

all: $(PROJECTS) $(TESTBED_APPS)
all: $(PROJECTS) $(OTAP_APPS) $(BOOTLOADER)

$(PROJECTS):
@echo "\e[1mBuilding project $@\e[0m"
"$(SEGGER_DIR)/bin/emBuild" $(PROJECT_FILE) -project $@ -config $(BUILD_CONFIG) $(PACKAGES_DIR_OPT) -rebuild -verbose
@echo "\e[1mDone\e[0m\n"

$(TESTBED_APPS):
@echo "\e[1mBuilding testbed application $@\e[0m"
"$(SEGGER_DIR)/bin/emBuild" $(PROJECT_FILE) -project $@ -config Release $(PACKAGES_DIR_OPT) -rebuild -verbose
$(OTAP_APPS):
@echo "\e[1mBuilding otap application $@\e[0m"
"$(SEGGER_DIR)/bin/emBuild" $(PROJECT_FILE) -project $@ -config $(BUILD_CONFIG) $(PACKAGES_DIR_OPT) -rebuild -verbose
@echo "\e[1mDone\e[0m\n"

$(BOOTLOADER):
@echo "\e[1mBuilding bootloader application $@\e[0m"
"$(SEGGER_DIR)/bin/emBuild" otap/$(BUILD_TARGET)-bootloader.emProject -project $@ -config Release $(PACKAGES_DIR_OPT) -rebuild -verbose
@echo "\e[1mDone\e[0m\n"

list-projects:
Expand Down
2 changes: 1 addition & 1 deletion bsp/nrf/nvmc.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void db_nvmc_write(const uint32_t *addr, const void *data, size_t len) {
// Length must be a multiple of 4 bytes
assert(len % 4 == 0);
// writes must be 4 bytes aligned
assert(!((uint32_t)addr % 4) && !((uint32_t)data % 4));
assert(((uint32_t)addr % 4) && ((uint32_t)data % 4));

uint32_t *dest_addr = (uint32_t *)addr;
const uint32_t *data_addr = data;
Expand Down
2 changes: 2 additions & 0 deletions dist/scripts/otap/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
private_key
public_key.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python

import os
import logging
import time

Expand All @@ -8,10 +9,13 @@
from enum import Enum

import click
from tqdm import tqdm

import serial
import structlog

from tqdm import tqdm
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

from dotbot.hdlc import hdlc_encode, HDLCHandler, HDLCState
from dotbot.protocol import PROTOCOL_VERSION
from dotbot.serial_interface import SerialInterface, SerialInterfaceException
Expand All @@ -25,14 +29,16 @@
"nrf52840": 4096,
"nrf5340-app": 4096,
}

PRIVATE_KEY_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "private_key")

class MessageType(Enum):
"""Types of bootloader message."""

OTA_MESSAGE_TYPE_FW = 0
OTA_MESSAGE_TYPE_FW_ACK = 1
OTA_MESSAGE_TYPE_INFO = 2
OTA_MESSAGE_TYPE_START = 0
OTA_MESSAGE_TYPE_START_ACK = 1
OTA_MESSAGE_TYPE_FW = 2
OTA_MESSAGE_TYPE_FW_ACK = 3
OTA_MESSAGE_TYPE_INFO = 4


class CpuType(Enum):
Expand Down Expand Up @@ -109,6 +115,7 @@ def __init__(self, port, baudrate, image):
self.hdlc_handler = HDLCHandler()
self.device_info = None
self.device_info_received = False
self.start_ack_received = False
pad_length = CHUNK_SIZE - (len(image) % CHUNK_SIZE)
self.image = image + bytearray(b"\xff") * (pad_length + 1)
self.last_acked_chunk = -1
Expand All @@ -121,21 +128,60 @@ def on_byte_received(self, byte):
payload = self.hdlc_handler.payload
if not payload:
return
if payload[0] == MessageType.OTA_MESSAGE_TYPE_FW_ACK.value:
if payload[0] == MessageType.OTA_MESSAGE_TYPE_START_ACK.value:
self.start_ack_received = True
elif payload[0] == MessageType.OTA_MESSAGE_TYPE_FW_ACK.value:
self.last_acked_chunk = int.from_bytes(payload[1:5], byteorder="little")
elif payload[0] == MessageType.OTA_MESSAGE_TYPE_INFO.value:
self.device_info = DeviceInfo.from_bytes(payload[1:])
self.device_info_received = True

def fetch_device_info(self):
while self.device_info_received is False:
# while self.device_info_received is False:
buffer = bytearray()
buffer += int(MessageType.OTA_MESSAGE_TYPE_INFO.value).to_bytes(
length=1, byteorder="little"
)
print("Fetching device info...")
self.serial.write(hdlc_encode(buffer))
timeout = 0 # ms
while self.device_info_received is False and timeout < 100:
timeout += 1
time.sleep(0.01)

def send_start_update(self, secure):
if secure is True:
digest = hashes.Hash(hashes.SHA256())
pos = 0
while pos + CHUNK_SIZE <= len(self.image) + 1:
digest.update(self.image[pos : pos + CHUNK_SIZE])
pos += CHUNK_SIZE
fw_hash = digest.finalize()
private_key_bytes = open(PRIVATE_KEY_PATH, "rb").read()
private_key = Ed25519PrivateKey.from_private_bytes(private_key_bytes)
attempts = 0
while attempts < 3:
buffer = bytearray()
buffer += int(MessageType.OTA_MESSAGE_TYPE_INFO.value).to_bytes(
buffer += int(MessageType.OTA_MESSAGE_TYPE_START.value).to_bytes(
length=1, byteorder="little"
)
buffer += int((len(self.image) - 1) / CHUNK_SIZE).to_bytes(
length=4, byteorder="little"
)
if secure is True:
buffer += fw_hash
signature = private_key.sign(bytes(buffer[1:]))
buffer += signature
print("Sending start update notification...")
self.serial.write(hdlc_encode(buffer))
time.sleep(0.2)
print("Fetching device info...")
attempts += 1
timeout = 0 # ms
while self.start_ack_received is False and timeout < 1000:
timeout += 1
time.sleep(0.01)
if self.start_ack_received is True:
break
return attempts < 3

def flash(self):
page_size = PAGE_SIZE_MAP[self.device_info.cpu]
Expand Down Expand Up @@ -172,14 +218,20 @@ def flash(self):
default="/dev/ttyACM0",
help="Serial port to use to send the firmware.",
)
@click.option(
"-s",
"--secure",
is_flag=True,
help="Use cryptographic security (hash and signature).",
)
@click.option(
"-y",
"--yes",
is_flag=True,
help="Continue flashing without prompt.",
)
@click.argument("image", type=click.File(mode="rb", lazy=True))
def main(port, yes, image):
def main(port, secure, yes, image):
# Disable logging configure in PyDotBot
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL),
Expand Down Expand Up @@ -216,6 +268,10 @@ def main(port, yes, image):
return
if yes is False:
click.confirm("Do you want to continue?", default=True, abort=True)
ret = flasher.send_start_update(secure)
if ret is False:
print("Error: No start acknowledment received. Aborting.")
return
flasher.flash()
print("Done")

Expand Down
54 changes: 54 additions & 0 deletions dist/scripts/otap/generate_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python

import os

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
Encoding,
PrivateFormat,
PublicFormat,
NoEncryption,
)

PRIVATE_KEY_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "private_key")
PUBLIC_KEY_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../../drv/ota/public_key.h")

HEADER_FORMAT = """/*
* PLEASE DON'T EDIT
*
* This file was automatically generated
*/
#ifndef __{name_upper}_H
#define __{name_upper}_H
#include <stdint.h>
const uint8_t {name}[] = {{
{data}
}};
#endif /* __{name_upper}_H */
"""


def save_as_c_array(path, name, data):
data_str = ", ".join([f"0x{data[i:i+2]}" for i in range(0, len(data), 2)])
print(f"Saving '{name}' to {os.path.abspath(path)}")
with open(path, "w") as f:
f.write(HEADER_FORMAT.format(name=name, name_upper=name.upper(), data=data_str))


def main():
key_pair = Ed25519PrivateKey.generate()
private_key = key_pair.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
public_key = key_pair.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)

print(f"Generated keys:\n - private key\t: {private_key.hex()}\n - public key\t: {public_key.hex()}")
print(f"Saving 'private key' to {os.path.abspath(PRIVATE_KEY_PATH)}")
with open(PRIVATE_KEY_PATH, "wb") as f:
f.write(private_key)
save_as_c_array(PUBLIC_KEY_PATH, "public_key", public_key.hex())

if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
click==8.1.7
cryptography==41.0.5
tqdm==4.66.1
pydotbot
2 changes: 1 addition & 1 deletion doc/doxygen/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ INPUT = \
../../bsp \
../../crypto \
../../drv \
../../otap \
../../projects \
../../testbed \
#
INPUT_ENCODING = UTF-8
FILE_PATTERNS = \
Expand Down
2 changes: 1 addition & 1 deletion doc/sphinx/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
getting_started
applications
examples
testbed
otap
api
```

Expand Down
2 changes: 1 addition & 1 deletion doc/sphinx/testbed.md → doc/sphinx/otap.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
```{include} ../../testbed/README.md
```{include} ../../otap/README.md
:relative-images:
:relative-docs: ../
```
3 changes: 2 additions & 1 deletion dotbot-v1.emProject
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
build_output_file_name="$(OutDir)/$(ProjectName)-$(BuildTarget)$(EXE)"
build_treat_warnings_as_errors="Yes"
c_additional_options="-Wno-missing-field-initializers"
c_preprocessor_definitions="ARM_MATH_CM4;NRF52833_XXAA;__nRF_FAMILY;CONFIG_NFCT_PINS_AS_GPIOS;FLASH_PLACEMENT=1;BOARD_DOTBOT_V1"
c_preprocessor_definitions="ARM_MATH_CM4;NRF52833_XXAA;__nRF_FAMILY;CONFIG_NFCT_PINS_AS_GPIOS;FLASH_PLACEMENT=1;BOARD_DOTBOT_V1;OTA_USE_CRYPTO"
c_user_include_directories="$(SolutionDir)/../bsp;$(SolutionDir)/../crypto;$(SolutionDir)/../drv;$(PackagesDir)/nRF/Device/Include;$(PackagesDir)/CMSIS_5/CMSIS/Core/Include"
debug_register_definition_file="$(PackagesDir)/nRF/XML/nrf52833_Registers.xml"
debug_stack_pointer_start="__stack_end__"
Expand Down Expand Up @@ -61,4 +61,5 @@
<import file_name="projects/projects-crypto.emProject" />
<import file_name="projects/projects-dotbot.emProject" />
<import file_name="projects/projects-log-dump.emProject" />
<import file_name="otap/otap.emProject" />
</solution>
3 changes: 2 additions & 1 deletion dotbot-v2.emProject
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
build_output_file_name="$(OutDir)/$(ProjectName)-$(BuildTarget)$(EXE)"
build_treat_warnings_as_errors="Yes"
c_additional_options="-Wno-strict-prototypes"
c_preprocessor_definitions="ARM_MATH_ARMV8MML;NRF5340_XXAA;NRF_APPLICATION;__NRF_FAMILY;CONFIG_NFCT_PINS_AS_GPIOS;FLASH_PLACEMENT=1;BOARD_DOTBOT_V2"
c_preprocessor_definitions="ARM_MATH_ARMV8MML;NRF5340_XXAA;NRF_APPLICATION;__NRF_FAMILY;CONFIG_NFCT_PINS_AS_GPIOS;FLASH_PLACEMENT=1;BOARD_DOTBOT_V2;OTA_USE_CRYPTO"
c_user_include_directories="$(SolutionDir)/../bsp;$(SolutionDir)/../crypto;$(SolutionDir)/../drv;$(PackagesDir)/nRF/Device/Include;$(PackagesDir)/CMSIS_5/CMSIS/Core/Include"
debug_register_definition_file="$(PackagesDir)/nRF/XML/nrf5340_application_Registers.xml"
debug_stack_pointer_start="__stack_end__"
Expand Down Expand Up @@ -62,4 +62,5 @@
<import file_name="projects/projects-dotbot.emProject" />
<import file_name="projects/projects-log-dump.emProject" />
<import file_name="projects/projects-nrf5340-app.emProject" />
<import file_name="otap/otap.emProject" />
</solution>
2 changes: 1 addition & 1 deletion drv/drv.emProject
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
<project Name="00drv_ota">
<configuration
Name="Common"
project_dependencies="00bsp_partition(bsp);00bsp_nvmc(bsp)"
project_dependencies="00bsp_partition(bsp);00bsp_nvmc(bsp);00crypto_ed25519(crypto);00crypto_sha256(crypto)"
project_directory="ota"
project_type="Library" />
<file file_name="ota.c" />
Expand Down
Loading

0 comments on commit 6b4c4dc

Please sign in to comment.