Bind installed releases to signed artifact receipts
This commit is contained in:
@@ -37,6 +37,10 @@ from govoplan_assessment.evidence import ( # noqa: E402
|
||||
review_installed_composition,
|
||||
validate_payload,
|
||||
)
|
||||
from govoplan_assessment.installer_receipt import ( # noqa: E402
|
||||
issue_installer_receipt,
|
||||
)
|
||||
from govoplan_release.artifact_identity import inspect_python_wheel # noqa: E402
|
||||
from tests.test_capability_fit_review import signed_catalog # noqa: E402
|
||||
|
||||
|
||||
@@ -89,6 +93,202 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
report["proof_scope"]["installed_artifacts"]["evidence_sha256"],
|
||||
)
|
||||
|
||||
def test_signed_catalog_artifact_identities_bind_installed_release_origin(
|
||||
self,
|
||||
) -> None:
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
release_artifacts=catalog_artifacts_for(evidence),
|
||||
)
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
)
|
||||
|
||||
origin = report["proof_scope"]["installed_release_origin"]
|
||||
self.assertEqual("current", report["status"])
|
||||
self.assertTrue(origin["checked"])
|
||||
self.assertTrue(origin["valid"])
|
||||
self.assertTrue(origin["release_origin_bound"])
|
||||
self.assertEqual(len(evidence["artifacts"]), origin["anchored_artifact_digest_count"])
|
||||
self.assertEqual(0, origin["signed_installer_receipt_count"])
|
||||
|
||||
def test_role_scoped_installer_receipt_admits_transformed_artifacts_and_boundary(
|
||||
self,
|
||||
) -> None:
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
artifact_rows = catalog_artifacts_for(evidence, requires_receipt=True)
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
release_artifacts=artifact_rows,
|
||||
)
|
||||
receipt, installer_keys = signed_installer_receipt(
|
||||
assessment=self.assessment,
|
||||
installed=evidence,
|
||||
catalog=catalog,
|
||||
catalog_artifacts=artifact_rows,
|
||||
)
|
||||
proof, proof_keys = signed_boundary_evidence(
|
||||
assessment=self.assessment,
|
||||
installed=evidence,
|
||||
claims=[boundary_claim("target_environment", "passed")],
|
||||
allowed_scopes=["target_environment"],
|
||||
)
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
installer_receipt=receipt,
|
||||
installer_authority_keyring=installer_keys,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=proof_keys,
|
||||
installed_evidence_mode="imported_unsigned",
|
||||
)
|
||||
|
||||
origin = report["proof_scope"]["installed_release_origin"]
|
||||
self.assertTrue(origin["valid"])
|
||||
self.assertEqual(0, origin["anchored_artifact_digest_count"])
|
||||
self.assertEqual(len(evidence["artifacts"]), origin["signed_installer_receipt_count"])
|
||||
self.assertTrue(report["proof_scope"]["target_environment"]["checked"])
|
||||
self.assertTrue(report["proof_scope"]["target_environment"]["valid"])
|
||||
self.assertTrue(
|
||||
report["proof_scope"]["installed_evidence_observation"][
|
||||
"receipt_authenticated"
|
||||
]
|
||||
)
|
||||
self.assertNotIn(
|
||||
"installed_evidence_unsigned_import",
|
||||
{item["code"] for item in report["findings"]},
|
||||
)
|
||||
|
||||
def test_installer_receipt_cannot_reuse_release_key_or_cover_forged_payload(
|
||||
self,
|
||||
) -> None:
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
artifact_rows = catalog_artifacts_for(evidence, requires_receipt=True)
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
release_artifacts=artifact_rows,
|
||||
)
|
||||
receipt, installer_keys = signed_installer_receipt(
|
||||
assessment=self.assessment,
|
||||
installed=evidence,
|
||||
catalog=catalog,
|
||||
catalog_artifacts=artifact_rows,
|
||||
)
|
||||
forged = deepcopy(evidence)
|
||||
forged["artifacts"][0]["record_integrity"]["installed_payload_identity"][
|
||||
"sha256"
|
||||
] = "f" * 64
|
||||
reused = deepcopy(installer_keys)
|
||||
reused["keys"][0]["public_key"] = keyring["keys"][0]["public_key"]
|
||||
|
||||
forged_report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=forged,
|
||||
installer_receipt=receipt,
|
||||
installer_authority_keyring=installer_keys,
|
||||
)
|
||||
reused_report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
installer_receipt=receipt,
|
||||
installer_authority_keyring=reused,
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"installer_receipt_evidence_mismatch",
|
||||
{item["code"] for item in forged_report["findings"]},
|
||||
)
|
||||
self.assertIn(
|
||||
"installer_authority_untrusted",
|
||||
{item["code"] for item in reused_report["findings"]},
|
||||
)
|
||||
self.assertFalse(
|
||||
forged_report["proof_scope"]["installed_release_origin"]["valid"]
|
||||
)
|
||||
self.assertFalse(
|
||||
reused_report["proof_scope"]["installed_release_origin"]["valid"]
|
||||
)
|
||||
|
||||
def test_receipt_issuer_refuses_file_like_or_unverified_observations(self) -> None:
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
artifacts = catalog_artifacts_for(evidence, requires_receipt=True)
|
||||
catalog, _ = signed_catalog(
|
||||
self.assessment,
|
||||
release_artifacts=artifacts,
|
||||
)
|
||||
private_key = Ed25519PrivateKey.generate()
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "same-process"):
|
||||
issue_installer_receipt(
|
||||
assessment=self.assessment,
|
||||
catalog=catalog,
|
||||
installed_evidence=evidence,
|
||||
receipt_id="install:test",
|
||||
key_id="installer:test",
|
||||
private_key=private_key,
|
||||
)
|
||||
evidence["artifacts"][0]["record_integrity"]["status"] = "partial"
|
||||
evidence["artifacts"][0]["record_integrity"]["unverifiable_file_count"] = 1
|
||||
with self.assertRaisesRegex(ValueError, "not a verified match"):
|
||||
issue_installer_receipt(
|
||||
assessment=self.assessment,
|
||||
catalog=catalog,
|
||||
installed_evidence=evidence,
|
||||
receipt_id="install:test",
|
||||
key_id="installer:test",
|
||||
private_key=private_key,
|
||||
same_process_observation=True,
|
||||
consumed_artifacts={
|
||||
str(item["package_name"]): Path("unused.whl")
|
||||
for item in evidence["artifacts"]
|
||||
},
|
||||
)
|
||||
|
||||
def test_installer_receipt_and_observation_must_be_current_together(self) -> None:
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
artifact_rows = catalog_artifacts_for(evidence, requires_receipt=True)
|
||||
catalog, keyring = signed_catalog(
|
||||
self.assessment,
|
||||
release_artifacts=artifact_rows,
|
||||
)
|
||||
receipt, installer_keys = signed_installer_receipt(
|
||||
assessment=self.assessment,
|
||||
installed=evidence,
|
||||
catalog=catalog,
|
||||
catalog_artifacts=artifact_rows,
|
||||
)
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
installer_receipt=receipt,
|
||||
installer_authority_keyring=installer_keys,
|
||||
installed_evidence_mode="imported_unsigned",
|
||||
verification_time=datetime(2026, 7, 23, 12, 5, 1, tzinfo=UTC),
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"installer_receipt_time_invalid",
|
||||
{item["code"] for item in report["findings"]},
|
||||
)
|
||||
self.assertFalse(
|
||||
report["proof_scope"]["installed_release_origin"]["valid"]
|
||||
)
|
||||
self.assertFalse(
|
||||
report["proof_scope"]["installed_evidence_observation"][
|
||||
"receipt_authenticated"
|
||||
]
|
||||
)
|
||||
|
||||
def test_catalog_trust_blocker_invalidates_dependent_evidence_scopes(
|
||||
self,
|
||||
) -> None:
|
||||
@@ -523,6 +723,7 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
text=True,
|
||||
)
|
||||
wheel = next(wheels.glob("govoplan_core-*.whl"))
|
||||
built_identity = inspect_python_wheel(wheel)
|
||||
subprocess.run(
|
||||
(
|
||||
str(build_python),
|
||||
@@ -568,6 +769,15 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
self.assertGreater(record["permitted_unhashed_file_count"], 0)
|
||||
self.assertGreater(record["generated_unhashed_file_count"], 0)
|
||||
self.assertEqual(0, record["unverifiable_file_count"])
|
||||
self.assertEqual(
|
||||
built_identity.payload_sha256,
|
||||
record["artifact_payload_identity"]["sha256"],
|
||||
)
|
||||
self.assertEqual(
|
||||
built_identity.payload_file_count,
|
||||
record["artifact_payload_identity"]["file_count"],
|
||||
)
|
||||
self.assertTrue(built_identity.requires_installer_receipt)
|
||||
self.assertEqual(
|
||||
(), validate_payload(payload=evidence, schema=self.installed_schema)
|
||||
)
|
||||
@@ -1162,6 +1372,8 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
catalog: dict[str, object],
|
||||
keyring: dict[str, object],
|
||||
installed_evidence: dict[str, object] | None = None,
|
||||
installer_receipt: dict[str, object] | None = None,
|
||||
installer_authority_keyring: dict[str, object] | None = None,
|
||||
boundary_evidence: dict[str, object] | None = None,
|
||||
authority_keyring: dict[str, object] | None = None,
|
||||
installed_evidence_mode: str = "direct_local",
|
||||
@@ -1179,6 +1391,16 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
installed_evidence_schema=(installed_schema or self.installed_schema)
|
||||
if installed_evidence is not None
|
||||
else None,
|
||||
installer_receipt=installer_receipt,
|
||||
installer_receipt_schema=load_json("docs/installer-receipt.schema.json")
|
||||
if installer_receipt is not None
|
||||
else None,
|
||||
installer_authority_keyring=installer_authority_keyring,
|
||||
installer_authority_keyring_schema=load_json(
|
||||
"docs/installer-receipt-authority-keyring.schema.json"
|
||||
)
|
||||
if installer_receipt is not None
|
||||
else None,
|
||||
boundary_evidence=boundary_evidence,
|
||||
boundary_evidence_schema=self.boundary_schema
|
||||
if boundary_evidence is not None
|
||||
@@ -1196,7 +1418,7 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
def matching_installed_evidence(assessment: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"$schema": "./installed-composition-evidence.schema.json",
|
||||
"schema_version": "0.3.0",
|
||||
"schema_version": "0.4.0",
|
||||
"evidence_kind": "govoplan.installed-composition",
|
||||
"assessment_id": assessment["assessment_id"],
|
||||
"assessment_release": assessment["release"]["ref"],
|
||||
@@ -1238,6 +1460,20 @@ def artifact(
|
||||
"unverifiable_file_count": 0,
|
||||
"missing_file_count": 0,
|
||||
"mismatched_file_count": 0,
|
||||
"artifact_payload_identity": {
|
||||
"algorithm": "govoplan-wheel-declared-payload-v1",
|
||||
"sha256": hashlib.sha256(
|
||||
f"artifact:{package_name}:{version}".encode()
|
||||
).hexdigest(),
|
||||
"file_count": 1,
|
||||
},
|
||||
"installed_payload_identity": {
|
||||
"algorithm": "govoplan-installed-record-payload-v1",
|
||||
"sha256": hashlib.sha256(
|
||||
f"installed:{package_name}:{version}".encode()
|
||||
).hexdigest(),
|
||||
"file_count": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1323,6 +1559,107 @@ def signed_boundary_evidence(
|
||||
return proof, keyring
|
||||
|
||||
|
||||
def catalog_artifacts_for(
|
||||
installed: dict[str, object], *, requires_receipt: bool = False
|
||||
) -> list[dict[str, object]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
for item in installed["artifacts"]:
|
||||
record = item["record_integrity"]
|
||||
rows.append(
|
||||
{
|
||||
"artifact_kind": "python-wheel",
|
||||
"package_name": item["package_name"],
|
||||
"package_version": item["package_version"],
|
||||
"archive_sha256": hashlib.sha256(
|
||||
f"wheel:{item['package_name']}:{item['package_version']}".encode()
|
||||
).hexdigest(),
|
||||
"archive_size": 1024,
|
||||
"installed_payload": deepcopy(record["artifact_payload_identity"]),
|
||||
"requires_installer_receipt": requires_receipt,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def signed_installer_receipt(
|
||||
*,
|
||||
assessment: dict[str, object],
|
||||
installed: dict[str, object],
|
||||
catalog: dict[str, object],
|
||||
catalog_artifacts: list[dict[str, object]],
|
||||
) -> tuple[dict[str, object], dict[str, object]]:
|
||||
private_key = Ed25519PrivateKey.generate()
|
||||
public_key = base64.b64encode(
|
||||
private_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw,
|
||||
)
|
||||
).decode("ascii")
|
||||
catalog_by_package = {
|
||||
str(item["package_name"]): item for item in catalog_artifacts
|
||||
}
|
||||
receipt: dict[str, object] = {
|
||||
"$schema": "./installer-receipt.schema.json",
|
||||
"schema_version": "0.1.0",
|
||||
"evidence_kind": "govoplan.installer-receipt",
|
||||
"receipt_id": "install:test",
|
||||
"assessment_id": assessment["assessment_id"],
|
||||
"assessment_release": assessment["release"]["ref"],
|
||||
"installed_evidence_sha256": canonical_sha256(installed),
|
||||
"catalog": {
|
||||
"channel": catalog["channel"],
|
||||
"sequence": catalog["sequence"],
|
||||
"sha256": hashlib.sha256(
|
||||
json.dumps(
|
||||
catalog,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
).encode("utf-8")
|
||||
).hexdigest(),
|
||||
},
|
||||
"issued_at": "2026-07-23T11:59:00Z",
|
||||
"artifacts": [
|
||||
{
|
||||
"package_name": item["package_name"],
|
||||
"package_version": item["package_version"],
|
||||
"catalog_archive_sha256": catalog_by_package[
|
||||
str(item["package_name"])
|
||||
]["archive_sha256"],
|
||||
"installed_payload": deepcopy(
|
||||
item["record_integrity"]["installed_payload_identity"]
|
||||
),
|
||||
}
|
||||
for item in installed["artifacts"]
|
||||
],
|
||||
}
|
||||
receipt["signatures"] = [
|
||||
{
|
||||
"algorithm": "ed25519",
|
||||
"key_id": "installer:test",
|
||||
"value": base64.b64encode(
|
||||
private_key.sign(canonical_bytes(receipt))
|
||||
).decode("ascii"),
|
||||
}
|
||||
]
|
||||
keyring = {
|
||||
"$schema": "./installer-receipt-authority-keyring.schema.json",
|
||||
"schema_version": "0.1.0",
|
||||
"purpose": "govoplan.installer-receipt-authorities",
|
||||
"keys": [
|
||||
{
|
||||
"key_id": "installer:test",
|
||||
"status": "active",
|
||||
"public_key": public_key,
|
||||
"allowed_scopes": ["installed_release_origin"],
|
||||
"not_before": "2026-07-01T00:00:00Z",
|
||||
"not_after": "2026-08-01T00:00:00Z",
|
||||
}
|
||||
],
|
||||
}
|
||||
return receipt, keyring
|
||||
|
||||
|
||||
def create_distribution(
|
||||
root: Path,
|
||||
) -> tuple[metadata.PathDistribution, Path]:
|
||||
|
||||
Reference in New Issue
Block a user