[noise] Consume the spare key inside the handshake and let noise own its define

NoiseResponderHandshake::init() now hands the slot straight to noise-c and
wipes it, so the api and ota call sites are unchanged, nothing copies the
key pair and no transport has to remember the wipe. The slot compiles
under USE_NOISE_SPARE_EPHEMERAL, which the api component enables as the
refiller, instead of the noise component keying on an api define. The
per tick check sits in loop() with the refill out of line, and the grace
predicate lives next to the handshake timeout it mirrors.
This commit is contained in:
J. Nick Koston
2026-09-07 12:33:50 +02:00
parent 866574ca14
commit 228f8894a9
15 changed files with 89 additions and 123 deletions
+1 -2
View File
@@ -10,7 +10,6 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config):
await real_to_code(config)
# The spare ephemeral slot only exists in builds with an encrypted api
cg.add_define("USE_API_NOISE")
cg.add_define("USE_NOISE_SPARE_EPHEMERAL")
manifest.to_code = to_code_testing
+22 -46
View File
@@ -3,7 +3,6 @@
#include <cstring>
#include <noise/protocol.h>
#include <sodium.h>
#include "esphome/components/noise/noise.h"
#include "esphome/components/noise/noise_handshake.h"
@@ -158,59 +157,36 @@ TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
noise_cipherstate_free(recv_cipher);
}
TEST(SpareEphemeralTest, EmptySlotHandsOutNothing) {
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());
EXPECT_FALSE(take_spare_ephemeral(out));
}
TEST(SpareEphemeralTest, KeyPairIsHandedOutExactlyOnce) {
ephemeral_keypair_t out;
take_spare_ephemeral(out);
prepare_spare_ephemeral();
ASSERT_TRUE(has_spare_ephemeral());
ASSERT_TRUE(take_spare_ephemeral(out));
// Taken once: the slot is empty and a second take gets nothing
EXPECT_FALSE(has_spare_ephemeral());
EXPECT_FALSE(take_spare_ephemeral(out));
// 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.data()), 0);
EXPECT_EQ(std::memcmp(check, out.data() + EPHEMERAL_PRIVATE_KEY_SIZE, EPHEMERAL_PUBLIC_KEY_SIZE), 0);
}
TEST(SpareEphemeralTest, SuppliedKeyPairCompletesHandshakeAndIsTheKeyOnTheWire) {
ephemeral_keypair_t spare;
take_spare_ephemeral(spare);
prepare_spare_ephemeral();
ASSERT_TRUE(take_spare_ephemeral(spare));
// Drive one full NNpsk0 handshake between a fresh initiator and responder
static void run_handshake(NoiseResponderHandshake &responder) {
const psk_t psk = make_psk(7);
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE), spare.data()), 0);
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0);
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
uint8_t msg[MAX_HANDSHAKE_SIZE];
size_t msg_len = initiator.write_message(msg, sizeof(msg));
ASSERT_EQ(responder.read_message(msg, msg_len), 0);
size_t reply_len = 0;
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.data() + EPHEMERAL_PRIVATE_KEY_SIZE, EPHEMERAL_PUBLIC_KEY_SIZE), 0);
ASSERT_EQ(initiator.read_message(msg, reply_len), 0);
initiator.split();
NoiseCipherState *send_cipher = nullptr;
NoiseCipherState *recv_cipher = nullptr;
ASSERT_EQ(responder.split(send_cipher, recv_cipher), 0);
ASSERT_NE(send_cipher, nullptr);
noise_cipherstate_free(send_cipher);
noise_cipherstate_free(recv_cipher);
ASSERT_EQ(responder.action(), Action::ACTION_SPLIT);
}
TEST(SpareEphemeralTest, EmptySlotLeavesHandshakeToGenerate) {
NoiseResponderHandshake responder;
run_handshake(responder);
EXPECT_FALSE(has_spare_ephemeral());
}
TEST(SpareEphemeralTest, SlotIsConsumedByExactlyOneHandshake) {
prepare_spare_ephemeral();
ASSERT_TRUE(has_spare_ephemeral());
NoiseResponderHandshake first;
run_handshake(first);
// Consumed: the next handshake finds no spare and still completes
EXPECT_FALSE(has_spare_ephemeral());
NoiseResponderHandshake second;
run_handshake(second);
EXPECT_FALSE(has_spare_ephemeral());
}
TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {