fix(assessment): establish independent release trust
This commit is contained in:
@@ -8,6 +8,7 @@ import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
@@ -28,6 +29,7 @@ from govoplan_assessment.capability_fit import ( # noqa: E402
|
||||
local_tag_provenance,
|
||||
render_review,
|
||||
review_capability_fit,
|
||||
trusted_keys_from_keyring,
|
||||
)
|
||||
|
||||
|
||||
@@ -52,7 +54,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("current", report["status"])
|
||||
@@ -64,7 +67,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
self.assertEqual(
|
||||
json.dumps(report, sort_keys=True), json.dumps(repeated, sort_keys=True)
|
||||
@@ -79,7 +83,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("review_required", report["status"])
|
||||
@@ -114,7 +119,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
@@ -147,7 +153,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("review_required", report["status"])
|
||||
@@ -201,7 +208,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
@@ -231,7 +239,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
self.assertEqual(
|
||||
replay_environment,
|
||||
@@ -241,24 +250,43 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
self.assertEqual("current", report["status"])
|
||||
self.assertTrue(report["proof_scope"]["catalog_signature_and_keyring"]["valid"])
|
||||
|
||||
def test_untrusted_or_substituted_keyring_blocks_rerun(self) -> None:
|
||||
catalog, _ = signed_catalog(self.assessment)
|
||||
_, other_keyring = signed_catalog(self.assessment)
|
||||
def test_self_consistent_substitution_fails_independent_trust_root(self) -> None:
|
||||
_, trusted_keyring = signed_catalog(self.assessment)
|
||||
catalog, published_keyring = signed_catalog(self.assessment)
|
||||
|
||||
report = review_capability_fit(
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=other_keyring,
|
||||
published_keyring=published_keyring,
|
||||
trusted_keyring=trusted_keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
codes = {item["code"] for item in report["findings"]}
|
||||
self.assertIn("catalog_trust", codes)
|
||||
self.assertIn("catalog_keyring_hash_mismatch", codes)
|
||||
self.assertNotIn("catalog_keyring_hash_mismatch", codes)
|
||||
self.assertTrue(report["proof_scope"]["assessment_schema"]["valid"])
|
||||
self.assertFalse(
|
||||
report["proof_scope"]["catalog_signature_and_keyring"]["valid"]
|
||||
report["proof_scope"]["catalog_signature_and_trusted_keyring"]["valid"]
|
||||
)
|
||||
self.assertTrue(report["proof_scope"]["published_keyring_hash"]["valid"])
|
||||
|
||||
def test_locally_pinned_rotation_keys_follow_core_status_semantics(self) -> None:
|
||||
keys = trusted_keys_from_keyring(
|
||||
{
|
||||
"keys": [
|
||||
{"key_id": "active", "status": "active", "public_key": "a"},
|
||||
{"key_id": "next", "status": "next", "public_key": "b"},
|
||||
{"key_id": "retired", "status": "retired", "public_key": "c"},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual({"active": "a", "next": "b"}, keys)
|
||||
self.assertEqual(
|
||||
{"legacy": "base64-key"},
|
||||
trusted_keys_from_keyring({"legacy": "base64-key"}),
|
||||
)
|
||||
|
||||
def test_schema_error_blocks_before_comparison(self) -> None:
|
||||
@@ -270,7 +298,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=assessment,
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
@@ -291,7 +320,8 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
assessment=deepcopy(self.assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
)
|
||||
|
||||
rendered = render_review(report)
|
||||
@@ -308,19 +338,190 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
with mock.patch.object(
|
||||
module,
|
||||
"fetch_json",
|
||||
return_value={
|
||||
"ok": False,
|
||||
"error": "Authorization: secret at https://internal.invalid",
|
||||
},
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
SystemExit, r"^Could not fetch fixed public catalog endpoint[.]$"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
trusted_path = Path(temp_dir) / "trusted-keyring.json"
|
||||
trusted_path.write_text('{"keys": []}\n', encoding="utf-8")
|
||||
with mock.patch.object(
|
||||
module,
|
||||
"fetch_json",
|
||||
return_value={
|
||||
"ok": False,
|
||||
"error": "Authorization: secret at https://internal.invalid",
|
||||
},
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
SystemExit, r"^Could not fetch fixed public catalog endpoint[.]$"
|
||||
):
|
||||
module.main(
|
||||
[
|
||||
"--public",
|
||||
"--trusted-keyring",
|
||||
str(trusted_path),
|
||||
"--skip-tag-provenance",
|
||||
]
|
||||
)
|
||||
|
||||
def test_cli_requires_an_independent_local_trust_root(self) -> None:
|
||||
script = META_ROOT / "tools" / "assessments" / "capability-fit.py"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"capability_fit_cli_trust", script
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{module.TRUSTED_KEYRING_FILE_ENV: ""},
|
||||
clear=False,
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, r"--trusted-keyring .* required"):
|
||||
module.main(["--public", "--skip-tag-provenance"])
|
||||
|
||||
def test_local_selected_tag_commit_and_object_must_match_exactly(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
workspace = Path(temp_dir)
|
||||
commit_sha, tag_object_sha = create_tagged_repository(
|
||||
workspace=workspace,
|
||||
repository="govoplan-core",
|
||||
version="0.1.0",
|
||||
)
|
||||
assessment = deepcopy(self.assessment)
|
||||
core = next(
|
||||
deepcopy(item)
|
||||
for item in assessment["composition"]
|
||||
if item["module_id"] == "core"
|
||||
)
|
||||
core["manifest_version"] = "0.1.0"
|
||||
core["commit"] = commit_sha[:12]
|
||||
assessment["composition"] = [core]
|
||||
selected_unit = {
|
||||
"repo": "govoplan-core",
|
||||
"version": "0.1.0",
|
||||
"tag": "v0.1.0",
|
||||
"commit_sha": commit_sha,
|
||||
"tag_object_sha": tag_object_sha,
|
||||
}
|
||||
catalog, keyring = signed_catalog(
|
||||
assessment,
|
||||
selected_units=[selected_unit],
|
||||
)
|
||||
|
||||
baseline = review_capability_fit(
|
||||
assessment=deepcopy(assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
workspace_root=workspace,
|
||||
)
|
||||
|
||||
self.assertEqual("current", baseline["status"])
|
||||
self.assertTrue(baseline["proof_scope"]["local_tag_provenance"]["valid"])
|
||||
|
||||
forged_commit = commit_sha[:12] + ("0" * 28)
|
||||
if forged_commit == commit_sha:
|
||||
forged_commit = commit_sha[:12] + ("f" * 28)
|
||||
forged_catalog, forged_keyring = signed_catalog(
|
||||
assessment,
|
||||
selected_units=[{**selected_unit, "commit_sha": forged_commit}],
|
||||
)
|
||||
commit_mismatch = review_capability_fit(
|
||||
assessment=deepcopy(assessment),
|
||||
schema=self.schema,
|
||||
catalog=forged_catalog,
|
||||
published_keyring=forged_keyring,
|
||||
trusted_keyring=forged_keyring,
|
||||
workspace_root=workspace,
|
||||
)
|
||||
|
||||
self.assertEqual("review_required", commit_mismatch["status"])
|
||||
self.assertIn(
|
||||
"tag_signed_commit_changed",
|
||||
{item["kind"] for item in commit_mismatch["changes"]},
|
||||
)
|
||||
|
||||
repo = workspace / "govoplan-core"
|
||||
git_text(repo, "tag", "-d", "v0.1.0")
|
||||
git_text(repo, "tag", "-a", "v0.1.0", "-m", "Re-annotated release")
|
||||
reannotated = review_capability_fit(
|
||||
assessment=deepcopy(assessment),
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
workspace_root=workspace,
|
||||
)
|
||||
|
||||
self.assertEqual("review_required", reannotated["status"])
|
||||
self.assertIn(
|
||||
"tag_object_changed",
|
||||
{item["kind"] for item in reannotated["changes"]},
|
||||
)
|
||||
self.assertTrue(reannotated["proof_scope"]["local_tag_provenance"]["checked"])
|
||||
self.assertFalse(reannotated["proof_scope"]["local_tag_provenance"]["valid"])
|
||||
|
||||
def test_missing_composition_entry_does_not_claim_complete_tag_proof(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
workspace = Path(temp_dir)
|
||||
core_commit, core_tag_object = create_tagged_repository(
|
||||
workspace=workspace,
|
||||
repository="govoplan-core",
|
||||
version="0.1.0",
|
||||
)
|
||||
tenancy_commit, _ = create_tagged_repository(
|
||||
workspace=workspace,
|
||||
repository="govoplan-tenancy",
|
||||
version="0.1.0",
|
||||
)
|
||||
reviewed_assessment = deepcopy(self.assessment)
|
||||
core = next(
|
||||
deepcopy(item)
|
||||
for item in reviewed_assessment["composition"]
|
||||
if item["module_id"] == "core"
|
||||
)
|
||||
tenancy = next(
|
||||
deepcopy(item)
|
||||
for item in reviewed_assessment["composition"]
|
||||
if item["module_id"] == "tenancy"
|
||||
)
|
||||
core.update(manifest_version="0.1.0", commit=core_commit[:12])
|
||||
tenancy.update(manifest_version="0.1.0", commit=tenancy_commit[:12])
|
||||
catalog_assessment = deepcopy(reviewed_assessment)
|
||||
catalog_assessment["composition"] = [core]
|
||||
reviewed_assessment["composition"] = [core, tenancy]
|
||||
catalog, keyring = signed_catalog(
|
||||
catalog_assessment,
|
||||
selected_units=[
|
||||
{
|
||||
"repo": "govoplan-core",
|
||||
"version": "0.1.0",
|
||||
"tag": "v0.1.0",
|
||||
"commit_sha": core_commit,
|
||||
"tag_object_sha": core_tag_object,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
report = review_capability_fit(
|
||||
assessment=reviewed_assessment,
|
||||
schema=self.schema,
|
||||
catalog=catalog,
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
workspace_root=workspace,
|
||||
)
|
||||
|
||||
proof = report["proof_scope"]["local_tag_provenance"]
|
||||
self.assertEqual("review_required", report["status"])
|
||||
self.assertFalse(proof["checked"])
|
||||
self.assertIsNone(proof["valid"])
|
||||
self.assertEqual(2, proof["attempted_count"])
|
||||
self.assertEqual(2, proof["expected_count"])
|
||||
self.assertNotIn(
|
||||
"tag_provenance_changed", {item["code"] for item in report["findings"]}
|
||||
)
|
||||
|
||||
def test_repository_path_traversal_is_not_resolved(self) -> None:
|
||||
with mock.patch("govoplan_assessment.capability_fit.subprocess.run") as run:
|
||||
result = local_tag_provenance(
|
||||
@@ -335,6 +536,36 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
||||
run.assert_not_called()
|
||||
|
||||
|
||||
def create_tagged_repository(
|
||||
*, workspace: Path, repository: str, version: str
|
||||
) -> tuple[str, str]:
|
||||
repo = workspace / repository
|
||||
git_text(workspace, "init", "-b", "main", str(repo))
|
||||
git_text(repo, "config", "user.name", "Capability Fit Test")
|
||||
git_text(repo, "config", "user.email", "capability-fit@example.invalid")
|
||||
(repo / "README.md").write_text(f"{repository}\n", encoding="utf-8")
|
||||
git_text(repo, "add", "README.md")
|
||||
git_text(repo, "commit", "-m", "Initial release fixture")
|
||||
git_text(repo, "tag", "-a", f"v{version}", "-m", f"Release v{version}")
|
||||
return (
|
||||
git_text(repo, "rev-parse", "HEAD"),
|
||||
git_text(repo, "rev-parse", f"refs/tags/v{version}"),
|
||||
)
|
||||
|
||||
|
||||
def git_text(cwd: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
("git", *args),
|
||||
cwd=cwd,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr or result.stdout)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def signed_catalog(
|
||||
assessment: dict[str, object],
|
||||
*,
|
||||
|
||||
72
tests/test_release_http_fetch.py
Normal file
72
tests/test_release_http_fetch.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
RELEASE_ROOT = META_ROOT / "tools" / "release"
|
||||
if str(RELEASE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(RELEASE_ROOT))
|
||||
|
||||
from govoplan_release.http_fetch import ( # noqa: E402
|
||||
DEFAULT_MAX_RESPONSE_BYTES,
|
||||
fetch_http,
|
||||
)
|
||||
|
||||
|
||||
class ReleaseHttpFetchTests(unittest.TestCase):
|
||||
def test_content_length_over_default_limit_blocks_before_read(self) -> None:
|
||||
response = FakeResponse(
|
||||
body=b"ignored",
|
||||
headers={"Content-Length": str(DEFAULT_MAX_RESPONSE_BYTES + 1)},
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"govoplan_release.http_fetch.urllib.request.urlopen",
|
||||
return_value=response,
|
||||
):
|
||||
with self.assertRaisesRegex(ValueError, "exceeds the configured size"):
|
||||
fetch_http("https://example.invalid/catalog.json", timeout=1)
|
||||
|
||||
self.assertEqual([], response.read_sizes)
|
||||
|
||||
def test_streamed_body_is_capped_when_length_is_absent(self) -> None:
|
||||
response = FakeResponse(body=b"x" * 9, headers={})
|
||||
|
||||
with mock.patch(
|
||||
"govoplan_release.http_fetch.urllib.request.urlopen",
|
||||
return_value=response,
|
||||
):
|
||||
with self.assertRaisesRegex(ValueError, "exceeds the configured size"):
|
||||
fetch_http(
|
||||
"https://example.invalid/catalog.json",
|
||||
timeout=1,
|
||||
max_response_bytes=8,
|
||||
)
|
||||
|
||||
self.assertEqual([9], response.read_sizes)
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, *, body: bytes, headers: dict[str, str]) -> None:
|
||||
self.status = 200
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
self.read_sizes: list[int] = []
|
||||
|
||||
def __enter__(self) -> FakeResponse:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
def read(self, size: int) -> bytes:
|
||||
self.read_sizes.append(size)
|
||||
return self.body[:size]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user