diff --git a/esphome/espota2.py b/esphome/espota2.py index 09b83cee0c3..3e7b5540855 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -479,12 +479,14 @@ class NoiseSocketWrapper: raise OTAError(f"Device rejected the noise handshake: {reason}") try: self._handshake.read_message(payload[1:]) - except (ValueError, self._invalid_tag) as err: - # InvalidTag is a wrong key; ValueError covers a device sending an - # invalid curve point, which cryptography rejects during the DH + except self._invalid_tag as err: raise OTAKeyRejected( "Noise handshake failed; is the OTA encryption key correct?" ) from err + except ValueError as err: + # An invalid curve point, rejected by cryptography during the DH; + # the device is broken, not the key + raise OTAError(f"Noise handshake failed: {err}") from err self._encrypt, self._decrypt = self._handshake.get_ciphers() def sendall(self, data: bytes) -> None: diff --git a/tests/unit_tests/test_espota2_noise.py b/tests/unit_tests/test_espota2_noise.py index 054dc7e86c8..f674032437a 100644 --- a/tests/unit_tests/test_espota2_noise.py +++ b/tests/unit_tests/test_espota2_noise.py @@ -550,21 +550,21 @@ def test_handshake_reject_with_other_reason() -> None: def test_handshake_garbage_second_message() -> None: - """A valid-looking point with a garbage MAC fails cleanly.""" + """A valid-looking point with a garbage MAC is a key failure.""" wrapper = _wrapper(_frame(b"\x00" + bytes(range(48)))) with pytest.raises( - espota2.OTAError, match="handshake failed; is the OTA encryption key" + espota2.OTAKeyRejected, match="handshake failed; is the OTA encryption key" ): wrapper.do_handshake() def test_handshake_invalid_curve_point() -> None: - """An all-zero x25519 point is rejected as a clean error, not a crash.""" + """An all-zero x25519 point is a clean error, not a crash, and not a + key failure: it must not spend the old_key retry.""" wrapper = _wrapper(_frame(b"\x00" + bytes(48))) - with pytest.raises( - espota2.OTAError, match="handshake failed; is the OTA encryption key" - ): + with pytest.raises(espota2.OTAError, match="handshake failed: ") as info: wrapper.do_handshake() + assert not isinstance(info.value, espota2.OTAKeyRejected) def test_recv_closed_at_frame_boundary_returns_empty() -> None: