about summary refs log tree commit diff
path: root/pkgs/os-specific/linux/kernel/update-hardened.py
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/os-specific/linux/kernel/update-hardened.py')
-rwxr-xr-xpkgs/os-specific/linux/kernel/update-hardened.py200
1 files changed, 200 insertions, 0 deletions
diff --git a/pkgs/os-specific/linux/kernel/update-hardened.py b/pkgs/os-specific/linux/kernel/update-hardened.py
new file mode 100755
index 0000000000000..089e991d06bcb
--- /dev/null
+++ b/pkgs/os-specific/linux/kernel/update-hardened.py
@@ -0,0 +1,200 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i python -p "python3.withPackages (ps: [ps.PyGithub])" git gnupg
+
+# This is automatically called by ./update.sh.
+
+import re
+import json
+import sys
+import os.path
+from glob import glob
+import subprocess
+from tempfile import TemporaryDirectory
+
+from github import Github
+
+HERE = os.path.dirname(os.path.realpath(__file__))
+HARDENED_GITHUB_REPO = 'anthraxx/linux-hardened'
+HARDENED_TRUSTED_KEY = os.path.join(HERE, 'anthraxx.asc')
+HARDENED_PATCHES_PATH = os.path.join(HERE, 'hardened-patches.json')
+MIN_KERNEL = (4, 14)
+
+HARDENED_VERSION_RE = re.compile(r'''
+    (?P<kernel_version> [\d.]+) \.
+    (?P<version_suffix> [a-z]+)
+''', re.VERBOSE)
+
+def parse_version(version):
+    match = HARDENED_VERSION_RE.fullmatch(version)
+    if match:
+        return match.groups()
+
+def run(*args, **kwargs):
+    try:
+        return subprocess.run(
+            args, **kwargs,
+            check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+        )
+    except subprocess.CalledProcessError as err:
+        print(
+            f'error: `{err.cmd}` failed unexpectedly\n'
+            f'status code: {err.returncode}\n'
+            f'stdout:\n{err.stdout.decode("utf-8").strip()}\n'
+            f'stderr:\n{err.stderr.decode("utf-8").strip()}',
+            file=sys.stderr,
+        )
+        sys.exit(1)
+
+def nix_prefetch_url(url):
+    output = run('nix-prefetch-url', '--print-path', url).stdout
+    return output.decode('utf-8').strip().split('\n')
+
+def verify_openpgp_signature(*, name, trusted_key, sig_path, data_path):
+    with TemporaryDirectory(suffix='.nixpkgs-gnupg-home') as gnupg_home:
+        run('gpg', '--homedir', gnupg_home, '--import', trusted_key)
+        keyring = os.path.join(gnupg_home, 'pubring.kbx')
+        try:
+            subprocess.run(
+                ('gpgv', '--keyring', keyring, sig_path, data_path),
+                check=True, stderr=subprocess.PIPE,
+            )
+            return True
+        except subprocess.CalledProcessError as err:
+            print(
+                f'error: signature for {name} failed to verify!',
+                file=sys.stderr,
+            )
+            print(err.stderr.decode('utf-8'), file=sys.stderr, end='')
+            return False
+
+def fetch_patch(*, name, release):
+    def find_asset(filename):
+        try:
+            return next(
+                asset.browser_download_url
+                for asset in release.get_assets()
+                if asset.name == filename
+            )
+        except StopIteration:
+            raise KeyError(filename)
+
+    try:
+        patch_url = find_asset(f'{name}.patch')
+        sig_url = find_asset(f'{name}.patch.sig')
+    except KeyError:
+        print(f'error: {name}.patch{{,sig}} not present', file=sys.stderr)
+        return None
+
+    sha256, patch_path = nix_prefetch_url(patch_url)
+    _, sig_path = nix_prefetch_url(sig_url)
+    sig_ok = verify_openpgp_signature(
+        name=name,
+        trusted_key=HARDENED_TRUSTED_KEY,
+        sig_path=sig_path,
+        data_path=patch_path,
+    )
+    if not sig_ok:
+        return None
+
+    return {
+        'url': patch_url,
+        'sha256': sha256,
+    }
+
+def commit_patches(*, kernel_version, message):
+    with open(HARDENED_PATCHES_PATH + '.new', 'w') as new_patches_file:
+        json.dump(patches, new_patches_file, indent=4, sort_keys=True)
+        new_patches_file.write('\n')
+    os.rename(HARDENED_PATCHES_PATH + '.new', HARDENED_PATCHES_PATH)
+    message = f'linux/hardened-patches/{kernel_version}: {message}'
+    print(message)
+    if os.environ.get('COMMIT'):
+        run(
+            'git', '-C', HERE, 'commit', f'--message={message}',
+            'hardened-patches.json',
+        )
+
+# Load the existing patches.
+with open(HARDENED_PATCHES_PATH) as patches_file:
+    patches = json.load(patches_file)
+
+NIX_VERSION_RE = re.compile(r'''
+    \s* version \s* =
+    \s* " (?P<version> [^"]*) "
+    \s* ; \s* \n
+''', re.VERBOSE)
+
+# Get the set of currently packaged kernel versions.
+kernel_versions = set()
+for filename in os.listdir(HERE):
+    filename_match = re.fullmatch(r'linux-(\d+)\.(\d+)\.nix', filename)
+    if filename_match:
+        if tuple(int(v) for v in filename_match.groups()) < MIN_KERNEL:
+            continue
+        with open(os.path.join(HERE, filename)) as nix_file:
+            for nix_line in nix_file:
+                match = NIX_VERSION_RE.fullmatch(nix_line)
+                if match:
+                    kernel_versions.add(match.group('version'))
+
+# Remove patches for old kernel versions.
+for kernel_version in patches.keys() - kernel_versions:
+    del patches[kernel_version]
+    commit_patches(kernel_version=kernel_version, message='remove')
+
+g = Github(os.environ.get('GITHUB_TOKEN'))
+repo = g.get_repo(HARDENED_GITHUB_REPO)
+releases = repo.get_releases()
+
+found_kernel_versions = set()
+failures = False
+
+for release in releases:
+    remaining_kernel_versions = kernel_versions - found_kernel_versions
+
+    if not remaining_kernel_versions:
+        break
+
+    version = release.tag_name
+    name = f'linux-hardened-{version}'
+    version_info = parse_version(version)
+    if not version_info:
+        continue
+    kernel_version, version_suffix = version_info
+
+    if kernel_version in remaining_kernel_versions:
+        found_kernel_versions.add(kernel_version)
+        try:
+            old_version_suffix = patches[kernel_version]['version_suffix']
+            old_version = f'{kernel_version}.{old_version_suffix}'
+            update = old_version_suffix < version_suffix
+        except KeyError:
+            update = True
+            old_version = None
+
+        if update:
+            patch = fetch_patch(name=name, release=release)
+            if patch is None:
+                failures = True
+            else:
+                patch['version_suffix'] = version_suffix
+                patches[kernel_version] = patch
+                if old_version:
+                    message = f'{old_version} -> {version}'
+                else:
+                    message = f'init at {version}'
+                commit_patches(kernel_version=kernel_version, message=message)
+
+missing_kernel_versions = kernel_versions - patches.keys()
+
+if missing_kernel_versions:
+    print(
+        f'warning: no patches for kernel versions ' +
+        ', '.join(missing_kernel_versions) +
+        '\nwarning: consider manually backporting older patches (bump '
+        'JSON key, set version_suffix to "NixOS-a")',
+        file=sys.stderr,
+    )
+
+if failures:
+    sys.exit(1)