diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 806131a11b..670b026127 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -284,10 +284,6 @@ APIError APINoiseFrameHelper::state_action_client_hello_() { APIError APINoiseFrameHelper::state_action_server_hello_() { // A verified resume offer (still in rx_buf_ from the client hello step) // replaces the whole handshake; any failure falls back to the full one. - uint8_t resume_ext[noise::RESUME_ACCEPT_SIZE]; - bool resume = this->ctx_.resume_cache().try_accept(this->rx_buf_.data(), this->rx_buf_.size(), this->prologue_.data(), - this->prologue_.size(), resume_ext, send_cipher_, recv_cipher_); - // send server hello const auto &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; @@ -314,8 +310,11 @@ APIError APINoiseFrameHelper::state_action_server_hello_() { // node mac, terminated by null byte std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE); + // The accept extension is written straight after the mac + bool resume = + this->ctx_.resume_cache().try_accept(this->rx_buf_.data(), this->rx_buf_.size(), this->prologue_.data(), + this->prologue_.size(), msg + total_size, send_cipher_, recv_cipher_); if (resume) { - std::memcpy(msg + total_size, resume_ext, noise::RESUME_ACCEPT_SIZE); total_size += noise::RESUME_ACCEPT_SIZE; } @@ -350,36 +349,40 @@ APIError APINoiseFrameHelper::state_action_handshake_() { HELPER_LOG("Bad action for handshake: %d", (int) action); return APIError::HANDSHAKESTATE_BAD_STATE; } -/// Resumed session: discard the client's already-in-flight handshake -/// message 1, then enter DATA. -APIError APINoiseFrameHelper::state_action_resume_discard_() { +/// Read one handshake frame into rx_buf_ and validate its status byte. +APIError APINoiseFrameHelper::read_handshake_frame_() { APIError aerr = this->try_read_frame_(); if (aerr != APIError::OK) { return this->handle_handshake_frame_error_(aerr); } - if (this->rx_buf_.empty() || this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) { - state_ = State::FAILED; - HELPER_LOG("Bad discarded handshake message"); + 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) { + 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->try_read_frame_(); + APIError aerr = this->read_handshake_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; - } 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 aerr; } int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index feb706596f..5ddd38a293 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -43,6 +43,7 @@ class APINoiseFrameHelper final : public APIFrameHelper { 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_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 9661e36792..77c81d707f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1373,9 +1373,7 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { return out.c_str(); } const char *NoiseResumeTicket::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseResumeTicket")); - dump_bytes_field(out, ESPHOME_PSTR("session_id"), this->session_id_ptr_, this->session_id_len_); - dump_bytes_field(out, ESPHOME_PSTR("secret"), this->secret_ptr_, this->secret_len_); + out.append_p(ESPHOME_PSTR("NoiseResumeTicket {}")); return out.c_str(); } #endif diff --git a/esphome/components/noise/noise_resume.cpp b/esphome/components/noise/noise_resume.cpp index 3abdbfc468..a027589408 100644 --- a/esphome/components/noise/noise_resume.cpp +++ b/esphome/components/noise/noise_resume.cpp @@ -8,30 +8,37 @@ namespace esphome::noise { -/// Noise-construction HKDF-SHA256; out2 may be scratch the caller wipes. -static bool resume_hkdf(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t *out1, - size_t out1_len, uint8_t *out2, size_t out2_len) { +/// 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) { NoiseHashState *hash = nullptr; if (noise_hashstate_new_by_id(&hash, NOISE_HASH_SHA256) != NOISE_ERROR_NONE) { return false; } - int err = noise_hashstate_hkdf(hash, key, key_len, data, data_len, out1, out1_len, out2, out2_len); + int err = NOISE_ERROR_NONE; + if (hash_in != nullptr) { + err = noise_hashstate_hash_one(hash, hash_in, hash_in_len, hash_out, 32); + } + if (err == NOISE_ERROR_NONE) { + err = noise_hashstate_hkdf(hash, secret, RESUME_SECRET_SIZE, data, data_len, out1, out1_len, out2, out2_len); + } noise_hashstate_free(hash); return err == NOISE_ERROR_NONE; } -template -static bool resume_mac(const uint8_t *secret, const char (&label)[N], const uint8_t *a, size_t a_len, const uint8_t *b, - size_t b_len, uint8_t *out_mac) { - constexpr size_t label_len = N - 1; // drop the terminating NUL - // label || a || b, largest use is "confirm"(7) + 16 + 16 = 39 +/// 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); + 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, RESUME_SECRET_SIZE, data, label_len + a_len + b_len, out_mac, RESUME_MAC_SIZE, scratch, - sizeof(scratch)); + 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; } @@ -47,51 +54,44 @@ bool ResumeTicketCache::issue(ResumeTicket &out) { return true; } -bool ResumeTicketCache::take_verified_(const uint8_t *offer, uint8_t *secret_out) { - const uint8_t *session_id = offer + RESUME_OFFER_SESSION_ID_OFFSET; - const uint8_t *client_nonce = offer + RESUME_OFFER_NONCE_OFFSET; - const uint8_t *offer_mac = offer + RESUME_OFFER_MAC_OFFSET; - for (uint8_t i = 0; i < SLOTS; i++) { - ResumeTicket &slot = this->slots_[i]; - if (!this->used_[i] || std::memcmp(slot.session_id, session_id, RESUME_SESSION_ID_SIZE) != 0) { - continue; - } - uint8_t expected[RESUME_MAC_SIZE]; - bool ok = resume_compute_offer_mac(slot.secret, session_id, client_nonce, expected) && - noise_is_equal(expected, offer_mac, RESUME_MAC_SIZE); - noise_clean(expected, sizeof(expected)); - if (!ok) { - // Bad MAC: leave the ticket so a forger cannot burn it - return false; - } - std::memcpy(secret_out, slot.secret, RESUME_SECRET_SIZE); - noise_clean(&slot, sizeof(slot)); - this->used_[i] = false; - return true; - } - return false; -} - bool ResumeTicketCache::try_accept(const uint8_t *offer, size_t offer_len, const uint8_t *prologue, size_t prologue_len, uint8_t *out_ext, NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher) { if (offer_len != RESUME_OFFER_SIZE || offer[0] != RESUME_OFFER_VERSION) { return false; } + const uint8_t *session_id = offer + RESUME_OFFER_SESSION_ID_OFFSET; + const uint8_t *client_nonce = offer + RESUME_OFFER_NONCE_OFFSET; uint8_t secret[RESUME_SECRET_SIZE]; - if (!this->take_verified_(offer, secret)) { + 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) { + continue; + } + uint8_t expected[RESUME_MAC_SIZE]; + ok = resume_compute_offer_mac(slot.secret, session_id, client_nonce, expected) && + noise_is_equal(expected, offer + RESUME_OFFER_MAC_OFFSET, RESUME_MAC_SIZE); + noise_clean(expected, sizeof(expected)); + if (!ok) { + // Bad MAC: leave the ticket so a forger cannot burn it + return false; + } + std::memcpy(secret, slot.secret, RESUME_SECRET_SIZE); + noise_clean(&slot, sizeof(slot)); + this->used_[i] = false; + } + if (!ok) { return false; } // From here every failure still falls back to the full handshake; the // ticket is spent, which is harmless (the client gets a fresh one). - const uint8_t *client_nonce = offer + RESUME_OFFER_NONCE_OFFSET; uint8_t *server_nonce = out_ext + 1; - uint8_t *confirm_mac = out_ext + 1 + RESUME_NONCE_SIZE; uint8_t k_c2d[32]; uint8_t k_d2c[32]; out_ext[0] = RESUME_ACCEPT_VERSION; - bool ok = random_bytes(server_nonce, RESUME_NONCE_SIZE) && - resume_compute_confirm_mac(secret, client_nonce, server_nonce, confirm_mac) && - resume_derive_keys(secret, client_nonce, server_nonce, prologue, prologue_len, k_c2d, k_d2c); + ok = random_bytes(server_nonce, RESUME_NONCE_SIZE) && + resume_compute_confirm_mac(secret, client_nonce, server_nonce, out_ext + 1 + RESUME_NONCE_SIZE) && + resume_derive_keys(secret, client_nonce, server_nonce, prologue, prologue_len, k_c2d, k_d2c); noise_clean(secret, sizeof(secret)); if (ok) { recv_cipher = resume_make_cipher(k_c2d); @@ -116,32 +116,25 @@ void ResumeTicketCache::clear() { 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", session_id, RESUME_SESSION_ID_SIZE, client_nonce, RESUME_NONCE_SIZE, out_mac); + return resume_mac(secret, "offer", 5, session_id, RESUME_SESSION_ID_SIZE, client_nonce, RESUME_NONCE_SIZE, out_mac); } 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", client_nonce, RESUME_NONCE_SIZE, server_nonce, RESUME_NONCE_SIZE, out_mac); + return resume_mac(secret, "confirm", 7, client_nonce, RESUME_NONCE_SIZE, server_nonce, RESUME_NONCE_SIZE, out_mac); } 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) { - NoiseHashState *hash = nullptr; - if (noise_hashstate_new_by_id(&hash, NOISE_HASH_SHA256) != NOISE_ERROR_NONE) { - return false; - } // "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); - int err = noise_hashstate_hash_one(hash, prologue, prologue_len, data + 4 + 2 * RESUME_NONCE_SIZE, 32); - if (err == NOISE_ERROR_NONE) { - err = noise_hashstate_hkdf(hash, secret, RESUME_SECRET_SIZE, data, sizeof(data), k_c2d, 32, k_d2c, 32); - } - noise_hashstate_free(hash); + 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 err == NOISE_ERROR_NONE; + return ok; } 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 a2dd6ca8b6..a75b00ec0e 100644 --- a/esphome/components/noise/noise_resume.h +++ b/esphome/components/noise/noise_resume.h @@ -68,8 +68,6 @@ class ResumeTicketCache { static constexpr uint8_t SLOTS = 2; protected: - bool take_verified_(const uint8_t *offer, uint8_t *secret_out); - ResumeTicket slots_[SLOTS]; bool used_[SLOTS]{}; uint8_t next_{0}; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dc3dd4b868..5d909da81c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2799,6 +2799,10 @@ def build_message_type( public_content.append(prot) # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used + # Sensitive messages (keys, tickets) dump only their name + if desc.name in SENSITIVE_MESSAGES: + dump = [] + # dump_to method declaration in header prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" prot += "const char *dump_to(DumpBuffer &out) const override;\n" @@ -2845,6 +2849,9 @@ def build_message_type( return out, cpp, dump_cpp +# Messages whose contents are secret; dump_to prints only the name +SENSITIVE_MESSAGES = {"NoiseResumeTicket"} + SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2