Raspberry Pi und NodeMCU kommunizieren über eine TLS verschlüsselte MQTT Verbindung

In unserem Haus habe ich in mehreren Zimmern zur Temperatur- und Raumfeuchtigkeitsmessung einen NodeMCU mit angeschlossenem DHT22 Sensor eingesetzt. Der Sensor kommuniziert über verschlüsseltes MQTT mit einem Mosquitto-Server.

Der Mosquitto Dienst läuft auf einem Raspberry Pi 3+, zusammen mit einer Influx-DB-, NodeRed- und Grafana-Installation. Als schlankes Betriebssystem habe ich mich für DietPi entschieden. DietPi ist sehr auf minimalen CPU- und Ramverbrauch ausgelegt und läßt sich einfach administrieren. Vielen Software-Pakete stehen installationsfertig zur Verfügung, so auch Mosquitto, Influx, NodeRed und Grafana.

Um den MQTT-Datenverkehr zwischen den Nodes und dem Host sicherer zu machen (und um mich auch mal wieder mit der gängigen OpenSSL Verschlüsselung auseinander zu setzen), habe ich die Datenübertragungen mit TLS verschlüsselt. Wie ich dabei genau vorgegangen bin, beschreibe ich in diesem Blogbeitrag.

Die MQTT-Clients bekommen alle ein „tactic“-Gehäuse, um zum Einen die benötigten Kabel zu fixieren und zum Anderen den NodeMCU-Microprozessor vor „Umwelteinflüssen“ zu schützen.. Der DHT22-Sensor ist ausserhalb des Gehäuses mit Heisskleber befestigt, um neutrale Bedingungen zum Messen zu erhalten.

Zu Beginn der Arbeit stellt sich die Frage, woher man die Zertifikate für die TLS-Verschlüsselung beziehen möchte. Grundsätzlich gibt es dafür verschiedene Möglichkeiten. Zum Beispiel kann man sich das CA-Zertifikat bei einer öffentlichen Zertifizierungsstelle (CA) kaufen (z. B. bei der Bundesdruckerei) oder z. B. bei Let’s Encrypt kostenlos beziehen. Der Unterschied zwischen beiden Möglichkeiten liegt im Wesentlichen im Vertrauen, welches man der jeweiligen Zertifizierungsstelle entgegenbringt.

Eine weitere Möglichkeit ist es, sich das Zertifikat selber auszustellen und zu unterschreiben. Oder aber man erstellt sich eine eigene Zertifizierungsstelle, mit der man sich Serverschlüssel und Zertifikate für alle dafür geeigneten Dienste (Webserver, MQTT-Server etc.) ausstellen kann. Welchen Weg man dabei wählt, hängt auch damit zusammen, ob man seinen Server ins Internet stellen möchte oder nicht. In meinem Fall ist der Plan, meine Server nur intern (ohne Verbindung ins INET) zu verwenden. Daher reicht in meinem Fall ein selbst signiertes Zertifikat, welches ich mit meiner eigenen Root-CA erstelle.

Ich benutze zum Erstellen der Zertifikate das Tool OpenSSL.

a) Generieren des privaten CA Schlüssels (CA Private Key). Als Besonderheit verwende ich hierbei mal keinen RSA-Schlüssel, sondern einen „elliptical curve key„.

openssl ecparam -name secp521r1 -genkey -noout -out ca.pem

b) als nächstes erzeuge ich mir ein selbstsigniertes Zertifikat (Self-Signed CA Cert)

Ich benutze dafür meinen gerade erzeugten privaten CA-Key. Zuerst wird dafür eine openssl.conf Datei angelegt, deren Inhalt in den CA-Key mit einfließt. Eine gute Beschreibung der „distinguished_name“ findet man hier.

root@DietPi:/etc/mosquitto# more openssl.conf
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName = DE
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName = Schleswig-Holstein
localityName = Locality Name (eg, city)
localityName = Arnis
organizationalUnitName  = Organizational Unit Name (eg, section)
organizationalUnitName  = Loobster IT
organizationalUnitName_default  = Domain Control Validated
commonName = 192.168.1.x
commonName_default = your.server.primary.url.or.ip.address
commonName_max  = 64

[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = 192.168.1.x

Hier wird das Zertifikat erzeugt (ich stelle mir das Zertifikat für eine Dauer von 10 Jahren aus) :

openssl req -new -x509 -days 3650 -key ca.pem -config openssl.conf -out ca.crt -sha256

c) Jetzt wird der MQTT Broker Private Key erzeugt

Jetzt bin ich soweit, auch dem MQTT-Broker einen eigenen privaten Schlüssel zu erstellen. Auch hier sind wieder die „elliptical curve“ im Spiel.

openssl ecparam -name secp521r1 -genkey -noout -out mqtt-serv.pem

d) Generieren einer MQTT-Broker-Zertifikatsignierungsanforderung (CSR)

Da ich für die verschlüsselten MQTT-Verbindungen ein eigenes MQTT-Zertifikat benötige, generiere ich jetzt im nächsten Schritt auf Basis des MQTT-Brokers Privat Keys einen sogenannten Zertifikats-Request.

openssl req -new -out mqtt-serv.csr -key mqtt-serv.pem -config openssl.conf -sha256

e) Generieren eines CA-signierten MQTT-Broker-Zertifikats

Dieser gerade erzeugte MQTT-Broker Zertifikats-Request wird jetzt in Verbindung mit der oben erstellten Root-CA benutzt, um für den MQTT-Broker ein eigenes Broker-Zertifikat zu erstellen, welches von der Root-CA signiert ist.

openssl x509 -req -days 3650 -in mqtt-serv.csr -CA /path/to/ca.crt -CAkey /path/to/ca.pem -CAcreateserial -out mqtt-serv.crt -extensions v3_req -extfile openssl.conf -sha256

f) Extrahieren unseres MQTT Broker Cert Fingerabdrucks (wird im Sketch zur Validierung des MQTT-Broker-Zertifikats benötigt)

openssl x509 -noout -in mqtt-serv.crt -fingerprint

2) Anpassungen der mosquitto.conf Datei

root@DietPi:/etc/mosquitto# more mosquitto.conf
# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example

port 1883

pid_file /var/run/mosquitto.pid
persistence true
persistence_location /var/lib/mosquitto/

log_dest file /var/log/mosquitto/mosquitto.log

#####################################
listener 8883
cafile /etc/mosquitto/ca.crt
certfile /etc/mosquitto/mqtt-serv.crt
keyfile /etc/mosquitto/mqtt-serv.pem
#####################################

#allow_anonymous true
#password_file /etc/mosquitto/passwd

include_dir /etc/mosquitto/conf.d

3) Sketch für NodeMCU

// Dieser Sketch liest einen DHT22 Sensor aus und übermittelt den Payload über verschlüsseltes MQTT an den MQTT-Broker.
// Die SSL-Routinen wurden zum Teil aus anderen Sketchen übernommen

#include <DHT.h>
#include <DHT_U.h>

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

#define INTERVAL 5 // Ableseintervall in Minuten
#define FAKTOR 60000 // 60.000 ms = 1 min

// DHT
#define DHTPIN D5
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE, 11);

String temperatur = "100";
String luftfeuchte = "100";

#define ESP8266_LED (5)
#define RELAY_SIGNAL_PIN (4)
#define SERIAL_DEBUG
#define TLS_DEBUG

char mqttSubjectTemperatur[] = "sensors/dht/temperatur/ssl-test";
char mqttSubjectFeuchte[] = "sensors/dht/luftfeuchte/ssl-test";

String getFormattedFloat(float x, uint8_t precision) {
  char buffer[10];
  dtostrf(x, 7, precision, buffer);
  return (buffer);
}

/* Certificate Authority info */
/* CA Cert in PEM format */
const char caCert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
jOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Yj2wD9X
7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c40bnj4JXMJTENAt74
72tTstwCruZmuc91mgC+RyRm9TxcwvztEOFDkWeKpVCrheILGH03zBqb93p9nTIa
bUMscnzM7kn/YrwmITRDaYQ2eF0jagBwYFK4EEACOhgYkDgYYABAFzgTPkco/CM1
Rm8Tnlq0l+rnFSst74VHqoj2wD9XOz7W8iFX1C0J4KsQy2N6FAccymFSst74VHqF
rheILGH03zBqb93p9nTIa72tTc91mgC+RyRm9TxcwvztEOFDkWeKpVCstwCruZmu
qoj2wD9XOzco/CM1hGbPfS2UEKITVxTth9OZ+4rplg==
-----END CERTIFICATE-----
)EOF";

/* MQTT broker cert SHA1 fingerprint, used to validate connection to right server */
//SHA1
const uint8_t mqttCertFingerprint[] = {0x30,0xC9,0xF3,0xB0,0xE9,0xED,0x99,0x1E,0xCA,0x4A,0x22,0x98,0x30,0xD4,0x27,0xF9,0xED,0x99,0x23,0xF3};
                                        
/* Other globals */
X509List caCertX509(caCert);        /* X.509 parsed CA Cert */
WiFiClientSecure espClient;         /* Secure client connection class, as opposed to WiFiClient */
PubSubClient mqttClient(espClient); /* MQTT Client connection */
String clientId = "DHT22_TLS-Test-"; /* MQTT client ID (will add random hex suffix during setup) */


int status = WL_IDLE_STATUS;
unsigned long lastMessage;



#ifdef TLS_DEBUG
/* verifytls()
 *  ssl-ssl-test WiFiClientSecure connection using supplied cert and fingerprint
 */
bool verifytls() {
  bool success = false;
    
#ifdef SERIAL_DEBUG
  Serial.print("Verifying TLS connection to ");
  Serial.println("192.168.1.x");
#endif

  success = espClient.connect("192.168.1.x", 8883);

#ifdef SERIAL_DEBUG
  if (success) {
    Serial.println("Connection complete, valid cert, valid fingerprint.");
  }
  else {
    Serial.println("Connection failed!");
  }
#endif

  return (success);
}
#endif

void reconnect() {
  /* Loop until we're reconnected */
  while (!mqttClient.connected()) {
#ifdef SERIAL_DEBUG
    Serial.print("Attempting MQTT broker connection...");
#endif
    /* Attempt to connect */
    if (mqttClient.connect(clientId.c_str())) {
#ifdef SERIAL_DEBUG
      Serial.println("connected");
#endif
      /* Once connected, resubscribe */
      mqttClient.subscribe("haus/#");      
    } 
    else {
#ifdef SERIAL_DEBUG
      Serial.print("Failed, rc=");
      Serial.print(mqttClient.state());
      Serial.println(". Trying again in 5 seconds...");
#endif
      /* Wait 5 seconds between retries */
      delay(5000);
    }
  }
}

void setup() 
{
  /* Set board's GPIO pins as outputs */
  pinMode(RELAY_SIGNAL_PIN, OUTPUT);
  pinMode(ESP8266_LED, OUTPUT);

#ifdef SERIAL_DEBUG
  /* Initialize serial output for debug */
  Serial.setDebugOutput(true);
  Serial.begin(19200, SERIAL_8N1);
  Serial.println();
#endif

dht.begin();
  delay(10);
  
  /*  Connect to local WiFi access point */
Serial.println("Connecting to AccessPoint ...");
  // attempt to connect to WiFi network
  // feste IP einstellen - muss nicht sein
  IPAddress ip(192, 168, 1, x);
  IPAddress gateway(192, 168, 1, 1);
  IPAddress subnet(255, 255, 255, 0);
  IPAddress dns(192, 168, 1, 1);
  WiFi.config(ip, dns, gateway, subnet); // auf feste IP einstellen

  
  WiFi.mode(WIFI_STA);
  WiFi.begin("SSID", "passwort");

#ifdef SERIAL_DEBUG
  Serial.print("Connecting");
#endif
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
#ifdef SERIAL_DEBUG
    Serial.print(".");
#endif
  }
