Feature: MQTT add TLS authentication

User asked for TLS client certificate based login from DTU to MQTT
server. This PR implements storage and use of x509 client certificate
and private key.

Signed-off-by: Martin Dummer <martin.dummer@gmx.net>
This commit is contained in:
Martin Dummer 2023-04-12 08:30:15 +02:00
parent 12d7349699
commit 971ae6d1be
No known key found for this signature in database
GPG Key ID: CE5F3E9B6DE05D12
13 changed files with 79 additions and 4 deletions

View File

@ -90,6 +90,9 @@ struct CONFIG_T {
bool Mqtt_Hass_IndividualPanels; bool Mqtt_Hass_IndividualPanels;
bool Mqtt_Tls; bool Mqtt_Tls;
char Mqtt_RootCaCert[MQTT_MAX_ROOT_CA_CERT_STRLEN + 1]; char Mqtt_RootCaCert[MQTT_MAX_ROOT_CA_CERT_STRLEN + 1];
bool Mqtt_TlsCertLogin;
char Mqtt_ClientCert[MQTT_MAX_ROOT_CA_CERT_STRLEN + 1];
char Mqtt_ClientKey[MQTT_MAX_ROOT_CA_CERT_STRLEN + 1];
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];

View File

@ -3,7 +3,7 @@
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#define MQTT_JSON_DOC_SIZE 3072 #define MQTT_JSON_DOC_SIZE 10240
class WebApiMqttClass { class WebApiMqttClass {
public: public:

View File

@ -66,6 +66,9 @@
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" \ "mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" \
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" \ "emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" \
"-----END CERTIFICATE-----\n" "-----END CERTIFICATE-----\n"
#define MQTT_TLSCERTLOGIN false
#define MQTT_TLSCLIENTCERT ""
#define MQTT_TLSCLIENTKEY ""
#define MQTT_LWT_TOPIC "dtu/status" #define MQTT_LWT_TOPIC "dtu/status"
#define MQTT_LWT_ONLINE "online" #define MQTT_LWT_ONLINE "online"
#define MQTT_LWT_OFFLINE "offline" #define MQTT_LWT_OFFLINE "offline"

View File

@ -65,6 +65,9 @@ bool ConfigurationClass::write()
JsonObject mqtt_tls = mqtt.createNestedObject("tls"); JsonObject mqtt_tls = mqtt.createNestedObject("tls");
mqtt_tls["enabled"] = config.Mqtt_Tls; mqtt_tls["enabled"] = config.Mqtt_Tls;
mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert;
mqtt_tls["certlogin"] = config.Mqtt_TlsCertLogin;
mqtt_tls["client_cert"] = config.Mqtt_ClientCert;
mqtt_tls["client_key"] = config.Mqtt_ClientKey;
JsonObject mqtt_hass = mqtt.createNestedObject("hass"); JsonObject mqtt_hass = mqtt.createNestedObject("hass");
mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled; mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled;
@ -202,6 +205,9 @@ bool ConfigurationClass::read()
JsonObject mqtt_tls = mqtt["tls"]; JsonObject mqtt_tls = mqtt["tls"];
config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS;
strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert));
config.Mqtt_TlsCertLogin = mqtt_tls["certlogin"] | MQTT_TLSCERTLOGIN;
strlcpy(config.Mqtt_ClientCert, mqtt_tls["client_cert"] | MQTT_TLSCLIENTCERT, sizeof(config.Mqtt_ClientCert));
strlcpy(config.Mqtt_ClientKey, mqtt_tls["client_key"] | MQTT_TLSCLIENTKEY, sizeof(config.Mqtt_ClientKey));
JsonObject mqtt_hass = mqtt["hass"]; JsonObject mqtt_hass = mqtt["hass"];
config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED;

View File

