Files
NODEDC_MISSION_CORE/plugins/xgrids-k1/packaging/build_deb.py
T

188 lines
6.9 KiB
Python

#!/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.9"
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 <noreply@example.invalid>
Section: admin
Priority: optional
Depends: mission-core-node (>= 0.8.9), 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)