From 35889a9f63785db1c4e210f8e11a344bb9d363aa Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 1 Aug 2024 12:37:50 +0200 Subject: [PATCH] Introduce RepositoryKeyFetch= This setting controls whether we'll fetch GPG keys remotely or not. We disable it by default so that we only rely on locally available GPG keys for checking package and repository metadata signatures. This new setting only affects dnf/zypper based distributions as apt and pacman do not support retrieving GPG keys remotely in the first place. zypper does not trust GPG keys listed in gpgkey= by default so we import local GPG keys manually with rpm to work around that. Fixes #757 --- .github/workflows/ci.yml | 8 ++++++ mkosi/__init__.py | 2 ++ mkosi/config.py | 12 +++++++++ mkosi/distributions/alma.py | 3 ++- mkosi/distributions/centos.py | 7 +++--- mkosi/distributions/fedora.py | 10 ++++++-- mkosi/distributions/mageia.py | 3 ++- mkosi/distributions/openmandriva.py | 3 ++- mkosi/distributions/opensuse.py | 23 ++++++++++++++--- mkosi/distributions/rhel.py | 3 ++- mkosi/distributions/rhel_ubi.py | 3 ++- mkosi/distributions/rocky.py | 3 ++- mkosi/installer/rpm.py | 38 +++++++++++++++++++++++++++-- mkosi/installer/zypper.py | 3 ++- mkosi/resources/mkosi.md | 13 ++++++++++ tests/test_json.py | 2 ++ tests/test_sysext.py | 1 + 17 files changed, 120 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f71fe4f6..9ee7a9827 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,6 +155,14 @@ jobs: QemuKvm=yes EOF + # TODO: Drop once distribution-gpg-keys is in noble-backports. + if [[ "${{ matrix.tools }}" =~ opensuse|fedora|ubuntu ]]; then + tee --append mkosi.local.conf < Co "--repositories", ",".join(config.tools_tree_repositories), "--package-manager-tree", ",".join(str(t) for t in config.tools_tree_package_manager_trees), "--repository-key-check", str(config.repository_key_check), + "--repository-key-fetch", str(config.repository_key_fetch), "--cache-only", str(config.cacheonly), *(["--output-dir", str(config.output_dir)] if config.output_dir else []), *(["--workspace-dir", str(config.workspace_dir)] if config.workspace_dir else []), diff --git a/mkosi/config.py b/mkosi/config.py index 2e17be0b4..96285ee73 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -1402,6 +1402,7 @@ class Config: mirror: Optional[str] local_mirror: Optional[str] repository_key_check: bool + repository_key_fetch: bool repositories: list[str] cacheonly: Cacheonly package_manager_trees: list[ConfigTree] @@ -1961,6 +1962,16 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple help="Controls signature and key checks on repositories", universal=True, ), + ConfigSetting( + dest="repository_key_fetch", + metavar="BOOL", + nargs="?", + section="Distribution", + default=False, + parse=config_parse_boolean, + help="Controls whether distribution GPG keys can be fetched remotely", + universal=True, + ), ConfigSetting( dest="repositories", metavar="REPOS", @@ -4066,6 +4077,7 @@ def bold(s: Any) -> str: Mirror: {none_to_default(config.mirror)} Local Mirror (build): {none_to_none(config.local_mirror)} Repo Signature/Key check: {yes_no(config.repository_key_check)} + Fetch Repository Keys: {yes_no(config.repository_key_fetch)} Repositories: {line_join_list(config.repositories)} Use Only Package Cache: {config.cacheonly} Package Manager Trees: {line_join_list(config.package_manager_trees)} diff --git a/mkosi/distributions/alma.py b/mkosi/distributions/alma.py index ac6285cac..16345ebb7 100644 --- a/mkosi/distributions/alma.py +++ b/mkosi/distributions/alma.py @@ -16,7 +16,8 @@ def gpgurls(context: Context) -> tuple[str, ...]: find_rpm_gpgkey( context, f"RPM-GPG-KEY-AlmaLinux-{context.config.release}", - ) or f"https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-{context.config.release}", + f"https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-{context.config.release}", + ), ) @classmethod diff --git a/mkosi/distributions/centos.py b/mkosi/distributions/centos.py index e6cd985de..e324620bc 100644 --- a/mkosi/distributions/centos.py +++ b/mkosi/distributions/centos.py @@ -100,7 +100,7 @@ def architecture(cls, arch: Architecture) -> str: def gpgurls(context: Context) -> tuple[str, ...]: rel = "RPM-GPG-KEY-CentOS-Official" if context.config.release == "9" else "RPM-GPG-KEY-CentOS-Official-SHA256" return tuple( - find_rpm_gpgkey(context, key) or f"https://www.centos.org/keys/{key}" + find_rpm_gpgkey(context, key, f"https://www.centos.org/keys/{key}") for key in (rel, "RPM-GPG-KEY-CentOS-SIG-Extras") ) @@ -196,7 +196,8 @@ def epel_repositories(cls, context: Context) -> Iterable[RpmRepository]: find_rpm_gpgkey( context, f"RPM-GPG-KEY-EPEL-{context.config.release}", - ) or f"https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-{context.config.release}", + f"https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-{context.config.release}", + ), ) if context.config.local_mirror: @@ -299,7 +300,7 @@ def sig_repositories(cls, context: Context) -> Iterable[RpmRepository]: ) for sig, components, keys in sigs: - gpgurls = tuple(find_rpm_gpgkey(context, key) or f"https://www.centos.org/keys/{key}" for key in keys) + gpgurls = tuple(find_rpm_gpgkey(context, key, f"https://www.centos.org/keys/{key}") for key in keys) for c in components: if mirror := context.config.mirror: diff --git a/mkosi/distributions/fedora.py b/mkosi/distributions/fedora.py index 551d31905..97008311a 100644 --- a/mkosi/distributions/fedora.py +++ b/mkosi/distributions/fedora.py @@ -20,8 +20,8 @@ @tuplify def find_fedora_rpm_gpgkeys(context: Context) -> Iterable[str]: - key1 = find_rpm_gpgkey(context, key=f"RPM-GPG-KEY-fedora-{context.config.release}-primary") - key2 = find_rpm_gpgkey(context, key=f"RPM-GPG-KEY-fedora-{context.config.release}-secondary") + key1 = find_rpm_gpgkey(context, key=f"RPM-GPG-KEY-fedora-{context.config.release}-primary", required=False) + key2 = find_rpm_gpgkey(context, key=f"RPM-GPG-KEY-fedora-{context.config.release}-secondary", required=False) if key1: # During branching, there is always a kerfuffle with the key transition. @@ -38,9 +38,15 @@ def find_fedora_rpm_gpgkeys(context: Context) -> Iterable[str]: yield key3 yield key1 + if key2: yield key2 + if not key1 and not key2: + if not context.config.repository_key_fetch: + die("Fedora GPG keys not found in /usr/share/distribution-gpg-keys", + hint="Make sure the distribution-gpg-keys package is installed") + yield "https://fedoraproject.org/fedora.gpg" diff --git a/mkosi/distributions/mageia.py b/mkosi/distributions/mageia.py index c0e3eb2b7..4b817fb51 100644 --- a/mkosi/distributions/mageia.py +++ b/mkosi/distributions/mageia.py @@ -34,7 +34,8 @@ def repositories(cls, context: Context) -> Iterable[RpmRepository]: find_rpm_gpgkey( context, "RPM-GPG-KEY-Mageia", - ) or "https://mirrors.kernel.org/mageia/distrib/$releasever/$basearch/media/core/release/media_info/pubkey", + "https://mirrors.kernel.org/mageia/distrib/$releasever/$basearch/media/core/release/media_info/pubkey", + ), ) if context.config.local_mirror: diff --git a/mkosi/distributions/openmandriva.py b/mkosi/distributions/openmandriva.py index 15a76c8c4..5238864a3 100644 --- a/mkosi/distributions/openmandriva.py +++ b/mkosi/distributions/openmandriva.py @@ -36,7 +36,8 @@ def repositories(cls, context: Context) -> Iterable[RpmRepository]: find_rpm_gpgkey( context, "RPM-GPG-KEY-OpenMandriva", - ) or "https://raw.githubusercontent.com/OpenMandrivaAssociation/openmandriva-repos/master/RPM-GPG-KEY-OpenMandriva", + "https://raw.githubusercontent.com/OpenMandrivaAssociation/openmandriva-repos/master/RPM-GPG-KEY-OpenMandriva", + ), ) if context.config.local_mirror: diff --git a/mkosi/distributions/opensuse.py b/mkosi/distributions/opensuse.py index 382f54eb3..f089a97eb 100644 --- a/mkosi/distributions/opensuse.py +++ b/mkosi/distributions/opensuse.py @@ -94,11 +94,28 @@ def repositories(cls, context: Context) -> Iterable[RpmRepository]: mirror = context.config.mirror or "https://download.opensuse.org" if context.config.release == "tumbleweed" or context.config.release.isdigit(): - gpgkeys = ( - *([p] if (p := find_rpm_gpgkey(context, key="RPM-GPG-KEY-openSUSE-Tumbleweed")) else []), - *([p] if (p := find_rpm_gpgkey(context, key="RPM-GPG-KEY-openSUSE")) else []), + gpgkeys = tuple( + p + for key in ("RPM-GPG-KEY-openSUSE-Tumbleweed", "RPM-GPG-KEY-openSUSE") + if (p := find_rpm_gpgkey(context, key, required=False)) ) + if not gpgkeys and not context.config.repository_key_fetch: + die("OpenSUSE GPG keys not found in /usr/share/distribution-gpg-keys", + hint="Make sure the distribution-gpg-keys package is installed") + + if zypper and gpgkeys: + run( + ["rpm", "--root=/buildroot", "--import", *(key.removeprefix("file://") for key in gpgkeys)], + sandbox=context.sandbox( + binary="rpm", + mounts=[ + Mount(context.root, "/buildroot"), + *finalize_crypto_mounts(context.config) + ], + ) + ) + if context.config.release == "tumbleweed": if context.config.architecture == Architecture.x86_64: subdir = "" diff --git a/mkosi/distributions/rhel.py b/mkosi/distributions/rhel.py index a70875199..ccc1e4a27 100644 --- a/mkosi/distributions/rhel.py +++ b/mkosi/distributions/rhel.py @@ -24,7 +24,8 @@ def gpgurls(context: Context) -> tuple[str, ...]: find_rpm_gpgkey( context, f"RPM-GPG-KEY-redhat{major}-release", - ) or "https://access.redhat.com/security/data/fd431d51.txt", + "https://access.redhat.com/security/data/fd431d51.txt", + ), ) @staticmethod diff --git a/mkosi/distributions/rhel_ubi.py b/mkosi/distributions/rhel_ubi.py index 78bfc233b..c921ecd1f 100644 --- a/mkosi/distributions/rhel_ubi.py +++ b/mkosi/distributions/rhel_ubi.py @@ -21,7 +21,8 @@ def gpgurls(context: Context) -> tuple[str, ...]: find_rpm_gpgkey( context, f"RPM-GPG-KEY-redhat{major}-release", - ) or "https://access.redhat.com/security/data/fd431d51.txt", + "https://access.redhat.com/security/data/fd431d51.txt", + ), ) @classmethod diff --git a/mkosi/distributions/rocky.py b/mkosi/distributions/rocky.py index af2c4d186..0a2cdfd08 100644 --- a/mkosi/distributions/rocky.py +++ b/mkosi/distributions/rocky.py @@ -16,7 +16,8 @@ def gpgurls(context: Context) -> tuple[str, ...]: find_rpm_gpgkey( context, f"RPM-GPG-KEY-Rocky-{context.config.release}", - ) or f"https://download.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-{context.config.release}", + f"https://download.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-{context.config.release}", + ), ) @classmethod diff --git a/mkosi/installer/rpm.py b/mkosi/installer/rpm.py index 94d07697a..185db91c6 100644 --- a/mkosi/installer/rpm.py +++ b/mkosi/installer/rpm.py @@ -4,9 +4,10 @@ import subprocess import textwrap from pathlib import Path -from typing import Optional +from typing import Literal, Optional, overload from mkosi.context import Context +from mkosi.log import die from mkosi.run import run from mkosi.types import PathString @@ -23,7 +24,33 @@ class RpmRepository: priority: Optional[int] = None -def find_rpm_gpgkey(context: Context, key: str) -> Optional[str]: +@overload +def find_rpm_gpgkey( + context: Context, + key: str, + fallback: Optional[str] = None, + *, + required: Literal[True] = True, +) -> str: ... + + +@overload +def find_rpm_gpgkey( + context: Context, + key: str, + fallback: Optional[str] = None, + *, + required: Literal[False] +) -> Optional[str]: ... + + +def find_rpm_gpgkey( + context: Context, + key: str, + fallback: Optional[str] = None, + *, + required: bool = True +) -> Optional[str]: root = context.config.tools() if context.config.tools_tree_certificates else Path("/") if gpgpath := next((root / "usr/share/distribution-gpg-keys").rglob(key), None): @@ -32,6 +59,13 @@ def find_rpm_gpgkey(context: Context, key: str) -> Optional[str]: if gpgpath := next(Path(context.pkgmngr / "etc/pki/rpm-gpg").rglob(key), None): return (Path("/") / gpgpath.relative_to(context.pkgmngr)).as_uri() + if context.config.repository_key_fetch: + return fallback + + if required: + die(f"{key} GPG key not found in /usr/share/distribution-gpg-keys", + hint="Make sure the distribution-gpg-keys package is installed") + return None diff --git a/mkosi/installer/zypper.py b/mkosi/installer/zypper.py index d58c6a6b8..90ef6ad34 100644 --- a/mkosi/installer/zypper.py +++ b/mkosi/installer/zypper.py @@ -109,9 +109,10 @@ def cmd(cls, context: Context) -> list[PathString]: "zypper", "--installroot=/buildroot", "--cache-dir=/var/cache/zypp", - "--gpg-auto-import-keys" if context.config.repository_key_check else "--no-gpg-checks", "--non-interactive", "--no-refresh", + *(["--gpg-auto-import-keys"] if context.config.repository_key_fetch else []), + *(["--no-gpg-checks"] if not context.config.repository_key_check else []), *([f"--plus-content={repo}" for repo in context.config.repositories]), ] diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 41ee2b61d..02fdd1fa6 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -452,6 +452,19 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, Useful to disable checks when combined with `--local-mirror=` and using only a repository from a local filesystem. +`RepositoryKeyFetch=`, `--repository-key-fetch=` +: Controls whether mkosi will fetch distribution GPG keys remotely. Disabled + by default. When disabled, the distribution GPG keys for the target distribution + have to be installed locally on the host system alongside the package manager for + that distribution. + + This setting is only implemented for distributions using dnf or zypper as their + package manager. For other distributions the distribution GPG keys are always looked + up locally regardless of the value of this setting. To make the distribution GPG keys + for distributions available without enabling this setting, the corresponding package + has to be installed on the host. This is usually one of `archlinux-keyring`, + `debian-keyring`, `ubuntu-keyring` or `distribution-gpg-keys` (for rpm-based distributions). + `Repositories=`, `--repositories=` : Enable package repositories that are disabled by default. This can be used to enable the EPEL repos for CentOS or different components of the Debian/Ubuntu repositories. diff --git a/tests/test_json.py b/tests/test_json.py index 43a51f1ab..783790412 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -264,6 +264,7 @@ def test_config() -> None: "RepartOffline": true, "Repositories": [], "RepositoryKeyCheck": false, + "RepositoryKeyFetch": true, "RootPassword": [ "test1234", false @@ -465,6 +466,7 @@ def test_config() -> None: repart_offline=True, repositories=[], repository_key_check=False, + repository_key_fetch=True, root_password=("test1234", False), root_shell="/bin/tcsh", runtime_build_sources=True, diff --git a/tests/test_sysext.py b/tests/test_sysext.py index 6b5cf244e..3d07c8ced 100644 --- a/tests/test_sysext.py +++ b/tests/test_sysext.py @@ -24,6 +24,7 @@ def test_sysext(config: ImageConfig) -> None: options=[ "--directory", "", "--incremental=no", + "--repository-key-fetch=yes", "--base-tree", Path(image.output_dir) / "image", "--overlay", "--package=dnsmasq",