cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

1-n-sigls
goq / scripts / verify-sigil-bootstrap.py
verify-sigil-bootstrap.py 5.9K executable
1#!/usr/bin/env python32"""Verify the public Sigil bootstrap's pinned release trust contract."""34from __future__ import annotations56import argparse7import base648import binascii9import pathlib10import re11import sys121314REPOSITORY = "FelineStateMachine/goq"15UNCONFIGURED = "unconfigured"16ASSET_TARGET_CONTRACT = "linux-glibc2.17-x86_64"17MINISIGN_VERSION = "0.12"18MINISIGN_URL = (19 "https://github.com/jedisct1/minisign/releases/download/0.12/"20 "minisign-0.12-linux.tar.gz"21)22MINISIGN_SHA256 = (23 "9a599b48ba6eb7b1e80f12f36b94ceca7c00b7a5173c95c3efc88d9822957e73"24)25TAG_PATTERN = re.compile(26 r"^v[0-9]+\.[0-9]+\.[0-9]+(?:[.-][0-9A-Za-z][0-9A-Za-z.-]*)?$"27)28READONLY_ASSIGNMENT_PATTERN = re.compile(29 r'^readonly (?P<name>[a-z_][a-z0-9_]*)="(?P<value>[^"\n]*)"$', re.MULTILINE30)313233class VerificationError(RuntimeError):34 """The bootstrap and repository trust pins are inconsistent."""353637def require(condition: bool, message: str) -> None:38 if not condition:39 raise VerificationError(message)404142def read_regular_file(path: pathlib.Path, description: str) -> str:43 require(path.is_file() and not path.is_symlink(), f"{description} is missing or unsafe")44 try:45 return path.read_text(encoding="utf-8")46 except (OSError, UnicodeError) as error:47 raise VerificationError(f"{description} is unreadable: {error}") from error484950def parse_bootstrap_pins(source: str) -> dict[str, str]:51 assignments: dict[str, list[str]] = {}52 for match in READONLY_ASSIGNMENT_PATTERN.finditer(source):53 assignments.setdefault(match.group("name"), []).append(match.group("value"))5455 pins: dict[str, str] = {}56 for name in (57 "repository",58 "publisher_key",59 "release_tag",60 "asset_target_contract",61 "minisign_version",62 "minisign_url",63 "minisign_sha256",64 ):65 values = assignments.get(name, [])66 require(len(values) == 1, f"bootstrap must declare readonly {name} exactly once")67 pins[name] = values[0]68 require(69 pins["repository"] == REPOSITORY,70 "bootstrap repository pin is not the Goq release repository",71 )72 require(73 pins["asset_target_contract"] == ASSET_TARGET_CONTRACT,74 "bootstrap asset target contract is not the reviewed build ABI",75 )76 require(77 pins["minisign_version"] == MINISIGN_VERSION,78 "bootstrap Minisign version is not the reviewed verifier version",79 )80 require(81 pins["minisign_url"] == MINISIGN_URL,82 "bootstrap Minisign URL is not the reviewed verifier asset",83 )84 require(85 pins["minisign_sha256"] == MINISIGN_SHA256,86 "bootstrap Minisign checksum is not the reviewed verifier digest",87 )88 return pins899091def parse_repository_public_key(source: str) -> str:92 stripped = source.strip()93 if stripped == UNCONFIGURED:94 return UNCONFIGURED95 lines = [line.strip() for line in source.splitlines() if line.strip()]96 require(bool(lines), "repository Sigil public key is unconfigured or malformed")97 key = lines[-1]98 require(99 all(line.startswith("untrusted comment:") for line in lines[:-1]),100 "repository Sigil public key contains unexpected content",101 )102 validate_public_key(key, "repository Sigil public key")103 return key104105106def validate_public_key(value: str, description: str) -> None:107 try:108 decoded = base64.b64decode(value, validate=True)109 except (binascii.Error, ValueError) as error:110 raise VerificationError(f"{description} is malformed") from error111 require(112 len(decoded) == 42,113 f"{description} is malformed: decoded Minisign key must be exactly 42 bytes",114 )115 require(116 decoded[:2] == b"Ed",117 f"{description} is malformed: unsupported Minisign signature algorithm",118 )119 require(120 base64.b64encode(decoded).decode("ascii") == value,121 f"{description} is malformed: public key encoding is not canonical",122 )123124125def verify_contract(bootstrap_source: str, public_key_source: str) -> str:126 pins = parse_bootstrap_pins(bootstrap_source)127 repository_key = parse_repository_public_key(public_key_source)128 embedded_key = pins["publisher_key"]129 release_tag = pins["release_tag"]130131 bootstrap_closed = embedded_key == UNCONFIGURED and release_tag == UNCONFIGURED132 repository_closed = repository_key == UNCONFIGURED133 if bootstrap_closed and repository_closed:134 return "closed"135136 require(137 embedded_key != UNCONFIGURED138 and release_tag != UNCONFIGURED139 and repository_key != UNCONFIGURED,140 "Sigil bootstrap trust pins are only partially configured",141 )142 validate_public_key(embedded_key, "bootstrap publisher key")143 require(144 embedded_key == repository_key,145 "bootstrap publisher key does not match release/sigil-minisign.pub",146 )147 require(148 TAG_PATTERN.fullmatch(release_tag) is not None,149 "bootstrap release tag is malformed",150 )151 return "open"152153154def parse_args(argv: list[str]) -> argparse.Namespace:155 parser = argparse.ArgumentParser(156 description="Verify that install-sigil and the repository Minisign key agree."157 )158 parser.add_argument("--bootstrap", required=True, type=pathlib.Path)159 parser.add_argument("--public-key-file", required=True, type=pathlib.Path)160 return parser.parse_args(argv)161162163def main(argv: list[str] | None = None) -> int:164 args = parse_args(sys.argv[1:] if argv is None else argv)165 try:166 state = verify_contract(167 read_regular_file(args.bootstrap, "Sigil bootstrap"),168 read_regular_file(args.public_key_file, "repository Sigil public key"),169 )170 except VerificationError as error:171 print(f"Sigil bootstrap verification failed: {error}", file=sys.stderr)172 return 1173 print("sigil_bootstrap_verification=ok")174 print(f"sigil_bootstrap_channel={state}")175 return 0176177178if __name__ == "__main__":179 raise SystemExit(main())