Harden RF node control and agent updates

This commit is contained in:
LittleSam129
2026-09-30 11:42:19 +03:00
parent a94f2db4ee
commit 0d6fff224e
34 changed files with 14010 additions and 0 deletions

View File

@@ -0,0 +1,537 @@
"""Software-only tests for the RF-001 HTTP control channel."""
from __future__ import annotations
import threading
import time
import pytest
import tools.rf001_http_control as control_module
from tools.rf001_http_control import (
AuthenticationError,
ControlHTTPServer,
ControlState,
PROTOCOL_VERSION,
ProtocolError,
RequestAuthenticator,
canonical_json,
request_json,
signed_headers,
)
TOKEN = b"software-only-test-token-0123456789abcdef0123456789abcdef"
def test_bootstrap_registration_uses_distinct_fixed_identifier(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
releases = tmp_path / "releases"
releases.mkdir()
package = releases / "rf001-bootstrap-2026.09.29.1.zip"
package.write_bytes(b"fixed bootstrap package")
monkeypatch.setattr(control_module, "DEFAULT_RELEASES_ROOT", releases)
state = ControlState(tmp_path / "state.json")
registered = state.register_bootstrap_release(
str(package),
release_id="rf001-bootstrap-2026.09.29.1",
agent_version="2026.09.29.1",
content_manifest_sha256="a" * 64,
token=TOKEN,
)
assert registered["package_kind"] == "BOOTSTRAP"
assert registered["rf_tx"] == 0
replacement = releases / "replacement.zip"
replacement.write_bytes(b"different bootstrap package")
with pytest.raises(ProtocolError, match="already registered differently"):
state.register_bootstrap_release(
str(replacement),
release_id="rf001-bootstrap-2026.09.29.1",
agent_version="2026.09.29.1",
content_manifest_sha256="b" * 64,
token=TOKEN,
)
with pytest.raises(ProtocolError, match="release_id"):
state.register_bootstrap_release(
str(package),
release_id="rf001-agent-2026.09.29.1",
agent_version="2026.09.29.1",
content_manifest_sha256="a" * 64,
token=TOKEN,
)
def test_request_authentication_rejects_replay_and_tampering() -> None:
authenticator = RequestAuthenticator(TOKEN, window_seconds=60)
body = canonical_json({"agent_id": "rx-user-pc", "rf_tx": 0})
target = "/api/v1/agent/check-in"
headers = signed_headers(
TOKEN,
"POST",
target,
body,
timestamp=1_000,
nonce="a" * 32,
)
authenticator.verify("POST", target, body, headers, now=1_000)
with pytest.raises(AuthenticationError, match="already used"):
authenticator.verify("POST", target, body, headers, now=1_000)
changed_headers = signed_headers(
TOKEN,
"POST",
target,
body,
timestamp=1_000,
nonce="b" * 32,
)
with pytest.raises(AuthenticationError, match="mismatch"):
authenticator.verify(
"POST",
target,
body + b" ",
changed_headers,
now=1_000,
)
def test_state_leases_inventory_and_records_idempotent_result(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
task = state.enqueue_inventory("rx-user-pc", include_hackrf_info=True)
response = state.check_in(
{
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"hostname": "User-PC", "rf_tx": 0},
"rf_tx": 0,
}
)
assert response["task"] == task
assert response["rf_tx"] == 0
result = {
"protocol_version": PROTOCOL_VERSION,
"task_id": task["task_id"],
"agent_id": "rx-user-pc",
"status": "COMPLETE",
"started_at": "2026-09-23T00:00:00+00:00",
"finished_at": "2026-09-23T00:00:01+00:00",
"result": {"hostname": "User-PC", "rf_tx": 0},
"errors": [],
"rf_tx": 0,
}
accepted = state.record_result(result)
assert accepted == {"accepted": True, "duplicate": False, "rf_tx": 0}
duplicate = state.record_result(result)
assert duplicate == {"accepted": True, "duplicate": True, "rf_tx": 0}
conflicting = dict(result)
conflicting["status"] = "FAILED"
with pytest.raises(ProtocolError, match="conflicting"):
state.record_result(conflicting)
def test_state_leases_fixed_offline_verify_task(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
expected_head = "a" * 40
task = state.enqueue_offline_verify(
"rx-user-pc",
expected_head=expected_head,
)
response = state.check_in(
{
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"hostname": "User-PC", "rf_tx": 0},
"rf_tx": 0,
}
)
assert response["task"] == task
assert task["kind"] == "OFFLINE_VERIFY"
assert task["expected_head"] == expected_head
assert task["rf_tx"] == 0
def test_offline_verify_rejects_invalid_expected_head(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
with pytest.raises(ProtocolError, match="expected_head"):
state.enqueue_offline_verify("rx-user-pc", expected_head="main")
def test_rx_capture_requires_exact_local_authorization(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
with pytest.raises(ProtocolError, match="exact RX authorization"):
state.enqueue_rx_capture(
"rx-user-pc",
expected_head="a" * 40,
authorization="yes",
)
task = state.enqueue_rx_capture(
"rx-user-pc",
expected_head="a" * 40,
authorization="AUTHORIZE RF RX",
)
assert task["kind"] == "RX_CAPTURE_START"
assert task["rx_serial"] == "000000000000000097c063dc2e5327d3"
assert task["rx_authorized"] is True
assert task["automatic_retry"] is False
assert task["rf_tx"] == 0
def test_expired_rx_capture_lease_is_not_retried(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
task = state.enqueue_rx_capture(
"rx-user-pc",
expected_head="a" * 40,
authorization="AUTHORIZE RF RX",
)
check_in = {
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"hostname": "User-PC", "rf_tx": 0},
"rf_tx": 0,
}
first = state.check_in(check_in)
assert first["task"] == task
state._data["tasks"][0]["leased_at_epoch"] = int(time.time()) - 31
second = state.check_in(check_in)
assert second["task"] is None
stored = state.snapshot()["tasks"][0]
assert stored["state"] == "LEASE_EXPIRED"
assert "lease_expired_at" in stored
assert stored["task_id"] == task["task_id"]
def test_status_and_decode_tasks_use_fixed_session(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
session_id = "RF001A_20260923T150000Z_1234ABCD"
status = state.enqueue_rx_status("rx-user-pc", session_id=session_id)
decode = state.enqueue_capture_decode(
"rx-user-pc",
session_id=session_id,
tx_started_at="2026-09-23T15:00:05+00:00",
tx_finished_at="2026-09-23T15:00:07+00:00",
)
diagnostics = state.enqueue_capture_diagnostics(
"rx-user-pc",
session_id=session_id,
tx_started_at="2026-09-23T15:00:05+00:00",
tx_finished_at="2026-09-23T15:00:07+00:00",
)
assert status["kind"] == "RX_CAPTURE_STATUS"
assert decode["kind"] == "CAPTURE_DECODE"
assert diagnostics["kind"] == "CAPTURE_DIAGNOSTICS"
assert status["session_id"] == decode["session_id"] == session_id
assert diagnostics["session_id"] == session_id
assert status["rf_tx"] == decode["rf_tx"] == diagnostics["rf_tx"] == 0
with pytest.raises(ProtocolError, match="invalid session_id"):
state.enqueue_rx_status("rx-user-pc", session_id="..\\outside")
def test_staircase_task_has_only_fixed_hashes_and_no_retry(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
session_id = "RF001A_20260925T125829Z_227EADB4"
task = state.enqueue_staircase_search(
"rx-user-pc",
session_id=session_id,
expected_rx_sha256="2" * 64,
expected_reference_sha256="f" * 64,
)
assert task["kind"] == "STAIRCASE_SEARCH"
assert task["session_id"] == session_id
assert task["expected_rx_sha256"] == "2" * 64
assert task["expected_reference_sha256"] == "f" * 64
assert task["hardware_accessed"] is False
assert task["automatic_retry"] is False
assert task["rf_tx"] == 0
assert "capture_path" not in task
assert "reference_path" not in task
assert "command" not in task
with pytest.raises(ProtocolError, match="invalid session_id"):
state.enqueue_staircase_search(
"rx-user-pc",
session_id="..\\outside",
expected_rx_sha256="2" * 64,
expected_reference_sha256="f" * 64,
)
with pytest.raises(ProtocolError, match="expected_rx_sha256"):
state.enqueue_staircase_search(
"rx-user-pc",
session_id=session_id,
expected_rx_sha256="bad",
expected_reference_sha256="f" * 64,
)
with pytest.raises(ProtocolError, match="expected_reference_sha256"):
state.enqueue_staircase_search(
"rx-user-pc",
session_id=session_id,
expected_rx_sha256="2" * 64,
expected_reference_sha256="bad",
)
def test_long_task_heartbeat_renews_expired_lease_without_reissue(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
task = state.enqueue_staircase_search(
"rx-user-pc",
session_id="RF001A_20260925T125829Z_227EADB4",
expected_rx_sha256="2" * 64,
expected_reference_sha256="f" * 64,
)
check_in = {
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"agent_version": "test", "rf_tx": 0},
"rf_tx": 0,
}
assert state.check_in(check_in)["task"] == task
state._data["tasks"][0]["leased_at_epoch"] = int(time.time()) - 300
active = {
**check_in,
"active_task": {
"task_id": task["task_id"],
"kind": "STAIRCASE_SEARCH",
"state": "RUNNING",
"phase": "SEARCHING",
"processed_samples": 400,
"total_samples": 1000,
"hardware_accessed": False,
"rf_tx": 0,
},
}
response = state.check_in(active)
assert response["task"] is None
stored = state.snapshot()["tasks"][0]
assert stored["state"] == "LEASED"
assert stored["progress"]["processed_samples"] == 400
def test_update_approval_is_bound_to_registered_release_and_not_repeated(
tmp_path,
) -> None:
state = ControlState(tmp_path / "state.json")
release_id = "rf001-agent-2026.09.29.2"
state._data["releases"][release_id] = {
"manifest": {
"schema_version": 1,
"release_id": release_id,
"agent_version": "2026.09.29.2",
"protocol_version": PROTOCOL_VERSION,
"package_kind": "AGENT_RELEASE",
"package_sha256": "a" * 64,
"package_bytes": 1234,
"content_manifest_sha256": "b" * 64,
},
"manifest_signature": "c" * 64,
"package_path": str(tmp_path / "release.zip"),
}
task = state.enqueue_agent_update(
"rx-user-pc",
release_id=release_id,
validity_seconds=3600,
)
assert task["kind"] == "UPDATE_AGENT"
assert task["release_id"] == release_id
assert task["package_sha256"] == "a" * 64
assert task["automatic_retry"] is False
assert task["hardware_accessed"] is False
assert task["rf_tx"] == 0
with pytest.raises(ProtocolError, match="already has an update task"):
state.enqueue_agent_update(
"rx-user-pc",
release_id=release_id,
validity_seconds=3600,
)
def test_queued_update_waits_for_active_software_task(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
running = state.enqueue_staircase_search(
"rx-user-pc",
session_id="RF001A_20260925T125829Z_227EADB4",
expected_rx_sha256="2" * 64,
expected_reference_sha256="f" * 64,
)
check_in = {
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"agent_version": "test", "rf_tx": 0},
"rf_tx": 0,
}
assert state.check_in(check_in)["task"] == running
release_id = "rf001-agent-2026.09.29.2"
state._data["releases"][release_id] = {
"manifest": {
"schema_version": 1,
"release_id": release_id,
"agent_version": "2026.09.29.2",
"protocol_version": PROTOCOL_VERSION,
"package_kind": "AGENT_RELEASE",
"package_sha256": "a" * 64,
"package_bytes": 1234,
"content_manifest_sha256": "b" * 64,
},
"manifest_signature": "c" * 64,
"package_path": str(tmp_path / "release.zip"),
}
update = state.enqueue_agent_update(
"rx-user-pc", release_id=release_id, validity_seconds=3600
)
response = state.check_in(
{
**check_in,
"active_task": {
"task_id": running["task_id"],
"kind": "STAIRCASE_SEARCH",
"state": "RUNNING",
"phase": "SEARCHING",
"processed_samples": 1,
"total_samples": 10,
"hardware_accessed": False,
"rf_tx": 0,
},
}
)
assert response["task"] is None
stored = next(
task
for task in state.snapshot()["tasks"]
if task["task_id"] == update["task_id"]
)
assert stored["state"] == "QUEUED"
assert stored["update_status"] == "WAITING_FOR_IDLE"
assert stored["blocked_by_task_id"] == running["task_id"]
def test_http_server_end_to_end_inventory_without_rf(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
server = ControlHTTPServer(
("127.0.0.1", 0),
state=state,
token=TOKEN,
allowed_agent_ip="127.0.0.1",
)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base_url = f"http://127.0.0.1:{server.server_address[1]}"
try:
task = request_json(
base_url,
TOKEN,
"POST",
"/api/v1/local/tasks/inventory",
{"agent_id": "rx-user-pc", "include_hackrf_info": True},
)
assert task["kind"] == "INVENTORY"
assert task["rf_tx"] == 0
check_in = request_json(
base_url,
TOKEN,
"POST",
"/api/v1/agent/check-in",
{
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"hostname": "User-PC", "rf_tx": 0},
"rf_tx": 0,
},
)
assert check_in["task"] == task
assert check_in["rf_tx"] == 0
result = {
"protocol_version": PROTOCOL_VERSION,
"task_id": task["task_id"],
"agent_id": "rx-user-pc",
"status": "COMPLETE",
"started_at": "2026-09-23T00:00:00+00:00",
"finished_at": "2026-09-23T00:00:01+00:00",
"result": {"hostname": "User-PC", "rf_tx": 0},
"errors": [],
"rf_tx": 0,
}
acknowledgement = request_json(
base_url,
TOKEN,
"POST",
"/api/v1/agent/result",
result,
)
assert acknowledgement["accepted"] is True
assert acknowledgement["rf_tx"] == 0
status = request_json(
base_url,
TOKEN,
"GET",
"/api/v1/local/status",
)
assert status["results"][task["task_id"]] == result
assert status["tasks"][0]["state"] == "COMPLETE"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_result_cannot_claim_rf_tx(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
task = state.enqueue_inventory("rx-user-pc", include_hackrf_info=False)
result = {
"protocol_version": PROTOCOL_VERSION,
"task_id": task["task_id"],
"agent_id": "rx-user-pc",
"status": "COMPLETE",
"result": {},
"errors": [],
"rf_tx": 1,
}
with pytest.raises(ProtocolError, match="rf_tx = 0"):
state.record_result(result)
def test_cancelled_task_is_not_released_or_completed(tmp_path) -> None:
state = ControlState(tmp_path / "state.json")
task = state.enqueue_rx_capture(
"rx-user-pc",
expected_head="a" * 40,
authorization="AUTHORIZE RF RX",
)
cancellation = state.cancel_task(
task["task_id"],
reason="software packaging failure before hardware access",
)
assert cancellation["state"] == "CANCELLED"
assert cancellation["rf_tx"] == 0
response = state.check_in(
{
"protocol_version": PROTOCOL_VERSION,
"agent_id": "rx-user-pc",
"inventory": {"hostname": "User-PC", "rf_tx": 0},
"rf_tx": 0,
}
)
assert response["task"] is None
result = {
"protocol_version": PROTOCOL_VERSION,
"task_id": task["task_id"],
"agent_id": "rx-user-pc",
"status": "FAILED",
"result": {"hardware_accessed": False, "rf_tx": 0},
"errors": ["packaging failure"],
"rf_tx": 0,
}
with pytest.raises(ProtocolError, match="cancelled"):
state.record_result(result)