diff --git a/tests/components/noise/__init__.py b/tests/components/noise/__init__.py new file mode 100644 index 0000000000..60a5740a83 --- /dev/null +++ b/tests/components/noise/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # to_code must run: it defines USE_NOISE and adds the noise-c library + # the component sources under test need. + manifest.enable_codegen() diff --git a/tests/components/noise/test_noise_handshake.cpp b/tests/components/noise/test_noise_handshake.cpp new file mode 100644 index 0000000000..691180b602 --- /dev/null +++ b/tests/components/noise/test_noise_handshake.cpp @@ -0,0 +1,191 @@ +#include + +#include + +#include + +#include "esphome/components/noise/noise.h" +#include "esphome/components/noise/noise_handshake.h" + +namespace esphome::noise::testing { + +using Action = NoiseResponderHandshake::Action; + +// A raw noise-c initiator driving the same Noise_NNpsk0_25519_ChaChaPoly_SHA256 +// pattern the responder class implements, so the tests exercise a real +// two-message handshake rather than mirrored calls into the class under test. +class Initiator { + public: + Initiator(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) { + 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, + }; + EXPECT_EQ(noise_handshakestate_new_by_id(&this->state_, &nid, NOISE_ROLE_INITIATOR), 0); + EXPECT_EQ(noise_handshakestate_set_pre_shared_key(this->state_, psk.data(), psk.size()), 0); + EXPECT_EQ(noise_handshakestate_set_prologue(this->state_, prologue, prologue_len), 0); + EXPECT_EQ(noise_handshakestate_start(this->state_), 0); + } + ~Initiator() { + if (this->state_ != nullptr) + noise_handshakestate_free(this->state_); + if (this->send_ != nullptr) + noise_cipherstate_free(this->send_); + if (this->recv_ != nullptr) + noise_cipherstate_free(this->recv_); + } + Initiator(const Initiator &) = delete; + Initiator &operator=(const Initiator &) = delete; + + size_t write_message(uint8_t *out, size_t capacity) { + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, out, capacity); + EXPECT_EQ(noise_handshakestate_write_message(this->state_, &mbuf, nullptr), 0); + return mbuf.size; + } + + int read_message(uint8_t *data, size_t len) { + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, data, len); + return noise_handshakestate_read_message(this->state_, &mbuf, nullptr); + } + + void split() { EXPECT_EQ(noise_handshakestate_split(this->state_, &this->send_, &this->recv_), 0); } + + NoiseCipherState *send_{nullptr}; + NoiseCipherState *recv_{nullptr}; + + private: + NoiseHandshakeState *state_{nullptr}; +}; + +static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'}; + +static psk_t make_psk(uint8_t seed) { + psk_t psk; + for (size_t i = 0; i < psk.size(); i++) { + psk[i] = static_cast(seed + i); + } + return psk; +} + +TEST(NoiseResponderHandshakeTest, ActionFailedBeforeInit) { + NoiseResponderHandshake handshake; + EXPECT_EQ(handshake.action(), Action::ACTION_FAILED); +} + +TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) { + // The class doc promises a noise-c error, not a crash, when the message + // methods run outside their action() step; pin the library's null check + NoiseResponderHandshake handshake; + uint8_t buf[MAX_HANDSHAKE_SIZE] = {}; + size_t out_len = 0; + EXPECT_NE(handshake.read_message(buf, sizeof(buf)), 0); + EXPECT_NE(handshake.write_message(buf, sizeof(buf), out_len), 0); + // Deliberately non-null: split() documents a nullptr postcondition on + // error, so a caller's uninitialized locals never hold garbage to free + auto *sentinel = reinterpret_cast(0x1); + NoiseCipherState *send_cipher = sentinel; + NoiseCipherState *recv_cipher = sentinel; + EXPECT_NE(handshake.split(send_cipher, recv_cipher), 0); + EXPECT_EQ(send_cipher, nullptr); + EXPECT_EQ(recv_cipher, nullptr); +} + +TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) { + const psk_t psk = make_psk(7); + NoiseResponderHandshake responder; + ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0); + EXPECT_EQ(responder.action(), Action::ACTION_READ); + + Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE)); + uint8_t msg[MAX_HANDSHAKE_SIZE]; + size_t msg_len = initiator.write_message(msg, sizeof(msg)); + ASSERT_GT(msg_len, 0u); + + ASSERT_EQ(responder.read_message(msg, msg_len), 0); + ASSERT_EQ(responder.action(), Action::ACTION_WRITE); + + size_t reply_len = 0; + ASSERT_EQ(responder.write_message(msg, sizeof(msg), reply_len), 0); + ASSERT_GT(reply_len, 0u); + ASSERT_EQ(responder.action(), Action::ACTION_SPLIT); + + 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); + ASSERT_NE(recv_cipher, nullptr); + // The handshake state is released by split(); the class reports FAILED after + EXPECT_EQ(responder.action(), Action::ACTION_FAILED); + EXPECT_EQ(static_cast(noise_cipherstate_get_mac_length(send_cipher)), MAC_SIZE); + + // Responder encrypts, initiator decrypts + uint8_t frame[64]; + static constexpr char PLAINTEXT[] = "encrypted ota"; + std::memcpy(frame, PLAINTEXT, sizeof(PLAINTEXT)); + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame, sizeof(PLAINTEXT), sizeof(frame)); + ASSERT_EQ(noise_cipherstate_encrypt(send_cipher, &mbuf), 0); + EXPECT_EQ(mbuf.size, sizeof(PLAINTEXT) + MAC_SIZE); + + noise_buffer_set_inout(mbuf, frame, mbuf.size, sizeof(frame)); + ASSERT_EQ(noise_cipherstate_decrypt(initiator.recv_, &mbuf), 0); + ASSERT_EQ(mbuf.size, sizeof(PLAINTEXT)); + EXPECT_EQ(std::memcmp(frame, PLAINTEXT, sizeof(PLAINTEXT)), 0); + + noise_cipherstate_free(send_cipher); + noise_cipherstate_free(recv_cipher); +} + +TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) { + // The documented retry shape: a repeated init() frees the previous state + // and starts over (ASAN in the test harness guards the single free) + NoiseResponderHandshake responder; + ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0); + EXPECT_EQ(responder.action(), Action::ACTION_READ); +} + +TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) { + NoiseResponderHandshake responder; + ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0); + + Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE)); + uint8_t msg[MAX_HANDSHAKE_SIZE]; + size_t msg_len = initiator.write_message(msg, sizeof(msg)); + ASSERT_GT(msg_len, 0u); + + int err = responder.read_message(msg, msg_len); + EXPECT_EQ(err, NOISE_ERROR_MAC_FAILURE); + EXPECT_EQ(responder.action(), Action::ACTION_FAILED); +} + +TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) { + // The prologue binds the plaintext preamble for downgrade resistance; a + // tampered preamble must fail even with the right key. + const psk_t psk = make_psk(7); + NoiseResponderHandshake responder; + ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0); + + static const uint8_t TAMPERED[] = {'x'}; + Initiator initiator(psk, TAMPERED, sizeof(TAMPERED)); + uint8_t msg[MAX_HANDSHAKE_SIZE]; + size_t msg_len = initiator.write_message(msg, sizeof(msg)); + ASSERT_GT(msg_len, 0u); + + EXPECT_EQ(responder.read_message(msg, msg_len), NOISE_ERROR_MAC_FAILURE); +} + +} // namespace esphome::noise::testing diff --git a/tests/components/noise/test_noise_primitives.cpp b/tests/components/noise/test_noise_primitives.cpp new file mode 100644 index 0000000000..018be9f717 --- /dev/null +++ b/tests/components/noise/test_noise_primitives.cpp @@ -0,0 +1,74 @@ +#include + +#include + +#include + +#include "esphome/components/noise/noise.h" + +namespace esphome::noise::testing { + +TEST(NoiseContextTest, AllZerosPskIsReserved) { + psk_t zeros{}; + EXPECT_TRUE(NoiseContext::is_all_zeros(zeros)); + + psk_t psk{}; + psk[31] = 1; + EXPECT_FALSE(NoiseContext::is_all_zeros(psk)); + + NoiseContext ctx; + EXPECT_FALSE(ctx.has_psk()); + ctx.set_psk(zeros); + EXPECT_FALSE(ctx.has_psk()); + ctx.set_psk(psk); + EXPECT_TRUE(ctx.has_psk()); + EXPECT_EQ(ctx.get_psk(), psk); +} + +TEST(WireFormatTest, FrameHeaderIsIndicatorPlusBigEndianLength) { + uint8_t header[FRAME_HEADER_SIZE]; + write_frame_header(header, 0x1234); + EXPECT_EQ(header[0], FRAME_INDICATOR); + EXPECT_EQ(header[1], 0x12); + EXPECT_EQ(header[2], 0x34); +} + +TEST(WireFormatTest, RejectPayloadCarriesStatusByteAndMacFailureContract) { + // The MAC failure string is a wire contract: clients match it to report a + // wrong key. Format the payload exactly the way the handshake read path does. + uint8_t buf[64]; + size_t len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_MAC_FAILURE)); + static constexpr char EXPECTED[] = "Handshake MAC failure"; + ASSERT_EQ(len, 1 + strlen(EXPECTED)); + EXPECT_EQ(buf[0], HANDSHAKE_STATUS_REJECT); + EXPECT_EQ(memcmp(buf + 1, EXPECTED, strlen(EXPECTED)), 0); + // The exported floor covers the full MAC failure payload exactly + EXPECT_EQ(MAC_FAILURE_PAYLOAD_SIZE, 1 + strlen(EXPECTED)); + + // Any other error maps to the generic reason + len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_INVALID_STATE)); + static constexpr char GENERIC[] = "Handshake error"; + ASSERT_EQ(len, 1 + strlen(GENERIC)); + EXPECT_EQ(memcmp(buf + 1, GENERIC, strlen(GENERIC)), 0); +} + +TEST(WireFormatTest, RejectPayloadTruncatesToCapacity) { + uint8_t buf[8]; + size_t len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_MAC_FAILURE)); + ASSERT_EQ(len, sizeof(buf)); + EXPECT_EQ(buf[0], HANDSHAKE_STATUS_REJECT); + EXPECT_EQ(memcmp(buf + 1, "Handsha", 7), 0); + + // A one-byte buffer still carries the status byte + uint8_t tiny[1]; + len = format_reject_payload(tiny, sizeof(tiny), reject_reason_for(NOISE_ERROR_MAC_FAILURE)); + ASSERT_EQ(len, 1u); + EXPECT_EQ(tiny[0], HANDSHAKE_STATUS_REJECT); + + // A zero-capacity buffer yields no payload and stays untouched + uint8_t none[1] = {0xAA}; + EXPECT_EQ(format_reject_payload(none, 0, reject_reason_for(NOISE_ERROR_MAC_FAILURE)), 0u); + EXPECT_EQ(none[0], 0xAA); +} + +} // namespace esphome::noise::testing