ESP8266 MQTT gateway SSL connection



  • Re: ESP8266 MQTT gateway SSL connection

    Guys,

    I tried to adapt the referenced proposition to MySensors 2.3.2. Here is MyGatewayTransportMQTTClient.cpp:

    /*
    * The MySensors Arduino library handles the wireless radio link and protocol
    * between your home built sensors/actuators and HA controller of choice.
    * The sensors forms a self healing radio network with optional repeaters. Each
    * repeater and gateway builds a routing tables in EEPROM which keeps track of the
    * network topology allowing messages to be routed to nodes.
    *
    * Created by Henrik Ekblad <henrik.ekblad@mysensors.org>
    * Copyright (C) 2013-2019 Sensnology AB
    * Full contributor list: https://github.com/mysensors/MySensors/graphs/contributors
    *
    * Documentation: http://www.mysensors.org
    * Support Forum: http://forum.mysensors.org
    *
    * This program is free software; you can redistribute it and/or
    * modify it under the terms of the GNU General Public License
    * version 2 as published by the Free Software Foundation.
    */
    
    
    // Topic structure: MY_MQTT_PUBLISH_TOPIC_PREFIX/NODE-ID/SENSOR-ID/CMD-TYPE/ACK-FLAG/SUB-TYPE
    
    #include "MyGatewayTransport.h"
    
    // housekeeping, remove for 3.0.0
    #ifdef MY_ESP8266_SSID
    #warning MY_ESP8266_SSID is deprecated, use MY_WIFI_SSID instead!
    #define MY_WIFI_SSID MY_ESP8266_SSID
    #undef MY_ESP8266_SSID // cleanup
    #endif
    
    #ifdef MY_ESP8266_PASSWORD
    #warning MY_ESP8266_PASSWORD is deprecated, use MY_WIFI_PASSWORD instead!
    #define MY_WIFI_PASSWORD MY_ESP8266_PASSWORD
    #undef MY_ESP8266_PASSWORD // cleanup
    #endif
    
    #ifdef MY_ESP8266_BSSID
    #warning MY_ESP8266_BSSID is deprecated, use MY_WIFI_BSSID instead!
    #define MY_WIFI_BSSID MY_ESP8266_BSSID
    #undef MY_ESP8266_BSSID // cleanup
    #endif
    #ifdef MY_ESP8266_HOSTNAME
    #warning MY_ESP8266_HOSTNAME is deprecated, use MY_HOSTNAME instead!
    #define MY_HOSTNAME MY_ESP8266_HOSTNAME
    #undef MY_ESP8266_HOSTNAME // cleanup
    #endif
    
    #ifndef MY_MQTT_USER
    #define MY_MQTT_USER NULL
    #endif
    
    #ifndef MY_MQTT_PASSWORD
    #define MY_MQTT_PASSWORD NULL
    #endif
    
    #if defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
    #if !defined(MY_WIFI_SSID)
    #error ESP8266/ESP32 MQTT gateway: MY_WIFI_SSID not defined!
    #endif
    #endif
    
    #if defined MY_CONTROLLER_IP_ADDRESS
    #define _brokerIp IPAddress(MY_CONTROLLER_IP_ADDRESS)
    #endif
    
    #if defined(MY_IP_ADDRESS)
    #define _MQTT_clientIp IPAddress(MY_IP_ADDRESS)
    #if defined(MY_IP_GATEWAY_ADDRESS)
    #define _gatewayIp IPAddress(MY_IP_GATEWAY_ADDRESS)
    #elif defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
    // Assume the gateway will be the machine on the same network as the local IP
    // but with last octet being '1'
    #define _gatewayIp IPAddress(_MQTT_clientIp[0], _MQTT_clientIp[1], _MQTT_clientIp[2], 1)
    #endif /* End of MY_IP_GATEWAY_ADDRESS */
    
    #if defined(MY_IP_SUBNET_ADDRESS)
    #define _subnetIp IPAddress(MY_IP_SUBNET_ADDRESS)
    #elif defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
    #define _subnetIp IPAddress(255, 255, 255, 0)
    #endif /* End of MY_IP_SUBNET_ADDRESS */
    #endif /* End of MY_IP_ADDRESS */
    
    #if defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
    #define EthernetClient WiFiClient
    #elif defined(MY_GATEWAY_LINUX)
    // Nothing to do here
    #elif defined(MY_GATEWAY_ESP8266_SECURE)
    #define EthernetClient WiFiClientSecure
    #else
    uint8_t _MQTT_clientMAC[] = { MY_MAC_ADDRESS };
    #endif /* End of MY_GATEWAY_ESPxy */
    
    #if defined(MY_SSL_FINGERPRINT)
    const char* fingerprint = MY_SSL_FINGERPRINT;
    #endif
    
    
    #if defined(MY_GATEWAY_TINYGSM)
    #if defined(MY_GSM_RX) && defined(MY_GSM_TX)
    SoftwareSerial SerialAT(MY_GSM_RX, MY_GSM_TX);
    #endif
    
    static TinyGsm modem(SerialAT);
    static TinyGsmClient _MQTT_ethClient(modem);
    #if defined(MY_GSM_BAUDRATE)
    uint32_t rate = MY_GSM_BAUDRATE;
    #else /* Else part of MY_GSM_BAUDRATE */
    uint32_t rate = 0;
    #endif /* End of MY_GSM_BAUDRATE */
    #else /* Else part of MY_GATEWAY_TINYGSM */
    static EthernetClient _MQTT_ethClient;
    #endif /* End of MY_GATEWAY_TINYGSM */
    
    #if defined(MY_GATEWAY_ESP8266_SECURE)
    static PubSubClient _MQTT_client(_MQTT_ethClient, fingerprint);
    #else
    static PubSubClient _MQTT_client(_MQTT_ethClient);
    #endif
    
    static bool _MQTT_connecting = true;
    static bool _MQTT_available = false;
    static MyMessage _MQTT_msg;
    
    bool gatewayTransportSend(MyMessage &message)
    {
            if (!_MQTT_client.connected()) {
                    return false;
            }
            setIndication(INDICATION_GW_TX);
            char *topic = protocolMyMessage2MQTT(MY_MQTT_PUBLISH_TOPIC_PREFIX, message);
            GATEWAY_DEBUG(PSTR("GWT:TPS:TOPIC=%s,MSG SENT\n"), topic);
    #if defined(MY_MQTT_CLIENT_PUBLISH_RETAIN)
            const bool retain = message.getCommand() == C_SET ||
                                (message.getCommand() == C_INTERNAL && message.getType() == I_BATTERY_LEVEL);
    #else
            const bool retain = false;
    #endif /* End of MY_MQTT_CLIENT_PUBLISH_RETAIN */
            return _MQTT_client.publish(topic, message.getString(_convBuffer), retain);
    }
    
    void incomingMQTT(char *topic, uint8_t *payload, unsigned int length)
    {
            GATEWAY_DEBUG(PSTR("GWT:IMQ:TOPIC=%s, MSG RECEIVED\n"), topic);
            _MQTT_available = protocolMQTT2MyMessage(_MQTT_msg, topic, payload, length);
            setIndication(INDICATION_GW_RX);
    }
    
    bool reconnectMQTT(void)
    {
            GATEWAY_DEBUG(PSTR("GWT:RMQ:CONNECTING...\n"));
            // Attempt to connect
            if (_MQTT_client.connect(MY_MQTT_CLIENT_ID, MY_MQTT_USER, MY_MQTT_PASSWORD)) {
                    GATEWAY_DEBUG(PSTR("GWT:RMQ:OK\n"));
                    // Send presentation of locally attached sensors (and node if applicable)
                    presentNode();
                    // Once connected, publish subscribe
                    if (__builtin_constant_p(MY_MQTT_SUBSCRIBE_TOPIC_PREFIX)) {
                            // to save some memory
                            _MQTT_client.subscribe(MY_MQTT_SUBSCRIBE_TOPIC_PREFIX "/+/+/+/+/+");
                    } else {
                            char inTopic[strlen(MY_MQTT_SUBSCRIBE_TOPIC_PREFIX) + strlen("/+/+/+/+/+")];
                            (void)strncpy(inTopic, MY_MQTT_SUBSCRIBE_TOPIC_PREFIX, strlen(MY_MQTT_SUBSCRIBE_TOPIC_PREFIX) + 1);
                            (void)strcat(inTopic, "/+/+/+/+/+");
                            _MQTT_client.subscribe(inTopic);
                    }
    
                    return true;
            }
            delay(1000);
            GATEWAY_DEBUG(PSTR("!GWT:RMQ:FAIL\n"));
            return false;
    }
    
    bool gatewayTransportConnect(void)
    {
    #if defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
            if (WiFi.status() != WL_CONNECTED) {
                    GATEWAY_DEBUG(PSTR("GWT:TPC:CONNECTING...\n"));
                    delay(1000);
                    return false;
            }
            GATEWAY_DEBUG(PSTR("GWT:TPC:IP=%s\n"), WiFi.localIP().toString().c_str());
    #elif defined(MY_GATEWAY_ESP8266_SECURE)
            while (WiFi.status() != WL_CONNECTED) {
                    wait(500);
                    MY_SERIALDEVICE.print(F("."));
            }
            MY_SERIALDEVICE.print(F("IP: "));
            MY_SERIALDEVICE.println(WiFi.localIP());
    #elif defined(MY_GATEWAY_LINUX)
    #if defined(MY_IP_ADDRESS)
            _MQTT_ethClient.bind(_MQTT_clientIp);
    #endif /* End of MY_IP_ADDRESS */
    #elif defined(MY_GATEWAY_TINYGSM)
            GATEWAY_DEBUG(PSTR("GWT:TPC:IP=%s\n"), modem.getLocalIP().c_str());
    #else
    #if defined(MY_IP_ADDRESS)
            Ethernet.begin(_MQTT_clientMAC, _MQTT_clientIp);
    #else /* Else part of MY_IP_ADDRESS */
            // Get IP address from DHCP
            if (!Ethernet.begin(_MQTT_clientMAC)) {
                    GATEWAY_DEBUG(PSTR("!GWT:TPC:DHCP FAIL\n"));
                    _MQTT_connecting = false;
                    return false;
            }
    #endif /* End of MY_IP_ADDRESS */
            GATEWAY_DEBUG(PSTR("GWT:TPC:IP=%" PRIu8 ".%" PRIu8 ".%" PRIu8 ".%" PRIu8 "\n"),
                          Ethernet.localIP()[0],
                          Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]);
            // give the Ethernet interface a second to initialize
            delay(1000);
    #endif
            return true;
    }
    
    bool gatewayTransportInit(void)
    {
            _MQTT_connecting = true;
    
    #if defined(MY_GATEWAY_TINYGSM)
    
    #if !defined(MY_GSM_BAUDRATE)
            rate = TinyGsmAutoBaud(SerialAT);
    #endif /* End of MY_GSM_BAUDRATE */
    
            SerialAT.begin(rate);
            delay(3000);
    
            modem.restart();
    
    #if defined(MY_GSM_PIN) && !defined(TINY_GSM_MODEM_ESP8266)
            modem.simUnlock(MY_GSM_PIN);
    #endif /* End of MY_GSM_PIN */
    
    #ifndef TINY_GSM_MODEM_ESP8266
            if (!modem.waitForNetwork()) {
                    GATEWAY_DEBUG(PSTR("!GWT:TIN:ETH FAIL\n"));
                    while (true);
            }
            GATEWAY_DEBUG(PSTR("GWT:TIN:ETH OK\n"));
    
            if (!modem.gprsConnect(MY_GSM_APN, MY_GSM_USR, MY_GSM_PSW)) {
                    GATEWAY_DEBUG(PSTR("!GWT:TIN:ETH FAIL\n"));
                    while (true);
            }
            GATEWAY_DEBUG(PSTR("GWT:TIN:ETH OK\n"));
            delay(1000);
    #else /* Else part of TINY_GSM_MODEM_ESP8266 */
            if (!modem.networkConnect(MY_GSM_SSID, MY_GSM_PSW)) {
                    GATEWAY_DEBUG(PSTR("!GWT:TIN:ETH FAIL\n"));
                    while (true);
            }
            GATEWAY_DEBUG(PSTR("GWT:TIN:ETH OK\n"));
            delay(1000);
    #endif /* End of TINY_GSM_MODEM_ESP8266 */
    #endif /* End of MY_GATEWAY_TINYGSM */
    
    #if defined(MY_CONTROLLER_IP_ADDRESS)
            _MQTT_client.setServer(_brokerIp, MY_PORT);
    #else
            _MQTT_client.setServer(MY_CONTROLLER_URL_ADDRESS, MY_PORT);
    #endif /* End of MY_CONTROLLER_IP_ADDRESS */
    
            _MQTT_client.setCallback(incomingMQTT);
    
    #if defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
            // Turn off access point
            WiFi.mode(WIFI_STA);
    #if defined(MY_GATEWAY_ESP8266)
            WiFi.hostname(MY_HOSTNAME);
    #elif defined(MY_GATEWAY_ESP32)
            WiFi.setHostname(MY_HOSTNAME);
    #endif
    #if defined(MY_IP_ADDRESS)
            WiFi.config(_MQTT_clientIp, _gatewayIp, _subnetIp);
    #endif /* End of MY_IP_ADDRESS */
            (void)WiFi.begin(MY_WIFI_SSID, MY_WIFI_PASSWORD, 0, MY_WIFI_BSSID);
    #endif
    
            gatewayTransportConnect();
    
            _MQTT_connecting = false;
            return true;
    }
    
    bool gatewayTransportAvailable(void)
    {
            if (_MQTT_connecting) {
                    return false;
            }
    #if defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
            if (WiFi.status() != WL_CONNECTED) {
    #if defined(MY_GATEWAY_ESP32)
                    (void)gatewayTransportInit();
    #endif
                    return false;
            }
    #endif
    #endif
    #if defined(MY_IP_ADDRESS)
            WiFi.config(_MQTT_clientIp, _gatewayIp, _subnetIp);
    #endif /* End of MY_IP_ADDRESS */
            (void)WiFi.begin(MY_WIFI_SSID, MY_WIFI_PASSWORD, 0, MY_WIFI_BSSID);
    #endif
    
            gatewayTransportConnect();
    
            _MQTT_connecting = false;
            return true;
    }
    
    bool gatewayTransportAvailable(void)
    {
            if (_MQTT_connecting) {
                    return false;
            }
    #if defined(MY_GATEWAY_ESP8266) || defined(MY_GATEWAY_ESP32)
            if (WiFi.status() != WL_CONNECTED) {
    #if defined(MY_GATEWAY_ESP32)
                    (void)gatewayTransportInit();
    #endif
                    return false;
            }
    #endif
            if (!_MQTT_client.connected()) {
                    //reinitialise client
                    if (gatewayTransportConnect()) {
                            reconnectMQTT();
                    }
                    return false;
            }
            _MQTT_client.loop();
            return _MQTT_available;
    }
    
    MyMessage & gatewayTransportReceive(void)
    {
            // Return the last parsed message
            _MQTT_available = false;
            return _MQTT_msg;
    }
    
    

    Then I have adapted the ESP8266 MQTT Gateway sample:

    // Enable debug prints to serial monitor
    #define MY_DEBUG
    
    // Use a bit lower baudrate for serial prints on ESP8266 than default in MyConfig.h
    #define MY_BAUD_RATE 9600
    
    // Enables and select radio type (if attached)
    //#define MY_RADIO_NRF24
    
    #define MY_GATEWAY_MQTT_CLIENT
    #define MY_GATEWAY_ESP8266_SECURE
    
    // Set this node's subscribe and publish topic prefix
    #define MY_MQTT_PUBLISH_TOPIC_PREFIX "mygateway1-out"
    #define MY_MQTT_SUBSCRIBE_TOPIC_PREFIX "mygateway1-in"
    
    // Set MQTT client id
    #define MY_MQTT_CLIENT_ID "mysensors-1"
    
    // Enable these if your MQTT broker requires usenrame/password
    //#define MY_MQTT_USER "user"
    //#define MY_MQTT_PASSWORD "password"
    
    // Set WIFI SSID and password
    #define MY_ESP8266_SSID "mySSID"
    #define MY_ESP8266_PASSWORD "myPasswd"
    
    // Set the hostname for the WiFi Client. This is the hostname
    // it will pass to the DHCP server if not static.
    // #define MY_ESP8266_HOSTNAME "mqtt-sensor-gateway"
    
    // Enable MY_IP_ADDRESS here if you want a static ip address (no DHCP)
    //#define MY_IP_ADDRESS 192,168,2,197
    
    // If using static ip you need to define Gateway and Subnet address as well
    //#define MY_IP_GATEWAY_ADDRESS 192,168,2,1
    //#define MY_IP_SUBNET_ADDRESS 255,255,255,0
    
    
    // MQTT broker ip address.
    #define MY_CONTROLLER_URL_ADDRESS "grammatico.me"
    
    // The MQTT broker port to to open
    #define MY_PORT 8883
    
    #define MY_SSL_FINGERPRINT "29:5C:A6:E9:D9:AB:75:16:B5:E6:F4:35:1E:B8:54:4F:DF:0C:D0:FC"
    
    #include <ESP8266WiFi.h>
    #include <MySensors.h>
    
    void setup()
    {
    }
    
    void presentation()
    {
    	// Present locally attached sensors here
    }
    
    
    void loop()
    {
    	// Send locally attech sensors data here
    }
    

    The fingerprint has been obtained with:

    openssl x509 -in etc/certs/cert.pem -noout -sha256 -fingerprint
    

    And another attempt with:

    openssl x509 -in etc/certs/cert.pem -noout -sha1 -fingerprint
    

    I have setup a mosquitto server with following ssl config:

    ...
    # -----------------------------------------------------------------
    # Certificate based SSL/TLS support
    # -----------------------------------------------------------------
    # The following options can be used to enable certificate based SSL/TLS support
    # for this listener. Note that the recommended port for MQTT over TLS is 8883,
    # but this must be set manually.
    #
    # See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS
    # support" section. Only one of certificate or PSK encryption support can be
    # enabled for any listener.
    
    # Both of certfile and keyfile must be defined to enable certificate based
    # TLS encryption.
    
    tls_version tlsv1.1
    
    # Path to the PEM encoded server certificate.
    certfile /etc/mosquitto/certs/cert.pem
    #certfile /etc/mosquitto/certs/server.crt
    
    # Path to the PEM encoded keyfile.
    keyfile /etc/mosquitto/certs/privkey.pem
    #keyfile /etc/mosquitto/certs/server.key
    # cafile and capath define methods of accessing the PEM encoded
    # Certificate Authority certificates that will be considered trusted when
    # checking incoming client certificates.
    # cafile defines the path to a file containing the CA certificates.
    # capath defines a directory that will be searched for files
    # containing the CA certificates. For capath to work correctly, the
    # certificate files must have ".crt" as the file ending and you must run
    # "openssl rehash <path to capath>" each time you add/remove a certificate.
    cafile /etc/mosquitto/certs/fullchain.pem
    #cafile /etc/mosquitto/certs/ca.crt
    ...
    

    When the MySensors gateway connects I have the following errors in the DEBUG messages:

    ip:192.168.0.17,mask:255.255.255.0,gw:192.168.0.254
    .IP: 192.168.0.17
    10624 MCO:BGN:STP
    10662 MCO:REG:NOT NEEDED
    10688 MCO:BGN:INIT OK,TSP=NA
    IP: 192.168.0.17
    10719 GWT:RMQ:CONNECTING...
    12002 !GWT:RMQ:FAIL
    IP: 192.168.0.17
    12024 GWT:RMQ:CONNECTING...
    13210 !GWT:RMQ:FAIL
    IP: 192.168.0.17
    13232 GWT:RMQ:CONNECTING...
    

    On the server side, I have the following messages in the logs:

    2022-04-28T18:57:52: mosquitto version 2.0.14 starting
    2022-04-28T18:57:52: Config loaded from /etc/mosquitto/mosquitto.conf.
    2022-04-28T18:57:52: Opening ipv4 listen socket on port 8883.
    2022-04-28T18:57:52: Opening ipv6 listen socket on port 8883.
    2022-04-28T18:57:52: Warning: Interface eth0 does not support IPv6 configuration.
    2022-04-28T18:57:52: mosquitto version 2.0.14 running
    2022-04-28T18:58:13: New connection from 82.64.195.150:64320 on port 8883.
    2022-04-28T18:58:14: Client <unknown> disconnected: Protocol error.
    2022-04-28T18:58:14: New connection from 82.64.195.150:52022 on port 8883.
    2022-04-28T18:58:16: Client <unknown> disconnected: Protocol error.
    
    

    SSL keys generated with letsencrypt, but same result with openssl.

    In parallel, Domoticz connects like a charm over SSL with the MySensors Gateway MQTT device defined.

    Any advices please ?

    I found also this option SSL support for MQTT on ESP8266 , but not convinced to upload the certificate to the ESP. Did someone gave it a try ?

    Thanks and regards,

    Eric.



  • Guys,

    I realized some stuff are now available in the dev branch. Unfortunately, I am not fully satisfied with it. Thus, decided to progress on this.

    Please have a look to my proposal in my fork:
    New SSL implementation

    • You are now able to submit up to three Certificate Authorities to validate the mqtt broker certificate.
      I made tests with Let's Encrypt and you may need to store three root Certificate Authorities to validate a server signed by them. Please see Chain of Trust - Let's Encrypt
    • You are now able to validate with the mqtt broker fingerprint if the previous point doesn't fit. it's easier, but less secure and less convenient. While root Certificate Authorities are updated rarely (some are valid more than 10 years), the fingerprint should be updated each time the mqtt broker certificate is updated. With Let's Encrypt it's every quarter.
    • At last, if you don't have a root Certificate Authority nor the fingerprint, you are good to go with insecure connection. The mqtt broker certificate is not validated. This setup is automatic if previous two setups are not done.
    • If required by the mqtt broker, you can setup a client certificate and key. This is not mandatory, only if required by the mqtt broker.

    I'll perform some further tests, as my original development are done in 2.3.2, and if it works, will submit my changes to the development branch.

    Thx and regards,

    Eric.



  • Pull request opened
    New SSL implementation



  • Thanks for this interesting information!


Log in to reply
 

Suggested Topics

26
Online

11.2k
Users

11.1k
Topics

112.5k
Posts