Verschlüsselte Requests mit dem Arduino & Letsencrypt

Wie heißt es in einem alten Sprichwort so schön: “Das ‘S’ in IoT steht für Sicherheit”. Durchsucht man das Internet nach Tutorials, wie man vom Arudino aus eine verschlüsselte Verbindungen aufbaut, findet man zwar einige Anleitungen, aber diese waren für mich alle ungenügend:

  1. Viele Anleitungen gehen den einfachen Weg und setzen ihren Client einfach auf insecure:
WiFiClientSecure client;
client.setInsecure();
client.connect("https://api.openweathermap.org", 443);
Code language: JavaScript (javascript)

Dieser Ansatz ist wahrscheinlich besser, als ein unverschlüsselter Request, da die übertragenen Daten verschlüsselt sind. Was aber fehlt, ist die Verifizierung des Zertifikates, also des Beweises, dass man mit dem gegenüber spricht, mit dem man glaubt zu sprechen. Bei einem Man-in-the-Middle-Angriff kann ein Bösewicht die übertragenen Daten potenziell mitlesen.

2. Andere Anleitungen gehen einen Schritt weiter und verifizieren den SHA-1 Fingerprint des Zertifikats der Gegenseite:

WiFiClientSecure client;

const char* fingerprint = "EE:AA:58:6D:4F:1F:42:F4:18:5B:7F:B0:F2:0A:4C:DD:97:47:7D:99";
const char* host = "api.openweathermap.org";

client.connect(host, 443);

if (client.verify(fingerprint, host)) {
  Serial.println("Certificate fingerprint ok.");
} else {
  Serial.println("Certificate fingerprint NOT ok.");
}
Code language: JavaScript (javascript)

Dieser Ansatz ist sicherer als der erste, da man mit dem Vergleich des Fingerprints verifiziert hat, dass die Stelle gegenüber das Zertifikat verwendet, welchem man glaubt. Diese Herangehensweise hat allerdings einen gravierenden Nachteil, welcher ihn für mich unbrauchbar macht: Letsencrypt-Zertifikate haben eine verhältnismäßig geringe Lebensdauer. Nach nur 90 Tagen läuft die Gültigkeit des Zertifikats ab und ein neues muss ausgestellt werden. In diesem Zuge ändert sich natürlich der Fingerprint und stimmt nicht mehr mit dem Fingerprint überein, welcher als festes Literal im Quellcode einkompiliert ist. Man müsste seine Applikation also alle drei Monate neu kompilieren und auf den Arudino aufspielen, was vollkommen unpraktikabel ist.

Wie funktioniert das eigentlich in einem Webbrowser?

Schickt ein Webbrowser eine Anfrage an einen Webserver, so antwortet dieser mit einer Zertifikatskette (eine Aneinanderreihung des individuellen Webserver-Zertifikats bis hin zum Root-Zertifikat). Der Client kann diese Zertifikatskette anschließend auswerten und beurteilen, ob er dem Webserver vertraut. Hierbei gleicht der Browser das Root-Zertifkat mit einer lokalen Kopie des Root-Zertifikats ab. Die Root-Zertifikate sind in einigen Browsern fest hinterlegt, andere nutzen die im Betriebssystem verfügbaren Zertifikate. Vertraut der Webserver dem Root-Zertifikat, kann eine vertraute und verschlüsselte Verbindung aufgebaut werden. Somit ist nicht nur gewährleistet, dass die Verbindung verschlüsselt und somit abhörsicher ist, sondern auch dass die Gegenstelle diejenige ist, für die man sie hält.

Weiterführende Informationen am Beispiel von Ubuntu siehe https://wiki.ubuntuusers.de/CA/

Die All-in-One Lösung

Die naheliegendste Herangehensweise wäre nun, dass man es einem Webbrowser nachmacht. Man könnte alle gängigen Root-Zertifikate (inklusive des der Internet Security Research Group, also Letsencrypt) auf dem Arduino hinterlegen und somit mit den meisten Webservern kommunizieren. Hierfür könnte man die Zertifikate im permanenten Speicher des Arduino hinterlegen. Im vorliegen Anwendungsfall soll allerdings nur mit einem HTTPS-Endfpunkt kommuniziert werden, der in jedem Falle ein Letsencrypt-Zertifikat verwendet. Somit wäre das Bereitstellen sämtliche Root-Zertifikate unnötig. Deshalb ist diese Lösung nicht Gegenstand dieser Anleitung. Möchte man allerdings mit Webservern kommunizeren, die zur Zeit der Kompilierung nicht bekannt sind, ist diese Lösung sinnvoll.

Die leichtgewichtige Lösung mit dem Letsencrypt Root-Zertifikat

Was die C++ Anwendung benötigt, um sichere Requests aufzubauen, ist das letsencrypt Root-Zertifikat, welches man sich erfreulicherweise auf der Seite von letsencrypt einfach im pem-Format herunterladen kann. Neben dem pem-Format gibt es das Zertifikat auch noch im der oder txt Format. Dabei handelt es sich um ein 4096 Bit RSA Zertifikat der Organisation ISRG, der Internet Security Research Group.

