diff --git a/id_configurator/config.py b/id_configurator/config.py index 2c2c153..766e575 100644 --- a/id_configurator/config.py +++ b/id_configurator/config.py @@ -27,7 +27,16 @@ def _yes_no(value: bool) -> str: class AptSource(BaseModel): - """A single APT source, in the DEB822 format (see sources.list(5)).""" + """A single APT source, in the DEB822 format (see sources.list(5)). + + Two ways to tell APT which key to trust this source with: + - `signed_by`: the ASCII-armored (.asc) *content* of a GPG public key. + The `apt` module dearmors it into a dedicated keyring and writes that + keyring's path as the rendered `Signed-By` field. + - `signed_by_file`: the *path* to a keyring already expected to exist on + the target system, written to the rendered `Signed-By` field verbatim. + If both are set, `signed_by` takes precedence. + """ model_config = ConfigDict(populate_by_name=True) @@ -42,6 +51,7 @@ class AptSource(BaseModel): enabled: bool = True trusted: bool | None = None signed_by: str | None = Field(default=None, alias="signed-by") + signed_by_file: str | None = Field(default=None, alias="signed-by-file") pdiffs: bool | None = None by_hash: bool | None = Field(default=None, alias="by-hash") allow_insecure: bool | None = Field(default=None, alias="allow-insecure") @@ -56,8 +66,15 @@ class AptSource(BaseModel): date_max_future: str | None = Field(default=None, alias="date-max-future") description: str | None = None - def to_deb822(self) -> str: - """Render this source as a DEB822 stanza (as used in /etc/apt/sources.list.d/*.sources).""" + def to_deb822(self, signed_by_path: str | None = None) -> str: + """Render this source as a DEB822 stanza (as used in + /etc/apt/sources.list.d/*.sources). + + `signed_by_path` is the value actually written as the `Signed-By` + field, if given - resolved by the caller (the `apt` module) from + `self.signed_by`/`self.signed_by_file`, neither of which is ever + rendered here directly. + """ fields = [ ("Types", " ".join(self.types)), ("URIs", " ".join(self.uris)), @@ -71,8 +88,8 @@ class AptSource(BaseModel): fields.append(("Languages", " ".join(self.languages))) if self.targets: fields.append(("Targets", " ".join(self.targets))) - if self.signed_by: - fields.append(("Signed-By", self.signed_by)) + if signed_by_path: + fields.append(("Signed-By", signed_by_path)) if self.trusted is not None: fields.append(("Trusted", _yes_no(self.trusted))) if self.pdiffs is not None: @@ -108,12 +125,12 @@ class AptSource(BaseModel): class AptConfig(BaseModel): sources: list[AptSource] = Field(default_factory=list) - keys: list[NamedContent] = Field(default_factory=list) class ApkRepository(BaseModel): """An Alpine APK repository (see /etc/apk/repositories), e.g. a URL ending in /main or /community.""" + name: str url: str key: str | None = None diff --git a/id_configurator/modules/apk.py b/id_configurator/modules/apk.py index 3510228..2469db9 100644 --- a/id_configurator/modules/apk.py +++ b/id_configurator/modules/apk.py @@ -71,7 +71,7 @@ class ApkModule(Module): lines = [] for repository in context.config.configurations.apk.repositories: lines.append(repository.url) - logger.info("added apk repository %s", repository.url) + logger.info("added apk repository %s", repository.name) if repository.key: filename = _key_filename(repository.key.encode("utf-8")) context.write_file( diff --git a/id_configurator/modules/apt.py b/id_configurator/modules/apt.py index 102af63..40ac11f 100644 --- a/id_configurator/modules/apt.py +++ b/id_configurator/modules/apt.py @@ -5,13 +5,29 @@ one-line `.list` files or DEB822 `.sources` files, whether at the top level or under `sources.list.d/`) is copied into `/etc/apt`, disabled so it no longer takes effect, and the sources declared in the configuration are added as new, enabled `.sources` files. + +Each source can be trusted two ways: `signed_by` holds the ASCII-armored +content of a GPG public key, which is converted with `gpg --dearmor` into a +binary keyring dedicated to this one source, written to +`/etc/apt/keyrings/.gpg` - it is that path, not the key's own content, +that ends up in the rendered `.sources` file's `Signed-By` field. +`signed_by_file` instead names a keyring already expected to exist on the +target system, used verbatim as `Signed-By` (no file is written for it); if +both are given, `signed_by` takes precedence. `gpg` is only used here for +the armored-to-binary format conversion; the actual signature verification +of the repository itself is left to APT when it fetches the repository's +Release/InRelease file. """ from __future__ import annotations import logging +import shutil +import subprocess +import tempfile from pathlib import Path +from id_configurator.config import AptSource from id_configurator.context import Context from id_configurator.modules.base import Module, register @@ -43,6 +59,28 @@ def _disable_deb822_content(content: str) -> str: return "\n\n".join(disabled_stanzas) + "\n" +def _dearmor_gpg_key(name: str, content: bytes) -> bytes: + """Convert an ASCII-armored GPG public key into apt's binary keyring format.""" + with tempfile.TemporaryDirectory() as gnupg_home: + path = shutil.which("gpg") + if path is not None: + result = subprocess.run( + [path, "--homedir", gnupg_home, "--batch", "--dearmor"], + input=content, + capture_output=True, + check=False, + ) + else: + raise RuntimeError( + "gpg is not available; the GNUPG package must be installed in the environment running id-configurator" + ) + if result.returncode != 0: + raise RuntimeError( + f"invalid GPG key for apt source {name}: {result.stderr.decode('utf-8', 'replace')}" + ) + return result.stdout + + @register class AptModule(Module): name = "apt" @@ -59,7 +97,6 @@ class AptModule(Module): def apply(self, context: Context) -> None: self._copy_existing_disabled(context) - self._write_configured_keys(context) self._write_configured_sources(context) @staticmethod @@ -82,19 +119,22 @@ class AptModule(Module): ) logger.info("disabled existing apt source %s", entry.name) - @staticmethod - def _write_configured_keys(context: Context) -> None: - for key in context.config.configurations.apt.keys: - context.write_file( - f"{_KEYRINGS_DIR}/{key.name}.asc", key.content.encode("utf-8") - ) - logger.info("installed apt keyring %s", key.name) + def _resolve_signed_by(self, context: Context, source: AptSource) -> str | None: + if source.signed_by: + keyring = _dearmor_gpg_key(source.name, source.signed_by.encode("utf-8")) + relative = f"{_KEYRINGS_DIR}/{source.name}.gpg" + context.write_file(relative, keyring) + logger.info("installed apt keyring for source %s", source.name) + return f"/{relative}" + if source.signed_by_file: + return source.signed_by_file + return None - @staticmethod - def _write_configured_sources(context: Context) -> None: + def _write_configured_sources(self, context: Context) -> None: for source in context.config.configurations.apt.sources: + signed_by_path = self._resolve_signed_by(context, source) context.write_file( f"{_SOURCES_LIST_D}/{source.name}.sources", - source.to_deb822().encode("utf-8"), + source.to_deb822(signed_by_path).encode("utf-8"), ) logger.info("added apt source %s", source.name) diff --git a/library/id_configurator.py b/library/id_configurator.py index 93dc657..95ad49e 100644 --- a/library/id_configurator.py +++ b/library/id_configurator.py @@ -123,7 +123,18 @@ options: description: DEB822 "Trusted" field. signed_by: type: str - description: DEB822 "Signed-By" field. + description: + - ASCII-armored content of a GPG public key to trust this + source with. The `apt` module dearmors it into a dedicated + keyring and writes that keyring's path as the rendered + Signed-By field. Takes precedence over signed_by_file if + both are set. + signed_by_file: + type: str + description: + - Path to a keyring already expected to exist on the target + system, used verbatim as the rendered Signed-By field. + Ignored if signed_by is also set. pdiffs: type: bool description: DEB822 "PDiffs" field. @@ -157,20 +168,6 @@ options: description: type: str description: DEB822 "Description" field. - keys: - type: list - elements: dict - default: [] - description: GPG keys trusted for the sources above (NamedContent). - suboptions: - name: - type: str - required: true - description: A short, unique name for this entry. - content: - type: str - required: true - description: PEM-encoded/ASCII-armored key content. apk: type: dict default: {} @@ -483,6 +480,7 @@ ARGUMENT_SPEC = { "enabled": {"type": "bool", "default": True}, "trusted": {"type": "bool"}, "signed_by": {"type": "str"}, + "signed_by_file": {"type": "str"}, "pdiffs": {"type": "bool"}, "by_hash": {"type": "bool"}, "allow_insecure": {"type": "bool"}, @@ -496,12 +494,6 @@ ARGUMENT_SPEC = { "description": {"type": "str"}, }, }, - "keys": { - "type": "list", - "elements": "dict", - "default": [], - "options": dict(_NAMED_CONTENT_OPTIONS), - }, }, }, "apk": { diff --git a/tests/configs/Apk.yaml.j2 b/tests/configs/Apk.yaml.j2 index 9cb96ef..9aa3480 100644 --- a/tests/configs/Apk.yaml.j2 +++ b/tests/configs/Apk.yaml.j2 @@ -8,10 +8,10 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: - - url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/ + - name: demo + url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/ key: | {{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }} yum: diff --git a/tests/configs/Apt.yaml.j2 b/tests/configs/Apt.yaml.j2 index 7642da6..0802a3b 100644 --- a/tests/configs/Apt.yaml.j2 +++ b/tests/configs/Apt.yaml.j2 @@ -13,8 +13,8 @@ configurations: uris: - https://host.docker.internal:{{ 'APT_REPO_PORT' | env }}/ suites: ["./"] - trusted: true - keys: [] + signed-by: | +{{ 'dockerfiles/repos/apt-signing-key.pub.asc' | file | indent(8, first=true) }} apk: repositories: [] yum: diff --git a/tests/configs/Certificate.yaml.j2 b/tests/configs/Certificate.yaml.j2 index cac01ea..93ec06d 100644 --- a/tests/configs/Certificate.yaml.j2 +++ b/tests/configs/Certificate.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Go.yaml.j2 b/tests/configs/Go.yaml.j2 index 8fff407..2f7ce8b 100644 --- a/tests/configs/Go.yaml.j2 +++ b/tests/configs/Go.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Java.yaml.j2 b/tests/configs/Java.yaml.j2 index 435fff5..532ffc4 100644 --- a/tests/configs/Java.yaml.j2 +++ b/tests/configs/Java.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Npm.yaml.j2 b/tests/configs/Npm.yaml.j2 index 5c34770..349ec0e 100644 --- a/tests/configs/Npm.yaml.j2 +++ b/tests/configs/Npm.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/NuGet.yaml.j2 b/tests/configs/NuGet.yaml.j2 index 5c6cb17..5fd3287 100644 --- a/tests/configs/NuGet.yaml.j2 +++ b/tests/configs/NuGet.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Php.yaml.j2 b/tests/configs/Php.yaml.j2 index 5439afd..11c1a0c 100644 --- a/tests/configs/Php.yaml.j2 +++ b/tests/configs/Php.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Python.yaml.j2 b/tests/configs/Python.yaml.j2 index 037f742..749151e 100644 --- a/tests/configs/Python.yaml.j2 +++ b/tests/configs/Python.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Ruby.yaml.j2 b/tests/configs/Ruby.yaml.j2 index a9b5a8c..ccb6447 100644 --- a/tests/configs/Ruby.yaml.j2 +++ b/tests/configs/Ruby.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Rust.yaml.j2 b/tests/configs/Rust.yaml.j2 index 4910f88..40ce080 100644 --- a/tests/configs/Rust.yaml.j2 +++ b/tests/configs/Rust.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/configs/Yum.yaml.j2 b/tests/configs/Yum.yaml.j2 index c108b93..a3ffacf 100644 --- a/tests/configs/Yum.yaml.j2 +++ b/tests/configs/Yum.yaml.j2 @@ -8,7 +8,6 @@ configurations: {{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }} apt: sources: [] - keys: [] apk: repositories: [] yum: diff --git a/tests/dockerfiles/Apt.Dockerfile b/tests/dockerfiles/Apt.Dockerfile index 06bb4ac..860d578 100644 --- a/tests/dockerfiles/Apt.Dockerfile +++ b/tests/dockerfiles/Apt.Dockerfile @@ -1,5 +1,7 @@ FROM debian:trixie-slim AS target FROM interdiode/id-configurator:latest AS configuration +RUN apt-get update && apt-get install -y --no-install-recommends gnupg \ + && rm -rf /var/lib/apt/lists/* COPY --from=target / /src/ COPY --from=generated-config Apt.yaml /config.yaml RUN id-configurator apply --config /config.yaml --src /src --dst /dst diff --git a/tests/dockerfiles/repos/apt-repo.Dockerfile b/tests/dockerfiles/repos/apt-repo.Dockerfile index 7737354..77db80a 100644 --- a/tests/dockerfiles/repos/apt-repo.Dockerfile +++ b/tests/dockerfiles/repos/apt-repo.Dockerfile @@ -1,12 +1,18 @@ # Minimal flat-format APT repository serving one hand-built .deb package over # TLS, used by tox-docker to exercise the `apt` module integration test -# (tests/configs/Apt.yaml.tmpl, tests/dockerfiles/Apt.Dockerfile). +# (tests/configs/Apt.yaml.j2, tests/dockerfiles/Apt.Dockerfile). Its +# InRelease file is genuinely signed with the dedicated key pair +# apt-signing-key.key.asc / apt-signing-key.pub.asc, whose public half is +# what tests/configs/Apt.yaml.j2 embeds as the configured source's +# `signed-by`: the `apt` module dearmors it into a keyring, and APT itself +# verifies the InRelease signature against that keyring on `apt-get update` +# - no `Trusted: yes` bypass involved. # # Built by tox-docker, whose build context is this directory (not the repo # root) - every COPY source below must live under tests/dockerfiles/repos/. FROM debian:trixie-slim AS packagebuild RUN apt-get update \ - && apt-get install -y --no-install-recommends dpkg-dev \ + && apt-get install -y --no-install-recommends dpkg-dev apt-utils gnupg \ && rm -rf /var/lib/apt/lists/* RUN set -eux; \ pkg=idconfiguratortestpkg; \ @@ -18,6 +24,13 @@ RUN set -eux; \ WORKDIR /srv RUN dpkg-scanpackages --multiversion . > Packages \ && gzip -9c Packages > Packages.gz +COPY apt-signing-key.key.asc /tmp/apt-signing-key.key.asc +RUN gnupg_home="$(mktemp -d)" \ + && gpg --homedir "$gnupg_home" --batch --import /tmp/apt-signing-key.key.asc \ + && key_id="$(gpg --homedir "$gnupg_home" --list-secret-keys --with-colons | awk -F: '/^sec/{print $5; exit}')" \ + && apt-ftparchive release . > Release \ + && gpg --homedir "$gnupg_home" --batch --yes --pinentry-mode loopback --default-key "$key_id" --clearsign -o InRelease Release \ + && rm -rf "$gnupg_home" /tmp/apt-signing-key.key.asc FROM nginx:alpine COPY --from=packagebuild /srv /srv diff --git a/tests/test_apk.py b/tests/test_apk.py index e23ef52..2a191cf 100644 --- a/tests/test_apk.py +++ b/tests/test_apk.py @@ -14,10 +14,11 @@ CONFIG_DATA = { "apk": { "repositories": [ { + "name": "main", "url": "https://example.invalid/v3.19/main", "key": KEY_BYTES, }, - {"url": "https://example.invalid/v3.19/community"}, + {"name": "community", "url": "https://example.invalid/v3.19/community"}, ] } }, diff --git a/tests/test_apt.py b/tests/test_apt.py index 92f5093..e51f8be 100644 --- a/tests/test_apt.py +++ b/tests/test_apt.py @@ -1,7 +1,15 @@ +import subprocess +from unittest.mock import MagicMock + +import pytest + from id_configurator.config import Configuration from id_configurator.context import Context from id_configurator.modules.apt import AptModule +KEY_CONTENT = "fake-armored-gpg-key" +KEYRING_BYTES = b"fake-dearmored-keyring-bytes" + SOURCE_CONTENT = { "uuid": "12345678-1234-5678-1234-567812345678", "organisation": "InterDiode", @@ -12,13 +20,7 @@ SOURCE_CONTENT = { "name": "interdiode", "uris": ["http://example.invalid/debian"], "suites": ["stable"], - "signed-by": "/etc/apt/keyrings/interdiode.asc", - } - ], - "keys": [ - { - "name": "interdiode", - "content": "fake-key-bytes", + "signed-by": KEY_CONTENT, } ], } @@ -30,6 +32,36 @@ def make_config() -> Configuration: return Configuration.model_validate(SOURCE_CONTENT) +def make_config_with(source_extra: dict) -> Configuration: + data = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + "configurations": { + "apt": { + "sources": [ + { + "name": "interdiode", + "uris": ["http://example.invalid/debian"], + "suites": ["stable"], + **source_extra, + } + ], + } + }, + } + return Configuration.model_validate(data) + + +def _mock_gpg_dearmor_success(monkeypatch, output: bytes = KEYRING_BYTES) -> MagicMock: + mock_run = MagicMock( + return_value=subprocess.CompletedProcess( + args=[], returncode=0, stdout=output, stderr=b"" + ) + ) + monkeypatch.setattr("id_configurator.modules.apt.subprocess.run", mock_run) + return mock_run + + def test_is_relevant_false_without_src(tmp_path) -> None: context = Context(config=make_config(), dst=tmp_path / "dst", src=None) assert AptModule().is_relevant(context) is False @@ -49,7 +81,10 @@ def test_is_relevant_true_with_apt_dir(tmp_path) -> None: assert AptModule().is_relevant(context) is True -def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> None: +def test_apply_disables_existing_sources_and_adds_configured_ones( + tmp_path, monkeypatch +) -> None: + mock_run = _mock_gpg_dearmor_success(monkeypatch) src = tmp_path / "src" apt_dir = src / "etc/apt" sources_list_d = apt_dir / "sources.list.d" @@ -98,19 +133,22 @@ def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> N assert not (dst / "etc/apt/sources.list.d/ignored.txt").exists() - keyring = (dst / "etc/apt/keyrings/interdiode.asc").read_bytes() - assert keyring == b"fake-key-bytes" + keyring = (dst / "etc/apt/keyrings/interdiode.gpg").read_bytes() + assert keyring == KEYRING_BYTES + mock_run.assert_called_once() + assert mock_run.call_args.kwargs["input"] == KEY_CONTENT.encode("utf-8") configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text( encoding="utf-8" ) assert configured_source == ( "Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n" - "Signed-By: /etc/apt/keyrings/interdiode.asc\nEnabled: yes\n" + "Signed-By: /etc/apt/keyrings/interdiode.gpg\nEnabled: yes\n" ) -def test_apply_without_existing_top_level_sources_list(tmp_path) -> None: +def test_apply_without_existing_top_level_sources_list(tmp_path, monkeypatch) -> None: + _mock_gpg_dearmor_success(monkeypatch) src = tmp_path / "src" (src / "etc/apt").mkdir(parents=True) dst = tmp_path / "dst" @@ -120,3 +158,93 @@ def test_apply_without_existing_top_level_sources_list(tmp_path) -> None: assert not (dst / "etc/apt/sources.list").exists() assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists() + + +def test_apply_uses_signed_by_file_when_no_key_content(tmp_path, monkeypatch) -> None: + mock_run = _mock_gpg_dearmor_success(monkeypatch) + dst = tmp_path / "dst" + context = Context( + config=make_config_with({"signed-by-file": "/etc/apt/keyrings/existing.gpg"}), + dst=dst, + src=None, + ) + + AptModule().apply(context) + + mock_run.assert_not_called() + assert not (dst / "etc/apt/keyrings").exists() + configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text( + encoding="utf-8" + ) + assert configured_source == ( + "Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n" + "Signed-By: /etc/apt/keyrings/existing.gpg\nEnabled: yes\n" + ) + + +def test_apply_signed_by_takes_precedence_over_signed_by_file( + tmp_path, monkeypatch +) -> None: + mock_run = _mock_gpg_dearmor_success(monkeypatch) + dst = tmp_path / "dst" + context = Context( + config=make_config_with( + { + "signed-by": KEY_CONTENT, + "signed-by-file": "/etc/apt/keyrings/existing.gpg", + } + ), + dst=dst, + src=None, + ) + + AptModule().apply(context) + + mock_run.assert_called_once() + assert (dst / "etc/apt/keyrings/interdiode.gpg").read_bytes() == KEYRING_BYTES + configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text( + encoding="utf-8" + ) + assert "Signed-By: /etc/apt/keyrings/interdiode.gpg\n" in configured_source + + +def test_apply_without_signed_by_writes_no_keyring_or_field( + tmp_path, monkeypatch +) -> None: + mock_run = _mock_gpg_dearmor_success(monkeypatch) + dst = tmp_path / "dst" + context = Context(config=make_config_with({}), dst=dst, src=None) + + AptModule().apply(context) + + mock_run.assert_not_called() + assert not (dst / "etc/apt/keyrings").exists() + configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text( + encoding="utf-8" + ) + assert configured_source == ( + "Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\nEnabled: yes\n" + ) + + +def test_resolve_signed_by_raises_when_gpg_not_on_path(tmp_path, monkeypatch) -> None: + monkeypatch.setattr("id_configurator.modules.apt.shutil.which", lambda _: None) + dst = tmp_path / "dst" + context = Context(config=make_config(), dst=dst, src=None) + + with pytest.raises(RuntimeError, match="gpg is not available"): + AptModule().apply(context) + + +def test_resolve_signed_by_raises_on_invalid_key(tmp_path, monkeypatch) -> None: + mock_run = MagicMock( + return_value=subprocess.CompletedProcess( + args=[], returncode=2, stdout=b"", stderr=b"gpg: no valid data found" + ) + ) + monkeypatch.setattr("id_configurator.modules.apt.subprocess.run", mock_run) + dst = tmp_path / "dst" + context = Context(config=make_config(), dst=dst, src=None) + + with pytest.raises(RuntimeError, match="invalid GPG key for apt source interdiode"): + AptModule().apply(context) diff --git a/tests/test_config.py b/tests/test_config.py index d30b748..11f0ba8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -34,7 +34,6 @@ FULL_CONFIG = { "signed-by": "/etc/apt/keyrings/interdiode.asc", } ], - "keys": [{"name": "interdiode", "content": "ZmFrZQ=="}], }, "yum": { "repositories": [ @@ -76,7 +75,6 @@ def test_parse_configuration_from_json() -> None: configuration.configurations.apt.sources[0].signed_by == "/etc/apt/keyrings/interdiode.asc" ) - assert configuration.configurations.apt.keys[0].name == "interdiode" assert configuration.configurations.yum.keys[0].name == "interdiode" @@ -191,7 +189,6 @@ def test_apt_source_to_deb822_full() -> None: enabled=False, trusted=True, **{ - "signed-by": "/etc/apt/keyrings/interdiode.asc", "by-hash": True, "allow-insecure": False, "allow-weak": False, @@ -205,7 +202,7 @@ def test_apt_source_to_deb822_full() -> None: pdiffs=True, description="InterDiode APT repository", ) - rendered = source.to_deb822() + rendered = source.to_deb822(signed_by_path="/etc/apt/keyrings/interdiode.asc") assert rendered.splitlines() == [ "Types: deb deb-src", "URIs: http://example.invalid/debian",