Welcome to libpull’s documentation!

GitHub license GitHub release Build Status CodeFactor PRs Welcome

Libpull is a library to perform software updates for Internet of Things devices. The library is build around three main principles:

  • Security: having support for the most used cryptographic libraries and hardware security modules;
  • Portability: aims to be OS agnostic, support a wide number of boards and work with several network protocols;
  • Platform constraints: aims to targets Class 1 devices, thus having a small memory footprint and reducing network bandwidth;

The library includes a bootloader and an update agent, however, it is possible to use the functions provided by the library to build your own agent that better fits all your platform and application needs.

Get Started

In the following pages you will learn how libpull is build, the main logic and how to contribute.

Library Logic

Libpull has been build around of three main properties:

  • security;
  • portability;
  • platform constraints;

It targets Class 1 constrained devices, characterized by 100 kB of ROM and 10 kB or RAM, as defined in RF 7228, but thanks to its small memory footprint and modular approach can be useful even on more powerful devices.

Documentation Terminology

To understand the following sections is necessary to agree on some terminology and concepts used to analyze and describe the update process.

terminology_image

We first define the terminology to describe an update:

  • image: the software that will be executed on the device. It can be a firmware, in case it must be loaded by the device MCU, or could be a software module that can be loaded at runtime by the OS;
  • manifest: the set of data related to the image, describing its size, its version and including all the data used for cryptographic verification;
  • update image: the union of image and manifest representing the update. This is the data that must be generated and transmitted by the server to the client.

We also define the terminology to describe how an update is stored in memory as:

  • memory object: the section of memory used to store the update. This can be a file, a segment of the internal or external flash, a device or any other memory abstraction defined by the user of the library;
  • running object: the currently running image;
  • device memory: a generic memory of the device that contains one running image and one or more memory objects.

Software Updates Components

Analyzing the structure of the update process we identified three distinct components:

  • The vendor server is the server owned by the vendor. This server is the first point where the update is built and thus, it is used to assert its integrity and authenticity. This server perform the following actions:
    • Builds the image;
    • Generates a manifest file;
    • Generates the update image;
    • Sends the update image to the provisioning server.
  • The provisioning server is the server in charge of communicating with the device. It may be managed by the device vendor or not. It performs the following actions:
    • Notifies updates availability;
    • Updates the manifest;
    • Sends the update image to the device;
    • Logs the device status.
  • The update agent is the code running on the device with the goal of getting the newest firmware available. It performs the following actions:
    • Checks the presence of updates;
    • Receives the updates;
    • Validates the update.

Portability Requirements

Portability can be considered as the possibility to reuse the same software in many environments. This required to designing the solution with the appropriate abstraction layers that allow to configure it according to the device capabilities and required logic. We identified four portability requirements:

  • Operating System agnostic: IoT devices are based on a multitude of operating systems (mainly RTOS). The libpull core has been designed as a freestanding core that does not uses any any OS-specific API but instead relies on abstraction layers that must be specialized for each OS.
  • Network protocol agnostic: Libpull has been designed to support many network protocols. For example, we already tested it with CoAP/CoAPS and BLE, but with more powerful devices, it can easily support the HTTP/HTTPS.
  • Cryptographic library agnostic: The library has been implemented to make sure it is compatible with a multitude of cryptographic libraries. We currently tested them with TinyDTLS, tinycrypt and the Atmel CryptoAuthLib.
  • Manifest encoding agnostic. The manifest describing the update can be encoded using different formats, such as simple binary format, JSON, CBOR, to support outcoming solutions and needs.

Constrained Devices Requirements

The library has a small memory footprint since it must be suitable for Class 1 devices. We are continuously monitoring this aspect using a script executed in the CI build, to make sure we know how each modification impacts the library size.

Moreover, the library supports many memory types. We tested it with standard Linux files but also with direct flash access. Moreover, the memory slots are configurable in a way that they can be placed in different memories, supporting also IoT devices with an external flash connected to them.

We also aims to cover several update methods, namely: static, dynamic and seamless software updates.

  • Dynamic Software Updates. In this configuration, the update image is represented by a module that can be loaded at runtime by the running image. The advantage of this configuration is that no-reboot is needed, making it suitable also for real-time application with high availability needs. To allow the use of this configuration, the OS must be capable of loading, and if necessary linking, the modules at runtime. The library does not explicitly manage the activation and relinking of the code, since it is a process highly bounded with the platform choises.
  • Static Software Updates. In this configuration, the update image is represented by the whole OS. In this method, the presence of a bootloader is required. The advantage of this configuration is the possibility to perform atomical updates, loading a new image, and avoiding the problems of dynamic linking. Moreover, this requires the reboot of the device that in many applications is not always possible.
  • Seamless Software Updates. Also known as A/B updates, seamless updates use one memory object to store the running object and another to store the update. All the logic to perform the update is placed in the image and the bootloader just needs to load the newer version, thus each boot will be performed at the same time. This configuration requires that the two memory objects are bootable and thus stored in the internal memory.

Libpull Agents

Libpull is a library that exposes all the features to create a complete update system. It already includes an update agent and a bootloader, but it can be used to build your own if necessary. In the next sections we will describe the agents included in the library.

Update Agents

The update agent is the application using the libpull library to effectively perform the update. It is in charge of communicating with the network and coordinate the operations to successfully download, verify and apply the update image. It should be normally executed in parallel to the standard application. In this way, when an update is available, the device will require the minimum time to obtain it.

The update agent defines all the configurations of the library, such as the endpoint used, the resources that must be accessed by the subscriber and the receiver, the type of connection that must be used, the polling timeout. Moreover, it must be able to recover from errors, and safely fail if the errors are not recoverable. For this reason, the update agent has been implemented as a while loop that exits only if an unrecoverable error has been encountered or the update process is successful.

update_agent_workflow

Bootloader

The first execution of the bootloader is called bootstrap. During the bootstrap the memory objects are erased, and if the recovery image is enabled, the running object is stored into a specific memory object, allowing a fast recovery in case of failures.

To recognize the first run from the other runs, the bootloader needs to store its state in a persistent memory. This is done defining a new memory object, called bootloader_ctx, that is stored in the last page of the internal memory and contains a bit, used to indicate if the bootloader is running for the first time. The bootloader_ctx is generated during the building phase and flashed to the board at the correct offset. The first_run bit is initially set to one and can be only set to 0 once by the bootloader since that memory is write protected.

If the bootloader finds a newest version in a memory object compared to the version of the running one, it verifies the signature of that object and, if valid, copies it to the running object. The verification is performed before the copy, since if the signature is invalid it avoids a write cycle.

An important operation performed by the bootloader is to write protect all the sectors of the Flash before loading the image, preventing in this way the update agent and all the software running after the bootloader from modifying the content of the internal memory and prevents an attacker to store persistent data in the internal Flash.

bootloader_exeuction_scheme

Build your own agent

If the agents included in the library are not sufficient for your application, you can still use the functions provided by the library to create your own update agent or bootloader that follows your specific logic.

To do that you may want to follow the API Documentation

Cryptographic Libraries

The implementation of the security modules consists of high-level interfaces implemented using different cryptographic libraries. This allows to perform the signature verification without changing the code of the library, but still using different cryptographic libraries.

Supported Libraries

Libpull currently supports three cryptographic libraries:

  • TinyDTLS is a library that provides all the functions to instantiate a DTLS connection. It supports many cryptographic algorithms, such as Rijndael (AES), SHA256, HMAC-SHA256, ECC (with secp256r1 key). It can perform the DTLS handshake using PSK or the ECDH algorithm. It is distributed under the MIT license and maintained by the Eclipse for IoT project.
  • TinyCrypt. It is a small-footprint cryptography library that explicitly targets constrained devices. It supports many cryptographic algorithms, such as SHA-256 hash functions, HMAC-SHA256, AES-128 (with AES-CBC, AES-CTR, and AES-CMAC encryption modes), ECC-DH key changes, and ECDSA. It is built in a modular way, allowing to include only the required modules.
  • Atmel CryptoAuthLib. This library is provided by Atmel and allows to interact with their CryptoAuthentication modules. It is a very modular library and bases its function on a HAL layer in charge of communicating with the device using I2C or SPI.

Cryptographic Libraries Memory Footprint

The choice of the cryptographic library to include was sustained by an analysis of the memory footprint of several cryptographic libraries, to identify the smallest in terms of Data and Text size.

The comparison has been performed building a simple application able to perform the verification with each library and comparing the size of the hashing and ECC functions. The output of the comparison is shown in the table above.

Library SHA2 ECC ECDSA
TinyDTLS 3800 7531 9888
tinycrypt 3656 8968 11241
PolarSSL 6056 23046 27735
MatrixSSL 3864 29103 34022
WolfSSL 4592 31443 34777
LibTomCrypt 4354 35959 38256

You can find more informations on the methodology used in the specific repository.

Documentation Guidelines

The libpull documentation is build using the following tools:

Documentation Overview

The documentation is partially contained in the build/doc folder and partially included as Doxygen comments in the code.

