Menu
BifrOSt
publicLatest 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())