Harden RF node control and agent updates
This commit is contained in:
172
tests/test_rf001_agent_update.py
Normal file
172
tests/test_rf001_agent_update.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Software-only tests for fixed RF-001 agent release archives."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.rf001_agent_update import (
|
||||
CONTENT_MANIFEST_NAME,
|
||||
RELEASE_SOURCE_FILES,
|
||||
ReleaseValidationError,
|
||||
build_release_archive,
|
||||
canonical_json_bytes,
|
||||
inspect_release_archive,
|
||||
sign_update_manifest,
|
||||
verify_update_manifest_signature,
|
||||
)
|
||||
|
||||
|
||||
def _source_tree(root: Path, *, agent_version: str = "2026.09.29.1") -> None:
|
||||
for index, relative in enumerate(RELEASE_SOURCE_FILES.values()):
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = f"# fixed fixture {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")
|
||||
|
||||
|
||||
def test_release_roundtrip_uses_exact_allowlist(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source"
|
||||
_source_tree(source)
|
||||
package = tmp_path / "rf001-agent-2026.09.29.1.zip"
|
||||
built = build_release_archive(
|
||||
source,
|
||||
package,
|
||||
release_id="rf001-agent-2026.09.29.1",
|
||||
agent_version="2026.09.29.1",
|
||||
protocol_version=1,
|
||||
)
|
||||
extraction = tmp_path / "extracted"
|
||||
checked = inspect_release_archive(
|
||||
package,
|
||||
expected_package_sha256=built.package_sha256,
|
||||
expected_package_bytes=built.package_bytes,
|
||||
expected_content_manifest_sha256=built.content_manifest_sha256,
|
||||
extraction_root=extraction,
|
||||
)
|
||||
assert checked == built
|
||||
assert {
|
||||
path.relative_to(extraction).as_posix()
|
||||
for path in extraction.rglob("*")
|
||||
if path.is_file()
|
||||
} == set(RELEASE_SOURCE_FILES) | {CONTENT_MANIFEST_NAME}
|
||||
|
||||
|
||||
def test_update_manifest_rejects_bad_signature() -> None:
|
||||
token = b"fixed-test-token-0123456789abcdef"
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"release_id": "rf001-agent-2026.09.29.1",
|
||||
"package_sha256": "a" * 64,
|
||||
}
|
||||
signature = sign_update_manifest(token, manifest)
|
||||
verify_update_manifest_signature(token, manifest, signature)
|
||||
with pytest.raises(ReleaseValidationError, match="mismatch"):
|
||||
verify_update_manifest_signature(token, manifest, "0" * 64)
|
||||
changed = dict(manifest, package_sha256="b" * 64)
|
||||
with pytest.raises(ReleaseValidationError, match="mismatch"):
|
||||
verify_update_manifest_signature(token, changed, signature)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"unsafe_name",
|
||||
(
|
||||
"../outside.py",
|
||||
"/absolute.py",
|
||||
"C:/absolute.py",
|
||||
"app\\tools\\backslash.py",
|
||||
),
|
||||
)
|
||||
def test_release_rejects_unsafe_paths(tmp_path: Path, unsafe_name: str) -> None:
|
||||
package = tmp_path / "unsafe.zip"
|
||||
with zipfile.ZipFile(package, "w") as archive:
|
||||
archive.writestr(unsafe_name, b"x")
|
||||
# zipfile normalizes a backslash to a slash before it can be inspected;
|
||||
# that spelling still cannot enter the fixed release allowlist.
|
||||
with pytest.raises(
|
||||
ReleaseValidationError,
|
||||
match="unsafe|drive-like|not allowlisted|manifest is absent",
|
||||
):
|
||||
inspect_release_archive(package)
|
||||
|
||||
|
||||
def test_release_rejects_symlink_and_duplicate_names(tmp_path: Path) -> None:
|
||||
symlink_package = tmp_path / "symlink.zip"
|
||||
with zipfile.ZipFile(symlink_package, "w") as archive:
|
||||
info = zipfile.ZipInfo("app/tools/link.py")
|
||||
info.create_system = 3
|
||||
info.external_attr = (stat.S_IFLNK | 0o777) << 16
|
||||
archive.writestr(info, b"target")
|
||||
with pytest.raises(ReleaseValidationError, match="links"):
|
||||
inspect_release_archive(symlink_package)
|
||||
|
||||
duplicate_package = tmp_path / "duplicate.zip"
|
||||
with zipfile.ZipFile(duplicate_package, "w") as archive:
|
||||
archive.writestr("app/tools/a.py", b"a")
|
||||
archive.writestr("APP/TOOLS/A.PY", b"b")
|
||||
with pytest.raises(ReleaseValidationError, match="duplicate"):
|
||||
inspect_release_archive(duplicate_package)
|
||||
|
||||
|
||||
def test_release_rejects_unsupported_protocol_and_sha(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source"
|
||||
_source_tree(source)
|
||||
package = tmp_path / "release.zip"
|
||||
built = build_release_archive(
|
||||
source,
|
||||
package,
|
||||
release_id="rf001-agent-2026.09.29.1",
|
||||
agent_version="2026.09.29.1",
|
||||
protocol_version=2,
|
||||
)
|
||||
assert built.protocol_version == 2
|
||||
with pytest.raises(ReleaseValidationError, match="package SHA256"):
|
||||
inspect_release_archive(package, expected_package_sha256="0" * 64)
|
||||
|
||||
|
||||
def test_content_manifest_must_be_canonical(tmp_path: Path) -> None:
|
||||
package = tmp_path / "noncanonical.zip"
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"release_id": "rf001-agent-2026.09.29.1",
|
||||
"agent_version": "2026.09.29.1",
|
||||
"protocol_version": 1,
|
||||
"files": [],
|
||||
}
|
||||
assert canonical_json_bytes(manifest) != json.dumps(manifest).encode("utf-8")
|
||||
with zipfile.ZipFile(package, "w") as archive:
|
||||
archive.writestr(CONTENT_MANIFEST_NAME, json.dumps(manifest))
|
||||
with pytest.raises(ReleaseValidationError, match="canonical"):
|
||||
inspect_release_archive(package)
|
||||
|
||||
|
||||
@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_release_builder_rejects_version_mismatch_before_artifact(
|
||||
tmp_path: Path,
|
||||
source_version: str,
|
||||
requested_version: str,
|
||||
) -> None:
|
||||
source = tmp_path / "source"
|
||||
_source_tree(source, agent_version=source_version)
|
||||
package = tmp_path / "must-not-exist.zip"
|
||||
with pytest.raises(ReleaseValidationError, match="AGENT_VERSION|release_id"):
|
||||
build_release_archive(
|
||||
source,
|
||||
package,
|
||||
release_id=f"rf001-agent-{requested_version}",
|
||||
agent_version=requested_version,
|
||||
protocol_version=1,
|
||||
)
|
||||
assert not package.exists()
|
||||
Reference in New Issue
Block a user