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
This commit is contained in:
commit
67e26be2d9
838 changed files with 157302 additions and 0 deletions
301
scripts/vendor/release-contract-core.py
vendored
Normal file
301
scripts/vendor/release-contract-core.py
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
#!/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())
|
||||
Loading…
Reference in a new issue