diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 61ad21964f..38d9180c5a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -902,8 +902,7 @@ message NoiseResumeTicket { option (source) = SOURCE_SERVER; option (ifdef) = "USE_API_NOISE"; - bytes session_id = 1; // 8 bytes - bytes secret = 2; // 32 bytes + bytes ticket = 1; // session_id(8) || secret(32) } // ==================== HOMEASSISTANT.SERVICE ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ec30efd881..229d1e81d0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1763,12 +1763,9 @@ void APIConnection::send_resume_ticket_() { return; } NoiseResumeTicket msg; - msg.set_session_id(ticket.session_id, noise::RESUME_SESSION_ID_SIZE); - msg.set_secret(ticket.secret, noise::RESUME_SECRET_SIZE); - if (!this->send_message(msg)) { - // Harmless: the client simply does a full handshake next time - API_LOG_MSG_DROPPED(TAG, "Resume ticket"); - } + msg.set_ticket(reinterpret_cast(&ticket), sizeof(ticket)); + // A dropped ticket is harmless: the client does a full handshake next time + static_cast(this->send_message(msg)); noise_clean(&ticket, sizeof(ticket)); } #endif diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 03fcfbe313..1c60bb87a5 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -283,7 +283,6 @@ class APIFrameHelper { CLOSED = 6, FAILED = 7, EXPLICIT_REJECT = 8, // Noise only - RESUME_DISCARD = 9, // Noise only: session resumed, discard the in-flight handshake msg 1 }; // Fast inline state check for read_packet/write_protobuf_messages hot path. diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 670b026127..9f0d1743fa 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -252,8 +252,6 @@ APIError APINoiseFrameHelper::state_action_() { return this->state_action_server_hello_(); case State::HANDSHAKE: return this->state_action_handshake_(); - case State::RESUME_DISCARD: - return this->state_action_resume_discard_(); case State::CLOSED: case State::FAILED: return APIError::BAD_STATE; @@ -323,18 +321,20 @@ APIError APINoiseFrameHelper::state_action_server_hello_() { return aerr; if (resume) { - this->prologue_.release(); + // A resuming client waits for this hello instead of pipelining + // handshake message 1, so the transport is ready now this->frame_footer_size_ = noise_cipherstate_get_mac_length(this->send_cipher_); - state_ = State::RESUME_DISCARD; - return APIError::OK; + HELPER_LOG("Session resumed!"); + state_ = State::DATA; + } else { + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + state_ = State::HANDSHAKE; } - - // start handshake - aerr = init_handshake_(); - if (aerr != APIError::OK) - return aerr; - - state_ = State::HANDSHAKE; + // init_handshake_ copied the prologue into the handshake state; the resume + // path is done with it too + this->prologue_.release(); return APIError::OK; } APIError APINoiseFrameHelper::state_action_handshake_() { @@ -349,41 +349,20 @@ APIError APINoiseFrameHelper::state_action_handshake_() { HELPER_LOG("Bad action for handshake: %d", (int) action); return APIError::HANDSHAKESTATE_BAD_STATE; } -/// Read one handshake frame into rx_buf_ and validate its status byte. -APIError APINoiseFrameHelper::read_handshake_frame_() { +APIError APINoiseFrameHelper::state_action_handshake_read_() { APIError aerr = this->try_read_frame_(); if (aerr != APIError::OK) { return this->handle_handshake_frame_error_(aerr); } + if (this->rx_buf_.empty()) { this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } - if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) { + } else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) { HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } - return APIError::OK; -} - -/// Resumed session: discard the client's already-in-flight handshake -/// message 1, then enter DATA. -APIError APINoiseFrameHelper::state_action_resume_discard_() { - APIError aerr = this->read_handshake_frame_(); - if (aerr != APIError::OK) { - return aerr; - } - HELPER_LOG("Session resumed!"); - state_ = State::DATA; - return APIError::OK; -} - -APIError APINoiseFrameHelper::state_action_handshake_read_() { - APIError aerr = this->read_handshake_frame_(); - if (aerr != APIError::OK) { - return aerr; - } int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); if (err != 0) { @@ -583,8 +562,6 @@ APIError APINoiseFrameHelper::init_handshake_() { APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; - // init copies the prologue into the handshakestate, so we can get rid of it now - prologue_.release(); return APIError::OK; } diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 5ddd38a293..05060c77de 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -42,8 +42,6 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError state_action_handshake_(); APIError state_action_handshake_read_(); APIError state_action_handshake_write_(); - APIError state_action_resume_discard_(); - APIError read_handshake_frame_(); APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type, diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 125a137ef5..37ae7e2b24 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1061,14 +1061,12 @@ uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { } uint8_t *NoiseResumeTicket::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->session_id_ptr_, this->session_id_len_); - ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->secret_ptr_, this->secret_len_); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->ticket_ptr_, this->ticket_len_); return pos; } uint32_t NoiseResumeTicket::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->session_id_len_); - size += ProtoSize::calc_length(1, this->secret_len_); + size += ProtoSize::calc_length(1, this->ticket_len_); return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 1b44587561..7a9728b44a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1136,21 +1136,15 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { class NoiseResumeTicket final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 152; - static constexpr uint8_t ESTIMATED_SIZE = 38; + static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("noise_resume_ticket"); } #endif - const uint8_t *session_id_ptr_{nullptr}; - size_t session_id_len_{0}; - void set_session_id(const uint8_t *data, size_t len) { - this->session_id_ptr_ = data; - this->session_id_len_ = len; - } - const uint8_t *secret_ptr_{nullptr}; - size_t secret_len_{0}; - void set_secret(const uint8_t *data, size_t len) { - this->secret_ptr_ = data; - this->secret_len_ = len; + const uint8_t *ticket_ptr_{nullptr}; + size_t ticket_len_{0}; + void set_ticket(const uint8_t *data, size_t len) { + this->ticket_ptr_ = data; + this->ticket_len_ = len; } uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; diff --git a/esphome/components/noise/noise_resume.cpp b/esphome/components/noise/noise_resume.cpp index a027589408..a78d6437dd 100644 --- a/esphome/components/noise/noise_resume.cpp +++ b/esphome/components/noise/noise_resume.cpp @@ -8,49 +8,45 @@ namespace esphome::noise { -/// Noise-construction HKDF-SHA256 keyed with the ticket secret. When -/// hash_in is given, SHA256(hash_in) is written to hash_out first with the -/// same hash state (hash_out is expected to point inside data). -static bool resume_hkdf(const uint8_t *secret, const uint8_t *data, size_t data_len, uint8_t *out1, size_t out1_len, - uint8_t *out2, size_t out2_len, const uint8_t *hash_in = nullptr, size_t hash_in_len = 0, - uint8_t *hash_out = nullptr) { +/// Noise-construction HKDF-SHA256 keyed with the ticket secret over +/// label || a || b [|| SHA256(hash_in)]. out2 == nullptr means MAC only. +static bool resume_kdf(const uint8_t *secret, const char *label, size_t label_len, const uint8_t *a, size_t a_len, + const uint8_t *b, size_t b_len, const uint8_t *hash_in, size_t hash_in_len, uint8_t *out1, + size_t out1_len, uint8_t *out2) { + // largest use is "confirm"(7) + 16 + 16, or "keys"(4) + 16 + 16 + 32 + uint8_t data[7 + RESUME_NONCE_SIZE + RESUME_NONCE_SIZE + 32]; + uint8_t scratch[32]; + size_t len = label_len + a_len + b_len; + std::memcpy(data, label, label_len); // NOLINT(bugprone-not-null-terminated-result) + std::memcpy(data + label_len, a, a_len); + std::memcpy(data + label_len + a_len, b, b_len); NoiseHashState *hash = nullptr; if (noise_hashstate_new_by_id(&hash, NOISE_HASH_SHA256) != NOISE_ERROR_NONE) { return false; } int err = NOISE_ERROR_NONE; if (hash_in != nullptr) { - err = noise_hashstate_hash_one(hash, hash_in, hash_in_len, hash_out, 32); + err = noise_hashstate_hash_one(hash, hash_in, hash_in_len, data + len, 32); + len += 32; } if (err == NOISE_ERROR_NONE) { - err = noise_hashstate_hkdf(hash, secret, RESUME_SECRET_SIZE, data, data_len, out1, out1_len, out2, out2_len); + err = noise_hashstate_hkdf(hash, secret, RESUME_SECRET_SIZE, data, len, out1, out1_len, + out2 != nullptr ? out2 : scratch, 32); } noise_hashstate_free(hash); + noise_clean(data, sizeof(data)); + noise_clean(scratch, sizeof(scratch)); return err == NOISE_ERROR_NONE; } -/// MAC = HKDF(secret, label || a || b).out1[:16] -static bool resume_mac(const uint8_t *secret, const char *label, size_t label_len, const uint8_t *a, size_t a_len, - const uint8_t *b, size_t b_len, uint8_t *out_mac) { - // largest use is "confirm"(7) + 16 + 16 = 39 - uint8_t data[7 + RESUME_NONCE_SIZE + RESUME_NONCE_SIZE]; - uint8_t scratch[32]; - std::memcpy(data, label, label_len); // NOLINT(bugprone-not-null-terminated-result) - std::memcpy(data + label_len, a, a_len); - std::memcpy(data + label_len + a_len, b, b_len); - bool ok = resume_hkdf(secret, data, label_len + a_len + b_len, out_mac, RESUME_MAC_SIZE, scratch, sizeof(scratch)); - noise_clean(scratch, sizeof(scratch)); - return ok; -} - bool ResumeTicketCache::issue(ResumeTicket &out) { - if (!random_bytes(out.session_id, RESUME_SESSION_ID_SIZE) || !random_bytes(out.secret, RESUME_SECRET_SIZE)) { + if (!random_bytes(reinterpret_cast(&out), sizeof(out))) { return false; } uint8_t slot = this->next_; this->next_ = static_cast((slot + 1) % SLOTS); this->slots_[slot] = out; - this->used_[slot] = true; + this->used_mask_ |= static_cast(1u << slot); return true; } @@ -65,7 +61,7 @@ bool ResumeTicketCache::try_accept(const uint8_t *offer, size_t offer_len, const bool ok = false; for (uint8_t i = 0; i < SLOTS && !ok; i++) { ResumeTicket &slot = this->slots_[i]; - if (!this->used_[i] || std::memcmp(slot.session_id, session_id, RESUME_SESSION_ID_SIZE) != 0) { + if (!(this->used_mask_ & (1u << i)) || std::memcmp(slot.session_id, session_id, RESUME_SESSION_ID_SIZE) != 0) { continue; } uint8_t expected[RESUME_MAC_SIZE]; @@ -78,7 +74,7 @@ bool ResumeTicketCache::try_accept(const uint8_t *offer, size_t offer_len, const } std::memcpy(secret, slot.secret, RESUME_SECRET_SIZE); noise_clean(&slot, sizeof(slot)); - this->used_[i] = false; + this->used_mask_ &= static_cast(~(1u << i)); } if (!ok) { return false; @@ -111,30 +107,25 @@ bool ResumeTicketCache::try_accept(const uint8_t *offer, size_t offer_len, const void ResumeTicketCache::clear() { noise_clean(this->slots_, sizeof(this->slots_)); - std::memset(this->used_, 0, sizeof(this->used_)); + this->used_mask_ = 0; } bool resume_compute_offer_mac(const uint8_t *secret, const uint8_t *session_id, const uint8_t *client_nonce, uint8_t *out_mac) { - return resume_mac(secret, "offer", 5, session_id, RESUME_SESSION_ID_SIZE, client_nonce, RESUME_NONCE_SIZE, out_mac); + return resume_kdf(secret, "offer", 5, session_id, RESUME_SESSION_ID_SIZE, client_nonce, RESUME_NONCE_SIZE, nullptr, 0, + out_mac, RESUME_MAC_SIZE, nullptr); } bool resume_compute_confirm_mac(const uint8_t *secret, const uint8_t *client_nonce, const uint8_t *server_nonce, uint8_t *out_mac) { - return resume_mac(secret, "confirm", 7, client_nonce, RESUME_NONCE_SIZE, server_nonce, RESUME_NONCE_SIZE, out_mac); + return resume_kdf(secret, "confirm", 7, client_nonce, RESUME_NONCE_SIZE, server_nonce, RESUME_NONCE_SIZE, nullptr, 0, + out_mac, RESUME_MAC_SIZE, nullptr); } bool resume_derive_keys(const uint8_t *secret, const uint8_t *client_nonce, const uint8_t *server_nonce, const uint8_t *prologue, size_t prologue_len, uint8_t *k_c2d, uint8_t *k_d2c) { - // "keys"(4) || client_nonce(16) || server_nonce(16) || SHA256(prologue)(32) - uint8_t data[4 + RESUME_NONCE_SIZE + RESUME_NONCE_SIZE + 32]; - std::memcpy(data, "keys", 4); // NOLINT(bugprone-not-null-terminated-result) - std::memcpy(data + 4, client_nonce, RESUME_NONCE_SIZE); - std::memcpy(data + 4 + RESUME_NONCE_SIZE, server_nonce, RESUME_NONCE_SIZE); - bool ok = resume_hkdf(secret, data, sizeof(data), k_c2d, 32, k_d2c, 32, prologue, prologue_len, - data + 4 + 2 * RESUME_NONCE_SIZE); - noise_clean(data, sizeof(data)); - return ok; + return resume_kdf(secret, "keys", 4, client_nonce, RESUME_NONCE_SIZE, server_nonce, RESUME_NONCE_SIZE, prologue, + prologue_len, k_c2d, 32, k_d2c); } NoiseCipherState *resume_make_cipher(const uint8_t *key) { diff --git a/esphome/components/noise/noise_resume.h b/esphome/components/noise/noise_resume.h index a75b00ec0e..dc9371097f 100644 --- a/esphome/components/noise/noise_resume.h +++ b/esphome/components/noise/noise_resume.h @@ -46,6 +46,8 @@ struct ResumeTicket { uint8_t session_id[RESUME_SESSION_ID_SIZE]; uint8_t secret[RESUME_SECRET_SIZE]; }; +// Sent on the wire as one blob: session_id || secret +static_assert(sizeof(ResumeTicket) == RESUME_SESSION_ID_SIZE + RESUME_SECRET_SIZE, "ticket must be packed"); /// Fixed-slot RAM cache of single-use resume tickets. Lost on reboot by /// design: clients fall back to a full handshake. @@ -69,7 +71,7 @@ class ResumeTicketCache { protected: ResumeTicket slots_[SLOTS]; - bool used_[SLOTS]{}; + uint8_t used_mask_{0}; uint8_t next_{0}; }; diff --git a/tests/components/noise/test_noise_resume.cpp b/tests/components/noise/test_noise_resume.cpp index 692e16239b..64aa2b3529 100644 --- a/tests/components/noise/test_noise_resume.cpp +++ b/tests/components/noise/test_noise_resume.cpp @@ -49,7 +49,7 @@ struct TestCache : ResumeTicketCache { void plant(const uint8_t *session_id, const uint8_t *secret) { std::memcpy(this->slots_[0].session_id, session_id, RESUME_SESSION_ID_SIZE); std::memcpy(this->slots_[0].secret, secret, RESUME_SECRET_SIZE); - this->used_[0] = true; + this->used_mask_ |= 1u; } };