Stand jetzt ist die Kommunikation zwischen ESP und Update Server verschlüsselt und das Auslesen von Paketen, um unser Update Artefakt zu erhalten ist nicht mehr möglich. Gleichzeitig können wir dank der Signatur des Artefaktes sicher sein, dass wir nur unseren und keinen fremden böswilligen Code ausführen. Allerdings führen wir keinerlei Client Authentifikation durch, d.h. ein fremder Client könnte unseren Update Server Anfragen und würde unser Update Artefakt einfach zugesendet bekommen. Um dies zu verhindern, nehmen wir uns ein Beispiel am MQTT Broker bei welchem wir in einem früheren Blogbeitrag schon Client Authentifikation durch signierte Zertifikate konfiguriert hatten.

Der Update Server sollte nur Anfragen bedienen, welche ein gültiges Zertifikat haben und nachweisen können, dass sie Besitzer dieses Zertifikat sind. Dies heißt ein jeder ESP braucht einen private Key, sowie ein Zertifikat, welches von unserer CA ausgestellt wurde. Wir müssen nun also:

  • Es müssen Client Zertifikate erstellt werden: Wir können als Proof-Of-Concept wiedereinmal die Keys und Zertifikate aus dem MQTT Blogbeitrag nutzen.
  • Der Update Server muss abgeändert werden, so dass alle Anfragen validiert werden: NodeJS in Verbindung mit ExpressJS und dem HTTPS Modul macht uns das ziemlich einfach.
  • Der ESP braucht sowohl privaten Client Key als auch das Zertifikat. Beides muss bei einer Anfrage nutzbar sein: Wie schon das Validieren des Update Server durch das Einbinden des CA Zertifikat, nutzen wir hier wieder den SecureWiFiClient, welcher passende Methoden bereitstellt.

Den Update Server muss gesagt werden, das Anfragen sich auch identifizieren müssen. Als Basis nutzen wir den Update Server, welchen wir im letzten Blogbeitrag zu verschlüsselten Updates vorgestellt haben.

const fs = require('fs');
const https = require('https');
const express = require('express');
const path = require('path');
- const app = express();

+ const clientAuthMiddleware = () => (req, res, next) => {
+   if (!req.client.authorized) {
+     return res.status(401).send('Invalid client certificate authentication.');
+   }
+   return next();
+ };
  
+ const app = express();
+ app.use(clientAuthMiddleware());

var PORT = 8080;

// Without middleware
app.get('/firmware.bin.signed', function(_req, res, next){

    var options = {
        root: path.join(__dirname)
    };
      
    var fileName = 'firmware.bin.signed';

    res.sendFile(fileName, options, function (err) {
        if (err) {
            next(err);
        } else {
            console.log('Sent:', fileName);
        }
    });
});
  
https
  .createServer(
    {
      // ...
      cert: fs.readFileSync('../../tls-mqtt/server/server.crt'),
      key: fs.readFileSync('../../tls-mqtt/server/server.key'),
      + ca: fs.readFileSync('../../tls-mqtt/ca.crt'),
      + requestCert: true,
      + rejectUnauthorized: false,
      // ...
    },
    app
  )
  .listen(PORT);

Die Konfiguration des Servers wird erweitert durch das Zertifikat der CA, sowie Einstellungen zu den Anforderungen von Anfragen. Zudem wird die Logik um eine Funktion erweitert, welche jede Anfrage auf die Authentifikation des Clients befragt, um sicher gehen zu können, dass nur Anfragen welche auch ein Zertifikat ausgestellt von unserer CA, besitzen, auch ein Update Artefakt erhalten.

Um zu testen, ob unser Update Server funktioniert, und auch nur verifizierte Anfragen bedient werden können wir curl in Verbindung mit den Zertifikaten aus dem MQTT-Beispiel einen GET Request an den laufenden Server schicken.

$ cd ${TWO_WAY_UPDATE_SERVER}
$ npm start
---
$ cd ${TLS_MQTT_EXAMPLE}
$ cd client
$ curl https://192.168.178.123:8080/firmware.bin.signed --cacert ../../tls-mqtt/ca.crt --cert client.crt --key client.key > test.bin.signed
$ xxd test.bin.signed | tail -n 2
0003fe60: 6664 01bb 55b7 6f52 20ce 04e0 d28f eee8  fd..U.oR .......
0003fe70: 0001 0000                                ....

