From 047dc9f9a44925a0335baca142eca3c4442f66a0 Mon Sep 17 00:00:00 2001 From: monkseekee Date: Sat, 1 Aug 2026 22:58:28 +0800 Subject: [PATCH] feat(governance): verify private mirror provenance Bind mirror source and target trees to a protected-main Ed25519 receipt. Keep every HMG source-read credential out of the target repository. --- .github/workflows/required-quality-gate.yml | 59 ++++ .gitignore | 2 + README.md | 4 + ...bb8a4468c8f7b0071aa96b0e3524f0b177508b.pem | 3 + .../private-source-mirror/test_verify.py | 295 ++++++++++++++++++ governance/private-source-mirror/verify.py | 234 ++++++++++++++ 6 files changed, 597 insertions(+) create mode 100644 governance/private-source-mirror/keys/ed25519-spki-sha256-6d20ed9a744fd0b6cd2d75bfc5bb8a4468c8f7b0071aa96b0e3524f0b177508b.pem create mode 100644 governance/private-source-mirror/test_verify.py create mode 100644 governance/private-source-mirror/verify.py diff --git a/.github/workflows/required-quality-gate.yml b/.github/workflows/required-quality-gate.yml index 3f0a9d0..6509314 100644 --- a/.github/workflows/required-quality-gate.yml +++ b/.github/workflows/required-quality-gate.yml @@ -101,3 +101,62 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }} run: bash policy-source/governance/workflow-policy/run.sh + + private-source-mirror-provenance: + if: github.repository == 'HMG-AI/HMG-DEV-brach' + name: private-source-mirror-provenance + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Reject unsupported private-mirror events + if: github.event_name != 'pull_request' + shell: bash + run: | + echo "::error::The private exact-source mirror currently accepts pull_request events only" + exit 2 + + - name: Check out immutable private-mirror policy + if: github.event_name == 'pull_request' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: HMG-AI/.github + ref: ${{ github.workflow_sha }} + path: governance-source + sparse-checkout: governance/private-source-mirror + persist-credentials: false + + - name: Check out exact private-mirror event head + if: github.event_name == 'pull_request' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: target-repository + persist-credentials: false + + - name: Exercise private-mirror contract fixtures + if: github.event_name == 'pull_request' + shell: bash + run: | + set -Eeuo pipefail + python3 -m unittest discover \ + -s governance-source/governance/private-source-mirror \ + -p 'test_*.py' + + - name: Verify signed exact-source provenance and tree equality + if: github.event_name == 'pull_request' + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + TARGET_BASE_REF: ${{ github.event.pull_request.base.ref }} + TARGET_REPOSITORY: ${{ github.repository }} + TARGET_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -Eeuo pipefail + python3 governance-source/governance/private-source-mirror/verify.py \ + --target-repository-dir target-repository \ + --target-sha "${TARGET_SHA}" \ + --event-name "${EVENT_NAME}" \ + --target-repository "${TARGET_REPOSITORY}" \ + --target-base-ref "${TARGET_BASE_REF}" \ + --trusted-public-key \ + governance-source/governance/private-source-mirror/keys/ed25519-spki-sha256-6d20ed9a744fd0b6cd2d75bfc5bb8a4468c8f7b0071aa96b0e3524f0b177508b.pem diff --git a/.gitignore b/.gitignore index 8ec2002..69bc941 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.swp *.tmp node_modules/ +__pycache__/ +*.py[cod] diff --git a/README.md b/README.md index 2a285a1..ca231d6 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,8 @@ Automated cross-repository publication must use a target-repository branch and p Every automated promotion commit carries exactly eight governed trailers: source repository, stable source tag, source SHA, successful source workflow run, aggregate asset digest, candidate Git tree, reviewed Ed25519 key ID, and detached Ed25519 signature. `reusable-hmg-public-quality.yml` is the read-only, centrally governed build, drift, leak, stable-version, event-binding, candidate-tree, and signature gate. `required-hmg-public-quality.yml` is its repository-specific required-workflow wrapper and calls the reusable workflow from the same immutable organization commit. It must be attached by a dedicated organization ruleset whose repository target includes only `HMG-public` and whose workflow reference is pinned to a reviewed full SHA; the generic organization quality ruleset remains separate and unchanged. The gate shellchecks the target-owned release scripts and runs their classifier and publisher policy fixtures for every candidate, including ordinary governance pull requests. It also classifies HMG trailers across the immutable event commit range, so a merge-queue ref cannot bypass provenance checks; a governed merge group must contain exactly one complete promotion commit and its event-head tree must still equal the signed tree. It deliberately cannot read the write-visible staging draft and receives no write credential. Before merge it proves the final `vMAJOR.MINOR.PATCH` tag is absent; after merge, the environment-protected target publisher independently verifies the signed commit and staging bytes, creates the final tag once, publishes the release, and relies on immutable-release plus tag rules to prevent mutation. Production callers and required-workflow rulesets must pin these files to reviewed full commit SHAs. +`HMG-DEV-brach` is a private exact-source candidate mirror, not an independent development authority. Its required quality workflow verifies that the pull-request head carries exactly eight `HMG-PRIVATE-SOURCE-MIRROR-V1` trailers, that an Ed25519 receipt issued from protected `HMG-AI/HMG/main` binds the source commit, source tree, workflow run, export tree, target repository, target branch, and reviewed key ID, and that the signed source and actual target Git trees are byte-for-byte identical. The verifier and public key are loaded from the immutable organization workflow commit and never execute candidate scripts. The target repository receives no credential capable of reading or writing HMG. + +The private-mirror gate deliberately fails closed for `merge_group` until merge-queue provenance semantics are specified and tested. Source changes, including fixes discovered by mirror CI, must merge into HMG first; only then may the protected HMG signer issue a receipt and a target pull request mirror that exact source tree. The signing key must remain in the review-protected, `main`-only HMG release environment; it must never be exposed to the target repository, artifacts, logs, or any job with a target write credential. Required-workflow rulesets must pin `required-quality-gate.yml` to a reviewed full organization commit SHA. + Organization members can read the canonical team guide in [`HMG-Documents/github-governance`](https://github.com/HMG-AI/HMG-Documents/tree/main/github-governance). diff --git a/governance/private-source-mirror/keys/ed25519-spki-sha256-6d20ed9a744fd0b6cd2d75bfc5bb8a4468c8f7b0071aa96b0e3524f0b177508b.pem b/governance/private-source-mirror/keys/ed25519-spki-sha256-6d20ed9a744fd0b6cd2d75bfc5bb8a4468c8f7b0071aa96b0e3524f0b177508b.pem new file mode 100644 index 0000000..86d2ddb --- /dev/null +++ b/governance/private-source-mirror/keys/ed25519-spki-sha256-6d20ed9a744fd0b6cd2d75bfc5bb8a4468c8f7b0071aa96b0e3524f0b177508b.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAkgMYaG1kRc5v6jqZuwsrgo6x+A4Ss9BT8Ram9iDs+mM= +-----END PUBLIC KEY----- diff --git a/governance/private-source-mirror/test_verify.py b/governance/private-source-mirror/test_verify.py new file mode 100644 index 0000000..7e15806 --- /dev/null +++ b/governance/private-source-mirror/test_verify.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Contract tests for signed HMG private exact-source mirror provenance.""" + +from __future__ import annotations + +import base64 +import hashlib +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +HERE = Path(__file__).resolve().parent +VERIFIER = HERE / "verify.py" + + +def run( + *args: str, + cwd: Path | None = None, + check: bool = True, + input_bytes: bytes | None = None, +) -> subprocess.CompletedProcess[bytes]: + result = subprocess.run( + list(args), + cwd=cwd, + input=input_bytes, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + if check and result.returncode != 0: + output = result.stdout.decode(errors="replace") + raise AssertionError(f"command failed ({result.returncode}): {' '.join(args)}\n{output}") + return result + + +def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[bytes]: + return run("git", "-C", str(repository), *args, check=check) + + +def git_output(repository: Path, *args: str) -> str: + return git(repository, *args).stdout.decode().strip() + + +class PrivateSourceMirrorVerifierTests(unittest.TestCase): + def setUp(self) -> None: + self._temp = tempfile.TemporaryDirectory() + self.root = Path(self._temp.name) + self.source = self.root / "source" + self.target = self.root / "target" + self.private_key = self.root / "private.pem" + self.public_key = self.root / "public.pem" + run("openssl", "genpkey", "-algorithm", "ED25519", "-out", str(self.private_key)) + run( + "openssl", + "pkey", + "-in", + str(self.private_key), + "-pubout", + "-out", + str(self.public_key), + ) + public_der = run( + "openssl", + "pkey", + "-pubin", + "-in", + str(self.public_key), + "-outform", + "DER", + ).stdout + self.key_id = f"ed25519-spki-sha256-{hashlib.sha256(public_der).hexdigest()}" + + self._init_repository(self.source) + self._write_exact_tree(self.source) + git(self.source, "add", "--all") + git(self.source, "commit", "-m", "feat(source): establish exact source tree") + self.source_sha = git_output(self.source, "rev-parse", "HEAD") + self.source_tree = f"sha1:{git_output(self.source, 'rev-parse', 'HEAD^{tree}')}" + + self._init_repository(self.target) + self._write_exact_tree(self.target) + self.target_sha = self._commit_target() + + def tearDown(self) -> None: + self._temp.cleanup() + + @staticmethod + def _init_repository(repository: Path) -> None: + repository.mkdir() + run("git", "init", "--initial-branch=main", str(repository)) + git(repository, "config", "user.name", "Mirror Contract Test") + git(repository, "config", "user.email", "mirror-contract@example.invalid") + + @staticmethod + def _write_exact_tree(repository: Path) -> None: + (repository / "README.md").write_text("exact private source mirror\n", encoding="utf-8") + script = repository / "run.sh" + script.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8") + script.chmod(0o755) + + def _statement( + self, + *, + source_sha: str, + source_tree: str, + export_tree: str, + workflow_run: str, + key_id: str, + ) -> bytes: + return ( + "\n".join( + ( + "HMG-PRIVATE-SOURCE-MIRROR-V1", + "source-repository=HMG-AI/HMG", + "source-ref=refs/heads/main", + f"source-sha={source_sha}", + f"source-tree={source_tree}", + f"export-tree={export_tree}", + f"workflow-run={workflow_run}", + "target-repository=HMG-AI/HMG-DEV-brach", + "target-branch=main", + f"key-id={key_id}", + ) + ) + + "\n" + ).encode() + + def _trailers( + self, + *, + source_sha: str | None = None, + source_tree: str | None = None, + export_tree: str | None = None, + workflow_run: str = "https://github.com/HMG-AI/HMG/actions/runs/123456789", + key_id: str | None = None, + ) -> list[str]: + resolved_source_sha = source_sha or self.source_sha + resolved_source_tree = source_tree or self.source_tree + resolved_export_tree = export_tree or self.source_tree + resolved_key_id = key_id or self.key_id + statement = self._statement( + source_sha=resolved_source_sha, + source_tree=resolved_source_tree, + export_tree=resolved_export_tree, + workflow_run=workflow_run, + key_id=resolved_key_id, + ) + statement_path = self.root / "statement.txt" + statement_path.write_bytes(statement) + signature = run( + "openssl", + "pkeyutl", + "-sign", + "-inkey", + str(self.private_key), + "-rawin", + "-in", + str(statement_path), + ).stdout + return [ + "HMG-Mirror-Contract: HMG-PRIVATE-SOURCE-MIRROR-V1", + "HMG-Source-Repository: HMG-AI/HMG", + f"HMG-Source-SHA: {resolved_source_sha}", + f"HMG-Source-Tree: {resolved_source_tree}", + f"HMG-Export-Tree: {resolved_export_tree}", + f"HMG-Workflow-Run: {workflow_run}", + f"HMG-Provenance-Key-ID: {resolved_key_id}", + f"HMG-Provenance-Signature-Ed25519: {base64.b64encode(signature).decode()}", + ] + + def _commit_target( + self, + *, + trailers: list[str] | None = None, + amend: bool = False, + ) -> str: + git(self.target, "add", "--all") + export_tree = f"sha1:{git_output(self.target, 'write-tree')}" + governed = trailers or self._trailers(export_tree=export_tree) + command = ["commit"] + if amend: + command.append("--amend") + command.extend( + [ + "-m", + "chore(mirror): import exact HMG source", + "-m", + "\n".join(governed), + ] + ) + git(self.target, *command) + return git_output(self.target, "rev-parse", "HEAD") + + def _amend_trailers(self, trailers: list[str]) -> None: + self.target_sha = self._commit_target(trailers=trailers, amend=True) + + def _verify( + self, + *, + event_name: str = "pull_request", + public_key: Path | None = None, + ) -> subprocess.CompletedProcess[bytes]: + return run( + "python3", + str(VERIFIER), + "--target-repository-dir", + str(self.target), + "--target-sha", + self.target_sha, + "--event-name", + event_name, + "--target-repository", + "HMG-AI/HMG-DEV-brach", + "--target-base-ref", + "main", + "--trusted-public-key", + str(public_key or self.public_key), + check=False, + ) + + def assert_failed(self, result: subprocess.CompletedProcess[bytes], text: str) -> None: + output = result.stdout.decode(errors="replace") + self.assertNotEqual(result.returncode, 0, output) + self.assertIn(text, output) + + def test_exact_signed_source_and_export_tree_pass(self) -> None: + result = self._verify() + output = result.stdout.decode() + self.assertEqual(result.returncode, 0, output) + self.assertIn("private source mirror verified", output) + + def test_missing_governed_trailer_fails(self) -> None: + self._amend_trailers(self._trailers()[:-1]) + self.assert_failed(self._verify(), "exactly the eight governed trailers") + + def test_duplicate_governed_trailer_fails(self) -> None: + trailers = self._trailers() + trailers.append(trailers[-1]) + self._amend_trailers(trailers) + self.assert_failed(self._verify(), "exactly the eight governed trailers") + + def test_malformed_source_sha_fails(self) -> None: + self._amend_trailers(self._trailers(source_sha=self.source_sha.upper())) + self.assert_failed(self._verify(), "invalid governed provenance value") + + def test_malformed_workflow_run_fails(self) -> None: + self._amend_trailers(self._trailers(workflow_run="https://example.invalid/run/1")) + self.assert_failed(self._verify(), "invalid governed provenance value") + + def test_unknown_key_id_fails(self) -> None: + self._amend_trailers(self._trailers(key_id=f"ed25519-spki-sha256-{'0' * 64}")) + self.assert_failed(self._verify(), "does not match the reviewed public key") + + def test_tampered_signature_fails(self) -> None: + trailers = self._trailers() + trailers[-1] = f"HMG-Provenance-Signature-Ed25519: {'A' * 86}==" + self._amend_trailers(trailers) + self.assert_failed(self._verify(), "signature is not trusted") + + def test_untrusted_public_key_fails(self) -> None: + other_private = self.root / "other-private.pem" + other_public = self.root / "other-public.pem" + run("openssl", "genpkey", "-algorithm", "ED25519", "-out", str(other_private)) + run("openssl", "pkey", "-in", str(other_private), "-pubout", "-out", str(other_public)) + self.assert_failed(self._verify(public_key=other_public), "does not match the reviewed public key") + + def test_signed_but_mismatched_source_and_export_tree_fails(self) -> None: + mismatched_tree = f"sha1:{'0' * 40}" + self._amend_trailers(self._trailers(source_tree=mismatched_tree)) + self.assert_failed(self._verify(), "signed source and export trees must be identical") + + def test_target_only_path_fails(self) -> None: + (self.target / "target-only.txt").write_text("not in HMG\n", encoding="utf-8") + self.target_sha = self._commit_target() + self.assert_failed(self._verify(), "signed source and export trees must be identical") + + def test_target_only_content_change_fails(self) -> None: + (self.target / "README.md").write_text("mirror-only fix\n", encoding="utf-8") + self.target_sha = self._commit_target() + self.assert_failed(self._verify(), "signed source and export trees must be identical") + + def test_target_only_mode_change_fails(self) -> None: + os.chmod(self.target / "run.sh", 0o644) + self.target_sha = self._commit_target() + self.assert_failed(self._verify(), "signed source and export trees must be identical") + + def test_merge_group_fails_closed_until_semantics_are_defined(self) -> None: + self.assert_failed(self._verify(event_name="merge_group"), "only pull_request events are supported") + + +if __name__ == "__main__": + unittest.main() diff --git a/governance/private-source-mirror/verify.py b/governance/private-source-mirror/verify.py new file mode 100644 index 0000000..6d1c03e --- /dev/null +++ b/governance/private-source-mirror/verify.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Fail-closed verifier for signed HMG private exact-source mirrors.""" + +from __future__ import annotations + +import argparse +import base64 +import binascii +import hashlib +import re +import subprocess +import sys +import tempfile +from pathlib import Path + + +CONTRACT = "HMG-PRIVATE-SOURCE-MIRROR-V1" +SOURCE_REPOSITORY = "HMG-AI/HMG" +SOURCE_AUTHORITY_REF = "refs/heads/main" +TARGET_REPOSITORY = "HMG-AI/HMG-DEV-brach" +TARGET_BASE_REF = "main" +GOVERNED_TRAILER_KEYS = ( + "HMG-Mirror-Contract", + "HMG-Source-Repository", + "HMG-Source-SHA", + "HMG-Source-Tree", + "HMG-Export-Tree", + "HMG-Workflow-Run", + "HMG-Provenance-Key-ID", + "HMG-Provenance-Signature-Ed25519", +) +SHA1_RE = re.compile(r"[0-9a-f]{40}") +TREE_RE = re.compile(r"sha1:[0-9a-f]{40}") +WORKFLOW_RUN_RE = re.compile(r"https://github\.com/HMG-AI/HMG/actions/runs/[1-9][0-9]*") +KEY_ID_RE = re.compile(r"ed25519-spki-sha256-[0-9a-f]{64}") +SIGNATURE_RE = re.compile(r"[A-Za-z0-9+/]{86}==") + + +class VerificationError(RuntimeError): + """A deterministic private-mirror contract failure.""" + + +def run(*args: str, input_bytes: bytes | None = None) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + list(args), + input=input_bytes, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[bytes]: + result = run("git", "-C", str(repository), *args) + if check and result.returncode != 0: + detail = (result.stderr or result.stdout).decode(errors="replace").strip() + raise VerificationError(detail or "git command failed") + return result + + +def git_output(repository: Path, *args: str) -> str: + return git(repository, *args).stdout.decode().strip() + + +def require_repository(path: Path, label: str) -> None: + if not path.is_dir(): + raise VerificationError(f"{label} repository directory is absent") + if git(path, "rev-parse", "--is-inside-work-tree", check=False).stdout.decode().strip() != "true": + raise VerificationError(f"{label} directory is not a Git worktree") + if git_output(path, "rev-parse", "--show-object-format") != "sha1": + raise VerificationError(f"{label} repository must use the governed sha1 object format") + + +def parse_trailers(target: Path, target_sha: str) -> dict[str, str]: + message = git(target, "show", "-s", "--format=%B", target_sha).stdout + parsed = run("git", "interpret-trailers", "--parse", input_bytes=message) + if parsed.returncode != 0: + raise VerificationError( + parsed.stderr.decode(errors="replace").strip() + or "could not parse target commit trailers" + ) + + lines = [line for line in parsed.stdout.decode().splitlines() if line] + entries: list[tuple[str, str]] = [] + for line in lines: + if ": " not in line: + raise VerificationError("mirror commit contains a malformed trailer") + key, value = line.split(": ", 1) + entries.append((key, value)) + + keys = [key for key, _ in entries] + if len(entries) != len(GOVERNED_TRAILER_KEYS) or sorted(keys) != sorted(GOVERNED_TRAILER_KEYS): + raise VerificationError("mirror commit must contain exactly the eight governed trailers") + return dict(entries) + + +def canonical_statement(trailers: dict[str, str]) -> bytes: + return ( + "\n".join( + ( + CONTRACT, + f"source-repository={SOURCE_REPOSITORY}", + f"source-ref={SOURCE_AUTHORITY_REF}", + f"source-sha={trailers['HMG-Source-SHA']}", + f"source-tree={trailers['HMG-Source-Tree']}", + f"export-tree={trailers['HMG-Export-Tree']}", + f"workflow-run={trailers['HMG-Workflow-Run']}", + f"target-repository={TARGET_REPOSITORY}", + f"target-branch={TARGET_BASE_REF}", + f"key-id={trailers['HMG-Provenance-Key-ID']}", + ) + ) + + "\n" + ).encode() + + +def trusted_key_id(public_key: Path) -> str: + if not public_key.is_file(): + raise VerificationError("the reviewed provenance public key is absent") + result = run("openssl", "pkey", "-pubin", "-in", str(public_key), "-outform", "DER") + if result.returncode != 0 or not result.stdout: + raise VerificationError("the reviewed provenance public key is invalid") + return f"ed25519-spki-sha256-{hashlib.sha256(result.stdout).hexdigest()}" + + +def decode_signature(value: str) -> bytes: + if SIGNATURE_RE.fullmatch(value) is None: + raise VerificationError("mirror provenance signature is not canonical Ed25519 base64") + try: + signature = base64.b64decode(value, validate=True) + except (binascii.Error, ValueError) as error: + raise VerificationError("mirror provenance signature is not canonical Ed25519 base64") from error + if len(signature) != 64 or base64.b64encode(signature).decode() != value: + raise VerificationError("mirror provenance signature is not canonical Ed25519 base64") + return signature + + +def verify_signature(public_key: Path, statement: bytes, signature: bytes) -> None: + with tempfile.TemporaryDirectory(prefix="hmg-private-mirror-verify-") as temp_dir: + statement_path = Path(temp_dir) / "statement.txt" + signature_path = Path(temp_dir) / "signature.bin" + statement_path.write_bytes(statement) + signature_path.write_bytes(signature) + result = run( + "openssl", + "pkeyutl", + "-verify", + "-pubin", + "-inkey", + str(public_key), + "-rawin", + "-in", + str(statement_path), + "-sigfile", + str(signature_path), + ) + if result.returncode != 0: + raise VerificationError("mirror provenance signature is not trusted") + + +def verify(args: argparse.Namespace) -> None: + if args.event_name != "pull_request": + raise VerificationError("only pull_request events are supported; merge_group fails closed") + if args.target_repository != TARGET_REPOSITORY: + raise VerificationError(f"target repository must be {TARGET_REPOSITORY}") + if args.target_base_ref != TARGET_BASE_REF: + raise VerificationError(f"target base ref must be {TARGET_BASE_REF}") + if SHA1_RE.fullmatch(args.target_sha) is None: + raise VerificationError("target SHA must be a lowercase 40-character SHA-1") + + target = args.target_repository_dir.resolve() + public_key = args.trusted_public_key.resolve() + require_repository(target, "target") + if git(target, "cat-file", "-e", f"{args.target_sha}^{{commit}}", check=False).returncode != 0: + raise VerificationError("target commit is absent") + + trailers = parse_trailers(target, args.target_sha) + if trailers["HMG-Mirror-Contract"] != CONTRACT: + raise VerificationError("invalid governed provenance value for HMG-Mirror-Contract") + if trailers["HMG-Source-Repository"] != SOURCE_REPOSITORY: + raise VerificationError("invalid governed provenance value for HMG-Source-Repository") + if SHA1_RE.fullmatch(trailers["HMG-Source-SHA"]) is None: + raise VerificationError("invalid governed provenance value for HMG-Source-SHA") + if TREE_RE.fullmatch(trailers["HMG-Source-Tree"]) is None: + raise VerificationError("invalid governed provenance value for HMG-Source-Tree") + if TREE_RE.fullmatch(trailers["HMG-Export-Tree"]) is None: + raise VerificationError("invalid governed provenance value for HMG-Export-Tree") + if WORKFLOW_RUN_RE.fullmatch(trailers["HMG-Workflow-Run"]) is None: + raise VerificationError("invalid governed provenance value for HMG-Workflow-Run") + if KEY_ID_RE.fullmatch(trailers["HMG-Provenance-Key-ID"]) is None: + raise VerificationError("invalid governed provenance value for HMG-Provenance-Key-ID") + + expected_key_id = trusted_key_id(public_key) + if trailers["HMG-Provenance-Key-ID"] != expected_key_id: + raise VerificationError("mirror provenance key ID does not match the reviewed public key") + signature = decode_signature(trailers["HMG-Provenance-Signature-Ed25519"]) + verify_signature(public_key, canonical_statement(trailers), signature) + + actual_export_tree = f"sha1:{git_output(target, 'rev-parse', f'{args.target_sha}^{{tree}}')}" + if actual_export_tree != trailers["HMG-Export-Tree"]: + raise VerificationError("actual target tree does not match HMG-Export-Tree") + if trailers["HMG-Source-Tree"] != trailers["HMG-Export-Tree"]: + raise VerificationError("signed source and export trees must be identical") + + print( + "private source mirror verified: " + f"source={SOURCE_REPOSITORY}@{trailers['HMG-Source-SHA']} " + f"target={TARGET_REPOSITORY}@{args.target_sha} " + f"tree={actual_export_tree}" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-repository-dir", type=Path, required=True) + parser.add_argument("--target-sha", required=True) + parser.add_argument("--event-name", required=True) + parser.add_argument("--target-repository", required=True) + parser.add_argument("--target-base-ref", required=True) + parser.add_argument("--trusted-public-key", type=Path, required=True) + return parser.parse_args() + + +def main() -> int: + try: + verify(parse_args()) + except VerificationError as error: + print(f"private source mirror verification failed: {error}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())