Die Library WiFiClientSecure von Espressif stellt sämtliche benötigte Funktionalität zur Verfügung.

Das Zertifikat wird als String in eine Variable gespeichert und daraus ein cert Objekt instanziiert, welches anschließen dem WiFiClientSecure zur Validierung zur Verfügung steht:

const char cert_XSRG_ROOT_X1[] PROGMEM = R"CERT(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)CERT";

X509List cert(cert_XSRG_ROOT_X1);
WiFiClientSecure client;Code language: PHP (php)

Anschließend wird ein HTTPClient erzeugt, welcher das client Objekt nutzt:

void sendToApi(String endpoint, String data) {
    HTTPClient http;
    String url = "https://api.mywebserver.de" + endpoint;
    Serial.println("HTTPS POST to " + url);
    if (http.begin(client, url))
    {
        http.addHeader("Content-Type", "application/json");
        http.POST(data);
        http.end();
    }
    else
    {
        Serial.println("Connection attempt to " + url + " was not successful");
    }
}Code language: JavaScript (javascript)

Eine Voraussetzung für die erfolgreiche Validierung des Zertifikats und somit den Aufbau einer gesicherten Verbindung ist, dass beide Seiten der Kommunikation, also der Arduino und der Webserver, eine möglichst gleiche Zeit eingestellt haben. Hierzu habe ich eine Methode implementiert, welche die Zeit des Arduino per NTP synchronisiert:

void syncTime() {
    configTime(3 * 3600, 0, "fritz.box", "pool.ntp.org", "time.nist.gov");

    Serial.print("Performing NTP time synchronization: ");
    time_t now = time(nullptr);
    while (now < 8 * 3600 * 2)
    {
        delay(500);
        Serial.print(".");
        now = time(nullptr);
    }
    Serial.println("");
    struct tm timeinfo;
    gmtime_r(&now, &timeinfo);
    Serial.print("Current time: ");
    Serial.print(asctime(&timeinfo));

    Serial.printf("Using certificate: %s\n", cert_XSRG_ROOT_X1);
    client.setTrustAnchors(&cert);

    Serial.print("Connecting to ");
    Serial.println(host);

    if (!client.connect(host, port))
    {
        Serial.println("Connection failed");
        return;
    }
}
Code language: PHP (php)

Entscheidend ist die Zeile 19. Hier wird auf dem client Objekt die Methode setTrustAnchors aufgerufen und das Zertifikats-Objekt übergeben. Das Zertifikat wird als vertrauensvoll deklariert.

Hier ein komplettes Code-Beispiel, welches eine sichere und vertrauenswürdige Verbindung über HTTPS aufbaut und ein JSON per HTTP POST sendet:

#include <Arduino.h>
#include <SoftwareSerial.h>
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>

const char *ssid = "WIFI-SSID";
const char *pass = "WIFI-Password";
const char *host = "api.myhost.de";
const uint16_t port = 443;

bool logValues = true;

const char cert_XSRG_ROOT_X1[] PROGMEM = R"CERT(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)CERT";

X509List cert(cert_XSRG_ROOT_X1);
WiFiClientSecure client;

void connectToWifi() {
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, pass);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
}

void syncTime() {
    Serial.print("Performing NTP time synchronization: ");
    configTime(3 * 3600, 0, "fritz.box", "pool.ntp.org", "time.nist.gov");

    time_t now = time(nullptr);
    while (now < 8 * 3600 * 2)
    {
        delay(500);
        Serial.print(".");
        now = time(nullptr);
    }
    Serial.println("");
    struct tm timeinfo;
    gmtime_r(&now, &timeinfo);
    Serial.print("Current time: ");
    Serial.print(asctime(&timeinfo));

    Serial.printf("Using certificate: %s\n", cert_XSRG_ROOT_X1);
    client.setTrustAnchors(&cert);

    Serial.print("Connecting to ");
    Serial.println(host);

    if (!client.connect(host, port))
    {
        Serial.println("Connection failed");
        return;
    }
}

void sendToApi(String endpoint, String data) {
    HTTPClient http;
    String url = host + endpoint;
    Serial.println("HTTP POST to " + url);
    if (http.begin(client, url))
    {
        http.addHeader("Content-Type", "application/json")
        http.POST(data);
        http.end();
    }
    else
    {
        Serial.println("Connection attempt to " + url + " was not successful");
    }
}

void setup()
{
    Serial.begin(9600);
    connectToWifi();
    syncTime();
}

void loop()
{
    syncTime();

    StaticJsonDocument<512> doc;
    doc["room_name"]    = "Arbeitszimmer";
    doc["temperature"]  = 23.5;

    String data;
    serializeJson(doc, data);

    sendToApi("/api/temperature", data);
    delay(1000 * 60 * 60);
}
Code language: C++ (cpp)

Leave a Reply

Your email address will not be published. Required fields are marked *