Bind installed releases to signed artifact receipts
This commit is contained in:
277
tests/test_release_artifact_identity.py
Normal file
277
tests/test_release_artifact_identity.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from datetime import UTC, datetime
|
||||
import io
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import zipfile
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||
if str(RELEASE_TOOLS_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
|
||||
|
||||
from govoplan_release.artifact_identity import ( # noqa: E402
|
||||
ArtifactIdentityError,
|
||||
inspect_python_wheel,
|
||||
selected_artifact_identity_issues,
|
||||
)
|
||||
from govoplan_release.selective_catalog import ( # noqa: E402
|
||||
apply_python_artifact_identities,
|
||||
)
|
||||
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
|
||||
if str(ASSESSMENT_TOOLS_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ASSESSMENT_TOOLS_ROOT))
|
||||
from govoplan_assessment.installer_receipt import issue_installer_receipt # noqa: E402
|
||||
|
||||
|
||||
class ReleaseArtifactIdentityTests(unittest.TestCase):
|
||||
def test_built_wheel_bytes_become_catalog_identity(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
wheel = self._wheel(Path(temp_dir), console_script=True)
|
||||
payload = self._catalog_payload()
|
||||
|
||||
changes = apply_python_artifact_identities(
|
||||
payload,
|
||||
repo_versions={"govoplan-demo": "1.2.3"},
|
||||
python_artifacts={"govoplan-demo": wheel},
|
||||
)
|
||||
|
||||
identity = payload["release"]["artifacts"][0]
|
||||
self.assertEqual("govoplan-demo", identity["package_name"])
|
||||
self.assertEqual("1.2.3", identity["package_version"])
|
||||
self.assertRegex(identity["archive_sha256"], r"^[0-9a-f]{64}$")
|
||||
self.assertTrue(identity["requires_installer_receipt"])
|
||||
self.assertEqual("release.artifact", changes[0].field)
|
||||
self.assertEqual((), selected_artifact_identity_issues(payload))
|
||||
|
||||
def test_version_update_without_wheel_removes_stale_identity_and_blocks_publish(self) -> None:
|
||||
payload = self._catalog_payload()
|
||||
payload["release"]["artifacts"] = [
|
||||
{
|
||||
"artifact_kind": "python-wheel",
|
||||
"package_name": "govoplan-demo",
|
||||
"package_version": "1.2.2",
|
||||
"archive_sha256": "a" * 64,
|
||||
"archive_size": 10,
|
||||
"installed_payload": {
|
||||
"algorithm": "govoplan-wheel-declared-payload-v1",
|
||||
"sha256": "b" * 64,
|
||||
"file_count": 1,
|
||||
},
|
||||
"requires_installer_receipt": False,
|
||||
}
|
||||
]
|
||||
|
||||
apply_python_artifact_identities(
|
||||
payload,
|
||||
repo_versions={"govoplan-demo": "1.2.3"},
|
||||
python_artifacts={},
|
||||
)
|
||||
|
||||
self.assertNotIn("artifacts", payload["release"])
|
||||
self.assertIn(
|
||||
"no built artifact identity",
|
||||
" ".join(selected_artifact_identity_issues(payload)),
|
||||
)
|
||||
|
||||
def test_tag_only_selection_needs_no_python_artifact_mapping(self) -> None:
|
||||
payload = self._catalog_payload()
|
||||
payload["release"]["selected_units"].append(
|
||||
{"repo": "govoplan", "version": "1.2.3"}
|
||||
)
|
||||
|
||||
apply_python_artifact_identities(
|
||||
payload,
|
||||
repo_versions={"govoplan": "1.2.3"},
|
||||
python_artifacts={},
|
||||
)
|
||||
|
||||
self.assertIn("selected_units", payload["release"])
|
||||
|
||||
def test_unsafe_or_mismatched_wheel_fails_closed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
unsafe = root / "govoplan_demo-1.2.3-py3-none-any.whl"
|
||||
with zipfile.ZipFile(unsafe, "w") as archive:
|
||||
archive.writestr("../escape", b"bad")
|
||||
archive.writestr(
|
||||
"govoplan_demo-1.2.3.dist-info/METADATA",
|
||||
"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: 1.2.3\n",
|
||||
)
|
||||
wrong = self._wheel(root, version="9.9.9")
|
||||
|
||||
with self.assertRaisesRegex(ArtifactIdentityError, "unsafe"):
|
||||
inspect_python_wheel(unsafe)
|
||||
with self.assertRaisesRegex(ValueError, "expected govoplan-demo 1.2.3"):
|
||||
apply_python_artifact_identities(
|
||||
self._catalog_payload(),
|
||||
repo_versions={"govoplan-demo": "1.2.3"},
|
||||
python_artifacts={"govoplan-demo": wrong},
|
||||
)
|
||||
|
||||
def test_receipt_issuer_hashes_exact_consumed_wheel(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
original = self._wheel(root / "original", value=b"VALUE = 1\n")
|
||||
different = self._wheel(root / "different", value=b"VALUE = 2\n")
|
||||
identity = inspect_python_wheel(original)
|
||||
catalog = self._catalog_payload()
|
||||
catalog["channel"] = "stable"
|
||||
catalog["sequence"] = 1
|
||||
catalog["release"]["artifacts"] = [identity.catalog_payload()]
|
||||
assessment = {
|
||||
"assessment_id": "assessment:test",
|
||||
"release": {"ref": "stable-catalog-1"},
|
||||
"composition": [{"module_id": "demo", "enabled": True}],
|
||||
}
|
||||
installed_payload = {
|
||||
"algorithm": "govoplan-installed-record-payload-v1",
|
||||
"sha256": "c" * 64,
|
||||
"file_count": identity.payload_file_count,
|
||||
}
|
||||
evidence = {
|
||||
"assessment_id": "assessment:test",
|
||||
"assessment_release": "stable-catalog-1",
|
||||
"collected_at": "2026-07-22T12:00:00Z",
|
||||
"artifacts": [
|
||||
{
|
||||
"package_name": "govoplan-demo",
|
||||
"package_version": "1.2.3",
|
||||
"record_integrity": {
|
||||
"status": "verified",
|
||||
"hashed_file_count": identity.payload_file_count,
|
||||
"permitted_unhashed_file_count": 1,
|
||||
"generated_unhashed_file_count": 0,
|
||||
"unverifiable_file_count": 0,
|
||||
"missing_file_count": 0,
|
||||
"mismatched_file_count": 0,
|
||||
"artifact_payload_identity": catalog["release"]["artifacts"][0]["installed_payload"],
|
||||
"installed_payload_identity": installed_payload,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
key = Ed25519PrivateKey.generate()
|
||||
|
||||
receipt = issue_installer_receipt(
|
||||
assessment=assessment,
|
||||
catalog=catalog,
|
||||
installed_evidence=evidence,
|
||||
receipt_id="install:test",
|
||||
key_id="installer:test",
|
||||
private_key=key,
|
||||
consumed_artifacts={"govoplan-demo": original},
|
||||
issued_at=datetime(2026, 7, 22, 12, 1, tzinfo=UTC),
|
||||
same_process_observation=True,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "differs from the signed"):
|
||||
issue_installer_receipt(
|
||||
assessment=assessment,
|
||||
catalog=catalog,
|
||||
installed_evidence=evidence,
|
||||
receipt_id="install:test",
|
||||
key_id="installer:test",
|
||||
private_key=key,
|
||||
consumed_artifacts={"govoplan-demo": different},
|
||||
issued_at=datetime(2026, 7, 22, 12, 1, tzinfo=UTC),
|
||||
same_process_observation=True,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "within five minutes"):
|
||||
issue_installer_receipt(
|
||||
assessment=assessment,
|
||||
catalog=catalog,
|
||||
installed_evidence=evidence,
|
||||
receipt_id="install:test",
|
||||
key_id="installer:test",
|
||||
private_key=key,
|
||||
consumed_artifacts={"govoplan-demo": original},
|
||||
issued_at=datetime(2026, 7, 22, 12, 6, tzinfo=UTC),
|
||||
same_process_observation=True,
|
||||
)
|
||||
|
||||
self.assertEqual(identity.archive_sha256, receipt["artifacts"][0]["catalog_archive_sha256"])
|
||||
|
||||
def test_path_replacement_during_one_descriptor_inspection_fails_closed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
target = self._wheel(root / "target", value=b"VALUE = 1\n")
|
||||
replacement = self._wheel(root / "replacement", value=b"VALUE = 2\n")
|
||||
real_zip = zipfile.ZipFile
|
||||
|
||||
def swap_path(file_or_path, *args, **kwargs):
|
||||
target.unlink()
|
||||
replacement.rename(target)
|
||||
return real_zip(file_or_path, *args, **kwargs)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"govoplan_release.artifact_identity.zipfile.ZipFile",
|
||||
side_effect=swap_path,
|
||||
),
|
||||
self.assertRaisesRegex(ArtifactIdentityError, "changed"),
|
||||
):
|
||||
inspect_python_wheel(target)
|
||||
|
||||
@staticmethod
|
||||
def _catalog_payload() -> dict[str, object]:
|
||||
return {
|
||||
"core_release": {},
|
||||
"modules": [
|
||||
{
|
||||
"module_id": "demo",
|
||||
"version": "1.2.3",
|
||||
"python_package": "govoplan-demo",
|
||||
"python_ref": "govoplan-demo @ git+ssh://git@example.test/acme/govoplan-demo.git@v1.2.3",
|
||||
}
|
||||
],
|
||||
"release": {
|
||||
"selected_units": [
|
||||
{"repo": "govoplan-demo", "version": "1.2.3"}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _wheel(
|
||||
root: Path,
|
||||
*,
|
||||
version: str = "1.2.3",
|
||||
console_script: bool = False,
|
||||
value: bytes = b"VALUE = 1\n",
|
||||
) -> Path:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
wheel = root / f"govoplan_demo-{version}-py3-none-any.whl"
|
||||
dist_info = f"govoplan_demo-{version}.dist-info"
|
||||
files: dict[str, bytes] = {
|
||||
"govoplan_demo.py": value,
|
||||
f"{dist_info}/METADATA": (
|
||||
f"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: {version}\n"
|
||||
).encode(),
|
||||
f"{dist_info}/WHEEL": b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
}
|
||||
if console_script:
|
||||
files[f"{dist_info}/entry_points.txt"] = (
|
||||
b"[console_scripts]\ngovoplan-demo = govoplan_demo:main\n"
|
||||
)
|
||||
record_buffer = io.StringIO()
|
||||
writer = csv.writer(record_buffer, lineterminator="\n")
|
||||
for name, encoded in files.items():
|
||||
writer.writerow((name, "", len(encoded)))
|
||||
writer.writerow((f"{dist_info}/RECORD", "", ""))
|
||||
files[f"{dist_info}/RECORD"] = record_buffer.getvalue().encode()
|
||||
with zipfile.ZipFile(wheel, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for name, encoded in files.items():
|
||||
archive.writestr(name, encoded)
|
||||
return wheel
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user