[noise] Hand the spare key pair out through a sized array

take_spare_ephemeral() fills an std::array of the key pair size, so a
caller cannot pass a buffer that is too small; the refill comment names
what the gate does not see and what the blocking warning does.
This commit is contained in:
J. Nick Koston
2026-09-06 22:42:16 +02:00
parent 40b374c944
commit 00b42c2d69
6 changed files with 26 additions and 22 deletions
@@ -549,10 +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_() {
uint8_t spare[noise::EPHEMERAL_KEYPAIR_SIZE];
const uint8_t *ephemeral = noise::take_spare_ephemeral(spare) ? spare : nullptr;
noise::ephemeral_keypair_t spare;
const uint8_t *ephemeral = noise::take_spare_ephemeral(spare) ? spare.data() : nullptr;
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size(), ephemeral);
sodium_memzero(spare, sizeof(spare));
sodium_memzero(spare.data(), spare.size());
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
+7 -4
View File
@@ -193,10 +193,13 @@ 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. On ESP8266 that is
// about 60 ms, over the 50 ms blocking warning; the same generation used to
// run inside the handshake, where it tripped the warning for longer.
// the network is up, and not while an api client is still in its handshake
// (an OTA handshake is not visible here; it took the previous spare and
// pays the refill instead, no worse than generating its own key). The
// generation blocks the loop for about 60 ms on ESP8266, over the 50 ms
// blocking warning, so the api component's warning threshold ratchets up
// on the first refill; the same generation used to run inside the
// handshake, where it tripped the warning for longer.
void APIServer::prepare_spare_ephemeral_() {
if (noise::has_spare_ephemeral() || !network::is_connected()) {
return;
@@ -75,14 +75,14 @@ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
// The api server keeps the spare; without an encrypted api there is none
const uint8_t *ephemeral = nullptr;
#ifdef USE_API_NOISE
uint8_t spare[noise::EPHEMERAL_KEYPAIR_SIZE];
noise::ephemeral_keypair_t spare;
if (noise::take_spare_ephemeral(spare)) {
ephemeral = spare;
ephemeral = spare.data();
}
#endif
int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue), ephemeral);
#ifdef USE_API_NOISE
sodium_memzero(spare, sizeof(spare));
sodium_memzero(spare.data(), spare.size());
#endif
if (err != 0) {
ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
+2 -2
View File
@@ -38,11 +38,11 @@ void prepare_spare_ephemeral() {
spare_ephemeral_ready = true;
}
bool take_spare_ephemeral(uint8_t *out) {
bool take_spare_ephemeral(ephemeral_keypair_t &out) {
if (!spare_ephemeral_ready) {
return false;
}
std::memcpy(out, spare_ephemeral, EPHEMERAL_KEYPAIR_SIZE);
std::memcpy(out.data(), spare_ephemeral, EPHEMERAL_KEYPAIR_SIZE);
sodium_memzero(spare_ephemeral, EPHEMERAL_KEYPAIR_SIZE);
spare_ephemeral_ready = false;
return true;
+4 -3
View File
@@ -42,6 +42,7 @@ const LogString *noise_err_to_logstr(int err);
static constexpr size_t EPHEMERAL_PRIVATE_KEY_SIZE = 32;
static constexpr size_t EPHEMERAL_PUBLIC_KEY_SIZE = 32;
static constexpr size_t EPHEMERAL_KEYPAIR_SIZE = EPHEMERAL_PRIVATE_KEY_SIZE + EPHEMERAL_PUBLIC_KEY_SIZE;
using ephemeral_keypair_t = std::array<uint8_t, EPHEMERAL_KEYPAIR_SIZE>;
#ifdef USE_API_NOISE
// A responder ephemeral key pair generated ahead of time. The base point
@@ -53,9 +54,9 @@ static constexpr size_t EPHEMERAL_KEYPAIR_SIZE = EPHEMERAL_PRIVATE_KEY_SIZE + EP
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);
/// Move the slot's key pair into out and empty the slot. Returns false,
/// leaving out untouched, when the slot is empty.
bool take_spare_ephemeral(ephemeral_keypair_t &out);
#endif
// Shared wire format for the noise transports (api and ota): every frame is
@@ -151,7 +151,7 @@ TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
}
TEST(SpareEphemeralTest, EmptySlotHandsOutNothing) {
uint8_t out[EPHEMERAL_KEYPAIR_SIZE];
ephemeral_keypair_t out;
// Drain whatever an earlier test left behind, then the slot must stay empty
take_spare_ephemeral(out);
EXPECT_FALSE(has_spare_ephemeral());
@@ -159,7 +159,7 @@ TEST(SpareEphemeralTest, EmptySlotHandsOutNothing) {
}
TEST(SpareEphemeralTest, KeyPairIsHandedOutExactlyOnce) {
uint8_t out[EPHEMERAL_KEYPAIR_SIZE];
ephemeral_keypair_t out;
take_spare_ephemeral(out);
prepare_spare_ephemeral();
ASSERT_TRUE(has_spare_ephemeral());
@@ -170,19 +170,19 @@ TEST(SpareEphemeralTest, KeyPairIsHandedOutExactlyOnce) {
// The pair is consistent: the public half is the base point multiple of the private half
uint8_t check[EPHEMERAL_PUBLIC_KEY_SIZE];
ASSERT_EQ(crypto_scalarmult_curve25519_base(check, out), 0);
EXPECT_EQ(std::memcmp(check, out + EPHEMERAL_PRIVATE_KEY_SIZE, EPHEMERAL_PUBLIC_KEY_SIZE), 0);
ASSERT_EQ(crypto_scalarmult_curve25519_base(check, out.data()), 0);
EXPECT_EQ(std::memcmp(check, out.data() + EPHEMERAL_PRIVATE_KEY_SIZE, EPHEMERAL_PUBLIC_KEY_SIZE), 0);
}
TEST(SpareEphemeralTest, SuppliedKeyPairCompletesHandshakeAndIsTheKeyOnTheWire) {
uint8_t spare[EPHEMERAL_KEYPAIR_SIZE];
ephemeral_keypair_t spare;
take_spare_ephemeral(spare);
prepare_spare_ephemeral();
ASSERT_TRUE(take_spare_ephemeral(spare));
const psk_t psk = make_psk(7);
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE), spare), 0);
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE), spare.data()), 0);
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
uint8_t msg[MAX_HANDSHAKE_SIZE];
@@ -193,7 +193,7 @@ TEST(SpareEphemeralTest, SuppliedKeyPairCompletesHandshakeAndIsTheKeyOnTheWire)
ASSERT_EQ(responder.write_message(msg, sizeof(msg), reply_len), 0);
// The responder's message starts with its ephemeral public key
ASSERT_GE(reply_len, static_cast<size_t>(EPHEMERAL_PUBLIC_KEY_SIZE));
EXPECT_EQ(std::memcmp(msg, spare + EPHEMERAL_PRIVATE_KEY_SIZE, EPHEMERAL_PUBLIC_KEY_SIZE), 0);
EXPECT_EQ(std::memcmp(msg, spare.data() + EPHEMERAL_PRIVATE_KEY_SIZE, EPHEMERAL_PUBLIC_KEY_SIZE), 0);
ASSERT_EQ(initiator.read_message(msg, reply_len), 0);
initiator.split();