The documentation is written partially using the reStructuredText markup language and part using MarkDown, to exploit the adavantages of both. In fact, writing MarkDown is much simpler and easier to maintain compared to reStructuredText. However, the latter allows to build complex structures and indexes.

The documentation generation performs logically the following steps:

  • Extract XML form the code using Doxygen;
  • Parse XML using Breathe and generate reStructuredText files;
  • Parse reStructuredText and MarkDown to build the finally documentation using sphinx.

Building the documentation

We build the documentation automatically using Read The Docs. The tools recursively searches for a conf.py script in the whole repository and executes sphinx inside of that folder. In the conf.py script you can find instructions to invoke Doxygen and to configure Breathe.

If you want to build the documentation locally you need to have the following tools installed:

  • Doxygen >= 1.8.13
  • Sphinx >= 1.7.6
  • Breathe >= 4.9.1

You will find a list of the required python packages in the requirements.txt file.

Once you have all the dependencies installed you can build the documentation using the Makefile contained in the build/doc folder.

You can see all the available targets invoking make. If you want to build the HTML documentation you can use html target, such as:

make html

Documentation CI

https://readthedocs.org/projects/libpull/badge/?version=latest

You can see the state of the current documentation by analyzing the Read The Docs builds for libpull. Moreover, we are building the documentation on Travis to be sure all functions are documented.

Contributing to Libpull

🚀 First of all, thank you for taking the time to contribute! 🚀

What should I know before I get started?

If you want to contribute to Libpull you should have understand the basics of the library. To do it a good approach is to read the documentation and follow the tutorial to test it on a real device.

Contributing

If you readed the documentation and checked the open issues, then you are ready to contribute it. The contribution may be diveded by the contribution goal.

 Increase portability

The libpull library aims to be a very portable library with a freestanding core and some platform specific modules.

Support a new RTOS

If you want to integrate a new RTOS we ask you to check the following points:

  • the OS is well known, open source and still maintained;
  • you are able to maintain and provide support for a certain period;
  • the OS has a minimum number of platforms on which is supported;

If the following checks passes, you can open an issue and discuss together the integration of the new RTOS.

Support a new MCU

If you want to provide support for a new MCU you should check the following points:

  • you are able to maintain and provide support for a certain period;
  • the drivers to interact with the MCU (such as writing memory, etc) are open source;
  • the MCU is still in production.

If the following checks passes, you can open an issue to discuss the integation of the new MCU. If you already have an implementation that follows the logic of the library you can may want to open directly a pull request.

Reporting bugs

Please open an issue indicating all the steps to the reproduce the bug/vulnerability. If you already have a solution please feel free to open a pull request, where it will be also easier to discuss the improvements.

Improve documentation

If you found an error or want to improve some documentation pages plase open a pull request with the fix. To check that the documentation still builds please follow the steps described in build/documentation/README.md.

Styleguides

Git commit messages

For the commit messages we use the conventional commits standardized approach. This allows us to have a structured message for each commit that can be used then to easily generate the changelog for each new release.

A conventional commit message is composed in the following way:

<type>[optional scope]: <description>

where type is one of the following prefix:

  • feat: introducing a new feature;
  • fix: fixing a bug/vulnerability;
  • docs: adding/improving documentation;
  • style: fixing indentation/code style;
  • refactor: refactoring code;
  • perf: a code change to improve performance;
  • test: adding/fixing unit tests;

Since this process can be tediuos while writing commits we suggest the cz-cli tool that helps in writing commits conformant to the conventional commits standards.

The optional scope can be used to specify where the type is applied. For example, in case we are adding a new test for the memory module we could write a commit message such as:

test(memory): add a test to detect a failure;

When writing the description considering the following rules:

  • Use the present tense (“Fix bug” and not “Fixed bug”)
  • Use the imperative mood (“Teach library to…” and not “Teaches library to…”)
C code

All the C code must be formatted with clang-format. When submitting PR please avoid reformatting the code until the changes has been approved to increase readibility of the diff.

We are using cppcheck to validate and find possible errors in the code. Check if there are new issues on Codacy.

Tutorial

To guide you in using libpull with your platform we provide a tutorial based on our reference platform. Integrating libpull with your solution should not be too far from the steps described in this tutorial.

Introduction

In this tutorial you will undestand the steps needed to configure libpull and integrate it with your platform.

This tutorial is based the Zephyr OS and the Nordic nRF52840 board.

What you will learn

In this tutorial you will learn the following concepts:

  • Build and test libpull;
  • Build and execute the libpull server;
  • Build and execute libpull on a device;
  • Send an update to a device using an OpenThread network.

What you will need

To complete this tutorial you will need the following components:

  • 2x Nordic nRF52840;
  • 2x Micro USB Male to USB A Male cable;
  • A computer with Linux or Mac OSX; (in the future we will provide another tutorial for Windows)
  • The Arm toolchain already installed;
board

To follow this guide we do not assume any specific IDE and we assume we are familiar with the command line interface.

Getting Started

Prepare the Toolchain

We assume you already downloaded and installed the ARM toolchain. You can test if it works typing:

$ arm-none-eabi-gcc --version

Clone the Zephyr repository

Create a folder in your home directory and move to it:

$ mkdir ~/libpull_tutorial
$ cd ~/libpull_tutorial

Clone the Zephyr repository:

$ git clone https://github.com/zephyrproject-rtos/zephyr

Build a Zephyr example

To test if your setup is ready to work with Zephyr and the nRF52840 board, build the hello world sample provided by the Zephyr project and load it to the board.

You can follow the official documentation for this task or the next steps:

$ cd zephyr
$ source zephyr-env.sh
$ cd samples/hello_world
$ mkdir build && cd build
$ cmake -GNinja -DBOARD=nrf52840_pca10056 ..
$ ninja

If the build was successfull you are now ready to flash the firmware on the device:

$ ninja flash

To read the serial output we use Minicom, but you can use every serial communication program you like (i.e., screen).

If everthing was correct you should see the following output:

***** Booting Zephyr OS v1.12.0-290-g7a7e4f583 *****
Hello World! arm

Install the flashing tool

To flash the libpull generated firmware we will use nrfjprog. You can have it instaling the nRF5x Command Line Tools.

This program is needed to interact with the nRF52840 board. To test if it works use the command:

$ nrfjprog --ids

that shows the serial numbers of all the boards connected to the computer.

Libpull Setup

In this part of the tutorial we will see how to work with libpull, setup the server, compile and flash the device.

Clone libpull

Moving back to the tutorial folder previously created we are now ready to clone the libpull repository.

$ cd ~/libpull_tutorial
$ git clone https://github.com/libpull/libpull
$ cd libpull

Build the library

The libpull build system is based on the GNU Build system and is currently compatible onlye with Linux and Mac OSX.


ℹ️ We are planning to move the build system to CMake.


To build the library we first need to download the dependencies:

$ ./autogen.sh

The script will download and compile the dependencies, and call the autoreconf program in charge of generating the configure script.

We can now configure the library using:

$ ./configure

Too see the configuration options type ./configure --help.

The previous script will generate the makefiles for the whole project. To build the library just type:

$ make

If the build is successfull we are ready to move forward.

Build and execute the server

Since the update must be downloaded OTA (Over The Air) we need a running server. Libpull currently provides a testing server.


⚠️ The server is not ready for production.


To execute the server you can use the makefile target run_server. It will automatically create the assets, build the server and execute it. Otherwise, to have a better undestanding on the process you can read the Makefile and execute each target individually:

$ make assets
$ make server
$ make run_server

The first target will invoke the script utils/assets_generator.sh that will create a new folder called assets and place inside it several files used by the unit tests.

Execute the library tests

If you want to be sure that the library has been built correctly and the can commmunicate correctly with the server, you can execute the Unit Tests with the following command:

make check

If all the tests passes you configuration is correct. If not, you should check the output printed on the server since you may have some network configuration (i.e. firewall) that is interfering with the connection.

If everything works we are now ready to start testing libpull on the device.

Network Setup

To send the firmware Over The Air using the OpenThread network we need to setup an OpenThread border router.


ℹ️ The setup suggested by the official OpenThread documentation requires to use a Raspberry Pi 3 or a BeagleBone Black. Since we want to keep the setup simple we will describe a border router configuration when the thread device is directly connected to a computer. However, if you already have such a setup or you prefer to follow the official guide skip the following sections.


Install a Linux Virtual Machine

If you are using Mac OSX you need to install a virtual machine since the OTBR tool provided by OpenThread works only on Linux.

 Download and flash the border router

To install a Thread border router you can follow two paths:

  • clone the repository and build it;
  • download an already built version;

Since OpenThread provides an already built version for our board we will follow the second approach.


⚠️ We assume you have some knowledge on the Thread networks. If not, you might want to run the OpenThread Simulation Codelab, to get familiar with the basics Thread concepts.


You can download and extract a prebuild version of the firmware using the following commands:

$ wget https://openthread.io/guides/ncp/ot-ncp-ftd-gccb354fb-nrf52840.tar.gz
$ tar -xzvf ot-ncp-ftd-gccb354fb-nrf52840.tar.gz

You should now have a hex file called ot-ncp-ftd-gccb354fb-nrf52840.hex containing the firmware. You can flash it following the steps at this link or following the next steps:

