Files
op-packages/smart-srun/scripts/make_go_manifest.py
T

255 lines
16 KiB
Python

#!/usr/bin/env python3
"""Create immutable Go release metadata from measured SDK build records.
Additional validation is accepted only as a report keyed by the package's
actual SHA256. A successful build never implies installation or campus testing.
Dirty builds require an explicit internal-test option and are not publishable.
"""
import argparse
import hashlib
import json
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
REPOSITORY = "matthewlu070111/smart-srun"
KINDS = {"smart-srun": "core", "luci-app-smart-srun": "luci", "luci-app-smart-srun-bundle": "bundle"}
CHECKS = ("build", "elf", "emulated_core", "openwrt_install", "hardware_core", "campus_auth")
def firmware_compatibility(evidence):
# Forks may keep opkg after upstream moved to APK. Keep the build's SDK
# identity intact and accept extra families only from exact-byte native
# installation reports, never from a firmware label or CPU/model guess.
reports = evidence.get("firmware_compatibility", {})
if not isinstance(reports, dict):
raise ValueError("Invalid firmware compatibility evidence")
result = {}
for digest, families in reports.items():
if not isinstance(digest, str) or not re.fullmatch("[0-9a-f]{64}", digest) or not isinstance(families, dict) or not 0 < len(families) <= 16:
raise ValueError("Invalid firmware compatibility evidence")
for family, report in families.items():
if not isinstance(family, str) or not re.fullmatch(r"\d{2}\.\d{2}", family) or report != {"openwrt_install": True} or type(report.get("openwrt_install")) is not bool:
raise ValueError("Extra firmware families require successful native installation evidence")
result[digest] = set(families)
return result
def release_filename(package, native_version, architecture, sdk, fmt):
# Native APK filenames omit architecture. Both formats also omit the SDK,
# so never flatten their original names into a multi-target Release.
# GitHub rewrites '~' during asset upload. Keep the native package version
# intact in its metadata, but use the upload-safe spelling in every URL,
# checksum entry and split archive member from the beginning.
filename_version = native_version.replace("~", ".")
separator = "_" if fmt == "ipk" else "-"
name = f"{package}{separator}{filename_version}_{architecture}_openwrt-{sdk}.{fmt}"
if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,199}", name):
raise ValueError("Release filename is not safe for GitHub asset upload")
return name
def apk_contents(path, apk, keys, version):
"""Compare signed APK content without treating randomized signatures as payload."""
if not apk or not keys:
raise ValueError("Non-identical LuCI APKs require --apk and --keys for native content verification")
subprocess.run([str(apk), "--keys-dir", str(keys), "verify", str(path)],
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, check=True, timeout=30)
# Native metadata includes file hashes, attributes, dependencies and scripts.
# Keep it bounded and verify the full archive first; matching sizes or source
# inputs alone do not prove two generated packages have the same contents.
with tempfile.TemporaryFile() as output:
subprocess.run([str(apk), "adbdump", "--format", "json", str(path)],
stdout=output, stderr=subprocess.PIPE, check=True, timeout=30)
output.seek(0)
data = output.read((256 << 10) + 1)
if len(data) > 256 << 10:
raise ValueError("Native LuCI APK metadata exceeds the comparison limit")
document = json.loads(data)
if not isinstance(document, dict) or not isinstance(document.get("info"), dict) or not document.get("paths"):
raise ValueError("Native LuCI APK metadata is incomplete")
info = document["info"]
if (info.get("name"), info.get("arch"), info.get("version")) != ("luci-app-smart-srun", "noarch", version):
raise ValueError("Native LuCI APK identity differs from its build record")
return document
def generate(records, evidence=None, internal=False, *, package_sources=None, apk=None, keys=None):
evidence = evidence or {"schema_version": 1, "assets": {}}
if evidence.get("schema_version") != 1 or not isinstance(evidence.get("assets"), dict):
raise ValueError("Invalid validation evidence")
compatibility = firmware_compatibility(evidence)
manifest = None
source_files = None
selections, names, sources, device_selections = {}, {}, {}, set()
for path in records:
path = Path(path)
record = json.loads(path.read_text(encoding="utf-8"))
version = record["display_version"]
if not re.fullmatch(r"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:rc[1-9]\d*)?", version):
raise ValueError("Invalid display version")
if int(version.split(".")[0]) < 2 or not re.fullmatch("[0-9a-f]{40}", record["source_commit"]):
raise ValueError("Not a Go release source")
if record.get("source_dirty") is not False and not internal:
raise ValueError("Dirty source is allowed only for explicit internal tests")
# Native RC spelling changes Makefile (~rc for opkg, _rc for APK).
# Compare the measured inputs before that one deliberate substitution,
# while retaining the SDK's exact staged file hashes in the build record.
files = record.get("source_template_files")
if not isinstance(files, dict) or not 2 <= len(files) <= 4096 or not {"Makefile", "core/go.mod"} <= files.keys():
raise ValueError("Missing measured source file identities")
for name, digest in files.items():
if not isinstance(name, str) or name.startswith("/") or "\\" in name or ":" in name or any(part in ("", ".", "..") for part in name.split("/")) or not isinstance(digest, str) or not re.fullmatch("[0-9a-f]{64}", digest):
raise ValueError("Invalid measured source file identity")
staged = record.get("source_files")
if not isinstance(staged, dict) or staged.keys() != files.keys() or any(
not isinstance(digest, str) or not re.fullmatch("[0-9a-f]{64}", digest)
or (name != "Makefile" and files[name] != digest) for name, digest in staged.items()):
raise ValueError("Unexpected SDK source substitution")
if source_files is None:
source_files = files
elif source_files != files:
raise ValueError("SDK builds do not contain the same source files")
if manifest is None:
manifest = dict(schema_version=1, release=version, channel="rc" if "rc" in version else "stable",
source_commit=record["source_commit"], assets=[])
if manifest["release"] != version or manifest["source_commit"] != record["source_commit"]:
raise ValueError("Build records do not share a release and source commit")
target = record["target"]
if target.get("goos") != "linux" or not isinstance(target.get("goarch"), str) or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}", target["goarch"]) or not re.fullmatch(r"[a-z0-9_-]+/[a-z0-9_-]+", target.get("target", "")) or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}", target.get("openwrt_arch", "")) or target["openwrt_arch"] in ("all", "noarch"):
raise ValueError("Invalid native SDK target identity")
manager, fmt = target["package_manager"], target["format"]
if (manager, fmt) not in (("opkg", "ipk"), ("apk", "apk")):
raise ValueError("Inconsistent package format")
sdk = target["sdk_release"]
if not re.fullmatch(r"\d{2}\.\d{2}\.\d+", sdk):
raise ValueError("Unpinned SDK release")
family = sdk.rsplit(".", 1)[0]
for artifact in record["artifacts"]:
name = artifact["file"]
if Path(name).name != name or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.~+-]{0,199}", name):
raise ValueError("Unsafe artifact filename")
file = path.parent / name
if not file.is_file() or file.is_symlink():
raise ValueError("Missing regular artifact")
with file.open("rb") as stream:
digest = hashlib.file_digest(stream, "sha256").hexdigest()
size = file.stat().st_size
if digest != artifact["sha256"] or size != artifact["bytes"] or not 0 < size <= 16 * 1024**2:
raise ValueError("Artifact bytes or checksum changed after build")
installed = artifact["installed_payload_bytes"]
if type(installed) is not int or not isinstance(artifact["files"], dict) or any(type(size) is not int or size < 0 for size in artifact["files"].values()) or sum(artifact["files"].values()) != installed or not 0 < installed <= 10 * 1024**2:
raise ValueError("Invalid measured installed payload")
kind = KINDS[artifact["name"]]
separator = "_" if fmt == "ipk" else "-"
if not name.startswith(artifact["name"] + separator) or not name.endswith("." + fmt):
raise ValueError("Artifact filename does not match package identity")
architecture = artifact["architecture"]
expected = ("all" if manager == "opkg" else "noarch") if kind == "luci" else target["openwrt_arch"]
if architecture != expected:
raise ValueError("Package architecture does not match its target/kind")
native = version.replace("rc", "~rc" if manager == "opkg" else "_rc")
if not re.fullmatch(re.escape(native) + r"-r[1-9]\d{0,8}", artifact["package_version"]):
raise ValueError("Unexpected native package version")
expected_name = (f"{artifact['name']}_{artifact['package_version']}_{architecture}.ipk" if fmt == "ipk"
else f"{artifact['name']}-{artifact['package_version']}.apk")
if name != expected_name:
raise ValueError("Filename does not identify the exact native package")
if manager == "apk" and not re.fullmatch("[0-9a-f]{64}", artifact.get("signature_spki_sha256") or ""):
raise ValueError("APK must have verified signing evidence")
if artifact.get("validation", {}).get("build") is not True:
raise ValueError("Build did not pass")
checked = evidence["assets"].get(digest, {})
if any(key not in CHECKS or type(value) is not bool for key, value in checked.items()):
raise ValueError("Invalid validation levels")
validation = {key: checked.get(key, False) for key in CHECKS}
validation["build"] = True
identifier = f"{kind}-{manager}-{architecture}-{family}"
export_name = release_filename(artifact["name"], artifact["package_version"], architecture, sdk, fmt)
if len(export_name) > 200:
raise ValueError("Release artifact filename exceeds the consumer limit")
asset = dict(id=identifier, kind=kind, package_manager=manager, format=fmt,
openwrt_arch=architecture, package_version=artifact["package_version"],
sdk_release=sdk, target=target["target"], goos=target["goos"], goarch=target["goarch"],
url=f"https://github.com/{REPOSITORY}/releases/download/{version}/{export_name}",
sha256=digest, bytes=size, installed_bytes=installed,
firmware_compat=sorted({family} | compatibility.get(digest, set())), validation=validation)
if identifier in selections:
previous = selections[identifier]
if kind != "luci" or any(previous[key] != asset[key] for key in
("package_version", "installed_bytes", "validation", "sdk_release", "firmware_compat")):
raise ValueError("Ambiguous packages for the same device selection")
if previous["sha256"] != digest:
previous_name = previous["url"].rsplit("/", 1)[1]
native_version = asset["package_version"]
if fmt != "apk" or apk_contents(sources[previous_name], apk, keys, native_version) != apk_contents(file, apk, keys, native_version):
raise ValueError("Ambiguous packages for the same device selection")
# ECDSA signatures differ even for identical noarch payloads.
# Keep the FIRST verified archive's exact bytes/hash/evidence;
# never transfer acceptance from the discarded signature variant.
continue # identical architecture-neutral LuCI from another SDK target
compatible = {(kind, manager, architecture, item) for item in asset["firmware_compat"]}
if device_selections & compatible:
raise ValueError("Ambiguous packages for overlapping firmware compatibility")
device_selections.update(compatible)
if export_name in names:
raise ValueError("Duplicate release asset filename")
selections[identifier], names[export_name] = asset, digest
sources[export_name] = file
manifest["assets"].append(asset)
if manifest is None or not manifest["assets"]:
raise ValueError("No artifacts")
if package_sources is not None:
package_sources.update(sources)
return manifest, "".join(f"{digest} {name}\n" for name, digest in sorted(names.items()))
def export_release(records, output, evidence=None, internal=False, *, apk=None, keys=None):
sources = {}
manifest, sums = generate(records, evidence, internal, package_sources=sources, apk=apk, keys=keys)
output = Path(output)
if output.exists() or output.is_symlink():
raise ValueError("Refusing to replace an existing release directory")
output.parent.mkdir(parents=True, exist_ok=True)
document = json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
manifest_hash = hashlib.sha256(document.encode()).hexdigest()
with tempfile.TemporaryDirectory(prefix=".smart-srun-release-", dir=output.parent) as temporary:
stage = Path(temporary)
for asset in manifest["assets"]:
name = asset["url"].rsplit("/", 1)[1]
destination = stage / name
shutil.copyfile(sources[name], destination)
with destination.open("rb") as stream:
digest = hashlib.file_digest(stream, "sha256").hexdigest()
if digest != asset["sha256"] or destination.stat().st_size != asset["bytes"]:
raise ValueError("Artifact changed while staging the release")
(stage / "SHA256SUMS").write_text(sums + f"{manifest_hash} release-manifest.json\n", encoding="utf-8", newline="\n")
(stage / "release-manifest.json").write_text(document, encoding="utf-8", newline="\n")
# Reserve the immutable destination exclusively after all byte checks.
# Install the manifest last; an interrupted export has no usable index
# and is kept for inspection rather than silently overwritten.
output.mkdir()
for name in [*sorted(sources), "SHA256SUMS", "release-manifest.json"]:
(stage / name).rename(output / name)
return manifest
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("records", nargs="+", type=Path)
parser.add_argument("--evidence", type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--internal-test", action="store_true")
parser.add_argument("--apk", type=Path, help="Native apk-tools for comparing independently signed noarch packages")
parser.add_argument("--keys", type=Path, help="Trusted public keys for native APK comparison")
args = parser.parse_args()
evidence = json.loads(args.evidence.read_text()) if args.evidence else None
manifest = export_release(args.records, args.output, evidence, args.internal_test, apk=args.apk, keys=args.keys)
print(f"{manifest['release']}: {len(manifest['assets'])} measured assets -> {args.output}")
if __name__ == "__main__":
main()