Snapshot assembled only from the signed corresponding-source archive. Source archive SHA-256: db91a270cdefc4be79056ed38a0b4a16747e1ab130592645eac40fa3809809df
88 lines
5.1 KiB
Python
88 lines
5.1 KiB
Python
"""Bind release packages to their corresponding source; never certify runtime QA."""
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import tarfile
|
|
|
|
|
|
PUBLIC_RELEASE_PROFILES = (
|
|
"debian13-amd64-py313", "ubuntu24.04-amd64-py312", "mint22-amd64-py312",
|
|
)
|
|
|
|
|
|
def release_source(manifest_path, version, profile_id, current_files, vendor_evidence):
|
|
"""Require the same reviewed source and ABI input used by this package.
|
|
|
|
The manifest and archive are release inputs kept beside one another. Their
|
|
signatures and the OS acceptance record are verified outside the builder.
|
|
"""
|
|
if profile_id not in PUBLIC_RELEASE_PROFILES or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version):
|
|
raise RuntimeError("Public release requires a stable version and an official release profile")
|
|
manifest_path = Path(manifest_path)
|
|
data = json.loads(manifest_path.read_text())
|
|
expected_name = "FrameBridge-" + version + "-source.tar.gz"
|
|
if (data.get("version"), data.get("status"), data.get("archive")) != (version, "RELEASE_SOURCE_ASSEMBLED", expected_name):
|
|
raise RuntimeError("Matching release source manifest is required")
|
|
archive = manifest_path.parent / expected_name
|
|
if archive.is_symlink() or not archive.is_file():
|
|
raise RuntimeError("Matching release source archive is missing")
|
|
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
|
|
if archive.stat().st_size != data.get("bytes") or digest != data.get("sha256"):
|
|
raise RuntimeError("Release source archive hash or size differs from its manifest")
|
|
vendors = data.get("vendor_inputs", {}).get("vendors", {})
|
|
if set(vendors) != {"cp312", "cp313"}:
|
|
raise RuntimeError("Release source must contain the reviewed cp312 and cp313 inputs")
|
|
abi = "cp313" if profile_id == "debian13-amd64-py313" else "cp312"
|
|
if vendors[abi].get("vendor_sha256") != vendor_evidence.get("vendor_sha256"):
|
|
raise RuntimeError("Package vendor input differs from corresponding source")
|
|
rows = data.get("files", [])
|
|
if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
|
|
raise RuntimeError("Invalid release source file manifest")
|
|
recorded = {row.get("path"): row.get("sha256") for row in rows}
|
|
if len(recorded) != len(rows):
|
|
raise RuntimeError("Duplicate release source manifest path")
|
|
seen = set()
|
|
prefix = "FrameBridge-" + version + "/"
|
|
with tarfile.open(archive, "r:gz") as source:
|
|
for member in source:
|
|
if not member.isfile() or not member.name.startswith(prefix):
|
|
raise RuntimeError("Unexpected release source archive member")
|
|
relative = member.name[len(prefix):]
|
|
if not relative or PurePosixPath(relative).is_absolute() or ".." in PurePosixPath(relative).parts:
|
|
raise RuntimeError("Unsafe release source archive path")
|
|
if relative in seen or relative not in recorded:
|
|
raise RuntimeError("Duplicate or unrecorded release source archive member")
|
|
seen.add(relative)
|
|
with source.extractfile(member) as stream:
|
|
if hashlib.sha256(stream.read()).hexdigest() != recorded[relative]:
|
|
raise RuntimeError("Release source archive content differs from its manifest")
|
|
if seen != set(recorded):
|
|
raise RuntimeError("Release source manifest contains files absent from the archive")
|
|
# Bind the ABI claims to the archived bytes, including the ABI not selected
|
|
# by this particular package. A manifest field alone is not vendor evidence.
|
|
for vendor_abi, expected_prefix in (
|
|
("cp313", "source/sources/original-geranas/FrameBridge-Fleet-Hub/vendor"),
|
|
("cp312", "source/target-vendors/cp312"),
|
|
):
|
|
if vendors[vendor_abi].get("path") != expected_prefix:
|
|
raise RuntimeError("Unexpected corresponding-source vendor path")
|
|
vendor_digest = hashlib.sha256()
|
|
# validate_vendor sorts Path objects, whose component ordering differs
|
|
# from slash-delimited strings for package/ versus package-1.dist-info/.
|
|
for path, file_digest in sorted(recorded.items(), key=lambda row: PurePosixPath(row[0]).parts):
|
|
if not path.startswith(expected_prefix + "/"):
|
|
continue
|
|
relative = path[len(expected_prefix) + 1:]
|
|
parts = PurePosixPath(relative).parts
|
|
if "__pycache__" in parts or "bin" in parts or relative.endswith(".pyc") or parts[-1] == "FB-ABI-PREPARED.json":
|
|
continue
|
|
vendor_digest.update(relative.encode() + b"\0")
|
|
vendor_digest.update(bytes.fromhex(file_digest))
|
|
if vendor_digest.hexdigest() != vendors[vendor_abi].get("vendor_sha256"):
|
|
raise RuntimeError("Archived vendor bytes differ from their recorded ABI identity")
|
|
for relative, path in current_files.items():
|
|
if recorded.get(relative) != hashlib.sha256(path.read_bytes()).hexdigest():
|
|
raise RuntimeError("Package source differs from corresponding archive: " + relative)
|
|
return {"filename": expected_name, "bytes": data["bytes"], "sha256": digest}
|