diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 138dbdddba..cb3fafe15a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -10,6 +10,7 @@ #include "proto.h" #include #include +#include #ifdef USE_ESP8266 #include @@ -548,7 +549,10 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { * @return 0 on success, -1 on error (check errno) */ APIError APINoiseFrameHelper::init_handshake_() { - int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size()); + uint8_t spare[noise::EPHEMERAL_KEYPAIR_SIZE]; + const uint8_t *ephemeral = noise::take_spare_ephemeral(spare) ? spare : nullptr; + int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size(), ephemeral); + sodium_memzero(spare, sizeof(spare)); APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 43d35363d3..72a8cad238 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -138,6 +138,9 @@ void APIServer::setup() { } void APIServer::loop() { +#ifdef USE_API_NOISE + this->prepare_spare_ephemeral_(); +#endif // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { this->accept_new_connections_(); @@ -188,6 +191,23 @@ void APIServer::loop() { } } +#ifdef USE_API_NOISE +// Refill the spare ephemeral key while nobody is waiting for it: not before +// the network is up, and not while a client is still in its handshake, since +// the key generation blocks the loop for its duration. +void APIServer::prepare_spare_ephemeral_() { + if (noise::has_spare_ephemeral() || !network::is_connected()) { + return; + } + for (auto &client : this->active_clients()) { + if (!client->is_connection_setup()) { + return; + } + } + noise::prepare_spare_ephemeral(); +} +#endif + void APIServer::remove_client_(uint8_t client_index) { auto &client = this->clients_[client_index]; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 072a583901..1dfe9f25f3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -357,6 +357,7 @@ class APIServer final : public Component, #endif #ifdef USE_API_NOISE + void prepare_spare_ephemeral_(); noise::NoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp index 7f8331cf96..738a87242b 100644 --- a/esphome/components/esphome/ota/ota_esphome_noise.cpp +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -7,6 +7,7 @@ #include #include +#include #ifdef USE_ESP8266 #include @@ -71,7 +72,10 @@ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { *p++ = ota::OTA_RESPONSE_FEATURE_FLAGS; *p++ = server_feature_flags; - int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue)); + uint8_t spare[noise::EPHEMERAL_KEYPAIR_SIZE]; + const uint8_t *ephemeral = noise::take_spare_ephemeral(spare) ? spare : nullptr; + int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue), ephemeral); + sodium_memzero(spare, sizeof(spare)); if (err != 0) { ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); this->cleanup_connection_(); diff --git a/esphome/components/noise/noise.cpp b/esphome/components/noise/noise.cpp index 95fab322db..a27ba3938b 100644 --- a/esphome/components/noise/noise.cpp +++ b/esphome/components/noise/noise.cpp @@ -1,11 +1,13 @@ #include "noise.h" #ifdef USE_NOISE +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include #include #include +#include #ifdef USE_ESP8266 #include @@ -15,6 +17,36 @@ namespace esphome::noise { static const char *const TAG = "noise"; +static uint8_t spare_ephemeral[EPHEMERAL_KEYPAIR_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool spare_ephemeral_ready = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +bool has_spare_ephemeral() { return spare_ephemeral_ready; } + +void prepare_spare_ephemeral() { + uint8_t *private_key = spare_ephemeral; + uint8_t *public_key = spare_ephemeral + 32; + // Same generation as noise-c's curve25519 backend: random bytes, X25519 + // clamping, then the public key. A random source failure leaves the slot + // empty; the handshake then generates its own key. + if (!random_bytes(private_key, 32)) { + return; + } + private_key[0] &= 0xF8; + private_key[31] = (private_key[31] & 0x7F) | 0x40; + crypto_scalarmult_curve25519_base(public_key, private_key); + spare_ephemeral_ready = true; +} + +bool take_spare_ephemeral(uint8_t *out) { + if (!spare_ephemeral_ready) { + return false; + } + std::memcpy(out, spare_ephemeral, EPHEMERAL_KEYPAIR_SIZE); + sodium_memzero(spare_ephemeral, EPHEMERAL_KEYPAIR_SIZE); + spare_ephemeral_ready = false; + return true; +} + const LogString *noise_err_to_logstr(int err) { if (err == NOISE_ERROR_NO_MEMORY) return LOG_STR("NO_MEMORY"); diff --git a/esphome/components/noise/noise.h b/esphome/components/noise/noise.h index f9da8d35b8..fcf59a2c6d 100644 --- a/esphome/components/noise/noise.h +++ b/esphome/components/noise/noise.h @@ -38,6 +38,19 @@ class NoiseContext { /// Convert a noise error code to a readable error const LogString *noise_err_to_logstr(int err); +// A responder ephemeral key pair generated ahead of time. The base point +// multiply behind one costs about 60 ms on ESP8266, so the api server fills +// the slot while idle and a connecting client does not wait for it. One slot +// serves every noise transport; a handshake that finds it empty generates +// its own key as before. +static constexpr size_t EPHEMERAL_KEYPAIR_SIZE = 64; // 32 byte private key then 32 byte public key +bool has_spare_ephemeral(); +/// Generate a key pair into the slot; blocks for the base point multiply. +void prepare_spare_ephemeral(); +/// Move the slot's key pair into out (EPHEMERAL_KEYPAIR_SIZE bytes) and empty +/// the slot. Returns false, leaving out untouched, when the slot is empty. +bool take_spare_ephemeral(uint8_t *out); + // Shared wire format for the noise transports (api and ota): every frame is // FRAME_INDICATOR, a 16-bit big-endian payload length, then the payload. // Handshake payloads start with a status byte; transport payloads end with diff --git a/esphome/components/noise/noise_handshake.cpp b/esphome/components/noise/noise_handshake.cpp index 6d426de012..712d6d8807 100644 --- a/esphome/components/noise/noise_handshake.cpp +++ b/esphome/components/noise/noise_handshake.cpp @@ -20,7 +20,8 @@ NoiseResponderHandshake::~NoiseResponderHandshake() { } } -int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) { +int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len, + const uint8_t *ephemeral_keypair) { if (this->handshake_ != nullptr) { noise_handshakestate_free(this->handshake_); this->handshake_ = nullptr; @@ -54,6 +55,13 @@ int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, siz HANDSHAKE_STEP_LOG("noise_handshakestate_set_prologue", err); return this->fail_init_(err); } + if (ephemeral_keypair != nullptr) { + err = noise_handshakestate_set_local_ephemeral(this->handshake_, ephemeral_keypair, 32, ephemeral_keypair + 32, 32); + if (err != 0) { + HANDSHAKE_STEP_LOG("noise_handshakestate_set_local_ephemeral", err); + return this->fail_init_(err); + } + } err = noise_handshakestate_start(this->handshake_); if (err != 0) { HANDSHAKE_STEP_LOG("noise_handshakestate_start", err); diff --git a/esphome/components/noise/noise_handshake.h b/esphome/components/noise/noise_handshake.h index 30596f35c2..7bd0fc7c95 100644 --- a/esphome/components/noise/noise_handshake.h +++ b/esphome/components/noise/noise_handshake.h @@ -38,7 +38,11 @@ class NoiseResponderHandshake { /// Create and start the handshake with the given PSK and prologue. A /// repeated call frees the previous handshake state and starts over. - [[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len); + /// ephemeral_keypair, when not null, is a key pair from + /// take_spare_ephemeral() that the handshake uses as its ephemeral key + /// instead of generating one. + [[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len, + const uint8_t *ephemeral_keypair = nullptr); /// ACTION_FAILED is the catch-all: returned before init(), after split() /// has released the state, and when noise-c reports a failed handshake. [[nodiscard]] Action action() const;