RedFlag/scripts/vendor/release-contract-core.py
Fimeg 67e26be2d9 projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally.

Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
2026-09-08 21:59:33 -04:00

301 lines
12 KiB
Python

#!/usr/bin/env python3
"""Assemble and verify shared release-contract v1 candidates."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import shutil
import sys
import tomllib
STATEMENT_TYPE = "https://in-toto.io/Statement/v1"
PREDICATE_TYPE = "https://slsa.dev/provenance/v1"
PROVENANCE_FORMAT = "in-toto-statement+slsa-provenance-v1"
class ContractError(ValueError):
pass
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def safe_filename(value: str) -> str:
if not value or value in {".", ".."} or Path(value).name != value or "/" in value or "\\" in value:
raise ContractError(f"unsafe candidate filename: {value!r}")
return value
def load_json(path: Path):
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ContractError(f"cannot read JSON {path}: {error}") from error
def write_json(path: Path, value) -> None:
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def require_text(mapping: dict, key: str) -> str:
value = mapping.get(key)
if not isinstance(value, str) or not value:
raise ContractError(f"{key} must be a non-empty string")
return value
def validate_commit(value: str) -> str:
if len(value) not in range(40, 65) or any(char not in "0123456789abcdef" for char in value):
raise ContractError("source commit must be 40-64 lowercase hexadecimal characters")
return value
def assemble(args: argparse.Namespace) -> None:
declaration_path = Path(args.declaration)
declaration = tomllib.loads(declaration_path.read_text(encoding="utf-8"))
if declaration.get("schema_version") != 1:
raise ContractError("declaration schema_version must be 1")
project = require_text(declaration, "project")
version = require_text(declaration, "version")
repository = require_text(declaration, "source_repository")
build_type = require_text(declaration, "build_type")
source_commit = validate_commit(args.source_commit)
source_tag = require_text(vars(args), "source_tag")
descriptors = load_json(Path(args.artifacts))
if not isinstance(descriptors, list) or not descriptors:
raise ContractError("artifact descriptors must be a non-empty JSON array")
output = Path(args.output)
if output.resolve() in {Path("/").resolve(), Path(".").resolve()}:
raise ContractError("refusing unsafe output directory")
if output.exists():
shutil.rmtree(output)
output.mkdir(parents=True)
manifest_artifacts = []
seen = set()
for descriptor in descriptors:
if not isinstance(descriptor, dict):
raise ContractError("each artifact descriptor must be an object")
source = Path(require_text(descriptor, "path"))
filename = safe_filename(require_text(descriptor, "filename"))
if filename in seen:
raise ContractError(f"duplicate artifact filename: {filename}")
seen.add(filename)
if not source.is_file() or source.is_symlink():
raise ContractError(f"artifact must be a regular non-symlink file: {source}")
destination = output / filename
shutil.copyfile(source, destination)
manifest_artifacts.append(
{
"filename": filename,
"media_type": require_text(descriptor, "media_type"),
"package_type": require_text(descriptor, "package_type"),
"target_arch": require_text(descriptor, "target_arch"),
"target_os": require_text(descriptor, "target_os"),
"size": destination.stat().st_size,
"sha256": sha256(destination),
}
)
manifest_artifacts.sort(key=lambda item: item["filename"])
build = {
"builder_id": args.builder_id,
"build_type": build_type,
"invocation_id": args.invocation_id,
"workflow_id": args.workflow_id,
}
for key, value in build.items():
if not value:
raise ContractError(f"{key} must be a non-empty string")
manifest = {
"artifacts": manifest_artifacts,
"build": build,
"evidence": {
"authenticated": False,
"provenance_format": PROVENANCE_FORMAT,
"slsa_level_claimed": False,
},
"project": project,
"schema_version": 1,
"source": {"commit": source_commit, "repository": repository, "tag": source_tag},
"version": version,
}
write_json(output / "manifest.json", manifest)
subjects = [
{"name": item["filename"], "digest": {"sha256": item["sha256"]}}
for item in manifest_artifacts
]
provenance = {
"_type": STATEMENT_TYPE,
"predicateType": PREDICATE_TYPE,
"subject": subjects,
"predicate": {
"buildDefinition": {
"buildType": build_type,
"externalParameters": {"project": project, "sourceTag": source_tag, "version": version},
"internalParameters": {},
"resolvedDependencies": [
{"uri": repository, "digest": {"gitCommit": source_commit}}
],
},
"runDetails": {
"builder": {"id": args.builder_id},
"metadata": {"invocationId": args.invocation_id},
},
},
}
write_json(output / "provenance.json", provenance)
receipt_paths = [output / item["filename"] for item in manifest_artifacts]
receipt_paths.extend([output / "manifest.json", output / "provenance.json"])
lines = [f"{sha256(path)} {path.name}" for path in sorted(receipt_paths, key=lambda path: path.name)]
(output / "files.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8")
def parse_receipt(candidate: Path) -> dict[str, str]:
receipt = candidate / "files.sha256"
try:
lines = receipt.read_text(encoding="utf-8").splitlines()
except OSError as error:
raise ContractError(f"cannot read receipt: {error}") from error
if not lines:
raise ContractError("receipt is empty")
entries: dict[str, str] = {}
for line in lines:
parts = line.split(" ", 1)
if len(parts) != 2 or len(parts[0]) != 64 or any(char not in "0123456789abcdef" for char in parts[0]):
raise ContractError(f"invalid receipt line: {line!r}")
filename = safe_filename(parts[1])
if filename in entries:
raise ContractError(f"duplicate receipt filename: {filename}")
entries[filename] = parts[0]
return entries
def verify(args: argparse.Namespace) -> None:
candidate = Path(args.candidate)
entries = parse_receipt(candidate)
for filename, expected in entries.items():
path = candidate / filename
if not path.is_file() or path.is_symlink():
raise ContractError(f"receipt file is absent or unsafe: {filename}")
if sha256(path) != expected:
raise ContractError(f"receipt digest mismatch: {filename}")
manifest = load_json(candidate / "manifest.json")
if manifest.get("schema_version") != 1:
raise ContractError("manifest schema_version must be 1")
if manifest.get("evidence") != {
"authenticated": False,
"provenance_format": PROVENANCE_FORMAT,
"slsa_level_claimed": False,
}:
raise ContractError("manifest evidence posture is invalid")
source = manifest.get("source")
build = manifest.get("build")
artifacts = manifest.get("artifacts")
if not isinstance(source, dict) or not isinstance(build, dict) or not isinstance(artifacts, list) or not artifacts:
raise ContractError("manifest source, build, or artifacts are invalid")
expected_values = {
"project": (manifest.get("project"), args.expect_project),
"source commit": (source.get("commit"), args.expect_source_commit),
"source tag": (source.get("tag"), args.expect_source_tag),
"builder": (build.get("builder_id"), args.expect_builder_id),
"workflow": (build.get("workflow_id"), args.expect_workflow_id),
}
for label, (actual, expected) in expected_values.items():
if expected is not None and actual != expected:
raise ContractError(f"unexpected {label}: {actual!r}")
artifact_digests: dict[str, str] = {}
expected_receipt = {"manifest.json", "provenance.json"}
for artifact in artifacts:
if not isinstance(artifact, dict):
raise ContractError("manifest artifact must be an object")
filename = safe_filename(require_text(artifact, "filename"))
if filename in artifact_digests:
raise ContractError(f"duplicate manifest artifact: {filename}")
path = candidate / filename
digest = require_text(artifact, "sha256")
if len(digest) != 64 or sha256(path) != digest or path.stat().st_size != artifact.get("size"):
raise ContractError(f"manifest artifact mismatch: {filename}")
artifact_digests[filename] = digest
expected_receipt.add(filename)
if set(entries) != expected_receipt:
raise ContractError("receipt membership does not equal manifest artifacts plus evidence")
provenance = load_json(candidate / "provenance.json")
if provenance.get("_type") != STATEMENT_TYPE or provenance.get("predicateType") != PREDICATE_TYPE:
raise ContractError("provenance statement type is invalid")
subjects = provenance.get("subject")
if not isinstance(subjects, list):
raise ContractError("provenance subjects are invalid")
subject_digests = {
safe_filename(item.get("name", "")): item.get("digest", {}).get("sha256")
for item in subjects if isinstance(item, dict) and isinstance(item.get("digest"), dict)
}
if subject_digests != artifact_digests:
raise ContractError("provenance subjects do not equal manifest artifacts")
definition = provenance.get("predicate", {}).get("buildDefinition", {})
details = provenance.get("predicate", {}).get("runDetails", {})
dependencies = definition.get("resolvedDependencies")
expected_dependency = [{"uri": source.get("repository"), "digest": {"gitCommit": source.get("commit")}}]
if definition.get("buildType") != build.get("build_type") or dependencies != expected_dependency:
raise ContractError("provenance build type or source dependency does not match manifest")
if details.get("builder", {}).get("id") != build.get("builder_id"):
raise ContractError("provenance builder does not match manifest")
if details.get("metadata", {}).get("invocationId") != build.get("invocation_id"):
raise ContractError("provenance invocation does not match manifest")
def parser() -> argparse.ArgumentParser:
root = argparse.ArgumentParser()
commands = root.add_subparsers(dest="command", required=True)
create = commands.add_parser("assemble")
create.add_argument("--declaration", required=True)
create.add_argument("--artifacts", required=True)
create.add_argument("--output", required=True)
create.add_argument("--source-commit", required=True)
create.add_argument("--source-tag", required=True)
create.add_argument("--builder-id", required=True)
create.add_argument("--workflow-id", required=True)
create.add_argument("--invocation-id", required=True)
create.set_defaults(func=assemble)
check = commands.add_parser("verify")
check.add_argument("--candidate", required=True)
check.add_argument("--expect-project")
check.add_argument("--expect-source-commit")
check.add_argument("--expect-source-tag")
check.add_argument("--expect-builder-id")
check.add_argument("--expect-workflow-id")
check.set_defaults(func=verify)
return root
def main() -> int:
args = parser().parse_args()
try:
args.func(args)
except (ContractError, OSError, tomllib.TOMLDecodeError) as error:
print(f"release-contract: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())