Match cnonce length to hash algorithm digest size

Use nonce_size // 2 as token_hex argument so MD5 auth produces
a 32-char cnonce and SHA256 auth produces a 64-char cnonce,
matching the original protocol behavior.

Rename mock_random fixture to mock_token_hex and use separate
mock cnonce constants per hash algorithm.
This commit is contained in:
J. Nick Koston
2026-02-08 07:11:56 -06:00
parent a8fd6c132e
commit 1b7efdd051
2 changed files with 28 additions and 16 deletions

View File

@@ -300,8 +300,8 @@ def perform_ota(
nonce = nonce_bytes.decode()
_LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce)
# Generate cnonce
cnonce = secrets.token_hex(32)
# Generate cnonce matching the hash algorithm's digest size
cnonce = secrets.token_hex(nonce_size // 2)
_LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce)
send_check(sock, cnonce, "auth cnonce")

View File

@@ -18,7 +18,8 @@ from esphome import espota2
from esphome.core import EsphomeError
# Test constants
MOCK_CNONCE = "a" * 64 # Mock 64-char hex string from secrets.token_hex(32)
MOCK_MD5_CNONCE = "a" * 32 # Mock 32-char hex string from secrets.token_hex(16)
MOCK_SHA256_CNONCE = "b" * 64 # Mock 64-char hex string from secrets.token_hex(32)
MOCK_MD5_NONCE = b"12345678901234567890123456789012" # 32 char nonce for MD5
MOCK_SHA256_NONCE = b"1234567890123456789012345678901234567890123456789012345678901234" # 64 char nonce for SHA256
@@ -54,12 +55,16 @@ def mock_time() -> Generator[None]:
@pytest.fixture
def mock_random() -> Generator[Mock]:
def mock_token_hex() -> Generator[Mock]:
"""Mock secrets.token_hex for predictable test values."""
with patch(
"esphome.espota2.secrets.token_hex", return_value=MOCK_CNONCE
) as mock_rand:
yield mock_rand
def _token_hex(nbytes: int) -> str:
if nbytes == 16:
return MOCK_MD5_CNONCE
return MOCK_SHA256_CNONCE
with patch("esphome.espota2.secrets.token_hex", side_effect=_token_hex) as mock:
yield mock
@pytest.fixture
@@ -237,7 +242,7 @@ def test_send_check_socket_error(mock_socket: Mock) -> None:
@pytest.mark.usefixtures("mock_time")
def test_perform_ota_successful_md5_auth(
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
) -> None:
"""Test successful OTA with MD5 authentication."""
# Setup socket responses for recv calls
@@ -273,8 +278,11 @@ def test_perform_ota_successful_md5_auth(
)
)
# Verify token_hex was called with MD5 digest size
mock_token_hex.assert_called_once_with(16)
# Verify cnonce was sent
cnonce = MOCK_CNONCE
cnonce = MOCK_MD5_CNONCE
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
# Verify auth result was computed correctly
@@ -367,7 +375,7 @@ def test_perform_ota_auth_without_password(mock_socket: Mock) -> None:
@pytest.mark.usefixtures("mock_time")
def test_perform_ota_md5_auth_wrong_password(
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
) -> None:
"""Test OTA fails when MD5 authentication is rejected due to wrong password."""
# Setup socket responses for recv calls
@@ -391,7 +399,7 @@ def test_perform_ota_md5_auth_wrong_password(
@pytest.mark.usefixtures("mock_time")
def test_perform_ota_sha256_auth_wrong_password(
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
) -> None:
"""Test OTA fails when SHA256 authentication is rejected due to wrong password."""
# Setup socket responses for recv calls
@@ -604,7 +612,7 @@ def test_progress_bar(capsys: CaptureFixture[str]) -> None:
# Tests for SHA256 authentication
@pytest.mark.usefixtures("mock_time")
def test_perform_ota_successful_sha256_auth(
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
) -> None:
"""Test successful OTA with SHA256 authentication."""
# Setup socket responses for recv calls
@@ -640,8 +648,11 @@ def test_perform_ota_successful_sha256_auth(
)
)
# Verify token_hex was called with SHA256 digest size
mock_token_hex.assert_called_once_with(32)
# Verify cnonce was sent
cnonce = MOCK_CNONCE
cnonce = MOCK_SHA256_CNONCE
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
# Verify auth result was computed correctly with SHA256
@@ -655,7 +666,7 @@ def test_perform_ota_successful_sha256_auth(
@pytest.mark.usefixtures("mock_time")
def test_perform_ota_sha256_fallback_to_md5(
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
) -> None:
"""Test SHA256-capable client falls back to MD5 for compatibility."""
# This test verifies the temporary backward compatibility
@@ -693,7 +704,8 @@ def test_perform_ota_sha256_fallback_to_md5(
)
# But authentication was done with MD5
cnonce = MOCK_CNONCE
mock_token_hex.assert_called_once_with(16)
cnonce = MOCK_MD5_CNONCE
expected_hash = hashlib.md5()
expected_hash.update(b"testpass")
expected_hash.update(MOCK_MD5_NONCE)