#ifdef SERIAL_DEBUG
  /* When WiFi connection is complete, debug log connection info */
  Serial.println();
  Serial.print("Connected, IP address: ");
  Serial.println(WiFi.localIP());
#endif

  /* Configure secure client connection */
  espClient.setTrustAnchors(&caCertX509);         /* Load CA cert into trust store */
  espClient.allowSelfSignedCerts();               /* Enable self-signed cert support */
  espClient.setFingerprint(mqttCertFingerprint);  /* Load SHA1 mqtt cert fingerprint for connection validation */
  
                                             
#ifdef TLS_DEBUG
  /* Call verifytls to verify connection can be done securely and validated - this is optional but was useful during debug */
  verifytls();
#endif

  /* Configure MQTT Broker settings */
  mqttClient.setServer("192.168.1.x",8883);
  //mqttClient.setCallback(subCallback);
lastMessage = millis() + (INTERVAL * FAKTOR);
  /* Add random hex client ID suffix once during each reboot */
  clientId += String(random(0xffff), HEX); 
}

void loop() 
{
  /* Main loop. Attempt to re-connect to MQTT broker if connection drops, and service the mqttClient task. */
  
  if(!mqttClient.connected()) {
    reconnect();
  }

  if ( millis() - lastMessage > INTERVAL * FAKTOR ) { 
    getData();
    sendMQTTMessage(mqttSubjectFeuchte, luftfeuchte);
    sendMQTTMessage(mqttSubjectTemperatur, temperatur);
    lastMessage = millis();
  }

  mqttClient.loop();
}

void getData() {
  delay(2000);
  Serial.println("Collecting temperature data.");
  // Reading temperature or humidity takes about 250 milliseconds!
  float h = dht.readHumidity();
  //Serial.println(h);
  // Read temperature as Celsius (the default)
  float t = dht.readTemperature();
  //Serial.println(t);
  // Check if any reads failed and exit early (to try again).
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    mqttClient.publish( "sensors/dht/temperatur/ssl-test", "no Data" );
    mqttClient.publish( "sensors/dht/luftfeuchte/ssl-test   ", "no Data" );
    return;
  }

  Serial.print("Humidity: ");
  Serial.print(h);
  Serial.print(" %\t");
  Serial.print("Temperature: ");
  Serial.print(t);
  Serial.println(" *C ");
  temperatur = getFormattedFloat(t, 1);
  luftfeuchte = getFormattedFloat(h, 1);
}

void sendMQTTMessage(char mqttSubject[], String message) {
  char payload[100];
  message.toCharArray( payload, 100 );
  mqttClient.publish( mqttSubject, payload );
  Serial.println( payload );   
}

4) Ausgabe in der Console beim Start des Sketche

⸮!⸮SK⸮oB⸮⸮ai⸮qE1⸮⸮R⸮aEa⸮iq⸮⸮ 
Connecting to AccessPoint ... 
Connecting. 
Connected, IP address: 192.168.1.x 
Verifying TLS connection to 192.168.1.x 
Connection complete, valid cert, valid fingerprint. 
Attempting MQTT broker connection...connected 

5) Test der Funktionsfähigkeit der SSL-Konfiguration

root@DietPi:~# mosquitto_sub -h 127.0.0.1 -p 8883 --cafile /etc/mosquitto/ca.crt -t sensors/dht11/+/werkstatt -v

sensors/dht/luftfeuchte/werkstatt    59.0
sensors/dht/temperatur/werkstatt     7.0
sensors/dht/luftfeuchte/werkstatt    59.1
sensors/dht/temperatur/werkstatt     7.1
sensors/dht/luftfeuchte/werkstatt    59.1
sensors/dht/temperatur/werkstatt     7.1

6) Node-Red Config auf SSL (Port 8883) anpassen

Wenn die Auswertung der per MQTT abonnierten Daten mit Hilfe des Programms Node-Red erledigt werden soll, dann ist bei der MQTT Quelle der Port auf 8883 (nicht 1883) zu ändern.

Weiterhin sind noch die Pfade der entsprechenden Zertifikate einzutragen.

H. Rode – Nucleon e. V.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Ich akzeptiere

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.