134 lines
6.1 KiB
Python
134 lines
6.1 KiB
Python
#!/usr/bin/python3
|
|
"""Opt-in root install acceptance in an owned image, without host services."""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from verify_prepared_release import WORKSPACE, execute
|
|
|
|
|
|
def verify(args):
|
|
result = {
|
|
"status": "running", "checks": [],
|
|
"transport": "local signed assets; actual root Bash entry and Rust bootstrap",
|
|
"online_install_and_update": "not_run",
|
|
"entry_sha256": hashlib.sha256(args.entry.read_bytes()).hexdigest(),
|
|
}
|
|
|
|
def passed(name):
|
|
result["checks"].append(name)
|
|
args.report.write_text(json.dumps(result, indent=2) + "\n")
|
|
print(f"PASS {name}", flush=True)
|
|
|
|
try:
|
|
with tempfile.TemporaryDirectory(prefix="dever-install-entry-", dir=args.scratch_parent) as directory:
|
|
scratch = Path(directory)
|
|
release = scratch / "release"
|
|
release.mkdir()
|
|
for name in ("manifest.json", "manifest.sig"):
|
|
shutil.copyfile(args.release / name, release / name)
|
|
manifest = json.loads((release / "manifest.json").read_bytes())
|
|
if manifest["platform"] != "linux-x86_64":
|
|
raise ValueError("this acceptance fixture requires a Linux x86_64 release")
|
|
result["payload_version"] = manifest["version"]
|
|
result["manifest_sha256"] = hashlib.sha256((release / "manifest.json").read_bytes()).hexdigest()
|
|
key = scratch / "release.pub"
|
|
shutil.copyfile(WORKSPACE / "skills/assets/release.pub", key)
|
|
extraction = scratch / "extract.json"
|
|
extraction.write_text(json.dumps({
|
|
"release": str(release),
|
|
"archive": str(args.assets / "dever-linux-x86_64.tar.zst"),
|
|
"trusted_key": str(key),
|
|
}))
|
|
execute([args.extractor, "--dever-extract", extraction])
|
|
passed("prepared_release_authenticated_by_existing_rust_extractor")
|
|
|
|
# A copied standalone entry has no adjacent skill or source checkout.
|
|
entry = scratch / "install"
|
|
shutil.copyfile(args.entry, entry)
|
|
image = scratch / "image"
|
|
image.mkdir()
|
|
arguments = ["--release", str(release), "--system-root", str(image), "--no-start"]
|
|
execute(["/bin/bash", entry, *arguments])
|
|
machine = image / "opt/dever"
|
|
launcher = machine / "bin/dever"
|
|
installed = machine / "versions" / manifest["version"]
|
|
if execute([launcher, "version"]) != manifest["version"]:
|
|
raise RuntimeError("installed launcher selected the wrong version")
|
|
skill = installed / "skills/dever-language"
|
|
if execute([launcher, "skill", "path"]) != str(skill):
|
|
raise RuntimeError("installed skill does not match the selected core")
|
|
if (image / "usr/local/bin/dever").readlink() != Path("/opt/dever/bin/dever"):
|
|
raise RuntimeError("public launcher symlink was not installed")
|
|
passed("standalone_bash_first_install_and_matching_skill")
|
|
|
|
tracked = [launcher, machine / "bin/deverd",
|
|
installed / "manifest.json", installed / "dever-core"]
|
|
baseline = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in tracked}
|
|
|
|
def unchanged():
|
|
current = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in tracked}
|
|
if current != baseline:
|
|
raise RuntimeError("existing installed state changed")
|
|
if list(image.glob(".dever-install-*")):
|
|
raise RuntimeError("installer left private staging behind")
|
|
|
|
repeated = subprocess.run(
|
|
["/bin/bash", "-s", "--", *arguments], input=entry.read_bytes(),
|
|
cwd=scratch, env={}, capture_output=True, timeout=180,
|
|
)
|
|
if repeated.returncode:
|
|
raise RuntimeError(repeated.stderr.decode(errors="replace"))
|
|
unchanged()
|
|
# Bootstrap records another activation even for the same version.
|
|
# Compare the selected version here, then freeze the journal for failure checks.
|
|
if execute([launcher, "version"]) != manifest["version"]:
|
|
raise RuntimeError("reinstall changed the selected version")
|
|
active = machine / "state/active-version"
|
|
tracked.append(active)
|
|
baseline[str(active)] = hashlib.sha256(active.read_bytes()).hexdigest()
|
|
passed("pipe_reinstall_preserves_installed_state_and_cleans_staging")
|
|
|
|
(release / "manifest.sig").write_text("00" * 64 + "\n")
|
|
rejected = subprocess.run(
|
|
["/bin/bash", entry, *arguments], cwd=scratch, env={},
|
|
capture_output=True, timeout=30,
|
|
)
|
|
if rejected.returncode == 0 or "签名校验失败" not in rejected.stderr.decode(errors="replace"):
|
|
raise RuntimeError("corrupt signature did not fail at authentication")
|
|
unchanged()
|
|
if execute([launcher, "version"]) != manifest["version"]:
|
|
raise RuntimeError("failed install changed the selected version")
|
|
passed("corrupt_signature_rejected_existing_install_preserved")
|
|
result["status"] = "passed"
|
|
except BaseException as error:
|
|
result["status"] = "failed"
|
|
result["error"] = str(error)
|
|
raise
|
|
finally:
|
|
args.report.write_text(json.dumps(result, indent=2) + "\n")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
for name in ("release", "assets", "extractor", "scratch-parent", "report"):
|
|
parser.add_argument(f"--{name}", required=True, type=Path)
|
|
parser.add_argument("--entry", type=Path, default=WORKSPACE / "install")
|
|
args = parser.parse_args()
|
|
if os.geteuid() != 0:
|
|
parser.error("requires root for its own image; never installs host services")
|
|
for name, value in vars(args).items():
|
|
setattr(args, name, value.absolute())
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
verify(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|