Keep an invalid curve point a plain handshake error, not a key failure

This commit is contained in:
J. Nick Koston
2026-09-30 01:17:28 +02:00
parent 12cc1fd9fc
commit b4433bc031
2 changed files with 11 additions and 9 deletions
+5 -3
View File
@@ -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:
+6 -6
View File
@@ -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: