AkurAI Build
Menu

BifrOSt

public

Latest change 36972df1d1a942fadabbe03341629b5238357c8a - Reject unprepared installer bootstrap packages by Ólafur Búi Ólafsson

#!/usr/bin/env python3
"""Verify the signed bifrost-system installer bootstrap."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys

FINGERPRINT = re.compile(r"^[0-9A-F]{40}$")
RELEASE_REVISION = re.compile(r"^[0-9a-f]{40}$")
RELEASE_DIGEST = re.compile(r"^[0-9a-f]{64}$")
VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")
PROFILE_DIGEST_EXCLUDES = [
    "airootfs/usr/share/bifrost/installed-root/usr/share/bifrost/release.json",
    "airootfs/usr/share/bifrost/alpm/**",
]


class VerificationError(RuntimeError):
    """The bootstrap does not satisfy the installer trust contract."""


def load_json(path: Path, label: str) -> dict[str, object]:
    try:
        document = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as error:
        raise VerificationError(f"{label} is not valid JSON: {path}") from error
    if not isinstance(document, dict):
        raise VerificationError(f"{label} must be a JSON object: {path}")
    return document


def load_os_release(path: Path) -> dict[str, str]:
    try:
        lines = path.read_text(encoding="utf-8").splitlines()
    except (OSError, UnicodeError) as error:
        raise VerificationError(f"live system identity is unreadable: {path}") from error
    identity: dict[str, str] = {}
    for line in lines:
        if "=" in line:
            key, value = line.split("=", 1)
            identity[key] = value.strip().strip('"')
    return identity


def signing_identity(line: str) -> str | None:
    fields = line.split()
    if not line.startswith("[GNUPG:] VALIDSIG ") or len(fields) <= 2:
        return None
    primary = fields[-1].upper()
    if len(fields) > 11 and FINGERPRINT.fullmatch(primary):
        return primary
    return fields[2].upper()


def valid_signature(path: Path, signature: Path, keyring: Path, fingerprint: str) -> None:
    result = subprocess.run(
        ["gpgv", "--keyring", str(keyring), "--status-fd", "1", str(signature), str(path)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
        text=True,
    )
    valid = {
        identity
        for line in result.stdout.splitlines()
        if (identity := signing_identity(line)) is not None
    }
    if result.returncode or valid != {fingerprint}:
        raise VerificationError(f"signature verification failed: {signature.name}")


def validate_release_document(release: dict[str, object], version: str) -> None:
    revision = release.get("source_revision")
    epoch = release.get("source_date_epoch")
    expected_build_id = (
        f"bifrost-{version}-{revision[:12]}-{epoch}"
        if isinstance(revision, str) and isinstance(epoch, int) and not isinstance(epoch, bool)
        else None
    )
    iso = release.get("iso")
    if (
        release.get("schema_version") != 1
        or release.get("provenance_status") != "build-input"
        or release.get("version") != version
        or not isinstance(revision, str)
        or not RELEASE_REVISION.fullmatch(revision)
        or not isinstance(epoch, int)
        or isinstance(epoch, bool)
        or epoch <= 0
        or release.get("build_id") != expected_build_id
        or not RELEASE_DIGEST.fullmatch(str(release.get("profile_sha256", "")))
        or release.get("profile_digest_excludes") != PROFILE_DIGEST_EXCLUDES
        or not isinstance(iso, dict)
        or iso.get("file") != f"bifrost-{version}-x86_64.iso"
        or iso.get("bytes") is not None
        or iso.get("sha256") is not None
    ):
        raise VerificationError("bifrost-system release provenance is not a prepared build input")


def validate_bootstrap(root: Path, os_release: Path) -> Path:
    manifest_path = root / "manifest.json"
    manifest_signature = root / "manifest.json.sig"
    keyring = root / "alpm-repository-key.gpg"
    public_key = root / "alpm-repository-key.asc"
    fingerprint_path = root / "alpm-repository-key.fingerprint"
    for required in (manifest_path, manifest_signature, keyring, public_key, fingerprint_path):
        if not required.is_file():
            raise VerificationError(f"signed bifrost-system bootstrap input is missing: {required.name}")

    manifest = load_json(manifest_path, "bootstrap manifest")
    try:
        fingerprint = fingerprint_path.read_text(encoding="ascii").strip().upper()
    except (OSError, UnicodeError) as error:
        raise VerificationError("bootstrap signing fingerprint is unreadable") from error
    live_identity = load_os_release(os_release)
    version = str(manifest.get("version", ""))
    if (
        manifest.get("schema_version") != 1
        or manifest.get("repository") != "bifrost-system"
        or manifest.get("architecture") != "any"
        or manifest.get("signing_fingerprint") != fingerprint
        or not FINGERPRINT.fullmatch(fingerprint)
        or not VERSION.fullmatch(version)
        or version != live_identity.get("VERSION_ID")
        or not re.fullmatch(
            rf"{re.escape(version)}-[1-9][0-9]*",
            str(manifest.get("package_version", "")),
        )
    ):
        raise VerificationError("bifrost-system bootstrap manifest identity is invalid")

    valid_signature(manifest_path, manifest_signature, keyring, fingerprint)
    shown = subprocess.run(
        ["gpg", "--batch", "--with-colons", "--show-keys", str(public_key)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
        text=True,
    )
    primary: list[str] = []
    waiting = False
    for line in shown.stdout.splitlines():
        fields = line.split(":")
        kind = fields[0] if fields else ""
        if kind == "pub":
            waiting = True
        elif waiting and kind == "fpr" and len(fields) > 9:
            primary.append(fields[9].upper())
            waiting = False
        elif kind in {"pub", "sub"}:
            waiting = False
    if shown.returncode or primary != [fingerprint]:
        raise VerificationError("armored bootstrap key does not match the pinned signing fingerprint")

    packages = sorted(root.glob("bifrost-system-*.pkg.tar.zst"))
    if len(packages) != 1:
        raise VerificationError("bootstrap must contain exactly one bifrost-system package")
    package = packages[0]
    package_signature = package.with_name(package.name + ".sig")
    files = manifest.get("files")
    if not isinstance(files, dict):
        raise VerificationError("bootstrap manifest file inventory is invalid")
    for path in (package, package_signature):
        record = files.get(path.name)
        if not path.is_file() or not isinstance(record, dict) or set(record) != {"sha256", "size"}:
            raise VerificationError(f"bootstrap package inventory failed: {path.name}")
        size = path.stat().st_size
        with path.open("rb") as source:
            digest = hashlib.file_digest(source, "sha256").hexdigest()
        if record["size"] != size or record["sha256"] != digest:
            raise VerificationError(f"bootstrap package inventory failed: {path.name}")
    valid_signature(package, package_signature, keyring, fingerprint)

    metadata = subprocess.run(
        ["bsdtar", "-xOf", str(package), ".PKGINFO"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
        text=True,
    )
    values: dict[str, str] = {}
    for line in metadata.stdout.splitlines():
        key, separator, value = line.partition(" = ")
        if separator and key in {"pkgname", "pkgver"}:
            if key in values:
                raise VerificationError(f"bootstrap package metadata repeats {key}")
            values[key] = value
    if (
        metadata.returncode
        or values.get("pkgname") != "bifrost-system"
        or values.get("pkgver") != manifest["package_version"]
    ):
        raise VerificationError("bootstrap package identity does not match its signed manifest")

    release_result = subprocess.run(
        ["bsdtar", "-xOf", str(package), "usr/share/bifrost/release.json"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
        text=True,
    )
    try:
        release = json.loads(release_result.stdout)
    except json.JSONDecodeError as error:
        raise VerificationError("bootstrap package has invalid installed release provenance") from error
    if release_result.returncode or not isinstance(release, dict):
        raise VerificationError("bootstrap package has invalid installed release provenance")
    validate_release_document(release, version)
    return package


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", type=Path, required=True, help="directory containing signed bootstrap inputs")
    parser.add_argument("--os-release", type=Path, required=True, help="BifrOSt live-system identity file")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    try:
        print(validate_bootstrap(args.root, args.os_release))
        return 0
    except (OSError, VerificationError) as error:
        print(f"bifrost-bootstrap: {error}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())