mirror of
https://github.com/esphome/esphome.git
synced 2026-08-29 17:16:45 +00:00
[noise] Trim resume flash: one KDF, no discard state, packed ticket
This commit is contained in:
@@ -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 ====================
|
||||
|
||||
@@ -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<const uint8_t *>(&ticket), sizeof(ticket));
|
||||
// A dropped ticket is harmless: the client does a full handshake next time
|
||||
static_cast<void>(this->send_message(msg));
|
||||
noise_clean(&ticket, sizeof(ticket));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<uint8_t *>(&out), sizeof(out))) {
|
||||
return false;
|
||||
}
|
||||
uint8_t slot = this->next_;
|
||||
this->next_ = static_cast<uint8_t>((slot + 1) % SLOTS);
|
||||
this->slots_[slot] = out;
|
||||
this->used_[slot] = true;
|
||||
this->used_mask_ |= static_cast<uint8_t>(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<uint8_t>(~(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) {
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user