#!/usr/bin/env python3 """RedFlag policy adapter for the unmodified shared release contract. scripts/vendor/release-contract-core.py is byte-identical to bin/release-contract-core.py from the shared release contract at commit 9fc81398b6696ee858468b774c66c0cba44a22b3. Provenance is held by that SHA rather than by the name of the infrastructure that hosts it. Three identities are kept apart, and must not be collapsed into one: the public source repository that a release is built from and signed against; the control repository whose reviewed workflow drives the build; and the invocation that a particular run happened under. Only the first belongs in public source, so the other two are supplied by the build environment and never committed here. The builder ID denotes the declared workflow lane, not a hardware attestation. """ import argparse import importlib.util import json import os from pathlib import Path import re import subprocess import tempfile from types import SimpleNamespace SPEC = importlib.util.spec_from_file_location("release_contract_core", Path(__file__).parent / "vendor/release-contract-core.py") CONTRACT = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(CONTRACT) SOURCE_REPOSITORY = "https://forge.caseytunturi.com/Fimeg/RedFlag" BUILD_TYPE = "https://caseytunturi.com/release-contract/build-types/gitea-actions/v1" WORKFLOW = "Fimeg/RedFlag/.gitea/workflows/release.yml" TARGETS = {"linux-amd64", "linux-arm64", "windows-amd64", "darwin-arm64"} def control_repository(): # The repository whose reviewed workflow drives the build. It is read from # the build environment rather than committed, so that public source never # has to name the forge that development is hosted on. Verification is not # weakened: the trusted stage supplies this and the value is still pinned. server = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") repository = os.environ.get("GITHUB_REPOSITORY", "") require(server and repository, "GITHUB_SERVER_URL and GITHUB_REPOSITORY are required") return server + "/" + repository def builder_id(): return control_repository() + "/.gitea/workflows/release.yml#stage_package" def require(condition, message): if not condition: raise CONTRACT.ContractError(message) def receipt(candidate, check_files=True): entries = CONTRACT.parse_receipt(candidate) require("manifest.json" in entries and "provenance.json" in entries, "receipt lacks evidence") require("files.sha256" not in entries, "receipt cannot name itself") for name, digest in entries.items(): require(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]*", name), "unsafe transport filename") if not check_files: continue path = candidate / name require(path.is_file() and not path.is_symlink(), "missing or unsafe receipt file: " + name) require(CONTRACT.sha256(path) == digest, "receipt digest mismatch: " + name) if check_files: require({p.name for p in candidate.iterdir()} == set(entries) | {"files.sha256"}, "unreceipted candidate files") return entries def verify(candidate, version, commit, tag): receipt(candidate) CONTRACT.verify(SimpleNamespace(candidate=str(candidate), expect_project="redflag", expect_source_commit=commit, expect_source_tag=tag, expect_builder_id=builder_id(), expect_workflow_id=WORKFLOW + "@" + commit)) manifest = CONTRACT.load_json(candidate / "manifest.json") provenance = CONTRACT.load_json(candidate / "provenance.json") require(manifest["source"]["repository"] == SOURCE_REPOSITORY, "unexpected source repository") require(manifest["version"] == version and tag == "v" + version, "unexpected version") require(manifest.get("tag") == tag and manifest.get("source_sha") == commit, "legacy source fields disagree") require(provenance["predicate"]["buildDefinition"]["externalParameters"] == {"project": "redflag", "sourceTag": tag, "version": version}, "provenance parameters disagree") invocation = manifest["build"]["invocation_id"] require(re.fullmatch(re.escape(control_repository()) + r"/actions/runs/[1-9][0-9]*", invocation), "invalid invocation identity") files = {a["filename"]: a for a in manifest["artifacts"]} require(all(a["target_os"] + "-" + a["target_arch"] in TARGETS for a in files.values()), "unexpected artifact target") for target in TARGETS: os_name, arch = target.split("-") for component in ("server", "agent", "helper"): if component == "helper" and os_name == "windows": continue name = f"redflag-{component}-{target}" + (".exe" if os_name == "windows" else "") require(name in files, "missing required artifact: " + name) item = files[name] platform = os_name if component == "agent" else component + "-" + os_name require(item.get("platform") == platform and item.get("architecture") == arch, "legacy target mismatch: " + name) require(item["target_os"] == os_name and item["target_arch"] == arch, "contract target mismatch: " + name) require(f"redflag_{version}_amd64.deb" in files, "missing Debian package") require("RedFlagSetup-windows-amd64.msi" in files, "missing Windows installer") desktop = "redflag-desktop-windows-amd64.exe" require(desktop in files, "missing native Windows Desktop: " + desktop) require(files[desktop].get("platform") == "desktop-windows" and files[desktop].get("architecture") == "amd64", "native Windows Desktop target mismatch") require(f"redflag-{version}-windows-desktop-amd64.zip" in files, "missing complete native Windows Desktop payload") def assemble(args): source = Path(args.artifacts) candidate = Path(args.candidate) require(not candidate.exists() and not candidate.is_symlink(), "candidate output already exists; choose a fresh directory") CONTRACT.validate_commit(args.commit) require(re.fullmatch(r"[0-9]+(?:\.[0-9]+){2,3}(?:[.-][0-9A-Za-z]+)*", args.version), "invalid version") require(args.tag == "v" + args.version, "version/tag mismatch") generated = int(subprocess.check_output(["git", "show", "-s", "--format=%ct", args.commit], text=True)) descriptors, legacy = [], {} for target in sorted(TARGETS): directory = source / ("release-" + target) snippet = directory / ("release-" + target + ".artifacts.json") for item in CONTRACT.load_json(snippet): name = CONTRACT.safe_filename(item["filename"]) require(name not in legacy, "duplicate snippet filename: " + name) path = directory / name require(path.is_file() and not path.is_symlink(), "snippet payload missing: " + name) require(CONTRACT.sha256(path) == item["sha256"] and path.stat().st_size == item["size"], "snippet digest mismatch: " + name) legacy[name] = item for path in sorted(directory.iterdir()): if path == snippet: continue require(path.is_file() and not path.is_symlink(), "unsafe payload") require(path.name not in {"manifest.json", "provenance.json", "files.sha256"}, "reserved payload name") os_name, arch = target.split("-") media, package = "application/octet-stream", "binary" for suffix, kind, mime in ((".tar.gz", "generic", "application/gzip"), (".zip", "generic", "application/zip"), (".deb", "deb", "application/vnd.debian.binary-package"), (".msi", "msi", "application/x-msi"), (".txt", "generic", "text/plain")): if path.name.endswith(suffix): media, package = mime, kind break descriptors.append(dict(path=str(path), filename=path.name, target_os=os_name, target_arch=arch, media_type=media, package_type=package)) with tempfile.TemporaryDirectory(prefix="redflag-contract-") as temporary: temporary = Path(temporary) declaration = temporary / "release.toml" declaration.write_text('schema_version = 1\nproject = "redflag"\nversion = ' + json.dumps(args.version) + '\nsource_repository = ' + json.dumps(SOURCE_REPOSITORY) + '\nbuild_type = ' + json.dumps(BUILD_TYPE) + '\n') descriptor_path = temporary / "artifacts.json" CONTRACT.write_json(descriptor_path, descriptors) CONTRACT.assemble(SimpleNamespace(declaration=str(declaration), artifacts=str(descriptor_path), output=str(candidate), source_commit=args.commit, source_tag=args.tag, builder_id=builder_id(), workflow_id=WORKFLOW + "@" + args.commit, invocation_id=control_repository() + "/actions/runs/" + args.run_id)) manifest = CONTRACT.load_json(candidate / "manifest.json") manifest.update(tag=args.tag, source_sha=args.commit, generated_at=generated, key_id="") manifest["components"] = [dict(name=name, kind="embedded" if name == "web" else "binary", required=name not in {"desktop", "installer"}, **({"version_cmd": "--version"} if name not in {"web", "installer"} else {})) for name in ("server", "agent", "helper", "desktop", "web", "installer")] manifest["components"][3]["provisioning"] = ["autostart_entry", "redflag-local_group", "desktop_user_membership"] for item in manifest["artifacts"]: if item["filename"] in legacy: item.update(legacy[item["filename"]]) CONTRACT.write_json(candidate / "manifest.json", manifest) names = [item["filename"] for item in manifest["artifacts"]] + ["manifest.json", "provenance.json"] (candidate / "files.sha256").write_text("".join(f"{CONTRACT.sha256(candidate / name)} {name}\n" for name in sorted(names))) verify(candidate, args.version, args.commit, args.tag) def main(): parser = argparse.ArgumentParser() sub = parser.add_subparsers(dest="command", required=True) for command in ("assemble", "verify", "receipt", "names"): child = sub.add_parser(command) child.add_argument("candidate", type=Path) if command not in {"receipt", "names"}: child.add_argument("version") child.add_argument("commit") child.add_argument("tag") if command == "assemble": child.add_argument("artifacts") child.add_argument("run_id") args = parser.parse_args() try: if args.command == "assemble": assemble(args) elif args.command == "verify": verify(args.candidate, args.version, args.commit, args.tag) else: for name in receipt(args.candidate, check_files=args.command != "names"): print(name) except (CONTRACT.ContractError, OSError, ValueError, KeyError, TypeError) as error: parser.exit(1, f"release contract: {error}\n") if __name__ == "__main__": main()