#!/usr/bin/env python3 """Build the optional onboard K1 integration, including a private autonomous edition. The public build contains no application material. --private-authority-stdin creates a mode-0600 private installer; the key never appears in arguments, provenance, stdout or a source file. This installer itself is a private artifact. """ import argparse import hashlib import json import os import subprocess import sys import tempfile from pathlib import Path PACKAGING = Path(__file__).resolve().parent REPOSITORY = PACKAGING.parents[2] sys.path.insert(0, str(REPOSITORY / "scripts/packaging")) from credential_install import PROFILE_ID, validate # noqa: E402 from debian import package # noqa: E402 from runtime_payload import files as runtime_files # noqa: E402 VERSION = "0.1.14" RESOURCES = ( "plugins/xgrids-k1/profile_loader.py", "plugins/xgrids-k1/plugin.manifest.json", "config/observatory-equipment-models.json", "config/observatory-recorded-capture-profiles.json", "plugins/xgrids-k1/profiles/fw-3.0.2/local-network.v2.json", ) def payload(wheel_root, *, authority=None): files = [] for name in ("k1_prepare.py", "k1_bootstrap.py", "install-k1-credential"): files.append( ( "usr/lib/mission-core-node/" + name, (PACKAGING / name).read_bytes(), 0o755 if name == "install-k1-credential" else 0o644, ) ) files.extend( [ ( "usr/lib/mission-core-node/k1-install/credential_install.py", (PACKAGING / "credential_install.py").read_bytes(), 0o644, ), ( "usr/lib/mission-core-node/prepare-k1-profile", (PACKAGING / "prepare-bundled-profile").read_bytes(), 0o755, ), ( "usr/lib/systemd/system/mission-core-k1.service", (PACKAGING / "mission-core-k1.service").read_bytes(), 0o644, ), ( "usr/share/polkit-1/rules.d/50-mission-core-k1.rules", (PACKAGING / "50-mission-core-k1.rules").read_bytes(), 0o644, ), ( "usr/share/mission-core-node/k1/bundle.json", (PACKAGING / "k1-bundle.json").read_bytes(), 0o644, ), ] ) wheels = json.loads((PACKAGING / "k1-bundle.json").read_text())["wheels"] for item in wheels: path = wheel_root / item["name"] if path.name != item["name"] or path.is_symlink(): raise ValueError("Unsafe K1 wheel path") data = path.read_bytes() if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]: raise ValueError("K1 wheel differs from the admitted bundle") files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644)) sources = runtime_files() for path in sources: files.append( ( "usr/lib/mission-core-node/k1/" + str(path.relative_to(REPOSITORY)), path.read_bytes(), 0o644, ) ) for relative in RESOURCES: files.append( ( "usr/lib/mission-core-node/k1/" + relative, (REPOSITORY / relative).read_bytes(), 0o644, ) ) # Hash only code and public metadata. Never hash private material into a # public per-file manifest or represent it as scanner identity. provenance = { "package": "mission-core-xgrids-k1", "version": VERSION + ("+private.1" if authority is not None else ""), "base_commit": subprocess.check_output( ["git", "rev-parse", "HEAD"], cwd=REPOSITORY, text=True ).strip(), "application_profile": PROFILE_ID, "application_material_included": authority is not None, "acceptance_scope": "owner-controlled-k1-fw-3.0.2-ubuntu-24.04-amd64", "files": {name: hashlib.sha256(data).hexdigest() for name, data, _ in files}, } files.append( ( "usr/share/doc/mission-core-xgrids-k1/provenance.json", (json.dumps(provenance, indent=2) + "\n").encode(), 0o644, ) ) if authority is not None: validate(authority) files.append( ("usr/share/mission-core-node/k1/private-application-key", bytes(authority), 0o600) ) return files def build(destination, wheel_root, *, authority=None): if authority is not None: validate(authority) version = VERSION + ("+private.1" if authority is not None else "") control = f"""Package: mission-core-xgrids-k1 Version: {version} Architecture: amd64 Maintainer: NODE.DC local build Section: admin Priority: optional Depends: mission-core-node (>= 0.8.13), mission-core-node (<< 0.9.0), systemd, python3, adduser, bluez, network-manager, iproute2, ffmpeg Breaks: mission-core-node (<< 0.8.0) Replaces: mission-core-node (<< 0.8.0) Description: Optional XGRIDS K1 Bridge integration for Mission Core Node Reviewed FW 3.0.2 runtime with a separate process and durable device journals. """.encode() controls = [("control", control, 0o644)] controls += [ (name, (PACKAGING / name).read_bytes(), 0o755) for name in ("preinst", "postinst", "prerm", "postrm") ] archive = package(controls, payload(wheel_root, authority=authority)) destination.parent.mkdir(parents=True, exist_ok=True) # Never replace a previously published release with different bytes. fd, staged = tempfile.mkstemp(prefix=".k1-package-", dir=destination.parent) try: with os.fdopen(fd, "wb") as stream: os.fchmod(stream.fileno(), 0o600 if authority is not None else 0o644) stream.write(archive) stream.flush() os.fsync(stream.fileno()) os.link(staged, destination) finally: os.unlink(staged) return { "package": destination.name, "version": version, "bytes": len(archive), "sha256": hashlib.sha256(archive).hexdigest(), "private": authority is not None, } if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--wheel-root", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--private-authority-stdin", action="store_true") args = parser.parse_args() secret = None try: if args.private_authority_stdin: secret = bytearray(sys.stdin.buffer.read(1025).strip()) print(json.dumps(build(args.output, args.wheel_root, authority=secret))) except Exception: raise SystemExit("K1 package build failed; check inputs and unused output path") from None finally: if secret is not None: secret[:] = b"\0" * len(secret)