$ nrfjprog -f nrf52 --chiperase --program ot-ncp-ftd-gccb354fb-nrf52840.hex --reset

If the flashing was successfull you should see the following output:

Parsing hex file.
Erasing user available code and UICR flash areas.
Applying system reset.
Checking that the area to write is not protected.
Programming device.
Applying system reset.
Run.

Connect and test the border router

Since the flashed firmware enables the use of native USB CDC ACM as a serial transport, we need to connect the board using the other micro USB.


ℹ️ You can have a visual description at this link.


  1. Power off the board;
  2. Disconnect the micro USB from the board;
  3. Change the nRF power source switch from VDD to USB;
  4. Attach the micro USB cable to the nRF USB port on the long side of the board;
  5. Power on the board;

If the previous passages have been performed correctly you should not see all leds off on the board.

 Install the OpenThread OTBR

If you prefer, you can follow the official Border Router guide at this link.


⚠️ If you are using a virtual machine you need to allow the access to the USB port. In virtualbox you can do it by adding a filter for the SEGGER J-Link USB device in the ports settings.


Clone the border router on your Linux machine:

$ git clone https://github.com/openthread/borderrouter

Install the dependencies (you admin password may be required):

$ cd borderrouter
$ ./script/bootstrap

Compile and install OTBR:

$ ./script/setup

Check if the device has been recognized by your Linux machine checking the available devices under /dev/tty.*.

Configure the device port in the wpantund configuration file /etc/wpantund.conf. The effective port depends on your configuration but, for example, can be /dev/ttyACM0 or /dev/ttyUSB0.

Rebooting the Linux machine all the installed services shluld be executed at startup.

To check if the services are running you can use the sudo systemctl status command. All the following services should be listed and enabled:

  • avahi-daemon.service
  • otbr-agent.service
  • otbr-web.service
  • wpantund.service

If not all of them are running you can use the script located at borderrouter/script/server to start them.

You can now verify if the borderrouter is successfully configured and the board has been recognized by using the wpanctl command.

$ sudo wpanctl status

If the configuration is correct you should see an output similar to:

wpan0 => [
        "NCP:State" => "offline"
        "Daemon:Enabled" => true
        "NCP:Version" => "OPENTHREAD/20170716-00506-gccb354fb-dirty; NRF52840; Mar 15 2018 14:43:28"
        "Daemon:Version" => "0.08.00d (/50eedbb; Aug  7 2018 08:22:56)"
        "Config:NCP:DriverName" => "spinel"
        "NCP:HardwareAddress" => [5DC574B951D3EADB]
]

If the value of NCP:State is different from “offline”, you can find some solutions at this link.

 Connnect to the border router web interface

The OpenThread border router has a web interface to configure the network. You can connect to it by accessing the address localhost:80 with a browser.


ℹ️ If you are using a virtual machine without a web interface you can route the port 80 to your host machine.


Once you can access the web interface you should move to the Form page to create a new network, as shown in the image.

image

Once you clicked the Form button you should see a popup with the following message FORM operation is successful.

Test the OpenThread network

We will now test the created OpenThread network using the other nRF52840 board and a Zephyr sample.

Move back to the Zephyr cloned repository:

$ cd ~/libpull_tutorial/zephyr

To test the network we will use the Echo client example. We need to create a configuration file that targets our specific board.

$ cd samples/net/echo_client
$ wget https://gist.githubusercontent.com/AntonioLangiu/5d4184085cf81a816c0b904b27b41c7e/raw/fe0bc763b991b64686534a5e0c2cd12c760a7771/prj_nrf52840_ot.conf

The configuration file contains the directive to enable OpenThread and the OpenThread shell that we will use for testing. To understand the configuration you can read the Zephyr documentation.

We can now build and flash the firmware on the board:

$ mkdir build && build
$ cmake -GNinja -DBOARD=nrf52840_pca10056 -DCONF_FILE=prj_nrf52840_ot.conf ..
$ ninja

If the build was successfull we can now flash the device and access the shell using Minicom:

$ ninja flash
$ minicom -D /dev/tty.usbmodem1411

⚠️ Since you have two devices connected, plase check the ID of the device you want to flash using the nrfjprog.


You should now have access to the OpenThread shell typing the following commands inside of minicom:

select ot
cmd help

Typing cmd scan you should see the following output:

| J | Network Name         | Extended PAN     | PAN  | MAC Address      | Ch | dBm | LQI |
+---+----------------------+------------------+------+------------------+----+-----+-----+
| 0 | Libpull Tutorial"""" | 1111111122222222 | 1234 | 8a246d3fd47592be | 15 | -44 |  50 |

OpenThread provides an official documentation for the CLI describing each command and how to use it.

To test if the device is able to communicate with the server using the OpenThread network already created we can use netcat to listed for udp packets and send an udp packet from the device.

First we need to understand the network configuration of our computer. With the command ifconfig we can see the various interfaces of our PC and the IP address assigned to them. You can find the global IPv6 address reachable from the OpenThread network checking the addresses assigned to the wpan0 interface.

In our case the configuration was as follow, but in your computer it will be different:

wpan0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST>  mtu 1280
        inet6 fdde:ad00:beef:0:666c:4b75:8e5a:ae76  prefixlen 64  scopeid 0x0<global>
        inet6 fe80::f0b9:99f1:64e8:d66b  prefixlen 64  scopeid 0x20<link>
        inet6 fd11:22::f0b9:99f1:64e8:d66b  prefixlen 64  scopeid 0x0<global>
        inet6 fe80::9cf5:8d3a:2c6c:171f  prefixlen 64  scopeid 0x20<link>
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 500  (UNSPEC)
        RX packets 13  bytes 696 (696.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 41  bytes 5844 (5.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

We can listen for incoming udp packets using the:

$ netcat -ul <global_ipv6_address_of_wpan0_interface> 2115

command. Moving to the minicom connection we can now send an udp packet through the OpenThread network using the:

ot> cmd udp send <global_ipv6_address_of_wpan0_interface> 2115 Test

We should now see the Test message printed from the netcat listening server.

To test if the communication is correctly working you can also use the ping command integrated into the OpenThread CLI:

ot> cmd ping <global_ipv6_address_of_wpan0_interface>

Moving forward

Setting an OpenThread network really depends from your available configuration. You should have a basic understanding of the OpenThread principles and how to configure a Linux network.

If you arrived here, it means you are able to send and receive packets from your device to the listening server using the OpenThread network. This is required to move formward since to receive the firmware the device will need to communicate with our testing server.

Libpull Build

In this part of the tutorial you will learn how to build the libpull bootloader, the update agent, and how to compose them togeather to create a flashable firmware.

Zephyr build

To build the bootloader we need to go to the specific platform build folder. Libpull defines a folder for each platform containing the all the scripts and tools necessary for the platform, in our case Zephyr.

Move to the Zephyr platform folder:

$ cd ~/libpull_tutorial/libpull/build/zephyr

On this folder you will find the following content:

  • autogen.sh: script to clone the dependencies;
  • application: folder containing the code and the build system for the application;
  • bootloader: folder containing the code and build system for the bootloader;
  • board: folder containing the boards specific files;
  • bootloader_ctx: folder containing the bootloader context files;
  • config.toml: configuration file of the firmware_tool program;
  • make_firmware.sh: a script to build together the bootloader and the application in a single flashable image;
  • test: a folder containing a set of tests used to test the board with libpull;

We will start executing the tests to see if they pass on the device, and also to start undestanding how the build process works.

First of all, let’s clone the dependencies using the:

$ ./autogen.sh

script contained in the build/zephyr folder.

Once Zephyr has been cloned, you have to import the zephyr-env.sh variables to your environment. You can do it with the commands:

$ cd ext/zephyr
$ source zephyr-env.sh

Execute libpull platform specific tests

Moving to the test folder we will find three tests with different goals:

  • memory: tests the ability to read and write data to the flash;
  • network: tests the ability to send and receive packets using our connection;
  • security: tests the ability to perform the firmware verification using a specific cryptographic library;

This tests ensure that libpull correctly works on the choosen platform.

For each test you can build it entering in the test folder and typing the following commands:

$ mkdir build && cd build
$ cmake -GNinja -DBOARD=nrf52840_pca10056 -DCONF_FILE=prj.conf ..
$ ninja
$ ninja flash
$ minicom -D /dev/tty.your_device

Analyzing the output of the test you can understand if that particular component of libpull correctly works on your board and with your configuration.

 Build the bootloader

To build the bootloader you need to perform the same Zephyr build steps in the bootloder directory:

$ cd ~/libpull_tutorial/libpull/build/zephyr/bootloader
$ mkdir build && cd build
$ cmake -GNinja -DBOARD=nrf52840_pca10056 -DCONF_FILE=prj.conf ..
$ ninja

The bootloader, once loaded on the device, needs to have a storage to save persistent data. This is what is in libpull is called the bootloader_ctx. To create a bootloader context we provide a program contained in the bootloader_ctx folder of each platform.

You can build it using the following commands:

$ cd bootloader_ctx
$ make

If the build was successfull you should now have a bootloader_ctx.bin file and you are ready to move to the next steps.

 Build the application

The application contains the update agent in charge of contacting the server to receive the update. This means that the update agent must be able to commicate with the server and must know its IP address.

The IP address of the server, that in this case is the one of your wpan0 network, must be hardcoded in the application/src/runner.c file, editing the SERVER ADDR preprocessor variable.

#define SERVER_ADDR <your_public_ipv6_address>

The public IPv6 address can be taken from the ifconfig command as previously shown in this tutorial.

To build the application you need to perform the same Zephyr build steps in the application directory:

$ cd ~/libpull_tutorial/libpull/build/zephyr/application
$ mkdir build && cd build
$ cmake -GNinja -DBOARD=nrf52840_pca10056 -DCONF_FILE=prj.conf ..
$ ninja

If the build was successfull you can now move to the next step.

Create the flashable firmware

Now that we have a bootloader and an application we can build a flashable firmware. Each platform folder contains a ./make_firmware.sh script that will invoke all the programs to create a flashable binary according to the specific variables of the board.

In the case of the nRF52840 you can find the board variables in the board/nrf52840_pca10056/Makefile.conf file.

To create a flashable firmware you can invoke the previosly described script:

$ ./make_firmware.sh

If the firmware has been successfully created you will find it on the main folder in two formats:

  • fimrware.hex (Intel Hex version)
  • firmare.bin (Binary version)

Flash the firmware

We can use now the nrfjprog to flash the firmware, using the command:

$ nrfjprog --eraseall
$ nrfjprog --program firmware.hex
$ nrfjprog --reset

Using minicom you can analyze the output. If everything is working correctly you should see the bootloader output:

***** Booting Zephyr OS v0.0.1-179-g67dce7f *****
Bootloader started
id: bootable 2 - non bootable 3
id: bootable 2 - non bootable 3
version: bootable 1 - non bootable 0
Start phase GET_OBJECT_MANIFEST
Start phase CALCULATING_DIGEST
Digest: initial offset 256u final offset 226396 size 226140
Start phase VERIFY_DIGEST
Start phase VERIFY_VENDOR_SIGNATURE
Vendor Signature Valid
Start phase VERIFY_SERVER_SIGNATURE
Server Signature Valid
loading object
loading address 9100

At the address 9100 the ./make_firmware.sh script placed the application that, once boooted should print the following messages:

***** Booting Zephyr OS v0.0.1-179-g67dce7f *****
Zephyr Shell, Zephyr version: 1.12.0
Type 'help' for a list of available commands
shell> [net] [INF] ot_state_changed_handler: State changed! Flags: 0x000010e4 Current role: 2
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x00000001 Current role: 2
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x00000200 Current role: 2
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x00000001 Current role: 2
Getting the newest firmware from memory
the id of the newest firmware is 2
Checking for updates
....

At this point we are ready for the last phase, starting the update server and sending a new image to the device.

Sending the update

In this last step of the tutorial you will learn how to put all the pieces togeather and send the update to the device.

Build the update

We first need to create an update with a version higher than the one currently running on the device.

The version of the update must be setted in the config.toml configuration file. It is used by the firmware_tool application provided with libpull.

The default firmware version is 0x0001. To make the update possible we should send to the device a firmware with a version stricly higher than the one already installed. We can, for example, set the version to 0x0002.

Once we have an higher version we can generate the firmware using the ./make_firmware.sh script.

This will generate, as before, a firmware containing both the application with the manifest and the bootloader. However, to send an update we want to send only the application part, since the bootloader cannot be changed on the device. Thus we need to take it from the firmware folder where the application firmware is generated.

Inside of the firmware folder (located at ~/libpull_tutorial/libpull/build/zephyr/firmware/) we can find the following files:

  • firmware.bin.tmp // The firmware generated by the application build system;
  • manifest.bin // The manifest created from it;
  • firmware.bin // A binary composed by the manifest + the firmware;

The server must send only the last one, thus we should pass it as a server argument.

Execute the server

The make run_server target used at the start of the tutorial prepares the server for the Unit Tests. However, we need to send the firware previsouly created, thus we can execute the server with the following commands:

$ cd ~/libpull_tutorial/libpull/
$ ./utils/server/server -f build/zephyr/firmware/firmware.bin

However, the previous command is not sufficient since, until we configured the internal network to route packets from wpan0 to the localhost, we also need to instruct the server to listend on the correct interface.

The server offers an option to do it:

./utils/server/server -A <your_public_ipv6_addr> -f firmware/firmware.bin

Where :raw-html-m2r:`<your_public_ipv6_addr>`* is a global IPv6 address assigned to your *wpan0 interface.

Update the device

Once the server is started, you can restart the device and wait for the update process to start. It should start immediately, downloading the manifest and then the whole update.

If the process was correct, you should see an output like this:

***** Booting Zephyr OS v0.0.1-179-g67dce7f *****
Bootloader started
id: bootable 2 - non bootable 3
version: bootable 1 - non bootable 0
Start phase GET_OBJECT_MANIFEST
Start phase CALCULATING_DIGEST
Digest: initial offset 256u final offset 226396 size 226140
Start phase VERIFY_DIGEST
Start phase VERIFY_VENDOR_SIGNATURE
Vendor Signature Valid
Start phase VERIFY_SERVER_SIGNATURE
Server Signature Valid
loading object
loading address 9100
[net] [INF] openthread_init: OpenThread version: OPENTHREAD/20170716-00461-gdb4759cc-dirty; NONE; Aug  7 2018 16:43:32
[net] [INF] openthread_init: Network name:   Libpull Tutorial
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x003f333f Current role: 1
***** Booting Zephyr OS v0.0.1-179-g67dce7f *****
Zephyr Shell, Zephyr version: 1.12.0
Type 'help' for a list of available commands
shell> [net] [INF] ot_state_changed_handler: State changed! Flags: 0x000010e4 Current role: 2
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x00000001 Current role: 2
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x00000200 Current role: 2
[net] [INF] ot_state_changed_handler: State changed! Flags: 0x00000001 Current role: 2
Getting the newest firmware from memory
the id of the newest firmware is 2
Checking for updates
subsriber_cb: message received
Latest_version:    1 Provisioning version    2
An update is available
An update is available
Pages erased correctly
Manifest still not received
Received 64 bytes. Expected 0 bytes
Manifest still not received
Received 128 bytes. Expected 0 bytes
Manifest still not received
Received 192 bytes. Expected 0 bytes
Manifest still not received
Received 256 bytes. Expected 0 bytes
Manifest still not received
Manifest received
Platform: beef
Version: 0002
Size: 226140
valid: 0101 0202 received 0101 0202
Received 320 bytes. Expected 226396 bytes
...
Received 226396 bytes. Expected 226396 bytes
Start phase GET_OBJECT_MANIFEST
Start phase CALCULATING_DIGEST
Digest: initial offset 256u final offset 226396 size 226140
Start phase VERIFY_DIGEST
Start phase VERIFY_VENDOR_SIGNATURE
Vendor Signature Valid
Start phase VERIFY_SERVER_SIGNATURE
Server Signature Valid

Supported Platforms

Libpull currently supports the following platforms:

Zephyr Platform

Libpull can be used to perform Software Updates for IoT devices using the Zephyr platform.

We tested and provide the following components:

  • an update agent to update a Zephyr device using OpenThread;
  • an update agent to update a Zephyr device using Bluetooth Low Energy (BLE);
  • a bootloader able to manage the images and upgrade once a new image is present;

For the Zephyr platform we currently support the following boards:

However, you can easily integrate another board by following this tutorial.

Contiki-NG Platform

Libpull can be used to perform Software Updates for IoT devices using the Contiki-NG platform.

We tested and provide the following components:

  • an update agent to update a Contiki-NG device using 6LoWPAN and CoAP;
  • a bootloader able to manage the images and upgrade once a new image is present;

For the Contiki-NG platform we currently support the following boards:

However, you can easily integrate another board by following this tutorial.

Riot Platform

Libpull can be used to perform Software Updates for IoT devices using the RIOT platform.

We tested and provide the following components:

  • an update agent to update a RIOT device using 6LoWPAN and CoAP;
  • a bootloader able to manage the images and upgrade once a new image is present;

For the RIOT platform we currently support the following boards:

However, you can easily integrate another board by following this tutorial.

New Platform

If you want to integrate a new platform you will find here a small tutorial suggesting the steps to do it in the right way.

Build libpull with the same build system

You have two approaches to build libpull:

  • build a static library and link it to your platform;
  • build it with your platform build system;

Both approaches should lead you to the same result, however, the second one is more easier and less error prone, thus is the one that we used for all our supported platforms and is what we suggest also in your case.

If you want to build libpull togeather with your build system you may want to follow the approach already used for the GNU Makefile, watching the Contiki-NG or RIOT platforms, or the CMake used when building Zephyr.

Build the tests

We provide three tests that can be easily executed on the board. Each of them targets a specific feature of libpull that is platform dependent.

  • memory test: allows to verify the correct implementation of the memory interface;
  • security test: allows to test the security functions (such as digest ECC key validation) directly on hardware;
  • network test: allows to test the correct implementation of the connection interface.

Only when all these tests passes you can be sure that libpull will work correctly with your platform and that you implemented all the interfaces correctly.

To build the tests you can take as example the already supported platforms.

Build the application

The application you will execute on the IoT device must periodically execute the update agent to check if a new update is available and start in that case the process.

Our update agent is build on top of coroutines. This means that you can easily use it also with platforms where abstractions such a thead or processes are not available and you only rely on a main while loop.

You can follow the examples on the other supported platforms to see how to invoke and configure the update agent. You will find informations on the API documentation.

Build the bootloader

Our bootloader can be easily integrated with the OS to benefit from all the facilities it offers. This allows, for example, to support all the boards supported by the OS just by providing support for it.

If you are building a bootloader for a specific solution and you do not want to rely on an OS as a basis for it, you can build it bare metal, still using the bootloader agent that we provide.

To see how to configure and execute the bootloader please check the API documentation.

Contributing?

Do you think the new OS you are supporting may be useful also for others? Please open an issue or a pull request to discuss a possible integration.

API Documentation

The API documentation is generated using Doxygen. to create a XML rappresentation from the sources. The XML is used by Breathe to generate the corresponding rst, used by Sphinx to generate the final documentation hosted on Read The Docs.

You can also generate the API documentation locally, by using the Doxyfile located in build/doc/source/Doxyfile

Libpull is logically organized in the following modules:

Common API

The common modules contains functions and interfaces that are shared in the whole library. This allows to grant consistency and to reduce code duplication. You can include all the common modules by including

#include <libpull/common.h>

Callback

typedef void (*callback)(pull_error err, const char *data, int len, void *more)

Callback used to handle network events.

Parameters
  • err: Error received or PULL_SUCCESS if no error.
  • data: Received data.
  • len: Lenght of received data.
  • more: Raw pointer passed during connection initialization.

Crc16

Warning

doxygengroup: Cannot find namespace “com_crc16” in doxygen xml output for project “libpull” from directory: ./doxyxml/

Error

enum com_error::pull_error

Each module should returns just errors of that particular module. In this way it is possible for the calling function to have a finite set of errors returned by the function.

Values:

PULL_SUCCESS = 0
GENERIC_ERROR

Generic error

NOT_IMPLEMENTED_ERROR

Method not implemented

INVALID_ARGUMENTS_ERROR
CONNECTION_INIT_ERROR

The connection initialization failed

CALLBACK_ERROR

The callback could not be setted

RESOLVER_ERROR

Error resolving the backend

INVALID_URL_ERROR

The URL of the provided resource is invalid

BLOCK_WISE_ERROR

Error during the Block-Wise transfer

INVALID_METHOD_ERROR

The request method is invalid or not supported

INVALID_RESOURCE_ERROR

The requested resource is invalid

INVALID_DATA_ERROR

The received data is invalid

INVALID_CONN_DATA_ERROR

The data used to inizialize the connection is invalid

REQUEST_ERROR

Error performing the request

REQUEST_RST_ERROR
SEND_ERROR
MEMORY_ERROR
INVALID_OBJECT_ERROR
INVALID_ACCESS_ERROR
MEMORY_MAPPING_ERROR
MEMORY_OPEN_ERROR
MEMORY_CLOSE_ERROR
MEMORY_ERASE_ERROR
MEMORY_READ_ERROR
MEMORY_WRITE_ERROR
MEMORY_FLUSH_ERROR
READ_MANIFEST_ERROR
GET_NEWEST_ERROR
GET_OLDEST_ERROR
COPY_FIRMWARE_ERROR
INVALIDATE_OBJECT_ERROR
WRITE_MANIFEST_ERROR
INVALID_MANIFEST_ERROR
RECEIVER_OPEN_ERROR
RECEIVER_CHUNK_ERROR
RECEIVER_CLOSE_ERROR
INVALID_SIZE_ERROR
INVALID_IDENTITY_ERROR
NETWORK_ERROR
SUBSCRIBE_ERROR
SUBSCRIBER_CHECK_ERROR
DIGEST_INIT_ERROR
DIGEST_UPDATE_ERROR
DIGEST_FINAL_ERROR
SHA256_INIT_ERROR
SHA256_UPDATE_ERROR
SHA256_FINAL_ERROR
NOT_SUPPORTED_CURVE_ERROR
VERIFICATION_FAILED_ERROR
SIGN_FAILED_ERROR
const char *err_as_str(pull_error err)

This function returns a string representing the literal representation of the error.

Return
Pointer to a string describing the error.
Parameters
  • err: The error to be printed.

GENERATE_ENUM(ENUM)
GENERATE_STRING(STRING)
FOREACH_ERROR(ERROR)

External

const mem_slot_t memory_slots[]

This structure contains a list of the memory slots needed by your application. We already have a standard definition but you can define it by yourself and personalize according to your needs.

An example of a correctly implemented memory slot is structure is:

 const mem_slot_t memory_slots[] = {
  {
      .id = OBJ_1,
      .bootable = true,
      .loaded = true
  },
  {
      .id = OBJ_2,
     .bootable = false,
     .loaded = false
 },
 {OBJ_END}
};

OBJ_END

OBJ_END must be used to end a memory slot list. Internally the library uses it to know when to stop cycling over the structure.

Identity

pull_error update_random(identity_t *identity)

Function to update the random using a PRNG. The implementation is platform dependent and must often rely on hardware solutions included in the platform since there is no entropy source in many RTOS.

Return
PULL_SUCCESS on success, an error otherwise.
Parameters
  • identity: Pointer to the identity struct containing the value to be updated.

pull_error validate_identity(identity_t valid_identity, identity_t received_identity)

Function to validate an identity comparing it to the one stored on the device.

Return
PULL_SUCCESS if the identity is valid, an error otherwise.
Parameters
  • valid_identity: Pointe to the identity stored on the device.
  • received_identity: Pointer to the identity received that must be validated.

struct identity_t
#include <identity.h>

Structure used to univocally identify a device.

Note
This struct is often passed by value in the library, so it must not contain any pointer.

Loader

void load_object(mem_id_t id)

Loader interface used to load the object once stored.

Parameters
  • id: Id of the memory object to be loaded.

Logger

uint8_t verbosity_level

The verbosity_level variable indicates the log_impling level. If you want to change it at runtime you can define the external symbol in your application. In case you don’t need to change it at runtime you can define it as a constant, defining the LOGGER_VERBOSITY define. In this way the compiler will be able to optimize the code and remove the non needed debugging directives.

4 logging level are supported:

  • Error: Just the errors are printed.
  • Warn: Error and warnings are printed.
  • Info: Error, warnign and also information strings.
  • Debug: All the output is printed. This affect heavily the memory footprint of the library.

VERBOSITY_ERROR
VERBOSITY_WARN
VERBOSITY_INFO
VERBOSITY_DEBUG
log_output(...)
log_impl(...)
log_err(...)
log_debug(...)
log_info(...)
log_error(err, ...)

This function takes as first parameter an error and then an arbitrary number of arguments that will be printed. The first arguemnt will be used to print the literal value of the error.

log_warn(...)

Pull Assert

PULL_ASSERT(cond)

Types

typedef uint16_t version_t

Version type to be used across the library.

typedef uint16_t platform_t

Platform type to be used across the library

typedef uint32_t address_t

Address type to be used across the library

typedef int8_t mem_id_t

Identifier for the memory objects. It supports at most 255 objects This must be a signed integer since negative values are used to define an invalid object

Memory API

Manifest

version_t get_version(const manifest_t *mt)
platform_t get_platform(const manifest_t *mt)
address_t get_size(const manifest_t *mt)
address_t get_offset(const manifest_t *mt)
uint8_t *get_server_key_x(const manifest_t *mt)
uint8_t *get_server_key_y(const manifest_t *mt)
uint8_t *get_digest(const manifest_t *mt)
uint8_t *get_vendor_signature_r(const manifest_t *mt, uint8_t *size)
uint8_t *get_vendor_signature_s(const manifest_t *mt, uint8_t *size)
uint8_t *get_server_signature_r(const manifest_t *mt, uint8_t *size)
uint8_t *get_server_signature_s(const manifest_t *mt, uint8_t *size)
void set_version(manifest_t *mt, version_t version)
void set_platform(manifest_t *mt, platform_t platform)
void set_size(manifest_t *mt, address_t size)
void set_offset(manifest_t *mt, address_t offset)
void set_server_key_x(manifest_t *mt, uint8_t *server_key_x)
void set_server_key_y(manifest_t *mt, uint8_t *server_key_y)
void set_digest(manifest_t *mt, uint8_t *digest)
int set_vendor_signature_r(manifest_t *mt, uint8_t *vendor_signature_r, uint8_t size)
int set_vendor_signature_s(manifest_t *mt, uint8_t *vendor_signature_s, uint8_t size)
int set_server_signature_r(manifest_t *mt, uint8_t *server_signature_r, uint8_t size)
int set_server_signature_s(manifest_t *mt, uint8_t *server_signature_s, uint8_t size)
void print_manifest(const manifest_t *mt)

Print manifest known values.

Parameters
  • mt: Pointer to a manifest structure.

identity_t get_identity(const manifest_t *mt)
void set_identity(manifest_t *mt, identity_t identity)
pull_error verify_signature(manifest_t *mt, digest_func df, const uint8_t *pub_x, const uint8_t *pub_y, ecc_func_t ef)
FOREACH_ITEM(ITEM)
DEFINE_GETTER(type, name)

The scope of this file is to define the interface of a manifest. It can be implemented using different encodings, but each approach should implement this interface to be usable by the library

DEFINE_SETTER(type, name)
DEFINE_GETTER_MEMORY(type, name)
DEFINE_SETTER_MEMORY(type, name)

Memory Interface

enum mem_memint::mem_mode_t

Values:

READ_ONLY = 0
WRITE_CHUNK = 1
WRITE_ALL = 2
SEQUENTIAL_REWRITE = 3
typedef struct mem_object_t mem_object_t
typedef struct mem_slot_t mem_slot_t
pull_error memory_open(mem_object_t *ctx, mem_id_t id, mem_mode_t mode)

Open a memory object.

The implementation of this function should open a memory object given the id. This can be mapped to any phisical object, such as a file, a ROM or even an allocated memory object. It depends on the needs of the platform and the application.

Return
PULL_SUCCESS if the memory was correctly open or the specific erro
Parameters
  • ctx: An unitialized memory object
  • id: The id of the memory object. The obj_id enum must be defined when implementing this interface.
  • mode: The mode used to open the memory_object (i.e. READ, WRITE, etc)

int memory_read(mem_object_t *ctx, void *memory_buffer, size_t size, address_t offset)

Read bytes from a memory object.

This function reads size bytes from a memory object at the specified offset into the given memory buffer.

Return
The number of readed bytes or a negative number in case of error.
Parameters
  • ctx: The already opened memory object.
  • memory_buffer: The memory buffer acting as destination.
  • size: The number of bytes to read.
  • offset: The offset in the memory object from where to start reading.

int memory_write(mem_object_t *ctx, const void *memory_buffer, size_t size, address_t offset)

Write bytes into a memory object.

This function writes size bytes into an opened memory object at the offset specified.

Return
The number of written bytes or a negative number in case of error.
Parameters
  • ctx: An opened memory object.
  • memory_buffer: The memory buffer to be written.
  • size: The size of the memory buffer.
  • offset: The offset into the memory object.

pull_error memory_close(mem_object_t *ctx)

Close the memory object.

This should close and deallocate all the initialized resources.

Return
PULL_SUCCESS on success or a specific error otherwise.
Parameters
  • ctx: The memory object.

Memory Objects

pull_error get_newest_firmware(mem_id_t *id, version_t *version, mem_object_t *obj_t, bool prefer_bootable)

Get the id of the memory object containing the newest firmware.

Return
PULL_SUCCESS on success or a specific error otherwise.
Parameters
  • id: The id of the newest object.
  • version: The version of the newest object.
  • obj_t: A temporary mem_object_t used by the function.

pull_error get_oldest_firmware(mem_id_t *obj, version_t *version, mem_object_t *obj_t, bool prefer_bootable)

Get the id of the memory object containing the oldest firmware.

Return
PULL_SUCCESS on success or a specific error otherwise.
Parameters
  • obj: The id of the oldest object
  • version: The version of the oldest object.
  • obj_t: A temporary mem_object_t used by the function.

pull_error copy_firmware(mem_object_t *src, mem_object_t *dst, uint8_t *buffer, size_t buffer_size)

Copy the firmware s into the firmware d.

This function will use the size specified in the s firmware manifest to correcly copy the firmware.

Note
The buffer will be used to read from object s and to write to obejct d. If you are working with flash and your memory implementation is not buffered you can pass a buffer with size equal to the size of a flash page.
Return
PULL_SUCCESS on success or a specific error otherwise.
Parameters
  • src: The source memory object.
  • dst: The destination memory object.
  • buffer: Buffer used to copy the object
  • buffer_size: The size of the buffer

pull_error swap_slots(mem_object_t *obj1, mem_object_t *obj2, mem_object_t *obj_swap, size_t swap_size, uint8_t *buffer, size_t buffer_size)

Swap two slots using a swap memory_object.

This function swaps two slots using a memory objects with id SWAP

This functions assumes that the size of the SWAP memory object is is at least as big as the buffer_size and that the swap size is a multiple of the buffer size, such that swap_size % buffer_size == 0

Note
The buffer will be used to read from object s and to write to obejct d. If you are working with flash and your memory implementation is not buffered you can pass a buffer with size equal to the size of a flash page.
Parameters
  • obj1: The fist memory object
  • obj2: The second memory object
  • obj_swap: The memory object used for swapping
  • swap_size: The size of the swapping memory object
  • buffer: Buffer used to copy the object
  • buffer_size: The size of the buffer

Return
PULL_SUCCESS on success or a specific error otherwise.

pull_error read_firmware_manifest(mem_object_t *obj, manifest_t *mt)

Read the manifest of the memory object.

This function will use the size specified in the s firmware manifest to correcly copy the firmware.

Return
PULL_SUCCESS on success or a specific error otherwise.
Parameters
  • obj_t: The memory object where the manifeset is stored.
  • mt: manifest of the memory object.

pull_error write_firmware_manifest(mem_object_t *obj_t, const manifest_t *mt)

Write the manifest into the memory object.

Return
PULL_SUCCESS on success or a specific error otherwise.
Parameters
  • obj_t: The memory object where the manifeset must be stored.
  • mt: The manifest to be written.

pull_error invalidate_object(mem_id_t id, mem_object_t *obj_t)

Invalidate a memory object.

Return
Parameters
  • id: Id of the object to invalidate.
  • obj_t: temporary variable used to open the object.

Network API

async_interface connection_config connection_interface gatt receiver subscriber writer

Async Interface

void loop_once(conn_ctx *ctx, uint32_t timeout)

Loop one time and return.

Parameters
  • ctx: An already initialized connection context.
  • timeout: Time to wait before returning if no event is scheduled.

void loop(conn_ctx *ctx, uint32_t timeout)

Blocking function to start the loop.

Parameters
  • ctx: An already initialized connection context.
  • timeout: Time to wait before returning if no event is scheduled.

void break_loop(conn_ctx *ctx)

This function breaks the loop. The execution will continue from the loop() function.

Parameters
  • ctx: An already initialized connection context.

Connection Config

pull_error conn_config(conn_config_t *cfg, char *endpoint, uint16_t port, conn_type type, void *conn_data, char *resource)

Connection Interface

enum net_connint::rest_method

Verbs supported by the REST connection

Values:

GET
PUT

Standard REST verb

POST

Standard REST verb

DELETE

Standard REST verb

OPTIONS

Standard REST verb

GET_BLOCKWISE2

Standard REST verb CoAP specific method

enum net_connint::conn_type

Values:

PULL_TCP
PULL_UDP
PULL_DTLS_PSK
PULL_DTLS_ECDH
typedef enum rest_method rest_method

Verbs supported by the REST connection

typedef enum conn_type conn_type
typedef struct conn_ctx conn_ctx
pull_error conn_init(conn_ctx *ctx, const char *addr, uint16_t port, conn_type type, void *data)

Init the connection context. This functions initialize the connection context and start the connection with the backend specified in the parameters.

Return
PULL_SUCCESS if success or the specific error otherwise.
Parameters
  • ctx:
  • addr: Backend address.
  • port: Backend port.
  • type: Connection type. This enumberation can be defined in the interface implementation. (i.e., TCP, UDP, DTLS, etc..);
  • data: This raw pointer stores data for the specific connection type (e.g., keys for a DTLS connection).

pull_error conn_on_data(conn_ctx *ctx, callback handler, void *more)

Set the callback to be called in case of a new event. The callback is related to the connection and not the single request. This means that all the requests must be handled by the same callback. This is not a problem in the way the connection module is used by the library because each connection is used just for a specific operation and, in case you want to reuse a connection, you must be sure that all the previous requests has been satisfied.

Return
PULL_SUCCESS if the callback has been correcly setted.
Parameters
  • ctx: An alredy initialized connection context.
  • handler: The callback handler.
  • more: A pointer that will be passed to the callback every time it is called.

pull_error conn_request(conn_ctx *ctx, rest_method method, const char *resource, const char *data, uint16_t length)

Perform a request to the backend. The request is performed using the method, the resource and the data specified.

Return
PULL_SUCCESS if the request was correcly sent or the specific error otherwise.
Parameters
  • ctx: An already initialized connection context.
  • method: The rest method to perform the request (e.g., GET, PUT, etc).
  • resource: The REST resource.
  • data: (Optional) The data to be sent or NULL.
  • length: The lenght of the data or 0;

pull_error conn_observe(conn_ctx *ctx, const char *resource, const char *token, uint8_t token_length)

TODO TO BE IMPLEMENTED

void conn_end(conn_ctx *ctx)

Close the connection connection. This function must free all the resources and close the connection with the server.

Parameters
  • ctx: The connection context to close.

Gatt

pull_error libpull_gatt_init()
LIBPULL_SERVICE_UUID
LIBPULL_VERSION_UUID
LIBPULL_PLATFORM_UUID
LIBPULL_IDENTITY_UUID
LIBPULL_UPDATE_UUID
LIBPULL_STATE_UUID
LIBPULL_RESULT_UUID
LIBPULL_RECEIVED_UUID
LIBPULL_IMAGE_UUID

Receiver

typedef struct receiver_ctx receiver_ctx

Receiver context used to hold data for the receiver function

pull_error receiver_open(receiver_ctx *ctx, conn_ctx *conn, identity_t *identity, const char *resource, mem_object_t *obj)

Open the receiver context. This function start the connection with the backend. It uses a connection object to communicate with it and needs a string rappresenting the resource we want to receive from the backend. It stores the received content into a memory object.

Return
PULL_SUCCESS in case the receiver was correcly initialized or the specific error otherwise.
Parameters
  • ctx: The receiver context that should be passed to every receiver function.
  • conn: The connection object. It must be already initialized.
  • identity: The device identity used for this particular request
  • resource: The resource we want to download from the backend.
  • obj: Memory object used to store the received data. It must be opened.

pull_error receiver_chunk(receiver_ctx *ctx)

Receive and store a chunk of the update into the memory object.

Return
PULL_SUCCESS in case the chunk was correcly downloaded and stored or the specific error otherwise.
Parameters
  • ctx: The previously initialized receiver context.

pull_error receiver_close(receiver_ctx *ctx)

Close the receiver context and close the connection with the server.

Return
PULL_SUCCESS in case the context was correcly closed or the specific error otherwise.
Parameters
  • ctx: The receiver context to close.

RECEIVER_H_
MESSAGE_VERSION
struct receiver_ctx
#include <receiver.h>

Receiver context used to hold data for the receiver function

Subscriber

void subscriber_cb(pull_error err, const char *data, int len, void *more)

Callback handling the data received by the server. There is a default implementation of this callback but it can be overridden by the user by passing its own callback to the check update function.

Note
The more parameter can be useful to pass receive some structure from the function creating the trasnport when the callback is called.
Parameters
  • err: Error received by the calling function. PULL_SUCCESS if no error.
  • data: Data received by the network. NULL if error.
  • len: Lenght of the received data. 0 if error.
  • more: Pointer passed during initialization of the connection object.

pull_error subscribe(subscriber_ctx *ctx, conn_ctx *conn, const char *resource, mem_object_t *obj_t)

Subscribe to a backend for updates. This functions initialize the subsciber context and subscribe to a specific resource. It requires an already initialized connection context.

Return
PULL_SUCCESS on success or the specific error otherwise.
Parameters
  • ctx: A pointer to the subscriber.
  • conn: An already opended connection object.
  • resource: A string rapresenting the resource.
  • obj_t: A temporary memory object.

pull_error check_updates(subscriber_ctx *ctx, callback cb)

Check the presence of an update. This blocking function perform a request to the specified backend and resource and handles the response using the provided callback. An already defined callback is provided with the library, however you can define your own logic matching the protocol used in your server.

Return
PULL_SUCCESS on success or the specific error otherwise.
Parameters
  • ctx: An already initialized ubscriber context.
  • cb: The callback.

pull_error unsubscribe(subscriber_ctx *ctx)

Unsubscribe from the backend. This function closes context, however does not closes the connection tham must be closed by the caller.

Return
PULL_SUCCESS on success or the specific error otherwise.
Parameters
  • ctx: The subscriber context to close.

Writer

typedef pull_error (*validate_mt)(manifest_t *mt, void *user_data)
typedef struct writer_ctx_t writer_ctx_t
pull_error writer_open(writer_ctx_t *ctx, mem_object_t *obj, validate_mt cb, void *user_data)
pull_error writer_chunk(writer_ctx_t *ctx, const char *data, uint32_t len)
pull_error writer_close(writer_ctx_t *ctx)
WRITER_BUFFER_LEN

Security API

Digest

typedef union digest_ctx digest_ctx
struct digest_func
#include <digest.h>

Abstraction to use different cryptographic libraries to calculate the hash

ECC

ECC_VERIFY(impl)
ECC_SIGN(impl)
ECC_FUNC(impl, size)

SHA 256

SHA256_INIT(lib)
SHA256_UPDATE(lib)
SHA256_FINAL(lib)
DIGEST_FUNC(lib)

This struct defines a set of default digest function. You can define your own structure adding the function you need.

Verifier

safestore_t
pull_error verify_object(mem_object_t *obj, digest_func f, const uint8_t *x, const uint8_t *y, ecc_func_t ef, uint8_t *buffer, size_t buffer_len)

This function verifies the signature on the object id.

The digest function and the curve must match the one used to generate the signature stored into the memory object metadata. The ECC signature does not accept any format to reduce space used to store keys.

Note
The size of the buffer must be greather or equal to the chunk of manifest to be hashed
Return
PULL_SUCCESS if verification succeded or the specific error otherwise.
Parameters
  • obj_t: The memory object to validate
  • f: The digest function.
  • x: The X parameter of the signer’s public key.
  • y: The Y parameter of the signer’s public key.
  • curve: The curve parameters.
  • buffer: The buffer used to read data from the object
  • buffer_len: The size of the buffer

union

Public Members

uint8_t struct::x[32]
uint8_t struct::y[32]

Agents API

Bootloader Agent

enum ag_bl::agent_event_t

Events returned by the bootloader agent.

Values:

EVENT_INIT = 0
EVENT_CONTINUE_START_
EVENT_BOOT
EVENT_VALIDATE_BOOTABLE_START
EVENT_VALIDATE_BOOTABLE_STOP
EVENT_BOOTSTRAP
EVENT_FIRST_BOOT
EVENT_GET_NEWEST_FIRMWARE
EVENT_GET_NEWEST_NON_BOOTABLE
EVENT_STORE_BOOTLAODER_CTX
EVENT_UPGRADE
EVENT_UPGRADE_COPY_START
EVENT_UPGRADE_COPY_STOP
EVENT_UPGRADE_SUCCESS
EVENT_VALIDATE_NON_BOOTABLE
EVENT_VALIDATE_NON_BOOTABLE_START
EVENT_VALIDATE_NON_BOOTABLE_STOP
EVENT_CONTINUE_STOP_
EVENT_FAILURE_START_
EVENT_VALIDATE_BOOTABLE_FAILURE
EVENT_BOOTSTRAP_FAILURE
EVENT_BOOTSTRAP_FAILURE_2
EVENT_FATAL_FAILURE
EVENT_FIRST_BOOT_FAILURE
EVENT_GET_NEWEST_FIRMWARE_FAILURE
EVENT_GET_NEWEST_FIRMWARE_FAILURE_2
EVENT_GET_NEWEST_NON_BOOTABLE_FAILURE
EVENT_STORE_BOOTLAODER_CTX_FAILURE
EVENT_UPGRADE_COPY_FAILURE
EVENT_UPGRADE_FAILURE
EVENT_UPGRADE_FAILURE_2
EVENT_UPGRADE_FAILURE_3
EVENT_UPGRADE_FAILURE_4
EVENT_VALIDATE_NON_BOOTABLE_INVALID
EVENT_VALIDATE_NON_BOOTABLE_FAILURE
EVENT_FAILURE_STOP_
EVENT_FINISH
EVENT_INIT = 0
EVENT_CONTINUE_START_
EVENT_SUBSCRIBE
EVENT_CHECKING_UPDATES
EVENT_CHECKING_UPDATES_TIMEOUT
EVENT_SEARCHING_SLOT
EVENT_CONN_RECEIVER
EVENT_RECEIVE
EVENT_VERIFY
EVENT_FINAL
EVENT_APPLY
EVENT_VERIFY_BEFORE
EVENT_VERIFY_AFTER
EVENT_CONTINUE_END_
EVENT_SEND_START_
EVENT_CHECKING_UPDATES_SEND
EVENT_RECEIVE_SEND
EVENT_SEND_END_
EVENT_RECOVER_START_
EVENT_CHECKING_UPDATES_RECOVER
EVENT_RECEIVE_RECOVER
EVENT_RECOVER_END_
EVENT_FAILURE_START_
EVENT_INIT_FAILURE
EVENT_SUBSCRIBE_FAILURE
EVENT_SEARCHING_SLOT_FAILURE
EVENT_SEARCHING_SLOT_FAILURE_2
EVENT_CONN_RECEIVER_FAILURE
EVENT_CONN_RECEIVER_FAILURE_2
EVENT_VERIFY_FAILURE
EVENT_INVALIDATE_OBJECT_FAILURE
EVENT_FAILURE_END_
typedef enum agent_event_t agent_event_t

Events returned by the bootloader agent.

typedef struct bootloader_agent_config bootloader_agent_config

Configuration structure of the bootloader agent.

static void bootloader_agent_vendor_keys(bootloader_agent_config *cfg, uint8_t *x, uint8_t *y)

Function to configure the vendor keys.

Parameters
  • cfg: Pointer to configuration structure.
  • x: X component of the vendor key.
  • y: Y component of the vendor key.

static void bootloader_agent_digest_func(bootloader_agent_config *cfg, digest_func df)

Function to configure the bootloader digest function.

Parameters
  • cfg: Pointer to the configuration structure.
  • df: Digest function to be used. (For a list of them check the security/digest documentation).

static void bootloader_agent_ecc_func(bootloader_agent_config *cfg, ecc_func_t ef)

Function to configure the bootloader ECC function.

Parameters
  • cfg: Pointer to the configuration structure.
  • ef: ECC function to be used. (For a list of them check the security/ecc documentation).

static void bootloader_agent_set_buffer(bootloader_agent_config *cfg, uint8_t *buffer, size_t buffer_size)

Function to set the buffer used by the bootloader.

Note
When using flash memory, to optimize the IO performance the buffer size should be equal to the page size of your flash memory.
Parameters
  • cfg: Pointer to the configuration structure.
  • buffer: Pointer to the buffer.
  • buffer_size: Size of the buffer.

agent_event_t bootloader_agent(bootloader_agent_config *cfg, void **event_data)

Function executing the bootloader agent. This function is internally build with a set of coroutines that stops the function at different execution points. This allows you to perform several actions during according to the returned phase, to check for errors and try to recover, etc.

Return
Structure indicating the current state and the data related to it.
Parameters
  • cfg: Pointer to the configuration structure.

IS_CONTINUE(event)
IS_FAILURE(event)
GET_BOOT_ID(event_data)
GET_ERROR(event_data)
FOREACH_IGNORED_EVENT(ACTION)
struct bootloader_agent_config
#include <bootloader_agent.h>

Configuration structure of the bootloader agent.

Update Agent

enum ag_update::agent_event_t

This states will be used by the update agent coroutines.

Values:

EVENT_INIT = 0
EVENT_CONTINUE_START_
EVENT_BOOT
EVENT_VALIDATE_BOOTABLE_START
EVENT_VALIDATE_BOOTABLE_STOP
EVENT_BOOTSTRAP
EVENT_FIRST_BOOT
EVENT_GET_NEWEST_FIRMWARE
EVENT_GET_NEWEST_NON_BOOTABLE
EVENT_STORE_BOOTLAODER_CTX
EVENT_UPGRADE
EVENT_UPGRADE_COPY_START
EVENT_UPGRADE_COPY_STOP
EVENT_UPGRADE_SUCCESS
EVENT_VALIDATE_NON_BOOTABLE
EVENT_VALIDATE_NON_BOOTABLE_START
EVENT_VALIDATE_NON_BOOTABLE_STOP
EVENT_CONTINUE_STOP_
EVENT_FAILURE_START_
EVENT_VALIDATE_BOOTABLE_FAILURE
EVENT_BOOTSTRAP_FAILURE
EVENT_BOOTSTRAP_FAILURE_2
EVENT_FATAL_FAILURE
EVENT_FIRST_BOOT_FAILURE
EVENT_GET_NEWEST_FIRMWARE_FAILURE
EVENT_GET_NEWEST_FIRMWARE_FAILURE_2
EVENT_GET_NEWEST_NON_BOOTABLE_FAILURE
EVENT_STORE_BOOTLAODER_CTX_FAILURE
EVENT_UPGRADE_COPY_FAILURE
EVENT_UPGRADE_FAILURE
EVENT_UPGRADE_FAILURE_2
EVENT_UPGRADE_FAILURE_3
EVENT_UPGRADE_FAILURE_4
EVENT_VALIDATE_NON_BOOTABLE_INVALID
EVENT_VALIDATE_NON_BOOTABLE_FAILURE
EVENT_FAILURE_STOP_
EVENT_FINISH
EVENT_INIT = 0
EVENT_CONTINUE_START_
EVENT_SUBSCRIBE
EVENT_CHECKING_UPDATES
EVENT_CHECKING_UPDATES_TIMEOUT
EVENT_SEARCHING_SLOT
EVENT_CONN_RECEIVER
EVENT_RECEIVE
EVENT_VERIFY
EVENT_FINAL
EVENT_APPLY
EVENT_VERIFY_BEFORE
EVENT_VERIFY_AFTER
EVENT_CONTINUE_END_
EVENT_SEND_START_
EVENT_CHECKING_UPDATES_SEND
EVENT_RECEIVE_SEND
EVENT_SEND_END_
EVENT_RECOVER_START_
EVENT_CHECKING_UPDATES_RECOVER
EVENT_RECEIVE_RECOVER
EVENT_RECOVER_END_
EVENT_FAILURE_START_
EVENT_INIT_FAILURE
EVENT_SUBSCRIBE_FAILURE
EVENT_SEARCHING_SLOT_FAILURE
EVENT_SEARCHING_SLOT_FAILURE_2
EVENT_CONN_RECEIVER_FAILURE
EVENT_CONN_RECEIVER_FAILURE_2
EVENT_VERIFY_FAILURE
EVENT_INVALIDATE_OBJECT_FAILURE
EVENT_FAILURE_END_
typedef enum agent_event_t agent_event_t

This states will be used by the update agent coroutines.

typedef struct update_agent_ctx_t update_agent_ctx_t

Context of the update agent.

conn_config_t update_agent_config::subscriber
conn_config_t update_agent_config::receiver
uint8_t update_agent_config::reuse_connection
identity_t update_agent_config::identity
uint8_t *update_agent_config::vendor_x
uint8_t *update_agent_config::vendor_y
digest_func update_agent_config::df
ecc_func_t update_agent_config::ef
uint8_t *update_agent_config::buffer
size_t update_agent_config::buffer_size
conn_ctx update_agent_ctx_t::sconn
subscriber_ctx update_agent_ctx_t::sctx
receiver_ctx update_agent_ctx_t::rctx
conn_ctx update_agent_ctx_t::rconn
mem_id_t update_agent_ctx_t::id
mem_object_t update_agent_ctx_t::new_obj
mem_object_t update_agent_ctx_t::obj_t
pull_error update_agent_ctx_t::err
static void update_agent_reuse_connection(update_agent_config *cfg, uint8_t reuse)

The update agents connects to the subscription server and the provisioning server. If the connection to both server should be done with the same connection than the connection must be reused.

Parameters
  • cfg: Pointer to the configuration structure.
  • reuse: Boolean indicating if the connection should be reused (1 to reuse).

static void update_agent_set_identity(update_agent_config *cfg, identity_t identity)

Function to set the device identity used to identify the device with the server.

Parameters
  • cfg: Pointer to the configuration structure.
  • identity: Identity structure.

static void update_agent_vendor_keys(update_agent_config *cfg, uint8_t *x, uint8_t *y)

Function to set the vendor keys.

Parameters
  • cfg: Pointer to the configuration structure.
  • x: The X component of the vendor key.
  • y: The Y component of the vendor key.

static void update_agent_digest_func(update_agent_config *cfg, digest_func df)

Function to set the digest function.

Parameters
  • cfg: Pointer to the configuration structure.
  • df: Digest function to be used. (To see all the available digest functions check the documentation at security/digest).

static void update_agent_ecc_func(update_agent_config *cfg, ecc_func_t ef)

Function to set the ECC function.

Parameters
  • cfg: Pointer to the configuration structure.
  • ef: ECC function to be used. (To see all the available ECC functions check the documentation at security/ECC).

static void update_agent_set_buffer(update_agent_config *cfg, uint8_t *buffer, size_t buffer_size)

Function to set the buffer for the update agent.

Parameters
  • cfg: Pointer to the configuration structure.
  • buffer: Pointer to the buffer.
  • buffer_size: Size of the buffer.

agent_event_t update_agent(update_agent_config *cfg, update_agent_ctx_t *ctx, void **agent_data)

Function to execute the update agent. This function will return several times, each time with a different message indicating the state of the update agent. In this way you can interact with the update agent modifying the states.

Return
Messages containing the current event.
Parameters
  • cfg: Pointer to the configuration structure.
  • ctx: Pointer to the update agent context.

AGENTS_UPDATE_H_
IS_CONTINUE(agent_event)
IS_SEND(agent_event)
IS_RECOVER(agent_event)
IS_FAILURE(agent_event)
GET_CONNECTION(event_data)
GET_ERROR(event_data)
FOREACH_IGNORED_EVENT(ACTION)
struct update_agent_config
#include <update.h>

Configuration structure for the update agent.

struct update_agent_ctx_t
#include <update.h>

Context of the update agent.

Utils

Bootloader Context
uint8_t bootloader_ctx::vendor_key[64]
uint8_t bootloader_ctx::buffer[63]
uint8_t bootloader_ctx::startup_flags
LIBPULL_AGENTS_BOOTLOADER_CTX_H_
FIRST_BOOT
struct bootloader_ctx
#include <bootloader_ctx.h>

Structure of the bootloader context. It is used to store data used by the bootloader.

Coroutines
AGENTS_COROUTINE_H_
TIMEOUT
PULL_BEGIN(ev)
PULL_FINISH(ev)
IGNORE_EVENT(event)
PULL_CONTINUE(ev, ev_data)
PULL_RETURN(ev, ev_data)
PULL_SEND(ev, ev_data)