@ -104,7 +104,12 @@ void MqttSettingsClass::performConnect()
if (config.Mqtt_Tls) { if (config.Mqtt_Tls) {
static_cast<espMqttClientSecure*>(mqttClient)->setCACert(config.Mqtt_RootCaCert); static_cast<espMqttClientSecure*>(mqttClient)->setCACert(config.Mqtt_RootCaCert);
static_cast<espMqttClientSecure*>(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port); static_cast<espMqttClientSecure*>(mqttClient)->setServer(config.Mqtt_Hostname, config.Mqtt_Port);
static_cast<espMqttClientSecure*>(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password); if (config.Mqtt_TlsCertLogin) {
static_cast<espMqttClientSecure*>(mqttClient)->setCertificate(config.Mqtt_ClientCert);
static_cast<espMqttClientSecure*>(mqttClient)->setPrivateKey(config.Mqtt_ClientKey);
} else {
static_cast<espMqttClientSecure*>(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password);
}
static_cast<espMqttClientSecure*>(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); static_cast<espMqttClientSecure*>(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline);
static_cast<espMqttClientSecure*>(mqttClient)->setClientId(clientId.c_str()); static_cast<espMqttClientSecure*>(mqttClient)->setClientId(clientId.c_str());
static_cast<espMqttClientSecure*>(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); static_cast<espMqttClientSecure*>(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1));

View File

@ -45,6 +45,8 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
root["mqtt_retain"] = config.Mqtt_Retain; root["mqtt_retain"] = config.Mqtt_Retain;
root["mqtt_tls"] = config.Mqtt_Tls; root["mqtt_tls"] = config.Mqtt_Tls;
root["mqtt_root_ca_cert_info"] = getRootCaCertInfo(config.Mqtt_RootCaCert); root["mqtt_root_ca_cert_info"] = getRootCaCertInfo(config.Mqtt_RootCaCert);
root["mqtt_tls_cert_login"] = config.Mqtt_TlsCertLogin;
root["mqtt_client_cert_info"] = getRootCaCertInfo(config.Mqtt_ClientCert);
root["mqtt_lwt_topic"] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; root["mqtt_lwt_topic"] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic;
root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; root["mqtt_publish_interval"] = config.Mqtt_PublishInterval;
root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled;
@ -76,6 +78,9 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
root["mqtt_retain"] = config.Mqtt_Retain; root["mqtt_retain"] = config.Mqtt_Retain;
root["mqtt_tls"] = config.Mqtt_Tls; root["mqtt_tls"] = config.Mqtt_Tls;
root["mqtt_root_ca_cert"] = config.Mqtt_RootCaCert; root["mqtt_root_ca_cert"] = config.Mqtt_RootCaCert;
root["mqtt_tls_cert_login"] = config.Mqtt_TlsCertLogin;
root["mqtt_client_cert"] = config.Mqtt_ClientCert;
root["mqtt_client_key"] = config.Mqtt_ClientKey;
root["mqtt_lwt_topic"] = config.Mqtt_LwtTopic; root["mqtt_lwt_topic"] = config.Mqtt_LwtTopic;
root["mqtt_lwt_online"] = config.Mqtt_LwtValue_Online; root["mqtt_lwt_online"] = config.Mqtt_LwtValue_Online;
root["mqtt_lwt_offline"] = config.Mqtt_LwtValue_Offline; root["mqtt_lwt_offline"] = config.Mqtt_LwtValue_Offline;
@ -137,6 +142,9 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("mqtt_topic") && root.containsKey("mqtt_topic")
&& root.containsKey("mqtt_retain") && root.containsKey("mqtt_retain")
&& root.containsKey("mqtt_tls") && root.containsKey("mqtt_tls")
&& root.containsKey("mqtt_tls_cert_login")
&& root.containsKey("mqtt_client_cert")
&& root.containsKey("mqtt_client_key")
&& root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_topic")
&& root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_online")
&& root.containsKey("mqtt_lwt_offline") && root.containsKey("mqtt_lwt_offline")
@ -212,8 +220,10 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
return; return;
} }
if (root["mqtt_root_ca_cert"].as<String>().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN) { if (root["mqtt_root_ca_cert"].as<String>().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN
retMsg["message"] = "Certificate must not longer then " STR(MQTT_MAX_ROOT_CA_CERT_STRLEN) " characters!"; || root["mqtt_client_cert"].as<String>().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN
|| root["mqtt_client_key"].as<String>().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN) {
retMsg["message"] = "Certificates must not be longer than " STR(MQTT_MAX_ROOT_CA_CERT_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttCertificateLength; retMsg["code"] = WebApiError::MqttCertificateLength;
retMsg["param"]["max"] = MQTT_MAX_ROOT_CA_CERT_STRLEN; retMsg["param"]["max"] = MQTT_MAX_ROOT_CA_CERT_STRLEN;
response->setLength(); response->setLength();
@ -291,6 +301,9 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
config.Mqtt_Retain = root["mqtt_retain"].as<bool>(); config.Mqtt_Retain = root["mqtt_retain"].as<bool>();
config.Mqtt_Tls = root["mqtt_tls"].as<bool>(); config.Mqtt_Tls = root["mqtt_tls"].as<bool>();
strlcpy(config.Mqtt_RootCaCert, root["mqtt_root_ca_cert"].as<String>().c_str(), sizeof(config.Mqtt_RootCaCert)); strlcpy(config.Mqtt_RootCaCert, root["mqtt_root_ca_cert"].as<String>().c_str(), sizeof(config.Mqtt_RootCaCert));
config.Mqtt_TlsCertLogin = root["mqtt_tls_cert_login"].as<bool>();
strlcpy(config.Mqtt_ClientCert, root["mqtt_client_cert"].as<String>().c_str(), sizeof(config.Mqtt_ClientCert));
strlcpy(config.Mqtt_ClientKey, root["mqtt_client_key"].as<String>().c_str(), sizeof(config.Mqtt_ClientKey));
config.Mqtt_Port = root["mqtt_port"].as<uint>(); config.Mqtt_Port = root["mqtt_port"].as<uint>();
strlcpy(config.Mqtt_Hostname, root["mqtt_hostname"].as<String>().c_str(), sizeof(config.Mqtt_Hostname)); strlcpy(config.Mqtt_Hostname, root["mqtt_hostname"].as<String>().c_str(), sizeof(config.Mqtt_Hostname));
strlcpy(config.Mqtt_Username, root["mqtt_username"].as<String>().c_str(), sizeof(config.Mqtt_Username)); strlcpy(config.Mqtt_Username, root["mqtt_username"].as<String>().c_str(), sizeof(config.Mqtt_Username));

View File

@ -259,6 +259,8 @@
"Retain": "Retain", "Retain": "Retain",
"Tls": "TLS", "Tls": "TLS",
"RootCertifcateInfo": "Root CA-Zertifikat-Informationen", "RootCertifcateInfo": "Root CA-Zertifikat-Informationen",
"TlsCertLogin": "Anmeldung mit TLS Zertifikat",
"ClientCertifcateInfo": "Client Zertifikat-Informationen",
"HassSummary": "Home Assistant MQTT-Auto-Discovery Konfigurationszusammenfassung", "HassSummary": "Home Assistant MQTT-Auto-Discovery Konfigurationszusammenfassung",
"Expire": "Ablaufen", "Expire": "Ablaufen",
"IndividualPanels": "Einzelne Paneele", "IndividualPanels": "Einzelne Paneele",
@ -387,6 +389,9 @@
"EnableRetain": "Retain Flag aktivieren", "EnableRetain": "Retain Flag aktivieren",
"EnableTls": "TLS aktivieren", "EnableTls": "TLS aktivieren",
"RootCa": "CA-Root-Zertifikat (Standard Letsencrypt):", "RootCa": "CA-Root-Zertifikat (Standard Letsencrypt):",
"TlsCertLoginEnable": "TLS Zertifikat Login",
"ClientCert": "TLS Client-Zertifikat:",
"ClientKey": "TLS Client-Key:",
"LwtParameters": "LWT-Parameter", "LwtParameters": "LWT-Parameter",
"LwtTopic": "LWT-Topic:", "LwtTopic": "LWT-Topic:",
"LwtTopicHint": "LWT-Topic, wird der Basis Topic angehängt", "LwtTopicHint": "LWT-Topic, wird der Basis Topic angehängt",

View File

@ -259,6 +259,8 @@
"Retain": "Retain", "Retain": "Retain",
"Tls": "TLS", "Tls": "TLS",
"RootCertifcateInfo": "Root CA Certifcate Info", "RootCertifcateInfo": "Root CA Certifcate Info",
"TlsCertLogin": "Login with TLS Certificate",
"ClientCertifcateInfo": "Client Certifcate Info",
"HassSummary": "Home Assistant MQTT Auto Discovery Configuration Summary", "HassSummary": "Home Assistant MQTT Auto Discovery Configuration Summary",
"Expire": "Expire", "Expire": "Expire",
"IndividualPanels": "Individual Panels", "IndividualPanels": "Individual Panels",
@ -387,6 +389,9 @@
"EnableRetain": "Enable Retain Flag", "EnableRetain": "Enable Retain Flag",
"EnableTls": "Enable TLS", "EnableTls": "Enable TLS",
"RootCa": "CA-Root-Certificate (default Letsencrypt):", "RootCa": "CA-Root-Certificate (default Letsencrypt):",
"TlsCertLoginEnable": "Enable TLS Certificate Login",
"ClientCert": "TLS Client-Certificate:",
"ClientKey": "TLS Client-Key:",
"LwtParameters": "LWT Parameters", "LwtParameters": "LWT Parameters",
"LwtTopic": "LWT Topic:", "LwtTopic": "LWT Topic:",
"LwtTopicHint": "LWT topic, will be append base topic", "LwtTopicHint": "LWT topic, will be append base topic",

View File

@ -259,6 +259,8 @@
"Retain": "Conserver", "Retain": "Conserver",
"Tls": "TLS", "Tls": "TLS",
"RootCertifcateInfo": "Informations sur le certificat de l'autorité de certification racine", "RootCertifcateInfo": "Informations sur le certificat de l'autorité de certification racine",
"TlsCertLogin": "Connexion avec un certificat TLS",
"ClientCertifcateInfo": "Informations sur le certificat du client",
"HassSummary": "Résumé de la configuration de la découverte automatique du MQTT de Home Assistant", "HassSummary": "Résumé de la configuration de la découverte automatique du MQTT de Home Assistant",
"Expire": "Expiration", "Expire": "Expiration",
"IndividualPanels": "Panneaux individuels", "IndividualPanels": "Panneaux individuels",
@ -387,6 +389,9 @@
"EnableRetain": "Activation du maintien", "EnableRetain": "Activation du maintien",
"EnableTls": "Activer le TLS", "EnableTls": "Activer le TLS",
"RootCa": "Certificat CA-Root (par défaut Letsencrypt)", "RootCa": "Certificat CA-Root (par défaut Letsencrypt)",
"TlsCertLoginEnable": "Activer la connexion par certificat TLS",
"ClientCert": "Certificat client TLS:",
"ClientKey": "Clé client TLS:",
"LwtParameters": "Paramètres LWT", "LwtParameters": "Paramètres LWT",
"LwtTopic": "Sujet LWT", "LwtTopic": "Sujet LWT",
"LwtTopicHint": "Sujet LWT, sera ajouté comme sujet de base", "LwtTopicHint": "Sujet LWT, sera ajouté comme sujet de base",

View File

@ -9,6 +9,9 @@ export interface MqttConfig {
mqtt_retain: boolean; mqtt_retain: boolean;
mqtt_tls: boolean; mqtt_tls: boolean;
mqtt_root_ca_cert: string; mqtt_root_ca_cert: string;
mqtt_tls_cert_login: boolean;
mqtt_client_cert: string;
mqtt_client_key: string;
mqtt_lwt_topic: string; mqtt_lwt_topic: string;
mqtt_lwt_online: string; mqtt_lwt_online: string;
mqtt_lwt_offline: string; mqtt_lwt_offline: string;

View File

@ -8,6 +8,8 @@ export interface MqttStatus {
mqtt_retain: boolean; mqtt_retain: boolean;
mqtt_tls: boolean; mqtt_tls: boolean;
mqtt_root_ca_cert_info: string; mqtt_root_ca_cert_info: string;
mqtt_tls_cert_login: boolean;
mqtt_client_cert_info: string;
mqtt_connected: boolean; mqtt_connected: boolean;
mqtt_hass_enabled: boolean; mqtt_hass_enabled: boolean;
mqtt_hass_expire: boolean; mqtt_hass_expire: boolean;

View File

@ -60,6 +60,21 @@
:label="$t('mqttadmin.RootCa')" :label="$t('mqttadmin.RootCa')"
v-model="mqttConfigList.mqtt_root_ca_cert" v-model="mqttConfigList.mqtt_root_ca_cert"
type="textarea" maxlength="2560" rows="10"/> type="textarea" maxlength="2560" rows="10"/>
<InputElement v-show="mqttConfigList.mqtt_tls"
:label="$t('mqttadmin.TlsCertLoginEnable')"
v-model="mqttConfigList.mqtt_tls_cert_login"
type="checkbox"/>
<InputElement v-show="mqttConfigList.mqtt_tls_cert_login"
:label="$t('mqttadmin.ClientCert')"
v-model="mqttConfigList.mqtt_client_cert"
type="textarea" maxlength="2560" rows="10"/>
<InputElement v-show="mqttConfigList.mqtt_tls_cert_login"
:label="$t('mqttadmin.ClientKey')"
v-model="mqttConfigList.mqtt_client_key"
type="textarea" maxlength="2560" rows="10"/>
</CardElement> </CardElement>
<CardElement :text="$t('mqttadmin.LwtParameters')" textVariant="text-bg-primary" add-space <CardElement :text="$t('mqttadmin.LwtParameters')" textVariant="text-bg-primary" add-space

View File

@ -46,6 +46,16 @@
<th>{{ $t('mqttinfo.RootCertifcateInfo') }}</th> <th>{{ $t('mqttinfo.RootCertifcateInfo') }}</th>
<td>{{ mqttDataList.mqtt_root_ca_cert_info }}</td> <td>{{ mqttDataList.mqtt_root_ca_cert_info }}</td>
</tr> </tr>
<tr>
<th>{{ $t('mqttinfo.TlsCertLogin') }}</th>
<td>
<StatusBadge :status="mqttDataList.mqtt_tls_cert_login" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" />
</td>
</tr>
<tr v-show="mqttDataList.mqtt_tls_cert_login">
<th>{{ $t('mqttinfo.ClientCertifcateInfo') }}</th>
<td>{{ mqttDataList.mqtt_client_cert_info }}</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>