324 lines
15 KiB
Python
324 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the fail-closed Linux RustFS/rc candidate runtime probe and sign its receipt."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any, Callable, Mapping, Sequence
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
SCHEMA = "yuqei.object-storage-runtime-probe.v1"
|
|
EXECUTOR = "yuqei-controlled-object-storage-runtime-probe-v1"
|
|
DOMAIN = b"yuqei-object-storage-runtime-probe-v1\x00"
|
|
DOCKER = "/usr/bin/docker"
|
|
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
SAFE_ENTRYPOINT_RE = re.compile(r"^/[A-Za-z0-9._/-]{1,255}$")
|
|
|
|
|
|
class ProbeError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def canonical(value: object) -> bytes:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
|
|
|
|
|
def _read_json(path: Path) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(path.read_bytes())
|
|
except (OSError, ValueError) as exc:
|
|
raise ProbeError(f"invalid JSON input: {path}") from exc
|
|
if not isinstance(value, dict):
|
|
raise ProbeError(f"JSON input must be an object: {path}")
|
|
return value
|
|
|
|
|
|
def _under(root: Path, relative: object) -> Path:
|
|
if not isinstance(relative, str):
|
|
raise ProbeError("artifact path is invalid")
|
|
pure = PurePosixPath(relative)
|
|
if pure.is_absolute() or ".." in pure.parts:
|
|
raise ProbeError("artifact path escapes artifact root")
|
|
path = root.joinpath(*pure.parts).resolve(strict=True)
|
|
try:
|
|
path.relative_to(root)
|
|
except ValueError as exc:
|
|
raise ProbeError("artifact path escapes artifact root") from exc
|
|
return path
|
|
|
|
|
|
def _run_default(argv: Sequence[str], *, input_bytes: bytes | None = None) -> bytes:
|
|
completed = subprocess.run(
|
|
list(argv), input=input_bytes, capture_output=True, check=False, timeout=120
|
|
)
|
|
if completed.returncode != 0:
|
|
raise ProbeError(f"controlled Docker command failed: {argv[1]}")
|
|
return completed.stdout
|
|
|
|
|
|
def _inspect(run: Callable[..., bytes], reference: str) -> dict[str, Any]:
|
|
try:
|
|
values = json.loads(run((DOCKER, "image", "inspect", reference)))
|
|
except (ValueError, TypeError) as exc:
|
|
raise ProbeError("Docker image inspect returned invalid JSON") from exc
|
|
if not isinstance(values, list) or len(values) != 1 or not isinstance(values[0], dict):
|
|
raise ProbeError("Docker image inspect must return exactly one image")
|
|
return values[0]
|
|
|
|
|
|
def _image(source: Mapping[str, Any], role: str) -> Mapping[str, Any]:
|
|
images = source.get("images")
|
|
if not isinstance(images, list):
|
|
raise ProbeError("source set image inventory is missing")
|
|
matches = [item for item in images if isinstance(item, Mapping) and item.get("role") == role]
|
|
if len(matches) != 1:
|
|
raise ProbeError(f"source set must contain exactly one {role} image")
|
|
return matches[0]
|
|
|
|
|
|
def _load_key(path: Path) -> Ed25519PrivateKey:
|
|
if path.is_symlink():
|
|
raise ProbeError("builder signing key must not be a symbolic link")
|
|
metadata = path.stat()
|
|
if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
|
|
raise ProbeError("builder signing key must be a single-link regular file")
|
|
if os.name == "posix" and metadata.st_mode & 0o077:
|
|
raise ProbeError("builder signing key permissions are not private")
|
|
raw = path.read_bytes()
|
|
try:
|
|
if raw.startswith(b"-----BEGIN"):
|
|
key = serialization.load_pem_private_key(raw, password=None)
|
|
if not isinstance(key, Ed25519PrivateKey):
|
|
raise TypeError
|
|
return key
|
|
return Ed25519PrivateKey.from_private_bytes(raw)
|
|
except (TypeError, ValueError) as exc:
|
|
raise ProbeError("builder signing key is not an Ed25519 private key") from exc
|
|
|
|
|
|
def _cleanup_created(
|
|
created: list[tuple[str, str]], run: Callable[..., bytes]
|
|
) -> None:
|
|
failures: list[str] = []
|
|
for kind, name in reversed(created):
|
|
command = (
|
|
(DOCKER, "rm", "--force", name)
|
|
if kind == "container"
|
|
else (DOCKER, kind, "rm", name)
|
|
)
|
|
try:
|
|
run(command)
|
|
except Exception:
|
|
failures.append(f"{kind}:{name}")
|
|
if failures:
|
|
raise ProbeError(
|
|
"object-storage probe cleanup failed: " + ", ".join(failures)
|
|
)
|
|
|
|
|
|
def execute(
|
|
*,
|
|
artifact_root: Path,
|
|
source_set_path: Path,
|
|
output_path: Path,
|
|
private_key_path: Path,
|
|
run: Callable[..., bytes] = _run_default,
|
|
require_host: bool = True,
|
|
) -> dict[str, Any]:
|
|
if require_host and (
|
|
os.name != "posix"
|
|
or platform.system() != "Linux"
|
|
or platform.machine().lower() not in {"x86_64", "amd64"}
|
|
or not Path(DOCKER).is_file()
|
|
):
|
|
raise ProbeError("probe requires controlled linux/amd64 with /usr/bin/docker")
|
|
root = artifact_root.resolve(strict=True)
|
|
source = _read_json(source_set_path.resolve(strict=True))
|
|
supplied = source.get("source_set_payload_sha256")
|
|
unsigned = dict(source)
|
|
unsigned.pop("source_set_payload_sha256", None)
|
|
if supplied != hashlib.sha256(canonical(unsigned)).hexdigest():
|
|
raise ProbeError("source set payload hash drifted")
|
|
candidate = source.get("candidate")
|
|
if not isinstance(candidate, dict) or set(candidate) != {"commit", "tag"}:
|
|
raise ProbeError("source set candidate binding is invalid")
|
|
records = [_image(source, "minio"), _image(source, "minio-client")]
|
|
inspected: dict[str, dict[str, Any]] = {}
|
|
for record in records:
|
|
archive_record = record.get("docker_archive")
|
|
if not isinstance(archive_record, Mapping):
|
|
raise ProbeError("object-storage archive record is missing")
|
|
archive = _under(root, archive_record.get("path"))
|
|
raw = archive.read_bytes()
|
|
if (
|
|
archive_record.get("sha256") != hashlib.sha256(raw).hexdigest()
|
|
or archive_record.get("size_bytes") != len(raw)
|
|
):
|
|
raise ProbeError("object-storage archive bytes drifted")
|
|
run((DOCKER, "image", "load", "--input", str(archive)))
|
|
reference = record.get("expected_repo_tag")
|
|
if not isinstance(reference, str):
|
|
raise ProbeError("loaded image reference is invalid")
|
|
details = _inspect(run, reference)
|
|
expected_config = record.get("config_digest")
|
|
expected_manifest = record.get("manifest_digest")
|
|
repo_digests = details.get("RepoDigests")
|
|
if (
|
|
not isinstance(expected_config, str)
|
|
or not DIGEST_RE.fullmatch(expected_config)
|
|
or details.get("Id") != expected_config
|
|
or not isinstance(expected_manifest, str)
|
|
or not DIGEST_RE.fullmatch(expected_manifest)
|
|
or not isinstance(repo_digests, list)
|
|
or not any(str(item).endswith("@" + expected_manifest) for item in repo_digests)
|
|
):
|
|
raise ProbeError(f"loaded image identity drifted: {record['role']}")
|
|
inspected[str(record["role"])] = details
|
|
|
|
suffix = str(candidate["commit"])[:12]
|
|
network = f"yuqei-os-probe-{suffix}"
|
|
server = network + "-server"
|
|
client = network + "-client"
|
|
volume = network + "-data"
|
|
created: list[tuple[str, str]] = []
|
|
cleanup_attempted = False
|
|
server_record, client_record = records
|
|
try:
|
|
run((DOCKER, "network", "create", "--internal", network)); created.append(("network", network))
|
|
run((DOCKER, "volume", "create", volume)); created.append(("volume", volume))
|
|
run((
|
|
DOCKER, "create", "--pull", "never", "--name", server, "--network", network, "--read-only",
|
|
"--tmpfs", "/tmp:rw,nosuid,nodev", "--mount", f"type=volume,source={volume},target=/data",
|
|
"--security-opt", "no-new-privileges:true", "-e", "MINIO_ROOT_USER=yuqei_probe",
|
|
"-e", "MINIO_ROOT_PASSWORD=yuqei_probe_password_123",
|
|
"--health-cmd", "test -f /data/.minio.sys/format.json",
|
|
"--health-interval", "2s", "--health-timeout", "2s",
|
|
"--health-retries", "30", "--health-start-period", "0s",
|
|
str(server_record["expected_repo_tag"]),
|
|
"server", "/data", "--console-address", ":9001",
|
|
)); created.append(("container", server))
|
|
run((DOCKER, "start", server))
|
|
server_state = None
|
|
for _attempt in range(30):
|
|
candidate_state = json.loads(
|
|
run((DOCKER, "container", "inspect", server))
|
|
)[0]
|
|
health = candidate_state.get("State", {}).get("Health", {}).get("Status")
|
|
if health == "healthy":
|
|
server_state = candidate_state
|
|
break
|
|
if health == "unhealthy" or candidate_state.get("State", {}).get("Running") is not True:
|
|
raise ProbeError("object-storage server failed its Docker healthcheck")
|
|
time.sleep(2)
|
|
if server_state is None:
|
|
raise ProbeError("object-storage server Docker healthcheck timed out")
|
|
server_ids = run((DOCKER, "exec", server, "/bin/sh", "-ceu", "id -u; stat -c %u /data; test -w /data; touch /data/.yuqei-probe; test -f /data/.minio.sys/format.json")).decode().splitlines()
|
|
uid, owner = (int(server_ids[0]), int(server_ids[1]))
|
|
client_config = (
|
|
f"http://{server}:9000\nyuqei_probe\nyuqei_probe_password_123\n"
|
|
).encode("utf-8")
|
|
entrypoint = ((inspected["minio-client"].get("Config") or {}).get("Entrypoint") or [None])[0]
|
|
if not isinstance(entrypoint, str) or not SAFE_ENTRYPOINT_RE.fullmatch(entrypoint):
|
|
raise ProbeError("rc image entrypoint is not a safe absolute executable")
|
|
client_script = (
|
|
"set -eu; IFS= read -r probe_endpoint; IFS= read -r probe_access; "
|
|
"IFS= read -r probe_secret; test -n \"$probe_endpoint\"; "
|
|
"test -n \"$probe_access\"; test -n \"$probe_secret\"; "
|
|
f"test -x {entrypoint}; {entrypoint} --version >/tmp/version; "
|
|
f"{entrypoint} alias set probe \"$probe_endpoint\" \"$probe_access\" \"$probe_secret\"; "
|
|
f"mkdir -p /tmp/src; printf candidate-bound >/tmp/src/marker; {entrypoint} mb --ignore-existing probe/probe; "
|
|
f"{entrypoint} mirror /tmp/src probe/probe; {entrypoint} cp probe/probe/marker /tmp/roundtrip; "
|
|
"test \"$(cat /tmp/roundtrip)\" = candidate-bound; test -r /probe-data/.yuqei-probe; "
|
|
"test ! -w /probe-data; id -u"
|
|
)
|
|
created.append(("container", client))
|
|
client_out = run((
|
|
DOCKER, "run", "--pull", "never", "--name", client, "--network", network, "--read-only",
|
|
"--tmpfs", "/tmp:rw,nosuid,nodev", "--security-opt", "no-new-privileges:true",
|
|
"--mount", f"type=volume,source={volume},target=/probe-data,readonly",
|
|
"-e", "MC_CONFIG_DIR=/tmp/.mc", "--entrypoint", "/bin/sh", "-i",
|
|
str(client_record["expected_repo_tag"]), "-ceu", client_script,
|
|
), input_bytes=client_config)
|
|
client_uid = int(client_out.decode().splitlines()[-1])
|
|
if uid <= 0 or owner != uid or client_uid <= 0 or server_state.get("State", {}).get("Running") is not True:
|
|
raise ProbeError("object-storage runtime identity or health checks failed")
|
|
payload = {
|
|
"candidate": dict(candidate),
|
|
"controls": {"network_mode": "isolated", "real_container_runtime": True, "synthetic": False},
|
|
"executor": EXECUTOR,
|
|
"images": [
|
|
{"role": "minio", "manifest_digest": server_record["manifest_digest"], "config_digest": server_record["config_digest"], "archive_sha256": server_record["docker_archive"]["sha256"], "checks": {"bin_sh_executable": True, "container_started": True, "data_volume_owner_uid": owner, "data_volume_writable": True, "effective_uid": uid, "entrypoint_invoked": True, "healthcheck_passed": True, "read_only_rootfs": True, "tmpfs_paths": ["/tmp"]}},
|
|
{"role": "minio-client", "manifest_digest": client_record["manifest_digest"], "config_digest": client_record["config_digest"], "archive_sha256": client_record["docker_archive"]["sha256"], "checks": {"bin_sh_executable": True, "config_from_stdin": True, "container_started": True, "effective_uid": client_uid, "entrypoint_invoked": True, "mirror_roundtrip_verified": True, "rc_config_uses_tmpfs": True, "rc_executable": True, "read_only_rootfs": True, "tmpfs_paths": ["/tmp"], "volume_permissions_verified": True}},
|
|
],
|
|
"platform": "linux/amd64",
|
|
}
|
|
cleanup_attempted = True
|
|
_cleanup_created(created, run)
|
|
created.clear()
|
|
signature = _load_key(private_key_path.absolute()).sign(DOMAIN + canonical(payload))
|
|
envelope = {"schema_version": SCHEMA, "payload": payload, "signature_algorithm": "Ed25519", "signature_base64": base64.b64encode(signature).decode("ascii")}
|
|
parent = output_path.parent.resolve(strict=True)
|
|
target = parent / output_path.name
|
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
if hasattr(os, "O_NOFOLLOW"):
|
|
flags |= os.O_NOFOLLOW
|
|
try:
|
|
descriptor = os.open(target, flags, 0o600)
|
|
with os.fdopen(descriptor, "wb") as stream:
|
|
metadata = os.fstat(stream.fileno())
|
|
if (
|
|
not stat.S_ISREG(metadata.st_mode)
|
|
or metadata.st_nlink != 1
|
|
or (os.name == "posix" and metadata.st_mode & 0o077)
|
|
):
|
|
raise ProbeError(
|
|
"probe receipt output is not a private single-link regular file"
|
|
)
|
|
stream.write(canonical(envelope) + b"\n")
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
if os.name == "posix":
|
|
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
directory_descriptor = os.open(parent, directory_flags)
|
|
try:
|
|
os.fsync(directory_descriptor)
|
|
finally:
|
|
os.close(directory_descriptor)
|
|
except OSError as exc:
|
|
raise ProbeError("probe receipt output must be a new private regular file") from exc
|
|
return envelope
|
|
finally:
|
|
if not cleanup_attempted and created:
|
|
_cleanup_created(created, run)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--artifact-root", type=Path, required=True)
|
|
parser.add_argument("--source-set", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--builder-private-key", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
execute(artifact_root=args.artifact_root, source_set_path=args.source_set, output_path=args.output, private_key_path=args.builder_private_key)
|
|
except ProbeError as exc:
|
|
print(f"FAILED_CLOSED: {exc}", file=os.sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|