Skip to content

Commit d8acfff

Browse files
Add cryptographically signed update support (#5213)
Using a pluggable architecture, allow updates delivered via the Update class to be verified as signed by a certificate. By using plugins, avoid pulling either axTLS or BearSSL into normal builds. A signature is appended to a binary image, followed by the size of the signature as a 32-bit int. The updater takes a verification function and checks this signature using whatever method it chooses, and if it fails the update is not applied. A SHA256 hash class is presently implemented for the signing hash (since MD5 is a busted algorithm). A BearSSLPublicKey based verifier is implemented for RSA keys. The application only needs the Public Key, while to sign you can use OpenSSL and your private key (which should never leave your control or be deployed on any endpoints). An example using automatic signing is included. Update the docs to show the signing steps and how to use it in the automatic and manual modes. Also remove one debugging line from the signing tool. Saves ~600 bytes when in debug mode by moving strings to PMEM Windows can't run the signing script, nor does it normally have OpenSSL installed. When trying to build an automatically signed binary, warn and don't run the python.
1 parent e5d50a6 commit d8acfff

File tree

11 files changed

+532
-27
lines changed

11 files changed

+532
-27
lines changed

Diff for: cores/esp8266/Updater.cpp

+94-19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66

77
//#define DEBUG_UPDATER Serial
88

9+
#include <Updater_Signing.h>
10+
#ifndef ARDUINO_SIGNING
11+
#define ARDUINO_SIGNING 0
12+
#endif
13+
14+
#if ARDUINO_SIGNING
15+
#include "../../libraries/ESP8266WiFi/src/BearSSLHelpers.h"
16+
static BearSSL::PublicKey signPubKey(signing_pubkey);
17+
static BearSSL::HashSHA256 hash;
18+
static BearSSL::SigningVerifier sign(&signPubKey);
19+
#endif
20+
921
extern "C" {
1022
#include "c_types.h"
1123
#include "spi_flash.h"
@@ -23,7 +35,12 @@ UpdaterClass::UpdaterClass()
2335
, _startAddress(0)
2436
, _currentAddress(0)
2537
, _command(U_FLASH)
38+
, _hash(nullptr)
39+
, _verify(nullptr)
2640
{
41+
#if ARDUINO_SIGNING
42+
installSignature(&hash, &sign);
43+
#endif
2744
}
2845

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

98115
#ifdef DEBUG_UPDATER
99-
DEBUG_UPDATER.printf("[begin] roundedSize: 0x%08zX (%zd)\n", roundedSize, roundedSize);
100-
DEBUG_UPDATER.printf("[begin] updateEndAddress: 0x%08zX (%zd)\n", updateEndAddress, updateEndAddress);
101-
DEBUG_UPDATER.printf("[begin] currentSketchSize: 0x%08zX (%zd)\n", currentSketchSize, currentSketchSize);
116+
DEBUG_UPDATER.printf_P(PSTR("[begin] roundedSize: 0x%08zX (%zd)\n"), roundedSize, roundedSize);
117+
DEBUG_UPDATER.printf_P(PSTR("[begin] updateEndAddress: 0x%08zX (%zd)\n"), updateEndAddress, updateEndAddress);
118+
DEBUG_UPDATER.printf_P(PSTR("[begin] currentSketchSize: 0x%08zX (%zd)\n"), currentSketchSize, currentSketchSize);
102119
#endif
103120

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

133150
#ifdef DEBUG_UPDATER
134-
DEBUG_UPDATER.printf("[begin] _startAddress: 0x%08X (%d)\n", _startAddress, _startAddress);
135-
DEBUG_UPDATER.printf("[begin] _currentAddress: 0x%08X (%d)\n", _currentAddress, _currentAddress);
136-
DEBUG_UPDATER.printf("[begin] _size: 0x%08zX (%zd)\n", _size, _size);
151+
DEBUG_UPDATER.printf_P(PSTR("[begin] _startAddress: 0x%08X (%d)\n"), _startAddress, _startAddress);
152+
DEBUG_UPDATER.printf_P(PSTR("[begin] _currentAddress: 0x%08X (%d)\n"), _currentAddress, _currentAddress);
153+
DEBUG_UPDATER.printf_P(PSTR("[begin] _size: 0x%08zX (%zd)\n"), _size, _size);
137154
#endif
138155

139-
_md5.begin();
156+
if (!_verify) {
157+
_md5.begin();
158+
}
140159
return true;
141160
}
142161

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

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

165184
_reset();
@@ -173,15 +192,68 @@ bool UpdaterClass::end(bool evenIfRemaining){
173192
_size = progress();
174193
}
175194

176-
_md5.calculate();
177-
if(_target_md5.length()) {
178-
if(strcasecmp(_target_md5.c_str(), _md5.toString().c_str()) != 0){
195+
uint32_t sigLen = 0;
196+
if (_verify) {
197+
ESP.flashRead(_startAddress + _size - sizeof(uint32_t), &sigLen, sizeof(uint32_t));
198+
#ifdef DEBUG_UPDATER
199+
DEBUG_UPDATER.printf_P(PSTR("[Updater] sigLen: %d\n"), sigLen);
200+
#endif
201+
if (sigLen != _verify->length()) {
202+
_setError(UPDATE_ERROR_SIGN);
203+
_reset();
204+
return false;
205+
}
206+
207+
int binSize = _size - sigLen - sizeof(uint32_t) /* The siglen word */;
208+
_hash->begin();
209+
#ifdef DEBUG_UPDATER
210+
DEBUG_UPDATER.printf_P(PSTR("[Updater] Adjusted binsize: %d\n"), binSize);
211+
#endif
212+
// Calculate the MD5 and hash using proper size
213+
uint8_t buff[128];
214+
for(int i = 0; i < binSize; i += sizeof(buff)) {
215+
ESP.flashRead(_startAddress + i, (uint32_t *)buff, sizeof(buff));
216+
size_t read = std::min((int)sizeof(buff), binSize - i);
217+
_hash->add(buff, read);
218+
}
219+
_hash->end();
220+
#ifdef DEBUG_UPDATER
221+
unsigned char *ret = (unsigned char *)_hash->hash();
222+
DEBUG_UPDATER.printf_P(PSTR("[Updater] Computed Hash:"));
223+
for (int i=0; i<_hash->len(); i++) DEBUG_UPDATER.printf(" %02x", ret[i]);
224+
DEBUG_UPDATER.printf("\n");
225+
#endif
226+
uint8_t *sig = (uint8_t*)malloc(sigLen);
227+
if (!sig) {
228+
_setError(UPDATE_ERROR_SIGN);
229+
_reset();
230+
return false;
231+
}
232+
ESP.flashRead(_startAddress + binSize, (uint32_t *)sig, sigLen);
233+
#ifdef DEBUG_UPDATER
234+
DEBUG_UPDATER.printf_P(PSTR("[Updater] Received Signature:"));
235+
for (size_t i=0; i<sigLen; i++) {
236+
DEBUG_UPDATER.printf(" %02x", sig[i]);
237+
}
238+
DEBUG_UPDATER.printf("\n");
239+
#endif
240+
if (!_verify->verify(_hash, (void *)sig, sigLen)) {
241+
_setError(UPDATE_ERROR_SIGN);
242+
_reset();
243+
return false;
244+
}
245+
#ifdef DEBUG_UPDATER
246+
DEBUG_UPDATER.printf_P(PSTR("[Updater] Signature matches\n"));
247+
#endif
248+
} else if (_target_md5.length()) {
249+
_md5.calculate();
250+
if (strcasecmp(_target_md5.c_str(), _md5.toString().c_str())) {
179251
_setError(UPDATE_ERROR_MD5);
180252
_reset();
181253
return false;
182254
}
183255
#ifdef DEBUG_UPDATER
184-
else DEBUG_UPDATER.printf("MD5 Success: %s\n", _target_md5.c_str());
256+
else DEBUG_UPDATER.printf_P(PSTR("MD5 Success: %s\n"), _target_md5.c_str());
185257
#endif
186258
}
187259

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

201273
#ifdef DEBUG_UPDATER
202-
DEBUG_UPDATER.printf("Staged: address:0x%08X, size:0x%08zX\n", _startAddress, _size);
274+
DEBUG_UPDATER.printf_P(PSTR("Staged: address:0x%08X, size:0x%08zX\n"), _startAddress, _size);
203275
}
204276
else if (_command == U_SPIFFS) {
205-
DEBUG_UPDATER.printf("SPIFFS: address:0x%08X, size:0x%08zX\n", _startAddress, _size);
277+
DEBUG_UPDATER.printf_P(PSTR("SPIFFS: address:0x%08X, size:0x%08zX\n"), _startAddress, _size);
206278
#endif
207279
}
208280

@@ -229,12 +301,12 @@ bool UpdaterClass::_writeBuffer(){
229301
if (_currentAddress == _startAddress + FLASH_MODE_PAGE) {
230302
flashMode = ESP.getFlashChipMode();
231303
#ifdef DEBUG_UPDATER
232-
DEBUG_UPDATER.printf("Header: 0x%1X %1X %1X %1X\n", _buffer[0], _buffer[1], _buffer[2], _buffer[3]);
304+
DEBUG_UPDATER.printf_P(PSTR("Header: 0x%1X %1X %1X %1X\n"), _buffer[0], _buffer[1], _buffer[2], _buffer[3]);
233305
#endif
234306
bufferFlashMode = ESP.magicFlashChipMode(_buffer[FLASH_MODE_OFFSET]);
235307
if (bufferFlashMode != flashMode) {
236308
#ifdef DEBUG_UPDATER
237-
DEBUG_UPDATER.printf("Set flash mode from 0x%1X to 0x%1X\n", bufferFlashMode, flashMode);
309+
DEBUG_UPDATER.printf_P(PSTR("Set flash mode from 0x%1X to 0x%1X\n"), bufferFlashMode, flashMode);
238310
#endif
239311

240312
_buffer[FLASH_MODE_OFFSET] = flashMode;
@@ -262,7 +334,9 @@ bool UpdaterClass::_writeBuffer(){
262334
_setError(UPDATE_ERROR_WRITE);
263335
return false;
264336
}
265-
_md5.add(_buffer, _bufferLen);
337+
if (!_verify) {
338+
_md5.add(_buffer, _bufferLen);
339+
}
266340
_currentAddress += _bufferLen;
267341
_bufferLen = 0;
268342
return true;
@@ -426,8 +500,9 @@ void UpdaterClass::printError(Print &out){
426500
} else if(_error == UPDATE_ERROR_STREAM){
427501
out.println(F("Stream Read Timeout"));
428502
} else if(_error == UPDATE_ERROR_MD5){
429-
//out.println(F("MD5 Check Failed"));
430-
out.printf("MD5 Failed: expected:%s, calculated:%s\n", _target_md5.c_str(), _md5.toString().c_str());
503+
out.printf_P(PSTR("MD5 Failed: expected:%s, calculated:%s\n"), _target_md5.c_str(), _md5.toString().c_str());
504+
} else if(_error == UPDATE_ERROR_SIGN){
505+
out.println(F("Signature verification failed"));
431506
} else if(_error == UPDATE_ERROR_FLASH_CONFIG){
432507
out.printf_P(PSTR("Flash config wrong real: %d IDE: %d\n"), ESP.getFlashChipRealSize(), ESP.getFlashChipSize());
433508
} else if(_error == UPDATE_ERROR_NEW_FLASH_CONFIG){

Diff for: cores/esp8266/Updater.h

+26
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#define UPDATE_ERROR_NEW_FLASH_CONFIG (9)
1818
#define UPDATE_ERROR_MAGIC_BYTE (10)
1919
#define UPDATE_ERROR_BOOTSTRAP (11)
20+
#define UPDATE_ERROR_SIGN (12)
2021

2122
#define U_FLASH 0
2223
#define U_SPIFFS 100
@@ -28,9 +29,30 @@
2829
#endif
2930
#endif
3031

32+
// Abstract class to implement whatever signing hash desired
33+
class UpdaterHashClass {
34+
public:
35+
virtual void begin() = 0;
36+
virtual void add(const void *data, uint32_t len) = 0;
37+
virtual void end() = 0;
38+
virtual int len() = 0;
39+
virtual const void *hash() = 0;
40+
};
41+
42+
// Abstract class to implement a signature verifier
43+
class UpdaterVerifyClass {
44+
public:
45+
virtual uint32_t length() = 0; // How many bytes of signature are expected
46+
virtual bool verify(UpdaterHashClass *hash, const void *signature, uint32_t signatureLen) = 0; // Verify, return "true" on success
47+
};
48+
3149
class UpdaterClass {
3250
public:
3351
UpdaterClass();
52+
53+
/* Optionally add a cryptographic signature verification hash and method */
54+
void installSignature(UpdaterHashClass *hash, UpdaterVerifyClass *verify) { _hash = hash; _verify = verify; }
55+
3456
/*
3557
Call this to check the space needed for the update
3658
Will return false if there is not enough space
@@ -165,6 +187,10 @@ class UpdaterClass {
165187

166188
int _ledPin;
167189
uint8_t _ledOn;
190+
191+
// Optional signed binary verification
192+
UpdaterHashClass *_hash;
193+
UpdaterVerifyClass *_verify;
168194
};
169195

170196
extern UpdaterClass Update;

Diff for: cores/esp8266/Updater_Signing.h

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This file will be overridden when automatic signing is used.
2+
// By default, no signing.
3+
#define ARDUINO_SIGNING 0

Diff for: doc/ota_updates/readme.rst

+95-6
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ Arduino IDE option is intended primarily for software development phase. The two
1717

1818
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.
1919

20-
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.
20+
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.
2121

22-
Security
23-
~~~~~~~~
22+
Security Disclaimer
23+
~~~~~~~~~~~~~~~~~~~
24+
25+
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.
26+
27+
Basic Security
28+
~~~~~~~~~~~~~~
2429

25-
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.
30+
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.
2631

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

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

3742
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.
3843

44+
Advanced Security - Signed Updates
45+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
46+
47+
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.
48+
49+
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.
50+
51+
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.
52+
53+
**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.**
54+
55+
Signed Binary Format
56+
^^^^^^^^^^^^^^^^^^^^
57+
58+
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.
59+
60+
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.
61+
62+
.. code::
63+
64+
NORMAL-BINARY <SIGNED HASH> <uint32 LENGTH-OF-SIGNING-DATA-INCLUDING-THIS-32-BITS>
65+
66+
Signed Binary Prequisites
67+
^^^^^^^^^^^^^^^^^^^^^^^^^
68+
69+
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.
70+
71+
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:
72+
73+
.. code:: bash
74+
75+
openssl genrsa -out private.key 2048
76+
openssl rsa -in private.key -outform PEM -pubout -out public.key
77+
78+
Automatic Signing -- Only available on Linux and Mac
79+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80+
81+
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.
82+
83+
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.
84+
85+
When the signing process starts, the message:
86+
87+
.. code::
88+
89+
Enabling binary signing
90+
91+
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:
92+
93+
.. code::
94+
95+
Signed binary: /full/path/to/sketch.bin.signed
96+
97+
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`:
98+
99+
.. code::
100+
101+
Not enabling binary signing
102+
... or ...
103+
Not signing the generated binary
104+
105+
Manual Signing Binaries
106+
^^^^^^^^^^^^^^^^^^^^^^^
107+
108+
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:
109+
110+
.. code:: cpp
111+
112+
<in globals>
113+
BearSSL::PublicKey signPubKey( ... key contents ... );
114+
BearSSL::HashSHA256 hash;
115+
BearSSL::SigningVerifier sign( &signPubKey );
116+
...
117+
<in setup()>
118+
Update.installSignature( &hash, &sign );
119+
120+
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.
121+
122+
Compile the sketch normally and, once a `.bin` file is available, sign it using the signer script:
123+
124+
.. code:: bash
125+
126+
<ESP8266ArduioPath>/tools/signing.py --mode sign --privatekey <path-to-private.key> --bin <path-to-unsigned-bin> --out <path-to-signed-binary>
127+
39128
Safety
40129
~~~~~~
41130

@@ -52,8 +141,8 @@ The following functions are provided with `ArduinoOTA <https://github.com/esp826
52141
void onProgress(OTA_CALLBACK_PROGRESS(fn));
53142
void onError(OTA_CALLBACK_ERROR (fn));
54143
55-
Basic Requirements
56-
~~~~~~~~~~~~~~~~~~
144+
OTA Basic Requirements
145+
~~~~~~~~~~~~~~~~~~~~~~
57146

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

0 commit comments

Comments
 (0)