1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-28 08:02:23 +01:00
This commit is contained in:
J. Nick Koston
2025-09-26 22:33:41 -05:00
parent 3bec6efdc3
commit abcc2d483b
2 changed files with 231 additions and 132 deletions

View File

@@ -1,11 +1,13 @@
#include "ota_esphome.h"
#ifdef USE_OTA
#ifdef USE_OTA_PASSWORD
#ifdef USE_OTA_MD5
#include "esphome/components/md5/md5.h"
#endif
#ifdef USE_OTA_SHA256
#include "esphome/components/sha256/sha256.h"
#endif
#endif
#include "esphome/components/network/util.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_arduino_esp32.h"
@@ -165,10 +167,7 @@ void ESPHomeOTAComponent::handle_handshake_() {
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
// Send error response (non-blocking, best effort)
uint8_t error = static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_MAGIC);
this->client_->write(&error, 1);
this->cleanup_connection_();
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
return;
}
@@ -221,10 +220,39 @@ void ESPHomeOTAComponent::handle_handshake_() {
return;
}
// Handshake complete, move to data phase
#ifdef USE_OTA_PASSWORD
// If password is set, move to auth phase
if (!this->password_.empty()) {
this->transition_ota_state_(OTAState::AUTH_SEND);
[[fallthrough]];
} else
#endif
{
// No password, move directly to data phase
this->transition_ota_state_(OTAState::DATA);
[[fallthrough]];
}
}
#ifdef USE_OTA_PASSWORD
case OTAState::AUTH_SEND: {
// Non-blocking authentication send
if (!this->handle_auth_send_()) {
return;
}
this->transition_ota_state_(OTAState::AUTH_READ);
[[fallthrough]];
}
case OTAState::AUTH_READ: {
// Non-blocking authentication read & verify
if (!this->handle_auth_read_()) {
return;
}
this->transition_ota_state_(OTAState::DATA);
[[fallthrough]];
}
#endif
case OTAState::DATA:
this->handle_data_();
@@ -240,8 +268,10 @@ void ESPHomeOTAComponent::handle_data_() {
/// Handle the OTA data transfer and update process.
///
/// This method is blocking and will not return until the OTA update completes,
/// fails, or times out. It handles authentication, receives the firmware data,
/// writes it to flash, and reboots on success.
/// fails, or times out. It receives the firmware data, writes it to flash,
/// and reboots on success.
///
/// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ.
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
bool update_started = false;
size_t total = 0;
@@ -253,80 +283,12 @@ void ESPHomeOTAComponent::handle_data_() {
size_t size_acknowledged = 0;
#endif
// The handshake has already been completed in handle_handshake_()
// The handshake and auth have already been completed
// We already have:
// - this->backend_ created
// - this->ota_features_ set
// - Feature acknowledgment sent
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
bool auth_success = false;
#ifdef USE_OTA_SHA256
// SECURITY HARDENING: Prefer SHA256 authentication on platforms that support it.
//
// This is a hardening measure to prevent future downgrade attacks where an attacker
// could force the use of MD5 authentication by manipulating the feature flags.
//
// While MD5 is currently still acceptable for our OTA authentication use case
// (where the password is a shared secret and we're only authenticating, not
// encrypting), at some point in the future MD5 will likely become so weak that
// it could be practically attacked.
//
// We enforce SHA256 now on capable platforms because:
// 1. We can't retroactively update device firmware in the field
// 2. Clients (like esphome CLI) can always be updated to support SHA256
// 3. This prevents any possibility of downgrade attacks in the future
//
// Devices that don't support SHA256 (due to platform limitations) will
// continue to use MD5 as their only option (see #else branch below).
bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
#ifdef ALLOW_OTA_DOWNGRADE_MD5
// Temporary compatibility mode: Allow MD5 for ~3 versions to enable OTA downgrades
// This prevents users from being locked out if they need to downgrade after updating
// TODO: Remove this entire ifdef block in 2026.1.0
if (client_supports_sha256) {
sha256::SHA256 sha_hasher;
auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH,
LOG_STR("SHA256"), sbuf);
} else {
#ifdef USE_OTA_MD5
ESP_LOGW(TAG, "Using MD5 auth for compatibility (deprecated)");
md5::MD5Digest md5_hasher;
auth_success =
this->perform_hash_auth_(&md5_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_AUTH, LOG_STR("MD5"), sbuf);
#endif // USE_OTA_MD5
}
#else
// Strict mode: SHA256 required on capable platforms (future default)
if (!client_supports_sha256) {
ESP_LOGW(TAG, "Client requires SHA256");
error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
sha256::SHA256 sha_hasher;
auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH,
LOG_STR("SHA256"), sbuf);
#endif // ALLOW_OTA_DOWNGRADE_MD5
#else
// Platform only supports MD5 - use it as the only available option
// This is not a security downgrade as the platform cannot support SHA256
#ifdef USE_OTA_MD5
md5::MD5Digest md5_hasher;
auth_success =
this->perform_hash_auth_(&md5_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_AUTH, LOG_STR("MD5"), sbuf);
#endif // USE_OTA_MD5
#endif // USE_OTA_SHA256
if (!auth_success) {
error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
}
#endif // USE_OTA_PASSWORD
// - Authentication completed (if password was set)
// Acknowledge auth OK - 1 byte
buf[0] = ota::OTA_RESPONSE_AUTH_OK;
@@ -594,6 +556,15 @@ void ESPHomeOTAComponent::cleanup_connection_() {
this->ota_state_ = OTAState::IDLE;
this->ota_features_ = 0;
this->backend_ = nullptr;
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
}
void ESPHomeOTAComponent::send_error_and_cleanup_(ota::OTAResponseTypes error) {
uint8_t error_byte = static_cast<uint8_t>(error);
this->client_->write(&error_byte, 1); // Best effort, non-blocking
this->cleanup_connection_();
}
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
@@ -602,86 +573,202 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
}
#ifdef USE_OTA_PASSWORD
void ESPHomeOTAComponent::log_auth_warning_(const LogString *action, const LogString *hash_name) {
ESP_LOGW(TAG, "Auth: %s %s failed", LOG_STR_ARG(action), LOG_STR_ARG(hash_name));
void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
bool ESPHomeOTAComponent::handle_auth_send_() {
// Determine which auth type to use based on platform capabilities and client support
uint8_t auth_type;
#ifdef USE_OTA_SHA256
bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
#ifdef ALLOW_OTA_DOWNGRADE_MD5
if (client_supports_sha256) {
auth_type = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
if (!this->auth_hasher_) {
this->auth_hasher_ = std::make_unique<sha256::SHA256>();
}
} else {
#ifdef USE_OTA_MD5
this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)"));
auth_type = ota::OTA_RESPONSE_REQUEST_AUTH;
if (!this->auth_hasher_) {
this->auth_hasher_ = std::make_unique<md5::MD5Digest>();
}
#else
this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
#endif // USE_OTA_MD5
}
#else // !ALLOW_OTA_DOWNGRADE_MD5
if (!client_supports_sha256) {
this->log_auth_warning_(LOG_STR("Client requires SHA256"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
}
auth_type = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
if (!this->auth_hasher_) {
this->auth_hasher_ = std::make_unique<sha256::SHA256>();
}
#endif // ALLOW_OTA_DOWNGRADE_MD5
#else // !USE_OTA_SHA256
#ifdef USE_OTA_MD5
auth_type = ota::OTA_RESPONSE_REQUEST_AUTH;
if (!this->auth_hasher_) {
this->auth_hasher_ = std::make_unique<md5::MD5Digest>();
}
#else
this->log_auth_warning_(LOG_STR("No auth methods available"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
#endif // USE_OTA_MD5
#endif // USE_OTA_SHA256
// Non-template function definition to reduce binary size
bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string &password, uint8_t auth_request,
const LogString *name, char *buf) {
// Get sizes from the hasher
const size_t hex_size = hasher->get_size() * 2; // Hex is twice the byte size
const size_t nonce_len = hasher->get_size() / 4; // Nonce is 1/4 of hash size in bytes
// Initialize auth buffer if not already done
if (!this->auth_buf_) {
// Calculate required buffer size
const size_t hex_size = this->auth_hasher_->get_size() * 2;
const size_t nonce_len = this->auth_hasher_->get_size() / 4;
// Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response)
this->auth_buf_size_ = 1 + hex_size + hex_size * 2;
this->auth_buf_ = std::make_unique<uint8_t[]>(this->auth_buf_size_);
this->auth_buf_pos_ = 0;
// Use the provided buffer for all operations
// Generate nonce seed bytes using random_bytes
// Generate nonce
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
this->log_auth_warning_(LOG_STR("Random bytes generation failed"), name);
this->log_auth_warning_(LOG_STR("Random bytes generation failed"));
this->cleanup_connection_();
return false;
}
hasher->init();
hasher->add(buf, nonce_len);
hasher->calculate();
this->auth_hasher_->init();
this->auth_hasher_->add(buf, nonce_len);
this->auth_hasher_->calculate();
// Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes)
buf[0] = auth_request;
hasher->get_hex(buf + 1);
this->auth_buf_[0] = auth_type;
this->auth_hasher_->get_hex(buf);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// Log nonce for debugging
buf[1 + hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s Nonce is %s", LOG_STR_ARG(name), buf + 1);
char log_buf[hex_size + 1];
memcpy(log_buf, buf, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
#endif
}
// Send auth_type + nonce in a single write
if (!this->writeall_(reinterpret_cast<uint8_t *>(buf), 1 + hex_size)) {
this->log_auth_warning_(LOG_STR("Writing auth type and nonce"), name);
// Try to write auth_type + nonce
const size_t hex_size = this->auth_hasher_->get_size() * 2;
const size_t to_write = 1 + hex_size;
size_t remaining = to_write - this->auth_buf_pos_;
ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
if (written == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return false; // Try again next loop
}
this->log_auth_warning_(LOG_STR("Writing auth type and nonce failed"));
this->cleanup_connection_();
return false;
}
// Start challenge: password + nonce (nonce is at buf + 1)
hasher->init();
hasher->add(password.c_str(), password.length());
hasher->add(buf + 1, hex_size);
this->auth_buf_pos_ += written;
// Read cnonce and add to hash
if (!this->readall_(reinterpret_cast<uint8_t *>(buf), hex_size * 2)) {
this->log_auth_warning_(LOG_STR("Reading cnonce response"), name);
// Check if we still have more to write
if (this->auth_buf_pos_ < to_write) {
return false; // More to write, try again next loop
}
// All written, prepare for reading phase
this->auth_buf_pos_ = 0;
// Start challenge hash: password + nonce
this->auth_hasher_->init();
this->auth_hasher_->add(this->password_.c_str(), this->password_.length());
this->auth_hasher_->add(reinterpret_cast<char *>(this->auth_buf_.get() + 1), hex_size);
return true;
}
bool ESPHomeOTAComponent::handle_auth_read_() {
const size_t hex_size = this->auth_hasher_->get_size() * 2;
const size_t to_read = hex_size * 2; // CNonce + Response
// Try to read remaining bytes
size_t remaining = to_read - this->auth_buf_pos_;
ssize_t read = this->client_->read(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
if (read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return false; // Try again next loop
}
this->log_auth_warning_(LOG_STR("Reading cnonce response failed"));
this->cleanup_connection_();
return false;
}
// Response is located after CNonce in the buffer
if (read == 0) {
this->log_auth_warning_(LOG_STR("Remote closed during auth read"));
this->cleanup_connection_();
return false;
}
this->auth_buf_pos_ += read;
// Check if we still need more data
if (this->auth_buf_pos_ < to_read) {
return false; // More to read, try again next loop
}
// We have all the data, verify it
char *buf = reinterpret_cast<char *>(this->auth_buf_.get());
const char *response = buf + hex_size;
hasher->add(buf, hex_size); // add CNonce in binary
hasher->calculate();
// Add CNonce to hash
this->auth_hasher_->add(buf, hex_size);
this->auth_hasher_->calculate();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[hex_size + 1];
// Log CNonce for debugging
memcpy(log_buf, buf, hex_size); // Save CNonce for logging
// Log CNonce
memcpy(log_buf, buf, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s CNonce is %s", LOG_STR_ARG(name), log_buf);
ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf);
// Log computed hash for debugging
hasher->get_hex(log_buf);
// Log computed hash
this->auth_hasher_->get_hex(log_buf);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s Result is %s", LOG_STR_ARG(name), log_buf);
ESP_LOGV(TAG, "Auth: Result is %s", log_buf);
// Log received response
memcpy(log_buf, response, hex_size); // Save response for logging
memcpy(log_buf, response, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s Response is %s", LOG_STR_ARG(name), log_buf);
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Auth: Response is %s", log_buf);
#endif
// Compare response directly with digest in hasher
bool matches = hasher->equals_hex(response);
// Compare response
bool matches = this->auth_hasher_->equals_hex(response);
if (!matches) {
this->log_auth_warning_(LOG_STR("Password mismatch"), name);
this->log_auth_warning_(LOG_STR("Password mismatch"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
}
return matches;
// Authentication successful - clean up auth state
this->cleanup_auth_();
return true;
}
void ESPHomeOTAComponent::cleanup_auth_() {
this->auth_hasher_ = nullptr;
this->auth_buf_ = nullptr;
this->auth_buf_size_ = 0;
this->auth_buf_pos_ = 0;
}
#endif // USE_OTA_PASSWORD

View File

@@ -20,7 +20,11 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
MAGIC_ACK, // Sending OK and version after magic bytes
FEATURE_READ, // Reading feature flags from client
FEATURE_ACK, // Sending feature acknowledgment
DATA, // Processing OTA data (authentication, update, etc.)
#ifdef USE_OTA_PASSWORD
AUTH_SEND, // Sending authentication request
AUTH_READ, // Reading authentication data
#endif // USE_OTA_PASSWORD
DATA, // BLOCKING! Processing OTA data (update, etc.)
};
#ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; }
@@ -40,9 +44,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
void handle_handshake_();
void handle_data_();
#ifdef USE_OTA_PASSWORD
bool perform_hash_auth_(HashBase *hasher, const std::string &password, uint8_t auth_request, const LogString *name,
char *buf);
void log_auth_warning_(const LogString *action, const LogString *hash_name);
bool handle_auth_send_();
bool handle_auth_read_();
void cleanup_auth_();
void log_auth_warning_(const LogString *msg);
#endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len);
@@ -56,6 +61,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
void log_start_(const LogString *phase);
void log_remote_closed_(const LogString *during);
void cleanup_connection_();
void send_error_and_cleanup_(ota::OTAResponseTypes error);
void yield_and_feed_watchdog_();
#ifdef USE_OTA_PASSWORD
@@ -72,6 +78,12 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
OTAState ota_state_{OTAState::IDLE};
uint8_t handshake_buf_pos_{0};
uint8_t ota_features_{0};
#ifdef USE_OTA_PASSWORD
std::unique_ptr<HashBase> auth_hasher_;
std::unique_ptr<uint8_t[]> auth_buf_;
size_t auth_buf_size_{0};
size_t auth_buf_pos_{0};
#endif // USE_OTA_PASSWORD
};
} // namespace esphome