Wie aus dem Beitrag zu signierten Updates bekannt, endet die Update Datei mit der Hexzahl 0001 000, wir können also vermuten, dass ein Update Artefakt zurückgegeben wurde. Nun sollten wir noch überprüfen, ob unautorisierte Anfragen auch bedient werden.

$ curl https://192.168.178.123:8080/firmware.bin.signed --cacert ../../tls-mqtt/ca.crt > should-not-work.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    42  100    42    0     0  10725      0 --:--:-- --:--:-- --:--:-- 14000
$ cat should-not-work.txt
Invalid client certificate authentication.%

Somit können wir sicher gehen, dass nur Anfragen ein Update Artefakt erhalten, welche ein von uns ausgestelltes Zertifikat vorweisen können und den zugehörigen privaten Key besitzen.

Nun müssen wir unseren ESP als Client auch mit diesen ausstatten, sowie den WiFiClient konfigurieren, dass beide bei einer Anfrage auch benutzt werden können. Sowohl Zertifikat als auch Key werden wir als char Array ähnlich dem öffentlich Key. Da jedoch der private Key eigentlich nicht mit in das Repository sollte wäre es angenehm diesen mit in die Datei secret.h zu legen. Jedoch habe ich nicht herausgefunden, wie man Präprozessor Statements mit einem char Array komibiniert der mehrere Newlines hat, daher fügen wir lieber noch eine Datei secret.cpp hinzu, in welcher die Secrets initialisiert werden, während sie in der secret.h als externe Variablen deklariert werden. Dies hat den Vorteil, dass wir sie in der main.cpp nutzen können, aber nicht in ihr deklarieren müssen.

$ cd ${OTA_PROJECT_SIGNING_SECURE_TWO_WAY]
$ cat src/secret.h
#ifndef SECRET_H
#define SECRET_H
#define APSSID "Your APPSID"
#define APPSK "YOUR PASSWORD"
extern const char cert[]; 
extern const char private_key[];
#endif
$ touch src/secret.cpp
$ cat src/secret.cpp
#include "secret.h"
#include <Arduino.h>

const char cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIDYDCCAkgCFAby5WtzERmyqC5tDuNvHyzZnwV1MA0GCSqGSIb3DQEBCwUAMHwx
CzAJBgNVBAYTAkRFMQswCQYDVQQIDAJCVzELMAkGA1UEBwwCS0ExDDAKBgNVBAoM
A2lvdDEMMAoGA1UECwwDaW90MRkwFwYDVQQDDBBteWRvbWFpbm5hbWUueHl6MRww
GgYJKoZIhvcNAQkBFg15b3VyQG1haWwuY29tMB4XDTIxMDYwNzE2NDYyNVoXDTIy
MDYwMjE2NDYyNVowXTELMAkGA1UEBhMCREUxCzAJBgNVBAgMAkJXMQswCQYDVQQH
DAJLQTEMMAoGA1UECgwDaW90MQwwCgYDVQQLDANpb3QxGDAWBgNVBAMMDzE5Mi4x
NjguMTc4LjEyMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMV2CyW0
lxuv+CZFbKUp2Qzs0rjafUminGbeFw54+IRHhJJSm4ddLmObm29dUYErtUOLDK+i
j7reWbA4xKyyP/3mYyRqVQnmLzqe7rCPe1ah3R/JhF1O95P9jKDlZfxAYw6jhAEG
0EYCEhoOMfnNRjcXE/rgJMAYzyXxqudISFKBGA8ci0nyFOSlQd5WL9zUBGt1L967
zLhTdOOGsVuMZ3sV2fvYRflMFjwMVoO+B81h66THJXo28FQ3eQju5dL3c+uxu2Cm
l33/wAJI6Sus8iM1N2270FDhiRkkdiNiQnxGMVuTp+I7hhF8lAtPGKxZwR4DU/zR
Gw8AYejc+XV4OmMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAC1TT5P6sLg2CmGlZ
8IG3AIBg/8Tnhon7nONiF/0pi/8uod093WHKGdHUW4KD+dyMk5vo32qwx7cT2oSz
LvtDibhF01Nivk633g2WHk31KKcvRZ9t72pxIdU/ZibnYNR4034m/3F050t6peI9
MfhIF3+QVkhjkyoLdX2gz1caeVo2u9aR0t7gnMbJByNYbvjXJu0qE94QfHnSSxMd
q6jVoZBd5eqYJKqZ2I+230CNbqJKsy0SEa0XnypkHK0MFOePjow70izN/345UreI
47CM224yaMJDMmjsbJRaPoOTeFSnkTl2DC2PDII60QSLk+1zRWwLhSpgqKz5ERw1
VhxzNA==
-----END CERTIFICATE-----
)EOF";

