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,319 @@
"""Software-only tests for the authenticated bootstrap package."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
from pathlib import PurePosixPath
import shutil
import subprocess
import unicodedata
import zipfile
import pytest
import tools.rf001_build_agent_packages as packages
from tools.rf001_agent_update import (
RELEASE_SOURCE_FILES,
ReleaseValidationError,
source_agent_version,
)
def _bootstrap_source(
root: Path,
*,
agent_version: str = "2026.09.29.1",
) -> Path:
fixed = {
"INSTALL_RX_AGENT.cmd": "@echo off\n",
"UNINSTALL_RX_AGENT.cmd": "@echo off\n",
"tools/rf001_bootstrap_readme.txt": "fixed bootstrap readme\n",
"tools/rf001_agent_launcher.py": "# fixed launcher\n",
"tools/install_rf001_rx_autostart.ps1": "# fixed installer\n",
"tools/uninstall_rf001_rx_autostart.ps1": "# fixed uninstaller\n",
}
for name, content in fixed.items():
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
for index, relative in enumerate(RELEASE_SOURCE_FILES.values()):
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
content = f"# fixed release source {index}\n"
if relative == RELEASE_SOURCE_FILES["app/tools/rf001_rx_http_agent.py"]:
content += f'AGENT_VERSION = "{agent_version}"\n'
path.write_text(content, encoding="utf-8")
reference = root / ".local/rf001_probe/lab065_reference_staircase.cs8"
reference.parent.mkdir(parents=True, exist_ok=True)
reference.write_bytes(b"fixed exact staircase fixture")
return reference
def _formed_installer_allowlist(release_id: str) -> list[str]:
powershell = shutil.which("powershell.exe")
if powershell is None:
pytest.skip("Windows PowerShell is required to evaluate the installer allowlist")
installer = Path(packages.__file__).with_name("install_rf001_rx_autostart.ps1")
environment = os.environ.copy()
environment["SDR_ROVER_INSTALLER_PATH"] = str(installer)
environment["SDR_ROVER_RELEASE_ID"] = release_id
command = r"""
$ErrorActionPreference = "Stop"
$tokens = $null
$parseErrors = $null
$syntax = [Management.Automation.Language.Parser]::ParseFile(
$env:SDR_ROVER_INSTALLER_PATH,
[ref]$tokens,
[ref]$parseErrors
)
if ($parseErrors.Count -ne 0) {
throw "Installer contains PowerShell parse errors"
}
$assignments = @($syntax.FindAll({
param($node)
$node -is [Management.Automation.Language.AssignmentStatementAst] -and
$node.Left -is [Management.Automation.Language.VariableExpressionAst] -and
$node.Left.VariablePath.UserPath -eq "expectedPaths"
}, $true))
if ($assignments.Count -ne 1) {
throw "Installer expectedPaths assignment is absent or ambiguous"
}
$releasePrefix = "versions/$($env:SDR_ROVER_RELEASE_ID)/"
. ([scriptblock]::Create($assignments[0].Extent.Text))
ConvertTo-Json -Compress -InputObject @($expectedPaths)
"""
completed = subprocess.run(
[powershell, "-NoProfile", "-NonInteractive", "-Command", command],
check=True,
capture_output=True,
encoding="utf-8",
env=environment,
)
value = json.loads(completed.stdout.lstrip("\ufeff"))
assert isinstance(value, list)
assert all(isinstance(path, str) for path in value)
return value
def test_bootstrap_is_authenticated_and_excludes_secret(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = tmp_path / "source"
reference = _bootstrap_source(source)
monkeypatch.setattr(
packages,
"REFERENCE_SHA256",
hashlib.sha256(reference.read_bytes()).hexdigest(),
)
token = tmp_path / "token.txt"
token.write_text("fixed-test-token-0123456789abcdef", encoding="utf-8")
package = tmp_path / "bootstrap.zip"
built = packages.build_bootstrap_archive(
source,
package,
bootstrap_id="rf001-bootstrap-2026.09.29.1",
agent_release_id="rf001-agent-2026.09.29.1",
agent_version="2026.09.29.1",
protocol_version=1,
token_path=token,
)
checked = packages.inspect_bootstrap_archive(package, token_path=token)
assert checked == built
assert checked.file_count == len(RELEASE_SOURCE_FILES) + 8
with zipfile.ZipFile(package) as archive:
names = archive.namelist()
manifest = json.loads(archive.read(packages.BOOTSTRAP_MANIFEST_NAME))
ready = json.loads(
archive.read("versions/rf001-agent-2026.09.29.1/READY.json")
)
assert not any("token" in name.casefold() for name in names)
assert not any(
b"fixed-test-token" in archive.read(name)
for name in names
)
actual_version = source_agent_version(
source / RELEASE_SOURCE_FILES["app/tools/rf001_rx_http_agent.py"]
)
assert manifest["agent_version"] == actual_version
assert ready["agent_version"] == actual_version
def test_new_bootstrap_preserves_existing_release_bytes(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = tmp_path / "source"
reference = _bootstrap_source(source)
monkeypatch.setattr(
packages,
"REFERENCE_SHA256",
hashlib.sha256(reference.read_bytes()).hexdigest(),
)
token = tmp_path / "token.txt"
token.write_text("fixed-test-token-0123456789abcdef", encoding="utf-8")
base_package = tmp_path / "bootstrap-4.zip"
next_package = tmp_path / "bootstrap-5.zip"
release_id = "rf001-agent-2026.09.29.1"
packages.build_bootstrap_archive(
source,
base_package,
bootstrap_id="rf001-bootstrap-2026.09.29.4",
agent_release_id=release_id,
agent_version="2026.09.29.1",
protocol_version=1,
token_path=token,
)
(source / "tools/rf001_agent_launcher.py").write_text(
"# corrected launcher\n",
encoding="utf-8",
)
packages.build_bootstrap_archive(
source,
next_package,
bootstrap_id="rf001-bootstrap-2026.09.29.5",
agent_release_id=release_id,
agent_version="2026.09.29.1",
protocol_version=1,
token_path=token,
agent_release_bootstrap_id="rf001-bootstrap-2026.09.29.4",
)
release_prefix = f"versions/{release_id}/"
with zipfile.ZipFile(base_package) as base, zipfile.ZipFile(next_package) as new:
release_names = sorted(
name for name in base.namelist() if name.startswith(release_prefix)
)
assert release_names
assert all(base.read(name) == new.read(name) for name in release_names)
assert base.read("launcher/rf001_agent_launcher.py") != new.read(
"launcher/rf001_agent_launcher.py"
)
manifest = json.loads(new.read(packages.BOOTSTRAP_MANIFEST_NAME))
ready = json.loads(new.read(f"{release_prefix}READY.json"))
assert manifest["bootstrap_id"] == "rf001-bootstrap-2026.09.29.5"
assert (
manifest["agent_release_bootstrap_id"]
== "rf001-bootstrap-2026.09.29.4"
)
assert ready["bootstrap_id"] == "rf001-bootstrap-2026.09.29.4"
def test_bootstrap_rejects_the_wrong_existing_secret(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = tmp_path / "source"
reference = _bootstrap_source(source)
monkeypatch.setattr(
packages,
"REFERENCE_SHA256",
hashlib.sha256(reference.read_bytes()).hexdigest(),
)
token = tmp_path / "token.txt"
token.write_text("fixed-test-token-0123456789abcdef", encoding="utf-8")
package = tmp_path / "bootstrap.zip"
packages.build_bootstrap_archive(
source,
package,
bootstrap_id="rf001-bootstrap-2026.09.29.1",
agent_release_id="rf001-agent-2026.09.29.1",
agent_version="2026.09.29.1",
protocol_version=1,
token_path=token,
)
wrong = tmp_path / "wrong.txt"
wrong.write_text("another-fixed-token-0123456789abcdef", encoding="utf-8")
with pytest.raises(ValueError, match="signature mismatch"):
packages.inspect_bootstrap_archive(package, token_path=wrong)
def test_installer_allowlist_matches_signed_manifest_and_has_safe_paths(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = tmp_path / "source"
reference = _bootstrap_source(source)
monkeypatch.setattr(
packages,
"REFERENCE_SHA256",
hashlib.sha256(reference.read_bytes()).hexdigest(),
)
token = tmp_path / "token.txt"
token.write_text("fixed-test-token-0123456789abcdef", encoding="utf-8")
package = tmp_path / "bootstrap.zip"
release_id = "rf001-agent-2026.09.29.1"
packages.build_bootstrap_archive(
source,
package,
bootstrap_id="rf001-bootstrap-2026.09.29.2",
agent_release_id=release_id,
agent_version="2026.09.29.1",
protocol_version=1,
token_path=token,
)
with zipfile.ZipFile(package) as archive:
manifest = json.loads(archive.read(packages.BOOTSTRAP_MANIFEST_NAME))
manifest_paths = {row["path"] for row in manifest["files"]}
formed_paths = _formed_installer_allowlist(release_id)
assert set(formed_paths) == manifest_paths
assert len(formed_paths) == len(manifest_paths)
assert f"versions/{release_id}/READY.json" in formed_paths
assert (
f"versions/{release_id}/app/tools/hackrf_process_check.py" in formed_paths
)
assert (
f"versions/{release_id}/app/tools/rf001_staircase_search.py" in formed_paths
)
for path in formed_paths:
assert not any(unicodedata.category(character) == "Cc" for character in path)
parsed = PurePosixPath(path)
assert not parsed.is_absolute()
assert all(part not in ("", ".", "..") for part in parsed.parts)
assert "\\" not in path
assert not any(":" in part for part in parsed.parts)
with zipfile.ZipFile(package, "a") as archive:
archive.writestr("unexpected/extra.txt", b"prohibited")
with pytest.raises(ValueError, match="unexpected files"):
packages.inspect_bootstrap_archive(package, token_path=token)
@pytest.mark.parametrize(
("source_version", "requested_version"),
(
("2026.09.29.2", "2026.09.29.1"),
("2026.09.29.1", "2026.09.29.2"),
),
)
def test_bootstrap_rejects_agent_version_mismatch_before_creating_artifact(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
source_version: str,
requested_version: str,
) -> None:
source = tmp_path / "source"
reference = _bootstrap_source(source, agent_version=source_version)
monkeypatch.setattr(
packages,
"REFERENCE_SHA256",
hashlib.sha256(reference.read_bytes()).hexdigest(),
)
token = tmp_path / "token.txt"
token.write_text("fixed-test-token-0123456789abcdef", encoding="utf-8")
package = tmp_path / "must-not-exist.zip"
with pytest.raises(ReleaseValidationError, match="AGENT_VERSION|release_id"):
packages.build_bootstrap_archive(
source,
package,
bootstrap_id="rf001-bootstrap-2026.09.29.3",
agent_release_id=f"rf001-agent-{requested_version}",
agent_version=requested_version,
protocol_version=1,
token_path=token,
)
assert not package.exists()