Files
KF-OnlineJudge/scripts/release/service_install.py
T

284 lines
11 KiB
Python

"""Root ordinary-unit installation; no service start, migration or readiness claim."""
import argparse
import hashlib
import json
import os
import re
import secrets
import stat
import subprocess
from pathlib import Path
from . import configuration, install, preflight, services
from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
UNITS = Path("/etc/kf-onlinejudge/units")
SYSTEM_UNITS = Path("/etc/systemd/system")
NAMES = {"kf-" + name + ".service" for name in services.PROFILES}
RECEIPT_FIELDS = {
"schema_version",
"source_sha",
"configuration_id",
"configuration_receipt_sha256",
"files",
"installed",
"services_started",
"execution_verified",
}
def validate_receipt(value: dict) -> dict:
value = fields(value, RECEIPT_FIELDS, "ordinary service receipt")
if (
type(value["schema_version"]) is not int
or value["schema_version"] != 1
or not isinstance(value["source_sha"], str)
or not re.fullmatch(r"[0-9a-f]{40}", value["source_sha"])
or not isinstance(value["configuration_id"], str)
or not re.fullmatch(r"cfg-[0-9a-f]{24}", value["configuration_id"])
or any(
value[key] is not False
for key in (
"installed",
"services_started",
"execution_verified",
)
)
):
raise DeliveryError("ordinary service receipt binding differs")
sha256(value["configuration_receipt_sha256"])
fields(value["files"], NAMES, "ordinary unit inventory")
for checksum in value["files"].values():
sha256(checksum)
return value
def batch_directory(value: dict) -> Path:
validate_receipt(value)
return UNITS / ("sd-" + value["source_sha"][:12] + "." + value["configuration_id"])
def previous_batch(target: str, name: str) -> Path:
"""Only a known immutable ordinary-unit target can ever be replaced."""
if name not in NAMES:
raise DeliveryError("unexpected ordinary unit")
path = Path(target)
if (
path.name != name
or path.parent.parent != UNITS
or not re.fullmatch(r"sd-[0-9a-f]{12}\.cfg-[0-9a-f]{24}", path.parent.name)
or path.as_posix() != target
):
raise DeliveryError("unknown system unit target; operator review required")
return path.parent
def current_receipt() -> tuple[dict, dict]:
selected = install.selection()
path = configuration.installed_receipt_path(
selected["release_id"], selected["configuration_id"]
)
installed = configuration.check_installed(path)
hashes = {
name: installed["configuration_files"][
str(
configuration.installed_path(
selected["release_id"],
selected["configuration_id"],
name,
)
)
]
for name in configuration.CONFIG_FILES
}
original = {**installed, "installed": False, "configuration_files": hashes}
configuration.validate_receipt(original, installed=False)
return selected, original
def check_group(directory: Path, receipt: dict) -> None:
preflight.protected(directory, directory=True)
if stat.S_IMODE(directory.stat().st_mode) != 0o700:
raise DeliveryError("private immutable unit batch required")
if {p.name for p in directory.iterdir()} != NAMES | {"receipt.json"}:
raise DeliveryError("unexpected installed unit member")
path = directory / "receipt.json"
preflight.protected(path)
configuration.private(path)
if canonical(validate_receipt(read_json(path, 16384))) != canonical(receipt):
raise DeliveryError("installed unit receipt differs")
for name in sorted(NAMES):
path = directory / name
preflight.protected(path)
if (
path.stat().st_size > 32768
or stat.S_IMODE(path.stat().st_mode) != 0o644
or digest(path) != receipt["files"][name]
):
raise DeliveryError("installed ordinary unit bytes/mode differ")
def known_link(path: Path) -> Path | None:
try:
info = path.lstat()
except FileNotFoundError:
return None
if not stat.S_ISLNK(info.st_mode) or info.st_uid != 0 or info.st_gid != 0:
raise DeliveryError("unknown system unit; refused overwrite")
directory = previous_batch(os.readlink(path), path.name)
receipt_path = directory / "receipt.json"
preflight.protected(receipt_path)
configuration.private(receipt_path)
receipt = validate_receipt(read_json(receipt_path, 16384))
if batch_directory(receipt) != directory:
raise DeliveryError("old unit target and receipt binding differ")
check_group(directory, receipt)
return directory
def systemctl(*arguments: str) -> str:
result = subprocess.run(
["/usr/bin/systemctl", *arguments],
timeout=15,
capture_output=True,
text=True,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C"},
)
if result.returncode or len(result.stdout) > 8192 or len(result.stderr) > 8192:
raise DeliveryError("bounded unit manager operation refused")
return result.stdout
def install_units(source: Path, expected: str) -> dict:
if os.getuid() != 0 or os.geteuid() != 0:
raise DeliveryError("ordinary unit installation requires root operator")
preflight.protected(source, directory=True)
path = source / "receipt.json"
preflight.protected(path)
configuration.private(path)
if digest(path) != sha256(expected):
raise DeliveryError("ordinary unit source receipt fingerprint differs")
receipt = validate_receipt(read_json(path, 16384))
selected, original = current_receipt()
if (
receipt["source_sha"] != original["source_sha"]
or receipt["configuration_id"] != selected["configuration_id"]
# Generated units bind the canonical *source* configuration receipt;
# installing that configuration changes its installed flag/path keys.
or receipt["configuration_receipt_sha256"]
!= hashlib.sha256(canonical(original)).hexdigest()
):
raise DeliveryError("ordinary unit source is not the selected configuration")
# A separately trusted operator cannot install a new launcher against an
# older delivery that lacks that launcher or has different bytes.
payload = install.RELEASES / selected["release_id"] / "payload"
for name in ("services.py", "service_bootstrap.py", "service_install.py"):
shipped = payload / "delivery/scripts/release" / name
preflight.protected(shipped)
if digest(shipped) != digest(Path(__file__).parent / name):
raise DeliveryError("ordinary unit installer differs from selected delivery")
if {p.name for p in source.iterdir()} != NAMES | {"receipt.json"}:
raise DeliveryError("unexpected ordinary unit source member")
contents = services.render(original)
for name in sorted(NAMES):
path = source / name
preflight.protected(path)
configuration.private(path)
if path.stat().st_size > 32768 or digest(path) != receipt["files"][name]:
raise DeliveryError("ordinary unit source fingerprint differs")
if path.read_bytes() != contents[name]:
raise DeliveryError("ordinary unit is not the fixed trusted composition")
install.require_stopped()
preflight.protected(SYSTEM_UNITS, directory=True)
# Check every existing pointer before changing any. Regular files, foreign
# links and incompletely recorded old batches require explicit review.
for name in sorted(NAMES):
known_link(SYSTEM_UNITS / name)
install.directory(UNITS, 0o700)
directory = batch_directory(receipt)
install.directory(directory, 0o700)
if not {p.name for p in directory.iterdir()} <= NAMES | {"receipt.json"}:
raise DeliveryError("unexpected partial unit batch member")
for path in directory.iterdir():
preflight.protected(path)
maximum = 16384 if path.name == "receipt.json" else 32768
if path.stat().st_size > maximum:
raise DeliveryError("partial unit batch member exceeds bound")
for name in sorted(NAMES):
install.exclusive_file(directory / name, contents[name], 0o644)
install.exclusive_file(directory / "receipt.json", canonical(receipt), 0o600)
check_group(directory, receipt)
if install.selection() != selected:
raise DeliveryError("selected configuration changed during unit preparation")
install.require_stopped()
for name in sorted(NAMES):
path = SYSTEM_UNITS / name
old = known_link(path)
if old == directory:
continue
temporary = SYSTEM_UNITS / (".kf-unit-" + secrets.token_hex(16))
created = None
try:
temporary.symlink_to(directory / name)
info = temporary.lstat()
created = (info.st_dev, info.st_ino)
os.replace(temporary, path)
install.sync_directory(SYSTEM_UNITS)
finally:
if created is not None and temporary.is_symlink():
info = temporary.lstat()
if (info.st_dev, info.st_ino) == created:
temporary.unlink()
systemctl("daemon-reload")
loaded = {}
for name in sorted(NAMES):
values = dict(
line.split("=", 1)
for line in systemctl(
"show",
name,
"--property=LoadState,ActiveState,MainPID,FragmentPath",
).splitlines()
if "=" in line
)
if (
values.get("LoadState") != "loaded"
or values.get("ActiveState") != "inactive"
or values.get("MainPID") != "0"
or values.get("FragmentPath") not in {str(SYSTEM_UNITS / name), str(directory / name)}
or known_link(SYSTEM_UNITS / name) != directory
):
raise DeliveryError("ordinary unit manager load readback differs")
loaded[name] = values
if install.selection() != selected:
raise DeliveryError("selected configuration changed during unit loading")
return {
"source_sha": receipt["source_sha"],
"configuration_id": receipt["configuration_id"],
"units_installed": True,
"unit_manager_readback": loaded,
"services_started": False,
"execution_verified": False,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Root ordinary-unit installation")
parser.add_argument("directory", type=Path)
parser.add_argument("--receipt-sha256", required=True)
args = parser.parse_args()
try:
print(json.dumps(install_units(args.directory, args.receipt_sha256), sort_keys=True))
except (OSError, ValueError, subprocess.SubprocessError) as error:
parser.exit(
1,
"Unit installation refused: "
+ (str(error) if isinstance(error, DeliveryError) else type(error).__name__)
+ "\n",
)
if __name__ == "__main__":
main()