mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: update APT module to support inline ASCII-armored GPG keys in Signed-By field
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -7,27 +7,19 @@ 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/<name>.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.
|
||||
content of a GPG public key, folded inline as a multi-line `Signed-By`
|
||||
field (per sources.list(5) - verified empirically against a real,
|
||||
genuinely signed repository, so no `gpg` binary or separate keyring file
|
||||
is needed at all to apply this configuration). `signed_by_file` instead
|
||||
names a keyring already expected to exist on the target system, used
|
||||
verbatim as `Signed-By`; if both are given, `signed_by` takes precedence.
|
||||
"""
|
||||
|
||||
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, sanitize_filename
|
||||
|
||||
@@ -36,7 +28,6 @@ logger = logging.getLogger(__name__)
|
||||
_APT_DIR = "etc/apt"
|
||||
_SOURCES_LIST = "etc/apt/sources.list"
|
||||
_SOURCES_LIST_D = "etc/apt/sources.list.d"
|
||||
_KEYRINGS_DIR = "etc/apt/keyrings"
|
||||
|
||||
|
||||
def _disable_list_content(content: str) -> str:
|
||||
@@ -59,28 +50,6 @@ 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"
|
||||
@@ -119,22 +88,14 @@ class AptModule(Module):
|
||||
)
|
||||
logger.info("disabled existing apt source %s", entry.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}/{sanitize_filename(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
|
||||
|
||||
def _write_configured_sources(self, context: Context) -> None:
|
||||
@staticmethod
|
||||
def _write_configured_sources(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}/{sanitize_filename(source.name)}.sources",
|
||||
source.to_deb822(signed_by_path).encode("utf-8"),
|
||||
source.to_deb822(
|
||||
signed_by_armored_key=source.signed_by,
|
||||
signed_by_path=source.signed_by_file,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
logger.info("added apt source %s", source.name)
|
||||
|
||||
Reference in New Issue
Block a user