const char private_key[] PROGMEM = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxXYLJbSXG6/4JkVspSnZDOzSuNp9SaKcZt4XDnj4hEeEklKb
h10uY5ubb11RgSu1Q4sMr6KPut5ZsDjErLI//eZjJGpVCeYvOp7usI97VqHdH8mE
XU73k/2MoOVl/EBjDqOEAQbQRgISGg4x+c1GNxcT+uAkwBjPJfGq50hIUoEYDxyL
SfIU5KVB3lYv3NQEa3Uv3rvMuFN044axW4xnexXZ+9hF+UwWPAxWg74HzWHrpMcl
ejbwVDd5CO7l0vdz67G7YKaXff/AAkjpK6zyIzU3bbvQUOGJGSR2I2JCfEYxW5On
4juGEXyUC08YrFnBHgNT/NEbDwBh6Nz5dXg6YwIDAQABAoIBAQCc3go9ChqBhGo+
/SgyjexAuGMvN2QQ+34EuqrWdIP5kldfZwDnqL8Roqz468m0NGTsI6sQXLSSX9Ig
jPixuWnc6woYA+FK2/LpPxmgalwxlqV0e0MMXY+Rofv2VkeO/hezqXNU3XTYKQz4
Zg6NxOXjHhJhW2/Wq97k5fg/hMzvvPlT/sCvPLa2yDLzK5a14BO9TJRIoFwnFObH
I58OaoINqiP1ZUag0wHys8QiWQ9DV1aQfQZ4nD1U55nxj3ebZ4JEJwFPvbQfpmrc
OULfLgWXvBITBgHVucwOeusqFC0t6sNZeconyVAP4AKyqLRyphlO3TaPJIL4yqFu
AcyZgWEBAoGBAOhR3JC45dweWtfOnG10pvJPO7E/XDStcnXPQ1WpiKAxvVdkUsy7
0l8z8tnFixu3bexn11m8HnZwPTTf/eGVltHVOwhsRa2m8VLcd8uRGn0BlW0j9caT
wBh8H3SAZKvN8Nim9RYZFg9y1xV/WrGk7ZXQ02p/TGAeSSuvCHREoaZFAoGBANmW
lQAphxQsuj0VZbBOGiRgCePZLL9/3Azx6StNRF2/IxVlmQD/q5RjQLYQqVD2IxK1
1qpHcb126dRSRuG6UEffMKdbSXM3EENH8L8TpQAyIx/3hWySgvwjM0wJRJgWOHeu
fz1w8AGWsz/U6Bg42wnXYevKK205qCKYEFZ7ghyHAoGASSfF+biPgTH5zy0bawgp
rfGvTVAzW88mVNywSmA5SqB/C+7md9vJEfuMxHCFLnQjZCcK1BH90bPkQisqigeN
14N6tFtL0bPZGAuemXaWzbha8mQ5d499FPi4+vmXOdZ+uepREOVTYgf6nKVezMOv
oNaCTG3LghTnW58hXWNjN7UCgYAm6HVWZRahdeoLmDLp1t132bCLDL+isrNfoTZn
ptZtyQr1/YfhlNZSn3jn1YzhTFIzO07afhIJpiTj8Z55KL7IS1HA62Lz9kmzLj8P
e+zKXyzGv5UdOAmyGn1GwHWCmJ6aUBqymupf7lm5NVIXWrtYRCpfZnRjgKbfIL/z
Jvy6KwKBgQCB7qtuxIxf6hjINBnJnZ8J4HBr4eteuCUZcnyMpD+cRY1UlkEiiWM0
Ma5jbaqddUFkABTraAf8SMxZ3TLFhte29wp8yCVibtv/5rbN8VPCevRy/5BOX5gD
/X36xOFF78qAL42jtYpK90KOKAyfoJ5H7HIRFcoMwTeWyluSaDjIlw==
-----END RSA PRIVATE KEY-----
)EOF";

