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

288 lines
10 KiB
Python

"""Complete bundle byte proofs limited to one live Root maintenance invocation."""
import hashlib
import os
import re
import stat
import time
from contextlib import contextmanager
from pathlib import Path
from . import preflight
from .policy import DeliveryError, canonical, digest, fields, integer, read_json, sha256
DIRECTORY = Path("/run/kf-node-verification")
RECORD = DIRECTORY / "byte-proof.json"
PARENT = "kf-node-maintenance.service"
GROUP = "/sys/fs/cgroup/kfonlinejudge.slice/"
# Privileged ExecStartPre checks retain their ordinary service's supplementary
# group. These are fixed unit identities, never groups supplied by a proof.
CONSUMERS = {
"kf-postgres.service": 1203,
"kf-rabbitmq.service": 1204,
"kf-sandbox-factory.service": 0,
"kf-coordinator.service": 1201,
"kf-api.service": 1200,
"kf-outbox.service": 1200,
"kf-problem-import.service": 1200,
"kf-worker.service": 1202,
"kf-proxy.service": 1205,
"kf-node-health.service": 0,
}
MAX_AGE_NS = 900 * 10**9
MAX_RECORD = 4096
_REPORTED: tuple[int, str] | None = None
def _root(allowed_groups: frozenset[int] = frozenset({0})) -> bool:
return (os.getuid(), os.geteuid(), os.getgid(), os.getegid()) == (
0,
0,
0,
0,
) and set(os.getgroups()) <= allowed_groups
def _now() -> int:
return time.clock_gettime_ns(time.CLOCK_BOOTTIME)
def _boot() -> str:
return Path("/proc/sys/kernel/random/boot_id").read_text().strip()
def _signature(value: dict) -> str:
return hashlib.sha256(canonical(value)).hexdigest()
def _manager(release: str, configuration: str) -> dict:
from . import service_install
expected = {
"LoadState": "loaded",
"ActiveState": "active",
"SubState": "running",
"Transient": "yes",
"ControlGroup": "/kfonlinejudge.slice/" + PARENT,
"Description": "KF fixed node-maintenance " + release + "." + configuration,
"User": "root",
"Group": "root",
"MemoryMax": "268435456",
"MemorySwapMax": "0",
"TasksMax": "32",
"CPUQuotaPerSecUSec": "500ms",
"RuntimeMaxUSec": "15min",
"NoNewPrivileges": "yes",
"CapabilityBoundingSet": "",
"AmbientCapabilities": "",
"Restart": "no",
"KillMode": "control-group",
"RuntimeDirectory": DIRECTORY.name,
"RuntimeDirectoryMode": "0700",
}
names = [*expected, "MainPID", "InvocationID"]
observed = dict(
line.split("=", 1)
for line in service_install.systemctl(
"show", PARENT, "--property=" + ",".join(names)
).splitlines()
if "=" in line
)
if (
set(observed) != set(names)
or any(observed[key] != value for key, value in expected.items())
or not re.fullmatch(r"[1-9][0-9]{0,9}", observed.get("MainPID", ""))
or not re.fullmatch(r"[0-9a-f]{32}", observed.get("InvocationID", ""))
or observed["InvocationID"] == "0" * 32
):
raise DeliveryError("bundle proof maintenance invocation unavailable")
return {"pid": int(observed["MainPID"]), "invocation": observed["InvocationID"]}
def _read() -> tuple[dict, tuple[int, ...]]:
preflight.protected(DIRECTORY, directory=True)
directory = DIRECTORY.lstat()
if directory.st_gid != 0 or stat.S_IMODE(directory.st_mode) != 0o700:
raise DeliveryError("private maintenance proof directory required")
preflight.protected(RECORD)
before = RECORD.lstat()
if before.st_gid != 0 or stat.S_IMODE(before.st_mode) != 0o400:
raise DeliveryError("private maintenance proof file required")
value = read_json(RECORD, MAX_RECORD)
after = RECORD.lstat()
if (before.st_dev, before.st_ino, before.st_ctime_ns) != (
after.st_dev,
after.st_ino,
after.st_ctime_ns,
):
raise DeliveryError("maintenance proof changed while reading")
validate(value)
return value, (
before.st_dev,
before.st_ino,
before.st_mode,
before.st_nlink,
before.st_uid,
before.st_gid,
before.st_size,
before.st_mtime_ns,
before.st_ctime_ns,
)
def validate(value: dict) -> dict:
fields(
value,
{
"schema_version",
"bundle",
"manifest_sha256",
"release_id",
"configuration_id",
"metadata_sha256",
"boot_id",
"issued_boottime_ns",
"pid",
"invocation",
},
"maintenance bundle proof",
)
integer(value["schema_version"], 1, 1, "maintenance proof schema")
integer(value["pid"], 1, 2**31 - 1, "maintenance proof PID")
integer(value["issued_boottime_ns"], 0, 2**63 - 1, "maintenance proof time")
for key, pattern in (
("release_id", r"sd-[0-9a-f]{12}"),
("configuration_id", r"cfg-[0-9a-f]{24}"),
("invocation", r"[0-9a-f]{32}"),
("boot_id", r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}"),
):
if not isinstance(value[key], str) or not re.fullmatch(pattern, value[key]):
raise DeliveryError("maintenance proof identity differs")
sha256(value["manifest_sha256"])
sha256(value["metadata_sha256"])
return value
def _live(value: dict) -> None:
if (
value["boot_id"] != _boot()
or not 0 <= _now() - value["issued_boottime_ns"] <= MAX_AGE_NS
or _manager(value["release_id"], value["configuration_id"])
!= {key: value[key] for key in ("pid", "invocation")}
):
raise DeliveryError("maintenance byte proof expired or owner changed")
def _observed(value: dict) -> None:
"""One fixed journal event per process/invocation, never proof authority."""
global _REPORTED
owner = (os.getpid(), value["invocation"])
if _REPORTED == owner:
return
message = (
"KF complete byte proof reused manifest="
+ value["manifest_sha256"]
+ " invocation="
+ value["invocation"]
+ "\n"
).encode()
if os.write(2, message) != len(message):
raise DeliveryError("maintenance byte proof observation unavailable")
_REPORTED = owner
def reuse(bundle: Path, expected: str) -> dict | None:
"""Missing/ineligible proofs use full bytes; eligible corrupt proofs refuse."""
allowed_groups = {GROUP + unit: frozenset({0, gid}) for unit, gid in CONSUMERS.items()}.get(
str(preflight.current_cgroup())
)
if allowed_groups is None or not _root(allowed_groups):
return None
try:
RECORD.lstat()
except FileNotFoundError:
return None
value, identity = _read()
if value["bundle"] != str(bundle) or value["manifest_sha256"] != expected:
raise DeliveryError("maintenance byte proof bundle differs")
_live(value)
preflight.protected(bundle, directory=True)
from . import manifest
before = manifest._bundle_signature(bundle)
if _signature(before) != value["metadata_sha256"]:
raise DeliveryError("maintenance byte proof metadata changed")
metadata = bundle / "manifest.json"
if digest(metadata) != expected:
raise DeliveryError("bundle manifest fingerprint mismatch")
data = manifest.validate(read_json(metadata))
if data["release_id"] != value["release_id"]:
raise DeliveryError("maintenance byte proof release differs")
manifest._unchanged_bundle(bundle, before)
reread, current = _read()
if current != identity or reread != value:
raise DeliveryError("maintenance byte proof replaced during use")
_live(value)
_observed(value)
return data
@contextmanager
def publish(selected: dict, bundle: Path):
"""Read all bytes once; systemd removes the private directory at unit exit."""
from . import install, manifest, operator_units
operator_units.guarded("node-maintenance")
if not _root() or str(preflight.current_cgroup()) != GROUP + PARENT:
raise DeliveryError("maintenance byte proof requires its fixed owner")
manager = _manager(selected["release_id"], selected["configuration_id"])
if manager["pid"] != os.getpid():
raise DeliveryError("only maintenance MainPID may publish byte proof")
if install.selection(check_configuration=False) != selected:
raise DeliveryError("maintenance proof selection changed")
preflight.protected(DIRECTORY, directory=True)
if RECORD.exists() or RECORD.is_symlink():
raise DeliveryError("existing maintenance byte proof refused")
created = None
try:
with manifest.verification_scope():
preflight.protected(bundle, directory=True)
before = manifest._bundle_signature(bundle)
if any(stamp[4:6] != (0, 0) or stamp[2] & 0o022 for stamp in before.values()):
raise DeliveryError("maintenance byte proof requires a protected Root tree")
data = manifest.verify(bundle, selected["manifest_sha256"])
manifest._unchanged_bundle(bundle, before)
if data["release_id"] != selected["release_id"]:
raise DeliveryError("maintenance byte proof release differs")
value = {
"schema_version": 1,
"bundle": str(bundle),
"manifest_sha256": selected["manifest_sha256"],
"release_id": selected["release_id"],
"configuration_id": selected["configuration_id"],
"metadata_sha256": _signature(before),
"boot_id": _boot(),
"issued_boottime_ns": _now(),
**manager,
}
contents = canonical(value)
if len(contents) > MAX_RECORD:
raise DeliveryError("maintenance byte proof exceeds bound")
install.exclusive_file(RECORD, contents, 0o400)
actual, created = _read()
if actual != value:
raise DeliveryError("maintenance byte proof publication differs")
_live(value)
yield
if install.selection(check_configuration=False) != selected:
raise DeliveryError("maintenance proof selection changed")
_live(value)
manifest._unchanged_bundle(bundle, before)
finally:
if created is not None:
actual, identity = _read()
if identity != created or actual != value:
raise DeliveryError("foreign maintenance proof; refused cleanup")
RECORD.unlink()
install.sync_directory(DIRECTORY)