#!/usr/bin/env python3 import json import tempfile import unittest from pathlib import Path import runpy _VERIFIER_PATH = Path(__file__).parent / "verify_offline_bundle.py" _mod = runpy.run_path(_VERIFIER_PATH.as_posix(), run_name="verify_offline_bundle") BundleReader = _mod["BundleReader"] validate_manifest = _mod["validate_manifest"] verify_files = _mod["verify_files"] verify_hashes = _mod["verify_hashes"] sha256_digest = _mod["sha256_digest"] def _write(path: Path, content: str) -> str: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") return sha256_digest(content.encode("utf-8")) class VerifyOfflineBundleTests(unittest.TestCase): def setUp(self) -> None: self.tmp = tempfile.TemporaryDirectory() self.root = Path(self.tmp.name) def tearDown(self) -> None: self.tmp.cleanup() def _build_manifest(self) -> Path: plan_hash = _write(self.root / "canonical-plan.json", '{"steps":[]}') inputs_lock_hash = _write(self.root / "inputs.lock", '{"inputs":{}}') sbom_hash = _write(self.root / "sbom.json", '{"bom":"demo"}') attest_hash = _write(self.root / "attestation.dsse", "attestation") approvals_ledger = json.dumps( { "schemaVersion": "stellaops.pack.approval-ledger.v1", "runId": "run-1", "gateId": "security-review", "planHash": plan_hash, "decision": "approved", "decidedAt": "2025-12-05T00:00:00Z", "tenantId": "demo-tenant", "approver": {"id": "approver@example.com", "summary": "LGTM"}, } ) approvals_hash = _write(self.root / "approvals-ledger.dsse", approvals_ledger) revocations_hash = _write(self.root / "revocations.json", '{"revoked":false}') bundle_dsse_hash = _write(self.root / "bundle.dsse", "bundle-dsse") att_dsse_hash = _write(self.root / "attestation.dsse.sig", "att-dsse") redaction_hash = _write(self.root / "redaction-policy.json", '{"mode":"hash"}') pack_blob_hash = _write(self.root / "packs/my-pack.tgz", "dummy pack") manifest = { "schemaVersion": "stellaops.pack.offline-bundle.v1", "pack": { "name": "demo-pack", "version": "1.0.0", "bundle": "packs/my-pack.tgz", "digest": pack_blob_hash, "registry": "demo.local/pack/demo:1.0.0", "sbom": "sbom.json", }, "plan": { "hashAlgorithm": "sha256", "hash": plan_hash, "canonicalPlanPath": "canonical-plan.json", "inputsLock": "inputs.lock", "rngSeed": "rng-demo", "timestampSource": "utc-iso8601", }, "evidence": { "attestation": "attestation.dsse", "approvalsLedger": "approvals-ledger.dsse", "timeline": "timeline.ndjson", }, "security": { "sandbox": { "mode": "sealed", "egressAllowlist": [], "cpuLimitMillicores": 250, "memoryLimitMiB": 256, "quotaSeconds": 120, }, "revocations": "revocations.json", "signatures": { "bundleDsse": "bundle.dsse", "attestationDsse": "attestation.dsse.sig", "registryCertChain": "certs.pem", }, "secretsRedactionPolicy": "redaction-policy.json", }, "hashes": [ {"path": "canonical-plan.json", "algorithm": "sha256", "digest": plan_hash}, {"path": "inputs.lock", "algorithm": "sha256", "digest": inputs_lock_hash}, {"path": "sbom.json", "algorithm": "sha256", "digest": sbom_hash}, {"path": "attestation.dsse", "algorithm": "sha256", "digest": attest_hash}, {"path": "approvals-ledger.dsse", "algorithm": "sha256", "digest": approvals_hash}, {"path": "revocations.json", "algorithm": "sha256", "digest": revocations_hash}, {"path": "bundle.dsse", "algorithm": "sha256", "digest": bundle_dsse_hash}, {"path": "attestation.dsse.sig", "algorithm": "sha256", "digest": att_dsse_hash}, {"path": "redaction-policy.json", "algorithm": "sha256", "digest": redaction_hash}, {"path": "packs/my-pack.tgz", "algorithm": "sha256", "digest": pack_blob_hash}, ], "slo": { "runP95Seconds": 300, "approvalP95Seconds": 900, "maxQueueDepth": 1000, "alertRules": "alerts.yaml", }, "tenant": "demo-tenant", "environment": "dev", "created": "2025-12-05T00:00:00Z", "expires": "2026-01-05T00:00:00Z", "verifyScriptVersion": "local-test", } manifest_path = self.root / "bundle.json" manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") return manifest_path def test_good_bundle_passes(self): manifest_path = self._build_manifest() reader = BundleReader(self.root.as_posix()) manifest = json.loads(manifest_path.read_text()) errors = [] errors.extend(validate_manifest(manifest)) errors.extend(verify_files(reader, manifest, require_dsse=True)) errors.extend(verify_hashes(reader, manifest)) self.assertFalse(errors, f"Expected no validation errors, got: {errors}") def test_missing_hash_fails(self): manifest_path = self._build_manifest() manifest = json.loads(manifest_path.read_text()) # Corrupt a hash to force a failure. manifest["hashes"][0]["digest"] = "sha256:" + "0" * 64 reader = BundleReader(self.root.as_posix()) errors = verify_hashes(reader, manifest) self.assertTrue(errors, "Expected hash verification to fail when hash entry is missing") def test_missing_quota_fails(self): manifest_path = self._build_manifest() manifest = json.loads(manifest_path.read_text()) del manifest["security"]["sandbox"]["quotaSeconds"] reader = BundleReader(self.root.as_posix()) errors = [] errors.extend(validate_manifest(manifest)) errors.extend(verify_files(reader, manifest, require_dsse=True)) errors.extend(verify_hashes(reader, manifest)) self.assertTrue( any(err.path == "security.sandbox.quotaSeconds" for err in errors), "Expected quotaSeconds validation failure" ) def test_invalid_approval_ledger_plan_hash_fails(self): manifest_path = self._build_manifest() manifest = json.loads(manifest_path.read_text()) ledger_path = self.root / "approvals-ledger.dsse" ledger = json.loads(ledger_path.read_text()) ledger["planHash"] = "not-a-digest" ledger_path.write_text(json.dumps(ledger), encoding="utf-8") reader = BundleReader(self.root.as_posix()) errors = [] errors.extend(validate_manifest(manifest)) errors.extend(verify_files(reader, manifest, require_dsse=True)) errors.extend(verify_hashes(reader, manifest)) self.assertTrue( any(err.path.startswith("approvalsLedger.planHash") for err in errors), "Expected approval ledger plan hash validation failure" ) if __name__ == "__main__": unittest.main()