Es wird die lokale Header Datei secret.h genutzt, da hier die Variablen initialisiert wurden. Zudem brauchen wir <Arduino.h> für das PROGMEM Keyword.

Mit char Array des Keys und Zertifikates können wir nun diese in der Logik des ESPs zum WiFiClient “hinzufügen”, um dann valide Anfragen an unseren Update Server stellen zu können. Als Basis für unseren Code ist das Beispiel zu den verschlüsselten und signierten OTA Updates.

...
#if MANUAL_SIGNING
BearSSL::PublicKey *signPubKey = nullptr;
BearSSL::HashSHA256 *hash;
BearSSL::SigningVerifier *sign;
#endif

+ BearSSL::X509List *list = nullptr;
+ BearSSL::PrivateKey *key = nullptr;
+
+ BearSSL::WiFiClientSecure client;

#include <time.h>

#include <FS.h>
#include <LittleFS.h>
...
  struct tm timeinfo;
  gmtime_r(&now, &timeinfo);
  Serial.print(F("Current time: "));
  Serial.print(asctime(&timeinfo));
}

+ void client_tls_setup() {
+   list = new BearSSL::X509List(cert);
+   key = new BearSSL::PrivateKey(private_key);
+   client.setClientRSACert(list, key);
+   delay(1500);
+ }

void setup() {
...
  int numCerts = certStore.initCertStore(LittleFS, PSTR("/certs.idx"), PSTR("/certs.ar"));
  Serial.print(F("Number of CA certs read: "));
  Serial.println(numCerts);
  if (numCerts == 0) {
    Serial.println(F("No certs found. Did you run certs-from-mozill.py and upload the LittleFS directory before running?"));
    return; // Can't connect to anything w/o certs!
  }
  + client_tls_setup();
}
...
void loop() {
  // wait for WiFi connection
  if ((WiFiMulti.run() == WL_CONNECTED)) {

    setClock();

    - BearSSL::WiFiClientSecure client;
...

Mit diesen Änderungen können wir nun den ESP flashen:

$ pio run --target upload; pio device monitor -b 115200

Processing nodemcuv2 (platform: espressif8266; board: nodemcuv2; framework: arduino)
...
Compressed 455120 bytes to 332607...
Writing at 0x00000000... (4 %)
...
Writing at 0x00050000... (100 %)
Wrote 455120 bytes (332607 compressed) at 0x00000000 in 29.3 seconds (effective 124.5 kbit/s)...
Hash of data verified.
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
[SETUP] WAIT 3...
[SETUP] WAIT 2...
[SETUP] WAIT 1...
Exists1
Number of CA certs read: 1
Waiting for NTP time sync:
Current time: Thu Jun 10 22:41:21 2021
MFLN supported: no
Local WiFi192.168.178.121
CALLBACK:  HTTP update process started
...
CALLBACK:  HTTP update process finished

 ets Jan  8 2013,rst cause:2, boot mode:(3,6)

load 0x4010f000, len 3584, room 16
tail 0
chksum 0xb0
csum 0xb0
v2843a5ac
@cp:0
ld

Tatsächlich ist es uns nun möglich das Update Artefakt über den ESP anzufragen und dieses als neue Logik auszuführen. Somit haben wir nun verschlüsselte und signierte Updates, welche nur dann möglich sind, wenn Client als auch Update Server valide Zertifikate besitzen. Allen Code gibt es so wie immer hier auf Github zu finden. Im nächsten Blogbeitrag erweitern, wie das Update um das eigentliche Element welches als Trigger dient. Hierfür werden wir uns anschauen, wie wir auch eine Verbindung mit einem MQTT Broker aufbauen können, wobei wir auch hier Verschlüsselung der ausgetauschten Nachrichten, als auch Validation von Broker und Client anstreben.

Update Sicherheit: Verifizierte ESP Clients

Schreibe einen Kommentar