Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cryptographically signed update support #5213

Merged
merged 27 commits into from
Dec 3, 2018
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
71bf243
Add cryptographically signed update support
earlephilhower Oct 6, 2018
ac25877
Add a simple example
earlephilhower Oct 6, 2018
882e546
Make verifier EC/RSA compatible at any bit length
earlephilhower Oct 6, 2018
29d8c63
Make certain hash bits constant
earlephilhower Oct 7, 2018
e33eb7d
Merge branch 'master' into signedupdates
d-a-v Oct 8, 2018
d9ce799
Merge branch 'master' into signedupdates
earlephilhower Oct 8, 2018
5fb8cd5
When update signed, don't do MD5 work or checking
earlephilhower Oct 10, 2018
b9344f6
Add python automatic signing if keys present
Oct 15, 2018
0ae91ae
Automatically include validation in updater
Oct 15, 2018
dd5c2b0
Merge branch 'master' into signedupdates
earlephilhower Oct 26, 2018
68f703e
Merge branch 'master' into signedupdates
earlephilhower Nov 5, 2018
9823290
Add documentation on signing process
Nov 5, 2018
3de43d6
Update documentation formatting
earlephilhower Nov 6, 2018
3f1013e
Merge branch 'master' into signedupdates
earlephilhower Nov 9, 2018
b3b7477
Move to new BearSSL:: namespace for classes
Nov 9, 2018
2b4016e
Merge branch 'master' into signedupdates
earlephilhower Nov 19, 2018
31b22fb
Move 2 strings into PROGMEM
earlephilhower Nov 29, 2018
b3ed528
Merge branch 'master' into signedupdates
earlephilhower Nov 29, 2018
4164bce
Add openssl return code error checking
earlephilhower Nov 29, 2018
5b2a882
Merge branch 'signedupdates' of https://github.com/earlephilhower/Ard…
earlephilhower Nov 29, 2018
e530a8a
Completely silence normal unsigned builds
earlephilhower Nov 29, 2018
5b243e2
Move debug strings to PMEM
earlephilhower Nov 29, 2018
f1eca3f
Merge branch 'master' into signedupdates
earlephilhower Nov 30, 2018
30e9d9b
Fix prebuild numbering, typo in docs
Nov 30, 2018
15ca564
Warn about Windows incompatibility in build and docs
earlephilhower Nov 30, 2018
d61a8ff
Merge branch 'master' into signedupdates
earlephilhower Dec 1, 2018
f9d340c
Merge branch 'master' into signedupdates
devyte Dec 3, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 94 additions & 19 deletions cores/esp8266/Updater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@

//#define DEBUG_UPDATER Serial

#include <Updater_Signing.h>
#ifndef ARDUINO_SIGNING
#define ARDUINO_SIGNING 0
#endif

#if ARDUINO_SIGNING
#include "../../libraries/ESP8266WiFi/src/BearSSLHelpers.h"
static BearSSL::PublicKey signPubKey(signing_pubkey);
static BearSSL::HashSHA256 hash;
static BearSSL::SigningVerifier sign(&signPubKey);
#endif

extern "C" {
#include "c_types.h"
#include "spi_flash.h"
Expand All @@ -23,7 +35,12 @@ UpdaterClass::UpdaterClass()
, _startAddress(0)
, _currentAddress(0)
, _command(U_FLASH)
, _hash(nullptr)
, _verify(nullptr)
{
#if ARDUINO_SIGNING
installSignature(&hash, &sign);
#endif
}

void UpdaterClass::_reset() {
Expand Down Expand Up @@ -96,9 +113,9 @@ bool UpdaterClass::begin(size_t size, int command, int ledPin, uint8_t ledOn) {
updateStartAddress = (updateEndAddress > roundedSize)? (updateEndAddress - roundedSize) : 0;

#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("[begin] roundedSize: 0x%08zX (%zd)\n", roundedSize, roundedSize);
DEBUG_UPDATER.printf("[begin] updateEndAddress: 0x%08zX (%zd)\n", updateEndAddress, updateEndAddress);
DEBUG_UPDATER.printf("[begin] currentSketchSize: 0x%08zX (%zd)\n", currentSketchSize, currentSketchSize);
DEBUG_UPDATER.printf_P(PSTR("[begin] roundedSize: 0x%08zX (%zd)\n"), roundedSize, roundedSize);
DEBUG_UPDATER.printf_P(PSTR("[begin] updateEndAddress: 0x%08zX (%zd)\n"), updateEndAddress, updateEndAddress);
DEBUG_UPDATER.printf_P(PSTR("[begin] currentSketchSize: 0x%08zX (%zd)\n"), currentSketchSize, currentSketchSize);
#endif

//make sure that the size of both sketches is less than the total space (updateEndAddress)
Expand Down Expand Up @@ -131,12 +148,14 @@ bool UpdaterClass::begin(size_t size, int command, int ledPin, uint8_t ledOn) {
_command = command;

#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("[begin] _startAddress: 0x%08X (%d)\n", _startAddress, _startAddress);
DEBUG_UPDATER.printf("[begin] _currentAddress: 0x%08X (%d)\n", _currentAddress, _currentAddress);
DEBUG_UPDATER.printf("[begin] _size: 0x%08zX (%zd)\n", _size, _size);
DEBUG_UPDATER.printf_P(PSTR("[begin] _startAddress: 0x%08X (%d)\n"), _startAddress, _startAddress);
DEBUG_UPDATER.printf_P(PSTR("[begin] _currentAddress: 0x%08X (%d)\n"), _currentAddress, _currentAddress);
DEBUG_UPDATER.printf_P(PSTR("[begin] _size: 0x%08zX (%zd)\n"), _size, _size);
#endif

_md5.begin();
if (!_verify) {
_md5.begin();
}
return true;
}

Expand All @@ -159,7 +178,7 @@ bool UpdaterClass::end(bool evenIfRemaining){

if(hasError() || (!isFinished() && !evenIfRemaining)){
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("premature end: res:%u, pos:%zu/%zu\n", getError(), progress(), _size);
DEBUG_UPDATER.printf_P(PSTR("premature end: res:%u, pos:%zu/%zu\n"), getError(), progress(), _size);
#endif

_reset();
Expand All @@ -173,15 +192,68 @@ bool UpdaterClass::end(bool evenIfRemaining){
_size = progress();
}

_md5.calculate();
if(_target_md5.length()) {
if(strcasecmp(_target_md5.c_str(), _md5.toString().c_str()) != 0){
uint32_t sigLen = 0;
if (_verify) {
ESP.flashRead(_startAddress + _size - sizeof(uint32_t), &sigLen, sizeof(uint32_t));
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf_P(PSTR("[Updater] sigLen: %d\n"), sigLen);
#endif
if (sigLen != _verify->length()) {
_setError(UPDATE_ERROR_SIGN);
_reset();
return false;
}

int binSize = _size - sigLen - sizeof(uint32_t) /* The siglen word */;
_hash->begin();
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf_P(PSTR("[Updater] Adjusted binsize: %d\n"), binSize);
#endif
// Calculate the MD5 and hash using proper size
uint8_t buff[128];
for(int i = 0; i < binSize; i += sizeof(buff)) {
ESP.flashRead(_startAddress + i, (uint32_t *)buff, sizeof(buff));
size_t read = std::min((int)sizeof(buff), binSize - i);
_hash->add(buff, read);
}
_hash->end();
#ifdef DEBUG_UPDATER
unsigned char *ret = (unsigned char *)_hash->hash();
DEBUG_UPDATER.printf_P(PSTR("[Updater] Computed Hash:"));
for (int i=0; i<_hash->len(); i++) DEBUG_UPDATER.printf(" %02x", ret[i]);
DEBUG_UPDATER.printf("\n");
#endif
uint8_t *sig = (uint8_t*)malloc(sigLen);
if (!sig) {
_setError(UPDATE_ERROR_SIGN);
_reset();
return false;
}
ESP.flashRead(_startAddress + binSize, (uint32_t *)sig, sigLen);
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf_P(PSTR("[Updater] Received Signature:"));
for (size_t i=0; i<sigLen; i++) {
DEBUG_UPDATER.printf(" %02x", sig[i]);
}
DEBUG_UPDATER.printf("\n");
#endif
if (!_verify->verify(_hash, (void *)sig, sigLen)) {
_setError(UPDATE_ERROR_SIGN);
_reset();
return false;
}
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf_P(PSTR("[Updater] Signature matches\n"));
#endif
} else if (_target_md5.length()) {
_md5.calculate();
if (strcasecmp(_target_md5.c_str(), _md5.toString().c_str())) {
_setError(UPDATE_ERROR_MD5);
_reset();
return false;
}
#ifdef DEBUG_UPDATER
else DEBUG_UPDATER.printf("MD5 Success: %s\n", _target_md5.c_str());
else DEBUG_UPDATER.printf_P(PSTR("MD5 Success: %s\n"), _target_md5.c_str());
#endif
}

Expand All @@ -199,10 +271,10 @@ bool UpdaterClass::end(bool evenIfRemaining){
eboot_command_write(&ebcmd);

#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("Staged: address:0x%08X, size:0x%08zX\n", _startAddress, _size);
DEBUG_UPDATER.printf_P(PSTR("Staged: address:0x%08X, size:0x%08zX\n"), _startAddress, _size);
}
else if (_command == U_SPIFFS) {
DEBUG_UPDATER.printf("SPIFFS: address:0x%08X, size:0x%08zX\n", _startAddress, _size);
DEBUG_UPDATER.printf_P(PSTR("SPIFFS: address:0x%08X, size:0x%08zX\n"), _startAddress, _size);
#endif
}

Expand All @@ -229,12 +301,12 @@ bool UpdaterClass::_writeBuffer(){
if (_currentAddress == _startAddress + FLASH_MODE_PAGE) {
flashMode = ESP.getFlashChipMode();
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("Header: 0x%1X %1X %1X %1X\n", _buffer[0], _buffer[1], _buffer[2], _buffer[3]);
DEBUG_UPDATER.printf_P(PSTR("Header: 0x%1X %1X %1X %1X\n"), _buffer[0], _buffer[1], _buffer[2], _buffer[3]);
#endif
bufferFlashMode = ESP.magicFlashChipMode(_buffer[FLASH_MODE_OFFSET]);
if (bufferFlashMode != flashMode) {
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("Set flash mode from 0x%1X to 0x%1X\n", bufferFlashMode, flashMode);
DEBUG_UPDATER.printf_P(PSTR("Set flash mode from 0x%1X to 0x%1X\n"), bufferFlashMode, flashMode);
#endif

_buffer[FLASH_MODE_OFFSET] = flashMode;
Expand Down Expand Up @@ -262,7 +334,9 @@ bool UpdaterClass::_writeBuffer(){
_setError(UPDATE_ERROR_WRITE);
return false;
}
_md5.add(_buffer, _bufferLen);
if (!_verify) {
_md5.add(_buffer, _bufferLen);
}
_currentAddress += _bufferLen;
_bufferLen = 0;
return true;
Expand Down Expand Up @@ -426,8 +500,9 @@ void UpdaterClass::printError(Print &out){
} else if(_error == UPDATE_ERROR_STREAM){
out.println(F("Stream Read Timeout"));
} else if(_error == UPDATE_ERROR_MD5){
//out.println(F("MD5 Check Failed"));
out.printf("MD5 Failed: expected:%s, calculated:%s\n", _target_md5.c_str(), _md5.toString().c_str());
out.printf_P(PSTR("MD5 Failed: expected:%s, calculated:%s\n"), _target_md5.c_str(), _md5.toString().c_str());
} else if(_error == UPDATE_ERROR_SIGN){
out.println(F("Signature verification failed"));
} else if(_error == UPDATE_ERROR_FLASH_CONFIG){
out.printf_P(PSTR("Flash config wrong real: %d IDE: %d\n"), ESP.getFlashChipRealSize(), ESP.getFlashChipSize());
} else if(_error == UPDATE_ERROR_NEW_FLASH_CONFIG){
Expand Down
26 changes: 26 additions & 0 deletions cores/esp8266/Updater.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#define UPDATE_ERROR_NEW_FLASH_CONFIG (9)
#define UPDATE_ERROR_MAGIC_BYTE (10)
#define UPDATE_ERROR_BOOTSTRAP (11)
#define UPDATE_ERROR_SIGN (12)

#define U_FLASH 0
#define U_SPIFFS 100
Expand All @@ -28,9 +29,30 @@
#endif
#endif

// Abstract class to implement whatever signing hash desired
class UpdaterHashClass {
public:
virtual void begin() = 0;
virtual void add(const void *data, uint32_t len) = 0;
virtual void end() = 0;
virtual int len() = 0;
virtual const void *hash() = 0;
};

// Abstract class to implement a signature verifier
class UpdaterVerifyClass {
public:
virtual uint32_t length() = 0; // How many bytes of signature are expected
virtual bool verify(UpdaterHashClass *hash, const void *signature, uint32_t signatureLen) = 0; // Verify, return "true" on success
};

class UpdaterClass {
public:
UpdaterClass();

/* Optionally add a cryptographic signature verification hash and method */
void installSignature(UpdaterHashClass *hash, UpdaterVerifyClass *verify) { _hash = hash; _verify = verify; }

/*
Call this to check the space needed for the update
Will return false if there is not enough space
Expand Down Expand Up @@ -165,6 +187,10 @@ class UpdaterClass {

int _ledPin;
uint8_t _ledOn;

// Optional signed binary verification
UpdaterHashClass *_hash;
UpdaterVerifyClass *_verify;
};

extern UpdaterClass Update;
Expand Down
3 changes: 3 additions & 0 deletions cores/esp8266/Updater_Signing.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file will be overridden when automatic signing is used.
// By default, no signing.
#define ARDUINO_SIGNING 0
101 changes: 95 additions & 6 deletions doc/ota_updates/readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ Arduino IDE option is intended primarily for software development phase. The two

In any case, the first firmware upload has to be done over a serial port. If the OTA routines are correctly implemented in a sketch, then all subsequent uploads may be done over the air.

There is no imposed security on OTA process from being hacked. It is up to developer to ensure that updates are allowed only from legitimate / trusted sources. Once the update is complete, the module restarts, and the new code is executed. The developer should ensure that the application running on the module is shut down and restarted in a safe manner. Chapters below provide additional information regarding security and safety of OTA process.
By default there is no imposed security on OTA process. It is up to developer to ensure that updates are allowed only from legitimate / trusted sources. Once the update is complete, the module restarts, and the new code is executed. The developer should ensure that the application running on the module is shut down and restarted in a safe manner. Chapters below provide additional information regarding security and safety of OTA process.

Security
~~~~~~~~
Security Disclaimer
~~~~~~~~~~~~~~~~~~~

No guarantees as to the level of security provided for your application by the following methods is implied. Please refer to the GNU LGPL license associated for this project for full disclaimers. If you do find security weaknesses, please don't hesitate to contact the maintainers or supply pull requests with fixes. The MD5 verification and password protection schemes are already known as supplying a very weak level of security.

Basic Security
~~~~~~~~~~~~~~

Module has to be exposed wirelessly to get it updated with a new sketch. That poses chances of module being violently hacked and loaded with some other code. To reduce likelihood of being hacked consider protecting your uploads with a password, selecting certain OTA port, etc.
The module has to be exposed wirelessly to get it updated with a new sketch. That poses chances of module being violently hacked and loaded with some other code. To reduce likelihood of being hacked consider protecting your uploads with a password, selecting certain OTA port, etc.

Check functionality provided with `ArduinoOTA <https://github.com/esp8266/Arduino/tree/master/libraries/ArduinoOTA>`__ library that may improve security:

Expand All @@ -36,6 +41,90 @@ Certain protection functionality is already built in and do not require any addi

Make your own risk analysis and depending on application decide what library functions to implement. If required, consider implementation of other means of protection from being hacked, e.g. exposing module for uploads only according to specific schedule, trigger OTA only be user pressing dedicated “Update” button wired to ESP, etc.

Advanced Security - Signed Updates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

While the above password-based security will dissuade casual hacking attempts, it is not highly secure. For applications where a higher level of security is needed, cryptographically signed OTA updates can be required. It uses SHA256 hashing in place of MD5 (which is known to be cryptographically broken) and RSA-2048 bit level encryption to guarantee only the holder of a cryptographic private key can generate code accepted by the OTA update mechanisms.

These are updates whose compiled binary are signed with a private key (held by the developer) and verified with a public key (stored in the application and available for all to see). The signing process computes a hash of the binary code, encrypts the hash with the developer's private key, and appends this encrypted hash to the binary that is uploaded (via OTA, web, or HTTP server). If the code is modified or replaced in any way by anyone by the developer with the key, the hash will not match and the ESP8266 will reject the upload and not accept it.

Cryptographic signing only protects against tampering of binaries delivered OTA. If someone has physical access they will always be able to flash the device over the serial port. Signing also does not encrypt anything but the hash (so that it can't be modified), so this does not provide protection for code inside the device. Again, if a user has physical access they can read out your program.

**Securing your private key is paramount. The same private/public keypair needs to be used to sign binaries as the original upload. Loss of the private key associated with a binary means that you will not be able to OTA to update any of your devices in the field. Alternatively, if the private key is copied, then the copy can be used to sign binaries which will be accepted.**

Signed Binary Format
^^^^^^^^^^^^^^^^^^^^

The format of a signed binary is compatible with the standard binary format, and can be uploaded to a non-signed ESP8266 via serial or OTA without any conditions. Note, however, that once an unsigned OTA app is overwritten by this signed version, further updates will require signing.

As shown below, the signed hash is appended to the unsigned binary followed by the total length of the signed hash (i.e. if the signed hash was 64 bytes, then this uint32 will contain 64). This format allows for extensibility (such as adding in a CA-based validation scheme allowing multiple signing keys all based off of a trust anchor), and pull requests are always welcome.

.. code::

NORMAL-BINARY <SIGNED HASH> <uint32 LENGTH-OF-SIGNING-DATA-INCLUDING-THIS-32-BITS>

Signed Binary Prequisites
^^^^^^^^^^^^^^^^^^^^^^^^^

OpenSSL is required to run the standard signing steps, and should be available on any UNIX-like or Windows system. As usual, the latest stable version of OpenSSL is recommended.

Signing requires the generation of an RSA-2048 key (other bit lengths are supported as well, but 2048 is a good selection today) using any appropriate tool. The following lines will generate a new public/private keypair. Run them in the sketch directory:

.. code:: bash

openssl genrsa -out private.key 2048
openssl rsa -in private.key -outform PEM -pubout -out public.key

Automatic Signing -- Only available on Linux and Mac
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The simplest way of implementing signing is to use the automatic mode, which is only possible on Linux and Mac presently due to missing tools under Windows. This mode uses the IDE to configure the source code to enable sigining verification with a given public key, and signs binaries as part of the standard build process using a given public key.

To enable this mode, just include `private.key` and `public.key` in the sketch `.ino` directory. The IDE will call a helper script (`tools/signing.py`) before the build begins to create a header to enable key validation using the given public key, and after the build process to actually do the signing, generating a `sketch.bin.signed` file. When OTA is enabled (ArduinoOTA, Web, or HTTP) the binary will only accept signed updates automatically.

When the signing process starts, the message:

.. code::

Enabling binary signing

Will appear in the IDE window before a compile is launched, and at the completion of the build the signed binary file well be displayed in the IDE build window as:

.. code::

Signed binary: /full/path/to/sketch.bin.signed

If you receive either of the following messages in the IDE window, the signing was not completed and you will need to verify the `public.key` and `private.key`:

.. code::

Not enabling binary signing
... or ...
Not signing the generated binary

Manual Signing Binaries
^^^^^^^^^^^^^^^^^^^^^^^

Users may also manually sign executables and require the OTA process to verify their signature. In the main code, before enabling any update methods, add the call:

.. code:: cpp

<in globals>
BearSSL::PublicKey signPubKey( ... key contents ... );
BearSSL::HashSHA256 hash;
BearSSL::SigningVerifier sign( &signPubKey );
...
<in setup()>
Update.installSignature( &hash, &sign );

The above snipped creates a BearSSL public key, a SHA256 hash verifier, and tells the Update object to use them to validate any updates it receives from any method.

Compile the sketch normally and, once a `.bin` file is available, sign it using the signer script:

.. code:: bash

<ESP8266ArduioPath>/tools/signing.py --mode sign --privatekey <path-to-private.key> --bin <path-to-unsigned-bin> --out <path-to-signed-binary>

Safety
~~~~~~

Expand All @@ -52,8 +141,8 @@ The following functions are provided with `ArduinoOTA <https://github.com/esp826
void onProgress(OTA_CALLBACK_PROGRESS(fn));
void onError(OTA_CALLBACK_ERROR (fn));

Basic Requirements
~~~~~~~~~~~~~~~~~~
OTA Basic Requirements
~~~~~~~~~~~~~~~~~~~~~~

Flash chip size should be able to hold the old sketch (currently running) and the new sketch (OTA) at the same time.

Expand Down
Loading