diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 40570f0d01..ec60fb9821 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,3 +1,5 @@ +import re + from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -36,6 +38,7 @@ from esphome.const import ( CONF_REBOOT_TIMEOUT, CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, + CONF_SSL_FINGERPRINT, CONF_STATE_TOPIC, CONF_TOPIC, CONF_TOPIC_PREFIX, @@ -196,6 +199,13 @@ def validate_config(value): return out +def validate_fingerprint(value): + value = cv.string(value) + if re.match(r"^[0-9a-f]{40}$", value) is None: + raise cv.Invalid("fingerprint must be valid SHA1 hash") + return value + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -244,6 +254,9 @@ CONFIG_SCHEMA = cv.All( ), validate_message_just_topic, ), + cv.Optional(CONF_SSL_FINGERPRINT): cv.All( + cv.only_on_esp8266, cv.string, validate_fingerprint + ), cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -383,10 +396,19 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) + if CONF_SSL_FINGERPRINT in config: + cg.add_define("USE_MQTT_SECURE_CLIENT") + fingerprint = config[CONF_SSL_FINGERPRINT] + arr = [cg.RawExpression(f"0x{fingerprint[i:i + 2]}") for i in range(0, 40, 2)] + cg.add(var.set_ssl_fingerprint(arr)) + + if CONF_SKIP_CERT_CN_CHECK in config: + cg.add_define("USE_MQTT_SECURE_CLIENT") + cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK])) + if CONF_CERTIFICATE_AUTHORITY in config: cg.add_define("USE_MQTT_SECURE_CLIENT") cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY])) - cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK])) if CONF_CLIENT_CERTIFICATE in config: cg.add(var.set_cl_certificate(config[CONF_CLIENT_CERTIFICATE])) cg.add(var.set_cl_key(config[CONF_CLIENT_CERTIFICATE_KEY])) diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.cpp b/esphome/components/mqtt/mqtt_backend_esp8266.cpp index 2fc9ce3b6d..2124425800 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp8266.cpp @@ -22,9 +22,12 @@ void MQTTBackendESP8266::initialize_() { if (this->ca_certificate_str_.has_value()) { this->ca_certificate_.append(this->ca_certificate_str_.value().c_str()); this->wifi_client_.setTrustAnchors(&this->ca_certificate_); - if (this->skip_cert_cn_check_) { - this->wifi_client_.setInsecure(); - } + } + if (this->ssl_fingerprint_.has_value()) { + this->wifi_client_.setFingerprint(this->ssl_fingerprint_.value().data()); + } + if (this->skip_cert_cn_check_) { + this->wifi_client_.setInsecure(); } #endif diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index bc0b902533..e7fc0f1259 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -93,6 +93,7 @@ class MQTTBackendESP8266 final : public MQTTBackend { void set_ca_certificate(const std::string &cert) { this->ca_certificate_str_ = cert; } void set_skip_cert_cn_check(bool skip_check) { this->skip_cert_cn_check_ = skip_check; } + void set_ssl_fingerprint(const std::array &fingerprint) { this->ssl_fingerprint_ = fingerprint; }; protected: void initialize_(); @@ -119,6 +120,7 @@ class MQTTBackendESP8266 final : public MQTTBackend { std::string lwt_message_; std::string client_id_; optional ca_certificate_str_; + optional> ssl_fingerprint_; BearSSL::X509List ca_certificate_; bool skip_cert_cn_check_{false}; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 21e49fbfd8..cd652d5e42 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -132,14 +132,25 @@ class MQTTClientComponent : public Component { bool is_discovery_enabled() const; bool is_discovery_ip_enabled() const; + /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. + * + * @warning This is *not* secure and *not* how SSL is usually done. You'll have to add + * a separate fingerprint for every certificate you use. Additionally, the hashing + * algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure. + * + * @param fingerprint The SSL fingerprint as a 20 value long std::array. + */ +#ifdef USE_ESP8266 + void set_ssl_fingerprint(const std::array &fingerprint) { + this->mqtt_backend_.set_ssl_fingerprint(fingerprint); + }; +#endif void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); } - #ifdef USE_ESP32 void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } void set_cl_key(const char *key) { this->mqtt_backend_.set_cl_key(key); } #endif - const Availability &get_availability(); /** Set the topic prefix that will be prepended to all topics together with "/". This will, in most cases, diff --git a/esphome/const.py b/esphome/const.py index 95773630d0..92d5a445bc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -789,7 +789,7 @@ CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" -CONF_SSL_FINGERPRINTS = "ssl_fingerprints" +CONF_SSL_FINGERPRINT = "ssl_fingerprint" CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index c1c45799cc..b46d128ebe 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -17,7 +17,7 @@ from esphome.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SSL_FINGERPRINTS, + CONF_SSL_FINGERPRINT, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, @@ -99,7 +99,7 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( + if config[CONF_MQTT].get(CONF_SSL_FINGERPRINT) or config[CONF_MQTT].get( CONF_CERTIFICATE_AUTHORITY ): tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member @@ -267,6 +267,6 @@ def get_fingerprint(config): safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}") safe_print( - f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" + f"Add the following line to the mqtt section of {CORE.config_path}:\n {CONF_SSL_FINGERPRINT}: {sha1}" ) return 0