feat(mcp): add immutable external artifact mirror lane
This commit is contained in:
393
scripts/security/tests/test_external_mcp_artifact_controller.py
Normal file
393
scripts/security/tests/test_external_mcp_artifact_controller.py
Normal file
@@ -0,0 +1,393 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
CONTROLLER_PATH = ROOT / "scripts/security/external_mcp_artifact_controller.py"
|
||||
POLICY_PATH = ROOT / "config/mcp/playwright-mcp-artifact-policy.json"
|
||||
WORKFLOW_PATH = ROOT / ".gitea/workflows/mcp-external-artifact-mirror.yaml"
|
||||
RUN_ID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
||||
TRACE_ID = f"mcp-artifact-{RUN_ID}"
|
||||
|
||||
|
||||
def _load_controller():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"external_mcp_artifact_controller", CONTROLLER_PATH
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
controller = _load_controller()
|
||||
|
||||
|
||||
def _verified_packages(policy):
|
||||
return tuple(
|
||||
controller.VerifiedPackage(
|
||||
policy=package,
|
||||
tarball=b"tarball",
|
||||
attestation=b"{}",
|
||||
metadata_sha256="a" * 64,
|
||||
tarball_sha512_hex=package.sha512_hex,
|
||||
tarball_sha1=package.shasum_sha1,
|
||||
attestation_sha256="b" * 64,
|
||||
unpacked_bytes=1024,
|
||||
archive_file_count=3,
|
||||
license_file_count=1,
|
||||
osv_response_sha256="c" * 64,
|
||||
vulnerability_ids=(),
|
||||
)
|
||||
for package in policy.packages
|
||||
)
|
||||
|
||||
|
||||
def _tarball(
|
||||
package,
|
||||
*,
|
||||
unsafe_symlink: bool = False,
|
||||
duplicate_manifest: bool = False,
|
||||
) -> bytes:
|
||||
package_json = json.dumps(
|
||||
{
|
||||
"name": package.name,
|
||||
"version": package.version,
|
||||
"license": package.license,
|
||||
"dependencies": package.dependencies,
|
||||
"optionalDependencies": package.optional_dependencies,
|
||||
}
|
||||
).encode()
|
||||
output = io.BytesIO()
|
||||
with tarfile.open(fileobj=output, mode="w:gz") as archive:
|
||||
for name, data in (
|
||||
("package/package.json", package_json),
|
||||
("package/LICENSE", b"Apache License 2.0"),
|
||||
):
|
||||
member = tarfile.TarInfo(name)
|
||||
member.size = len(data)
|
||||
archive.addfile(member, io.BytesIO(data))
|
||||
if duplicate_manifest:
|
||||
member = tarfile.TarInfo("package/package.json")
|
||||
member.size = len(package_json)
|
||||
archive.addfile(member, io.BytesIO(package_json))
|
||||
if unsafe_symlink:
|
||||
member = tarfile.TarInfo("package/escape")
|
||||
member.type = tarfile.SYMTYPE
|
||||
member.linkname = "../../outside"
|
||||
archive.addfile(member)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
class ExternalMcpArtifactControllerTest(unittest.TestCase):
|
||||
def test_policy_is_exact_discovery_only_and_fail_closed(self) -> None:
|
||||
policy = controller.load_policy(POLICY_PATH)
|
||||
raw = POLICY_PATH.read_text(encoding="utf-8").lower()
|
||||
|
||||
self.assertEqual(policy.candidate_id, "external.playwright-verifier")
|
||||
self.assertEqual(policy.risk_level, "high")
|
||||
self.assertEqual(len(policy.packages), 3)
|
||||
self.assertEqual(policy.mirror_tag, "0.0.78-bundle-v1")
|
||||
self.assertNotIn("@latest", raw)
|
||||
self.assertNotIn(":latest", raw)
|
||||
self.assertNotIn("npx -y", raw)
|
||||
self.assertNotIn("github.com", raw)
|
||||
self.assertNotIn("ghcr.io", raw)
|
||||
self.assertTrue(all(package.version for package in policy.packages))
|
||||
self.assertTrue(all(package.sha512_hex for package in policy.packages))
|
||||
|
||||
def test_gitea_workflow_has_no_remote_action_or_floating_supply_chain(self) -> None:
|
||||
raw = WORKFLOW_PATH.read_text(encoding="utf-8")
|
||||
lowered = raw.lower()
|
||||
|
||||
self.assertNotIn("uses:", lowered)
|
||||
self.assertNotIn("actions/checkout", lowered)
|
||||
self.assertNotIn("github.com", lowered)
|
||||
self.assertNotIn("api.github.com", lowered)
|
||||
self.assertNotIn("raw.githubusercontent.com", lowered)
|
||||
self.assertNotIn("codeload.github.com", lowered)
|
||||
self.assertNotIn("ghcr.io", lowered)
|
||||
self.assertNotIn("npx -y", lowered)
|
||||
self.assertNotIn("@latest", lowered)
|
||||
self.assertNotIn(":latest", lowered)
|
||||
self.assertNotIn("git push --force", lowered)
|
||||
self.assertIn("runs-on: awoooi-non110-host", raw)
|
||||
self.assertIn("docker login", raw)
|
||||
self.assertIn("--password-stdin", raw)
|
||||
self.assertIn("docker logout", raw)
|
||||
self.assertIn("verify_external_mcp_artifact_receipt.py", raw)
|
||||
self.assertIn("git merge --no-edit gitea/main", raw)
|
||||
self.assertIn("git push gitea HEAD:main", raw)
|
||||
self.assertIn('cron: "13 2 * * 3"', raw)
|
||||
|
||||
def test_dependency_closure_rejects_drift(self) -> None:
|
||||
payload = json.loads(POLICY_PATH.read_text(encoding="utf-8"))
|
||||
payload["packages"][0]["dependencies"]["playwright"] = "1.2.3"
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp) / "policy.json"
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with self.assertRaisesRegex(
|
||||
controller.ControllerError, "dependency_closure_invalid"
|
||||
):
|
||||
controller.load_policy(path)
|
||||
|
||||
def test_run_and_trace_identity_must_match(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
controller.ControllerError, "run_trace_identity_mismatch"
|
||||
):
|
||||
controller.main(
|
||||
[
|
||||
"--policy",
|
||||
str(POLICY_PATH),
|
||||
"--run-id",
|
||||
RUN_ID,
|
||||
"--trace-id",
|
||||
"mcp-artifact-00000000-0000-4000-8000-000000000000",
|
||||
"check",
|
||||
]
|
||||
)
|
||||
|
||||
def test_noncanonical_run_id_is_rejected(self) -> None:
|
||||
with self.assertRaisesRegex(controller.ControllerError, "run_id_not_canonical"):
|
||||
controller.main(
|
||||
[
|
||||
"--policy",
|
||||
str(POLICY_PATH),
|
||||
"--run-id",
|
||||
RUN_ID.upper(),
|
||||
"--trace-id",
|
||||
TRACE_ID,
|
||||
"check",
|
||||
]
|
||||
)
|
||||
|
||||
def test_check_mode_emits_bound_no_write_receipt(self) -> None:
|
||||
policy = controller.load_policy(POLICY_PATH)
|
||||
verified = _verified_packages(policy)
|
||||
with (
|
||||
patch.object(controller, "verify_upstream_bundle", return_value=verified),
|
||||
patch.object(controller, "build_cyclonedx_sbom", return_value={}),
|
||||
patch.object(
|
||||
controller,
|
||||
"_write_bundle",
|
||||
return_value=("d" * 64, "e" * 64),
|
||||
),
|
||||
io.StringIO() as output,
|
||||
redirect_stdout(output),
|
||||
):
|
||||
result = controller.main(
|
||||
[
|
||||
"--policy",
|
||||
str(POLICY_PATH),
|
||||
"--run-id",
|
||||
RUN_ID,
|
||||
"--trace-id",
|
||||
TRACE_ID,
|
||||
"check",
|
||||
]
|
||||
)
|
||||
receipt = json.loads(output.getvalue())
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(receipt["run_id"], RUN_ID)
|
||||
self.assertEqual(receipt["trace_id"], TRACE_ID)
|
||||
self.assertEqual(
|
||||
receipt["status"], "completed_check_mode_pending_independent_verifier"
|
||||
)
|
||||
self.assertFalse(receipt["promotion_allowed"])
|
||||
self.assertFalse(receipt["canary_allowed"])
|
||||
self.assertFalse(
|
||||
receipt["stage_receipts"]["sensor_source_receipt"]
|
||||
["runtime_request_or_response_body_stored"]
|
||||
)
|
||||
self.assertTrue(
|
||||
receipt["stage_receipts"]["sensor_source_receipt"]
|
||||
["upstream_supply_chain_attestation_bundled"]
|
||||
)
|
||||
self.assertEqual(
|
||||
receipt["stage_receipts"]["rollback_or_no_write_terminal"]["status"],
|
||||
"no_write_pending_external_verifier",
|
||||
)
|
||||
self.assertEqual(
|
||||
receipt["stage_receipts"]["independent_post_verifier"]["status"],
|
||||
"pending_external_verifier",
|
||||
)
|
||||
|
||||
def test_mirror_apply_requires_receipt_path(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
controller.ControllerError, "apply_receipt_path_required"
|
||||
):
|
||||
controller.main(
|
||||
[
|
||||
"--policy",
|
||||
str(POLICY_PATH),
|
||||
"--run-id",
|
||||
RUN_ID,
|
||||
"--trace-id",
|
||||
TRACE_ID,
|
||||
"mirror",
|
||||
"--apply",
|
||||
]
|
||||
)
|
||||
|
||||
def test_tarball_inspection_accepts_exact_manifest(self) -> None:
|
||||
package = controller.load_policy(POLICY_PATH).packages[0]
|
||||
|
||||
unpacked, files, licenses = controller.inspect_tarball(
|
||||
package,
|
||||
_tarball(package),
|
||||
maximum_unpacked_bytes=1024 * 1024,
|
||||
)
|
||||
|
||||
self.assertGreater(unpacked, 0)
|
||||
self.assertEqual(files, 2)
|
||||
self.assertEqual(licenses, 1)
|
||||
|
||||
def test_tarball_inspection_rejects_symlink(self) -> None:
|
||||
package = controller.load_policy(POLICY_PATH).packages[0]
|
||||
with self.assertRaisesRegex(
|
||||
controller.ControllerError, "package_archive_unsafe_entry"
|
||||
):
|
||||
controller.inspect_tarball(
|
||||
package,
|
||||
_tarball(package, unsafe_symlink=True),
|
||||
maximum_unpacked_bytes=1024 * 1024,
|
||||
)
|
||||
|
||||
def test_tarball_inspection_rejects_duplicate_manifest(self) -> None:
|
||||
package = controller.load_policy(POLICY_PATH).packages[0]
|
||||
with self.assertRaisesRegex(
|
||||
controller.ControllerError,
|
||||
"package_archive_duplicate_or_invalid_file",
|
||||
):
|
||||
controller.inspect_tarball(
|
||||
package,
|
||||
_tarball(package, duplicate_manifest=True),
|
||||
maximum_unpacked_bytes=1024 * 1024,
|
||||
)
|
||||
|
||||
def test_slsa_subject_must_match_exact_tarball_digest(self) -> None:
|
||||
package = controller.load_policy(POLICY_PATH).packages[0]
|
||||
statement = {
|
||||
"_type": controller.IN_TOTO_STATEMENT_TYPE,
|
||||
"predicateType": controller.SLSA_PREDICATE_TYPE,
|
||||
"subject": [
|
||||
{
|
||||
"name": package.slsa_subject_name,
|
||||
"digest": {"sha512": package.slsa_subject_sha512_hex},
|
||||
}
|
||||
],
|
||||
}
|
||||
payload = {
|
||||
"attestations": [
|
||||
{
|
||||
"predicateType": controller.SLSA_PREDICATE_TYPE,
|
||||
"bundle": {
|
||||
"dsseEnvelope": {
|
||||
"payload": base64.b64encode(
|
||||
json.dumps(statement).encode()
|
||||
).decode()
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
controller.validate_slsa_attestation(package, payload)
|
||||
bad_statement = dict(statement)
|
||||
bad_statement["subject"] = [
|
||||
{
|
||||
"name": package.slsa_subject_name,
|
||||
"digest": {"sha512": "0" * 128},
|
||||
}
|
||||
]
|
||||
payload["attestations"][0]["bundle"]["dsseEnvelope"]["payload"] = (
|
||||
base64.b64encode(json.dumps(bad_statement).encode()).decode()
|
||||
)
|
||||
with self.assertRaisesRegex(controller.ControllerError, "slsa_subject_mismatch"):
|
||||
controller.validate_slsa_attestation(package, payload)
|
||||
|
||||
def test_sbom_has_exact_npm_purls_and_dependency_edges(self) -> None:
|
||||
policy = controller.load_policy(POLICY_PATH)
|
||||
|
||||
sbom = controller.build_cyclonedx_sbom(
|
||||
policy,
|
||||
_verified_packages(policy),
|
||||
)
|
||||
|
||||
refs = {row["bom-ref"] for row in sbom["components"]}
|
||||
self.assertIn("pkg:npm/%40playwright/mcp@0.0.78", refs)
|
||||
self.assertNotIn("pkg:npm/%40playwright%2Fmcp@0.0.78", refs)
|
||||
root = next(
|
||||
row
|
||||
for row in sbom["dependencies"]
|
||||
if row["ref"] == "pkg:npm/%40playwright/mcp@0.0.78"
|
||||
)
|
||||
self.assertEqual(len(root["dependsOn"]), 2)
|
||||
|
||||
def test_osv_records_fail_closed(self) -> None:
|
||||
policy = controller.load_policy(POLICY_PATH)
|
||||
package = policy.packages[0]
|
||||
|
||||
class OsvClient:
|
||||
def post_json(self, *_args, **_kwargs):
|
||||
return b'{"vulns":[{"id":"OSV-TEST-1"}]}'
|
||||
|
||||
response_hash, vulnerabilities = controller._query_osv(
|
||||
package,
|
||||
OsvClient(),
|
||||
policy.maximum_metadata_bytes,
|
||||
)
|
||||
self.assertEqual(len(response_hash), 64)
|
||||
self.assertEqual(vulnerabilities, ("OSV-TEST-1",))
|
||||
|
||||
def test_mirror_receipt_never_promotes_runtime(self) -> None:
|
||||
policy = controller.load_policy(POLICY_PATH)
|
||||
verified = _verified_packages(policy)
|
||||
|
||||
receipt = controller.build_receipt(
|
||||
policy=policy,
|
||||
verified=verified,
|
||||
sbom_sha256="a" * 64,
|
||||
bundle_manifest_sha256="b" * 64,
|
||||
run_id=RUN_ID,
|
||||
trace_id=TRACE_ID,
|
||||
mode="apply",
|
||||
internal_mirror_ref=(
|
||||
"registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp@sha256:"
|
||||
+ "c" * 64
|
||||
),
|
||||
runtime_mirror_ref=(
|
||||
"192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp@sha256:"
|
||||
+ "c" * 64
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
receipt["status"],
|
||||
"completed_internal_mirror_pending_independent_verifier",
|
||||
)
|
||||
self.assertFalse(receipt["promotion_allowed"])
|
||||
self.assertFalse(receipt["replay_allowed"])
|
||||
self.assertFalse(receipt["shadow_allowed"])
|
||||
self.assertFalse(receipt["canary_allowed"])
|
||||
self.assertFalse(
|
||||
receipt["stage_receipts"]["bounded_execution"]
|
||||
["production_route_changed"]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,471 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
VERIFIER_PATH = ROOT / "scripts/security/verify_external_mcp_artifact_receipt.py"
|
||||
RUN_ID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
||||
TRACE_ID = f"mcp-artifact-{RUN_ID}"
|
||||
|
||||
|
||||
def _load_verifier():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"verify_external_mcp_artifact_receipt", VERIFIER_PATH
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
verifier = _load_verifier()
|
||||
|
||||
|
||||
def _canonical(payload) -> bytes:
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
|
||||
|
||||
def _fixture():
|
||||
package_data = {
|
||||
"@playwright/mcp": b"root-package",
|
||||
"playwright": b"playwright-package",
|
||||
"playwright-core": b"playwright-core-package",
|
||||
}
|
||||
versions = {
|
||||
"@playwright/mcp": "0.0.78",
|
||||
"playwright": "1.62.0-alpha-1783623505000",
|
||||
"playwright-core": "1.62.0-alpha-1783623505000",
|
||||
}
|
||||
packages = []
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
private_key = root / "private.pem"
|
||||
public_key = root / "public.der"
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"ecparam",
|
||||
"-name",
|
||||
"prime256v1",
|
||||
"-genkey",
|
||||
"-noout",
|
||||
"-out",
|
||||
str(private_key),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"pkey",
|
||||
"-in",
|
||||
str(private_key),
|
||||
"-pubout",
|
||||
"-outform",
|
||||
"DER",
|
||||
"-out",
|
||||
str(public_key),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
public_key_base64 = base64.b64encode(public_key.read_bytes()).decode()
|
||||
for index, (name, data) in enumerate(package_data.items()):
|
||||
integrity = "sha512-" + base64.b64encode(
|
||||
hashlib.sha512(data).digest()
|
||||
).decode()
|
||||
message = f"{name}@{versions[name]}:{integrity}".encode()
|
||||
message_path = root / f"message-{index}.bin"
|
||||
signature_path = root / f"signature-{index}.der"
|
||||
message_path.write_bytes(message)
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"dgst",
|
||||
"-sha256",
|
||||
"-sign",
|
||||
str(private_key),
|
||||
"-out",
|
||||
str(signature_path),
|
||||
str(message_path),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
packages.append(
|
||||
{
|
||||
"name": name,
|
||||
"version": versions[name],
|
||||
"integrity": integrity,
|
||||
"signature": base64.b64encode(
|
||||
signature_path.read_bytes()
|
||||
).decode(),
|
||||
"slsa_subject_name": (
|
||||
f"pkg:npm/{quote(name, safe='/')}@{versions[name]}"
|
||||
),
|
||||
"slsa_subject_sha512_hex": hashlib.sha512(data).hexdigest(),
|
||||
"shasum_sha1": hashlib.sha1(
|
||||
data, usedforsecurity=False
|
||||
).hexdigest(),
|
||||
}
|
||||
)
|
||||
policy = {
|
||||
"schema_version": verifier.POLICY_SCHEMA_VERSION,
|
||||
"work_item_id": "MCP-ARTIFACT-PLAYWRIGHT-TEST",
|
||||
"candidate_id": "external.playwright-verifier",
|
||||
"risk_level": "high",
|
||||
"registry_contract": {
|
||||
"signature_key": {
|
||||
"public_key_der_base64": public_key_base64,
|
||||
}
|
||||
},
|
||||
"packages": packages,
|
||||
"internal_mirror": {
|
||||
"push_repository": (
|
||||
"registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp"
|
||||
),
|
||||
"runtime_repository": (
|
||||
"192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp"
|
||||
),
|
||||
},
|
||||
}
|
||||
policy_checksum = "sha256:" + hashlib.sha256(_canonical(policy)).hexdigest()
|
||||
sbom = {
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.5",
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": f"pkg:npm/{quote(row['name'], safe='/')}@{row['version']}"
|
||||
}
|
||||
for row in packages
|
||||
],
|
||||
}
|
||||
sbom_bytes = _canonical(sbom) + b"\n"
|
||||
files = {"artifact/sbom.cdx.json": sbom_bytes}
|
||||
manifest_rows = []
|
||||
receipt_rows = []
|
||||
for row in packages:
|
||||
safe_name = row["name"].replace("@", "").replace("/", "-")
|
||||
filename = f"{safe_name}-{row['version']}.tgz"
|
||||
statement = {
|
||||
"_type": verifier.IN_TOTO_STATEMENT_TYPE,
|
||||
"predicateType": verifier.SLSA_PREDICATE_TYPE,
|
||||
"subject": [
|
||||
{
|
||||
"name": row["slsa_subject_name"],
|
||||
"digest": {"sha512": row["slsa_subject_sha512_hex"]},
|
||||
}
|
||||
],
|
||||
}
|
||||
attestation = _canonical(
|
||||
{
|
||||
"attestations": [
|
||||
{
|
||||
"predicateType": verifier.SLSA_PREDICATE_TYPE,
|
||||
"bundle": {
|
||||
"dsseEnvelope": {
|
||||
"payload": base64.b64encode(
|
||||
_canonical(statement)
|
||||
).decode()
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
files[f"artifact/tarballs/{filename}"] = package_data[row["name"]]
|
||||
files[f"artifact/attestations/{filename}.attestations.json"] = attestation
|
||||
manifest_rows.append(
|
||||
{
|
||||
"name": row["name"],
|
||||
"version": row["version"],
|
||||
"tarball_path": f"tarballs/{filename}",
|
||||
"tarball_sha512_hex": row["slsa_subject_sha512_hex"],
|
||||
"tarball_sha1": row["shasum_sha1"],
|
||||
"attestation_path": (
|
||||
f"attestations/{filename}.attestations.json"
|
||||
),
|
||||
"attestation_sha256": hashlib.sha256(attestation).hexdigest(),
|
||||
"npm_registry_signature_verified": True,
|
||||
"slsa_subject_verified": True,
|
||||
"archive_safety_verified": True,
|
||||
"vulnerability_ids": [],
|
||||
}
|
||||
)
|
||||
receipt_rows.append(
|
||||
{
|
||||
"name": row["name"],
|
||||
"version": row["version"],
|
||||
"tarball_sha512_hex": row["slsa_subject_sha512_hex"],
|
||||
"tarball_sha1": row["shasum_sha1"],
|
||||
"npm_registry_signature_verified": True,
|
||||
"slsa_subject_verified": True,
|
||||
"archive_safety_verified": True,
|
||||
"license_decision": "passed",
|
||||
"vulnerability_decision": "passed_no_known_osv_records",
|
||||
"vulnerability_ids": [],
|
||||
}
|
||||
)
|
||||
manifest = {
|
||||
"candidate_id": policy["candidate_id"],
|
||||
"work_item_id": policy["work_item_id"],
|
||||
"policy_checksum": policy_checksum,
|
||||
"package_count": 3,
|
||||
"packages": manifest_rows,
|
||||
"sbom": {"sha256": hashlib.sha256(sbom_bytes).hexdigest()},
|
||||
"operation_boundaries": {
|
||||
"mcp_server_started": False,
|
||||
"browser_started": False,
|
||||
"runtime_request_or_response_body_stored": False,
|
||||
},
|
||||
}
|
||||
manifest_bytes = _canonical(manifest) + b"\n"
|
||||
files["artifact/bundle-manifest.json"] = manifest_bytes
|
||||
digest = "sha256:" + "c" * 64
|
||||
receipt = {
|
||||
"schema_version": verifier.CONTROLLER_RECEIPT_SCHEMA_VERSION,
|
||||
"run_id": RUN_ID,
|
||||
"trace_id": TRACE_ID,
|
||||
"work_item_id": policy["work_item_id"],
|
||||
"candidate_id": policy["candidate_id"],
|
||||
"risk_level": "high",
|
||||
"mode": "apply",
|
||||
"status": "completed_internal_mirror_pending_independent_verifier",
|
||||
"policy_checksum": policy_checksum,
|
||||
"packages": receipt_rows,
|
||||
"internal_mirror_ref": (
|
||||
policy["internal_mirror"]["push_repository"] + "@" + digest
|
||||
),
|
||||
"runtime_mirror_ref": (
|
||||
policy["internal_mirror"]["runtime_repository"] + "@" + digest
|
||||
),
|
||||
"bundle_manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
|
||||
"cyclonedx_sbom_sha256": hashlib.sha256(sbom_bytes).hexdigest(),
|
||||
"promotion_allowed": False,
|
||||
"replay_allowed": False,
|
||||
"shadow_allowed": False,
|
||||
"canary_allowed": False,
|
||||
"stage_receipts": {
|
||||
"sensor_source_receipt": {
|
||||
"redirects_followed": 0,
|
||||
"runtime_request_or_response_body_stored": False,
|
||||
"upstream_supply_chain_attestation_bundled": True,
|
||||
},
|
||||
"check_mode": {
|
||||
"status": "passed",
|
||||
"mcp_process_started": False,
|
||||
"browser_started": False,
|
||||
"production_write_performed": False,
|
||||
},
|
||||
"bounded_execution": {
|
||||
"status": "internal_oci_bundle_pushed",
|
||||
"internal_registry_write_performed": True,
|
||||
"external_runtime_mutation_performed": False,
|
||||
"production_route_changed": False,
|
||||
},
|
||||
"independent_post_verifier": {
|
||||
"status": "pending_external_verifier",
|
||||
"internal_digest_ref_verified": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
return policy, receipt, files
|
||||
|
||||
|
||||
def _write_image_archive(path: Path, files: dict[str, bytes], *, symlink=False):
|
||||
layer_buffer = io.BytesIO()
|
||||
with tarfile.open(fileobj=layer_buffer, mode="w") as layer:
|
||||
for name, data in files.items():
|
||||
member = tarfile.TarInfo(name)
|
||||
member.size = len(data)
|
||||
layer.addfile(member, io.BytesIO(data))
|
||||
if symlink:
|
||||
member = tarfile.TarInfo("artifact/unsafe-link")
|
||||
member.type = tarfile.SYMTYPE
|
||||
member.linkname = "../../outside"
|
||||
layer.addfile(member)
|
||||
layer_bytes = layer_buffer.getvalue()
|
||||
manifest_bytes = json.dumps([{"Layers": ["layer.tar"]}]).encode()
|
||||
with tarfile.open(path, mode="w") as outer:
|
||||
for name, data in (
|
||||
("manifest.json", manifest_bytes),
|
||||
("layer.tar", layer_bytes),
|
||||
):
|
||||
member = tarfile.TarInfo(name)
|
||||
member.size = len(data)
|
||||
outer.addfile(member, io.BytesIO(data))
|
||||
|
||||
|
||||
class ExternalMcpArtifactIndependentVerifierTest(unittest.TestCase):
|
||||
def test_controller_receipt_identity_and_boundaries_are_validated(self) -> None:
|
||||
policy, receipt, _ = _fixture()
|
||||
|
||||
identity = verifier.validate_controller_receipt(policy, receipt)
|
||||
|
||||
self.assertEqual(identity["run_id"], RUN_ID)
|
||||
self.assertEqual(identity["trace_id"], TRACE_ID)
|
||||
self.assertEqual(identity["digest"], "sha256:" + "c" * 64)
|
||||
|
||||
def test_controller_receipt_rejects_promotion_claim(self) -> None:
|
||||
policy, receipt, _ = _fixture()
|
||||
receipt["canary_allowed"] = True
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
verifier.VerifierError,
|
||||
"controller_receipt_promotion_boundary_invalid",
|
||||
):
|
||||
verifier.validate_controller_receipt(policy, receipt)
|
||||
|
||||
def test_bundle_files_are_rehashed_independently(self) -> None:
|
||||
policy, receipt, files = _fixture()
|
||||
|
||||
evidence = verifier.verify_bundle_files(
|
||||
files,
|
||||
policy=policy,
|
||||
controller_receipt=receipt,
|
||||
)
|
||||
|
||||
self.assertEqual(evidence["verified_package_count"], 3)
|
||||
self.assertEqual(evidence["artifact_file_count"], 8)
|
||||
|
||||
def test_bundle_tarball_tamper_is_rejected(self) -> None:
|
||||
policy, receipt, files = _fixture()
|
||||
tampered = deepcopy(files)
|
||||
root_tarball = next(
|
||||
key
|
||||
for key in tampered
|
||||
if key.startswith("artifact/tarballs/playwright-mcp-")
|
||||
)
|
||||
tampered[root_tarball] += b"tamper"
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
verifier.VerifierError,
|
||||
"bundle_package_evidence_mismatch",
|
||||
):
|
||||
verifier.verify_bundle_files(
|
||||
tampered,
|
||||
policy=policy,
|
||||
controller_receipt=receipt,
|
||||
)
|
||||
|
||||
def test_bundle_extra_file_is_rejected(self) -> None:
|
||||
policy, receipt, files = _fixture()
|
||||
files["artifact/unexpected.bin"] = b"unexpected"
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
verifier.VerifierError,
|
||||
"bundle_file_set_invalid",
|
||||
):
|
||||
verifier.verify_bundle_files(
|
||||
files,
|
||||
policy=policy,
|
||||
controller_receipt=receipt,
|
||||
)
|
||||
|
||||
def test_independent_npm_signature_rejects_tamper(self) -> None:
|
||||
policy, _, files = _fixture()
|
||||
package = deepcopy(policy["packages"][0])
|
||||
package["signature"] = base64.b64encode(b"invalid-signature").decode()
|
||||
tarball_path = next(
|
||||
key
|
||||
for key in files
|
||||
if key.startswith("artifact/tarballs/playwright-mcp-")
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
verifier.VerifierError,
|
||||
"bundled_npm_signature_verification_failed",
|
||||
):
|
||||
verifier.verify_bundled_npm_signature(
|
||||
policy,
|
||||
package,
|
||||
files[tarball_path],
|
||||
)
|
||||
|
||||
def test_independent_slsa_subject_rejects_tamper(self) -> None:
|
||||
policy, _, files = _fixture()
|
||||
package = deepcopy(policy["packages"][0])
|
||||
package["slsa_subject_sha512_hex"] = "0" * 128
|
||||
attestation_path = next(
|
||||
key
|
||||
for key in files
|
||||
if key.startswith("artifact/attestations/playwright-mcp-")
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
verifier.VerifierError,
|
||||
"bundled_slsa_subject_mismatch",
|
||||
):
|
||||
verifier.verify_bundled_slsa_subject(
|
||||
package,
|
||||
files[attestation_path],
|
||||
)
|
||||
|
||||
def test_image_archive_is_inspected_as_data(self) -> None:
|
||||
_, _, files = _fixture()
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
archive = Path(temp) / "image.tar"
|
||||
_write_image_archive(archive, files)
|
||||
|
||||
extracted = verifier.extract_image_files(archive)
|
||||
|
||||
self.assertEqual(extracted, files)
|
||||
|
||||
def test_image_archive_rejects_symlink(self) -> None:
|
||||
_, _, files = _fixture()
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
archive = Path(temp) / "image.tar"
|
||||
_write_image_archive(archive, files, symlink=True)
|
||||
with self.assertRaisesRegex(
|
||||
verifier.VerifierError,
|
||||
"image_layer_unsafe_entry",
|
||||
):
|
||||
verifier.extract_image_files(archive)
|
||||
|
||||
def test_verifier_receipt_keeps_runtime_promotion_disabled(self) -> None:
|
||||
policy, receipt, files = _fixture()
|
||||
identity = verifier.validate_controller_receipt(policy, receipt)
|
||||
evidence = verifier.verify_bundle_files(
|
||||
files,
|
||||
policy=policy,
|
||||
controller_receipt=receipt,
|
||||
)
|
||||
|
||||
result = verifier.build_verifier_receipt(
|
||||
identity=identity,
|
||||
controller_receipt_bytes=_canonical(receipt) + b"\n",
|
||||
bundle_evidence=evidence,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "completed_internal_mirror_verified")
|
||||
self.assertFalse(result["promotion_allowed"])
|
||||
self.assertFalse(result["replay_allowed"])
|
||||
self.assertFalse(result["shadow_allowed"])
|
||||
self.assertFalse(result["canary_allowed"])
|
||||
self.assertFalse(
|
||||
result["independent_post_verifier"]["container_or_mcp_started"]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user