[noise] Assert the spare key reaches the wire, drop the ESP8266 warning threshold

The gtest now checks that the responder's message carries the slot's
public key and that the next handshake uses a different one, so a
silently refused spare cannot pass. The api refill sites are guarded by
the feature define they use. The ESP8266 blocking threshold change is
left to a separate core change for operations that cannot be shortened.
This commit is contained in:
J. Nick Koston
2026-09-07 16:35:54 +02:00
parent a595590386
commit bf718a28b9
3 changed files with 48 additions and 16 deletions
+2 -7
View File
@@ -41,11 +41,6 @@ void APIServer::setup() {
ControllerRegistry::register_controller(this);
#ifdef USE_API_NOISE
#ifdef USE_ESP8266
// The spare ephemeral refill blocks ~60 ms here and shares the pass with
// the client loops; keep the whole pass under the blocking warning
this->warn_if_blocking_over_ = 10; // centiseconds
#endif
// Always reserve the slot: flash preferences are positional on esp8266, so
// a yaml key build must keep the layout of a runtime key build
uint32_t hash = 88491486UL;
@@ -150,7 +145,7 @@ void APIServer::loop() {
// Checked once per pass for the refill and for the clients below
const bool connected = network::is_connected();
#ifdef USE_API_NOISE
#ifdef USE_NOISE_SPARE_EPHEMERAL
// Only the flag test is inline; refilling is the rare path
if (connected && !noise::has_spare_ephemeral()) {
this->refill_spare_ephemeral_();
@@ -201,7 +196,7 @@ void APIServer::loop() {
}
}
#ifdef USE_API_NOISE
#ifdef USE_NOISE_SPARE_EPHEMERAL
// Called with the network up; refill only while no api client is still
// connecting (an OTA handshake is not visible here and just pays the refill
// it triggered).
+3 -1
View File
@@ -363,8 +363,10 @@ class APIServer final : public Component,
uint8_t provisioning_source_{0};
#endif
#ifdef USE_API_NOISE
#ifdef USE_NOISE_SPARE_EPHEMERAL
void refill_spare_ephemeral_();
#endif
#ifdef USE_API_NOISE
noise::NoiseContext noise_ctx_;
#ifndef USE_API_NOISE_PSK_FROM_YAML
SavedNoisePsk saved_psk_{}; // backs noise_ctx_ for a runtime provisioned key
@@ -157,8 +157,11 @@ TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
noise_cipherstate_free(recv_cipher);
}
// Drive one full NNpsk0 handshake between a fresh initiator and responder
static void run_handshake(NoiseResponderHandshake &responder) {
// Drive one full NNpsk0 handshake between a fresh initiator and responder;
// responder_e receives the ephemeral public key the responder put on the
// wire (the clear text start of its message, taken before the initiator
// consumes the buffer in place)
static void run_handshake(NoiseResponderHandshake &responder, uint8_t responder_e[32]) {
const psk_t psk = make_psk(7);
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0);
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
@@ -167,25 +170,57 @@ static void run_handshake(NoiseResponderHandshake &responder) {
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);
ASSERT_GE(reply_len, 32u);
std::memcpy(responder_e, msg, 32);
ASSERT_EQ(initiator.read_message(msg, reply_len), 0);
ASSERT_EQ(responder.action(), Action::ACTION_SPLIT);
}
TEST(SpareEphemeralTest, EmptySlotLeavesHandshakeToGenerate) {
NoiseResponderHandshake responder;
run_handshake(responder);
uint8_t responder_e[32];
run_handshake(responder, responder_e);
EXPECT_FALSE(has_spare_ephemeral());
}
TEST(SpareEphemeralTest, SlotIsConsumedByExactlyOneHandshake) {
TEST(SpareEphemeralTest, ConsumeHandsTheKeyToANewState) {
prepare_spare_ephemeral();
ASSERT_TRUE(has_spare_ephemeral());
NoiseResponderHandshake first;
run_handshake(first);
// Consumed: the next handshake finds no spare and still completes
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
NoiseHandshakeState *state = nullptr;
ASSERT_EQ(noise_handshakestate_new_by_id(&state, &nid, NOISE_ROLE_RESPONDER), 0);
const psk_t psk = make_psk(7);
ASSERT_EQ(noise_handshakestate_set_pre_shared_key(state, psk.data(), psk.size()), 0);
ASSERT_EQ(noise_handshakestate_set_prologue(state, PROLOGUE, sizeof(PROLOGUE)), 0);
EXPECT_EQ(consume_spare_ephemeral(state), 0);
EXPECT_FALSE(has_spare_ephemeral());
noise_handshakestate_free(state);
}
TEST(SpareEphemeralTest, SlotKeyIsOnTheWireAndConsumedOnce) {
prepare_spare_ephemeral();
ASSERT_TRUE(has_spare_ephemeral());
uint8_t expected_pub[32];
std::memcpy(expected_pub, spare_ephemeral + 32, sizeof(expected_pub));
NoiseResponderHandshake first;
uint8_t responder_e[32];
run_handshake(first, responder_e);
// The spare, not a generated key, went out; and it went out once
EXPECT_EQ(std::memcmp(responder_e, expected_pub, sizeof(expected_pub)), 0);
EXPECT_FALSE(has_spare_ephemeral());
NoiseResponderHandshake second;
run_handshake(second);
run_handshake(second, responder_e);
EXPECT_NE(std::memcmp(responder_e, expected_pub, sizeof(expected_pub)), 0);
EXPECT_FALSE(has_spare_ephemeral());
}