Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at devShellTools-shell 309 lines 9.7 kB view raw
1#! /usr/bin/env nix-shell 2#! nix-shell -i python -p "python3.withPackages (ps: [ps.pygithub ps.packaging])" git gnupg 3 4# This is automatically called by ../update.sh. 5 6from __future__ import annotations 7 8import json 9import os 10import re 11import subprocess 12import sys 13from dataclasses import dataclass 14from pathlib import Path 15from tempfile import TemporaryDirectory 16from typing import ( 17 Dict, 18 Iterator, 19 List, 20 Optional, 21 Sequence, 22 Tuple, 23 TypedDict, 24 Union, 25) 26 27from github import Github 28from github.GitRelease import GitRelease 29 30from packaging.version import parse as parse_version, Version 31 32VersionComponent = Union[int, str] 33Version = List[VersionComponent] 34 35 36PatchData = TypedDict("PatchData", {"name": str, "url": str, "sha256": str, "extra": str}) 37Patch = TypedDict("Patch", { 38 "patch": PatchData, 39 "version": str, 40 "sha256": str, 41}) 42 43 44def read_min_kernel_branch() -> List[str]: 45 with open(NIXPKGS_KERNEL_PATH / "kernels-org.json") as f: 46 return list(parse_version(sorted(json.load(f).keys())[0]).release) 47 48 49@dataclass 50class ReleaseInfo: 51 version: Version 52 release: GitRelease 53 54 55HERE = Path(__file__).resolve().parent 56NIXPKGS_KERNEL_PATH = HERE.parent 57NIXPKGS_PATH = HERE.parents[4] 58HARDENED_GITHUB_REPO = "anthraxx/linux-hardened" 59HARDENED_TRUSTED_KEY = HERE / "anthraxx.asc" 60HARDENED_PATCHES_PATH = HERE / "patches.json" 61MIN_KERNEL_VERSION: Version = read_min_kernel_branch() 62 63 64def run(*args: Union[str, Path]) -> subprocess.CompletedProcess[bytes]: 65 try: 66 return subprocess.run( 67 args, 68 check=True, 69 stdout=subprocess.PIPE, 70 stderr=subprocess.PIPE, 71 encoding="utf-8", 72 ) 73 except subprocess.CalledProcessError as err: 74 print( 75 f"error: `{err.cmd}` failed unexpectedly\n" 76 f"status code: {err.returncode}\n" 77 f"stdout:\n{err.stdout.strip()}\n" 78 f"stderr:\n{err.stderr.strip()}", 79 file=sys.stderr, 80 ) 81 sys.exit(1) 82 83 84def nix_prefetch_url(url: str) -> Tuple[str, Path]: 85 output = run("nix-prefetch-url", "--print-path", url).stdout 86 sha256, path = output.strip().split("\n") 87 return sha256, Path(path) 88 89 90def verify_openpgp_signature( 91 *, name: str, trusted_key: Path, sig_path: Path, data_path: Path, 92) -> bool: 93 with TemporaryDirectory(suffix=".nixpkgs-gnupg-home") as gnupg_home_str: 94 gnupg_home = Path(gnupg_home_str) 95 run("gpg", "--homedir", gnupg_home, "--import", trusted_key) 96 keyring = gnupg_home / "pubring.kbx" 97 try: 98 subprocess.run( 99 ("gpgv", "--keyring", keyring, sig_path, data_path), 100 check=True, 101 stderr=subprocess.PIPE, 102 encoding="utf-8", 103 ) 104 return True 105 except subprocess.CalledProcessError as err: 106 print( 107 f"error: signature for {name} failed to verify!", 108 file=sys.stderr, 109 ) 110 print(err.stderr, file=sys.stderr, end="") 111 return False 112 113 114def fetch_patch(*, name: str, release_info: ReleaseInfo) -> Optional[Patch]: 115 release = release_info.release 116 extra = f'-{release_info.version[-1]}' 117 118 def find_asset(filename: str) -> str: 119 try: 120 it: Iterator[str] = ( 121 asset.browser_download_url 122 for asset in release.get_assets() 123 if asset.name == filename 124 ) 125 return next(it) 126 except StopIteration: 127 raise KeyError(filename) 128 129 patch_filename = f"{name}.patch" 130 try: 131 patch_url = find_asset(patch_filename) 132 sig_url = find_asset(patch_filename + ".sig") 133 except KeyError: 134 print(f"error: {patch_filename}{{,.sig}} not present", file=sys.stderr) 135 return None 136 137 sha256, patch_path = nix_prefetch_url(patch_url) 138 _, sig_path = nix_prefetch_url(sig_url) 139 sig_ok = verify_openpgp_signature( 140 name=name, 141 trusted_key=HARDENED_TRUSTED_KEY, 142 sig_path=sig_path, 143 data_path=patch_path, 144 ) 145 if not sig_ok: 146 return None 147 148 kernel_ver = re.sub(r"v?(.*)(-hardened[\d]+)$", r'\1', release_info.release.tag_name) 149 major = kernel_ver.split('.')[0] 150 sha256_kernel, _ = nix_prefetch_url(f"mirror://kernel/linux/kernel/v{major}.x/linux-{kernel_ver}.tar.xz") 151 152 return Patch( 153 patch=PatchData(name=patch_filename, url=patch_url, sha256=sha256, extra=extra), 154 version=kernel_ver, 155 sha256=sha256_kernel 156 ) 157 158 159def parse_version(version_str: str) -> Version: 160 # There have been two variants v6.10[..] and 6.10[..], drop the v 161 version_str_without_v = version_str[1:] if not version_str[0].isdigit() else version_str 162 version: Version = [] 163 164 for component in re.split(r'\.|\-', version_str_without_v): 165 try: 166 version.append(int(component)) 167 except ValueError: 168 version.append(component) 169 return version 170 171 172def version_string(version: Version) -> str: 173 return ".".join(str(component) for component in version) 174 175 176def major_kernel_version_key(kernel_version: Version) -> str: 177 return version_string(kernel_version[:-1]) 178 179 180def commit_patches(*, kernel_key: str, message: str) -> None: 181 new_patches_path = HARDENED_PATCHES_PATH.with_suffix(".new") 182 with open(new_patches_path, "w") as new_patches_file: 183 json.dump(patches, new_patches_file, indent=4, sort_keys=True) 184 new_patches_file.write("\n") 185 os.rename(new_patches_path, HARDENED_PATCHES_PATH) 186 message = f"linux/hardened/patches/{kernel_key}: {message}" 187 print(message) 188 if os.environ.get("COMMIT"): 189 run( 190 "git", 191 "-C", 192 NIXPKGS_PATH, 193 "commit", 194 f"--message={message}", 195 HARDENED_PATCHES_PATH, 196 ) 197 198 199# Load the existing patches. 200patches: Dict[str, Patch] 201with open(HARDENED_PATCHES_PATH) as patches_file: 202 patches = json.load(patches_file) 203 204# Get the set of currently packaged kernel versions. 205kernel_versions = {} 206with open(NIXPKGS_KERNEL_PATH / "kernels-org.json") as kernel_versions_json: 207 kernel_versions = json.load(kernel_versions_json) 208 for kernel_branch_str in kernel_versions: 209 if kernel_branch_str == "testing": continue 210 kernel_branch = [int(i) for i in kernel_branch_str.split(".")] 211 if kernel_branch < MIN_KERNEL_VERSION: continue 212 kernel_version = [int(i) for i in kernel_versions[kernel_branch_str]["version"].split(".")] 213 kernel_versions[kernel_branch_str] = kernel_version 214 215# Remove patches for unpackaged kernel versions. 216for kernel_key in sorted(patches.keys() - kernel_versions.keys()): 217 del patches[kernel_key] 218 commit_patches(kernel_key=kernel_key, message="remove") 219 220g = Github(os.environ.get("GITHUB_TOKEN")) 221repo = g.get_repo(HARDENED_GITHUB_REPO) 222failures = False 223 224# Match each kernel version with the best patch version. 225releases = {} 226i = 0 227for release in repo.get_releases(): 228 # Dirty workaround to make sure that we don't run into issues because 229 # GitHub's API only allows fetching the last 1000 releases. 230 # It's not reliable to exit earlier because not every kernel minor may 231 # have hardened patches, hence the naive search below. 232 i += 1 233 if i > 100: 234 break 235 236 version = parse_version(release.tag_name) 237 # needs to look like e.g. 5.6.3-hardened1 238 if len(version) < 4: 239 continue 240 241 if not (isinstance(version[-2], int)): 242 continue 243 244 kernel_version = version[:-1] 245 246 kernel_key = major_kernel_version_key(kernel_version) 247 try: 248 packaged_kernel_version = kernel_versions[kernel_key] 249 except KeyError: 250 continue 251 252 release_info = ReleaseInfo(version=version, release=release) 253 254 if kernel_version == packaged_kernel_version: 255 releases[kernel_key] = release_info 256 else: 257 # Fall back to the latest patch for this major kernel version, 258 # skipping patches for kernels newer than the packaged one. 259 if '.'.join(str(x) for x in kernel_version) > '.'.join(str(x) for x in packaged_kernel_version): 260 continue 261 elif ( 262 kernel_key not in releases or releases[kernel_key].version < version 263 ): 264 releases[kernel_key] = release_info 265 266# Update hardened-patches.json for each release. 267for kernel_key in sorted(releases.keys()): 268 release_info = releases[kernel_key] 269 release = release_info.release 270 version = release_info.version 271 version_str = release.tag_name 272 name = f"linux-hardened-{version_str}" 273 274 old_version: Optional[Version] = None 275 old_version_str: Optional[str] = None 276 update: bool 277 try: 278 old_filename = patches[kernel_key]["patch"]["name"] 279 old_version_str = old_filename.replace("linux-hardened-", "").replace( 280 ".patch", "" 281 ) 282 old_version = parse_version(old_version_str) 283 update = old_version < version 284 except KeyError: 285 update = True 286 287 if update: 288 patch = fetch_patch(name=name, release_info=release_info) 289 if patch is None: 290 failures = True 291 else: 292 patches[kernel_key] = patch 293 if old_version: 294 message = f"{old_version_str} -> {version_str}" 295 else: 296 message = f"init at {version_str}" 297 commit_patches(kernel_key=kernel_key, message=message) 298 299missing_kernel_versions = kernel_versions.keys() - patches.keys() 300 301if missing_kernel_versions: 302 print( 303 f"warning: no patches for kernel versions " 304 + ", ".join(missing_kernel_versions), 305 file=sys.stderr, 306 ) 307 308if failures: 309 sys.exit(1)