52 lines
2.2 KiB
Python
52 lines
2.2 KiB
Python
"""Digest-pinned, owner-configured Python runtimes for bwrap."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
RUNTIME_MOUNT = "/opt/sandboxer/runtime"
|
|
|
|
|
|
def runtime_digest(root: Path) -> str:
|
|
"""Hash the complete artifact, including modes and internal symlink targets."""
|
|
if not root.is_absolute() or root.resolve(strict=True) != root or not root.is_dir():
|
|
raise ValueError("runtime path must be a canonical absolute directory")
|
|
records = []
|
|
for path in sorted(root.rglob("*")):
|
|
mode = path.lstat().st_mode
|
|
if stat.S_ISLNK(mode):
|
|
target = os.readlink(path)
|
|
if Path(target).is_absolute() or not path.resolve(strict=True).is_relative_to(root):
|
|
raise ValueError("runtime symlink escapes the artifact")
|
|
value = ["symlink", target]
|
|
elif stat.S_ISREG(mode):
|
|
with path.open("rb") as stream:
|
|
value = ["file", hashlib.file_digest(stream, "sha256").hexdigest()]
|
|
elif stat.S_ISDIR(mode):
|
|
value = ["directory"]
|
|
else:
|
|
raise ValueError("runtime contains an unsupported special file")
|
|
records.append([path.relative_to(root).as_posix(), stat.S_IMODE(mode), *value])
|
|
return hashlib.sha256(json.dumps(records, separators=(",", ":")).encode()).hexdigest()
|
|
|
|
|
|
def verified_runtime(config: dict) -> Path | None:
|
|
"""Only owner extension configuration can select a runtime artifact."""
|
|
runtime = config.get("runtime")
|
|
if runtime is None:
|
|
return None
|
|
if not isinstance(runtime, dict) or set(runtime) != {"path", "sha256"}:
|
|
raise ValueError("runtime requires exactly path and sha256")
|
|
if not isinstance(runtime["path"], str) or not isinstance(runtime["sha256"], str):
|
|
raise ValueError("runtime path and sha256 must be strings")
|
|
root = Path(runtime["path"])
|
|
if (not (root / "pyvenv.cfg").is_file() or not (root / "bin/python3").is_file()
|
|
or (root / ".git").exists()):
|
|
raise ValueError("runtime must be a standalone Python environment, not a checkout")
|
|
if runtime_digest(root) != runtime["sha256"]:
|
|
raise ValueError("runtime artifact digest does not match owner configuration")
|
|
return root
|