import: Trust subkeys included in signature

With gpg sub keys one can rotate signing keys while having a stable
trust anchor. So far one still had to ship the sub key out of band but
a newer gpg has the option to include the sub key in the signature and
import it automatically. This is safe if we only allow importing a sub
key signed by the top key we already have in the key ring.
Add the --auto-key-import argument to gpg to import subkeys but also
set --import-options=merge-only,import-clean to restrict what we import
to only be sub keys signed by the top key we have in the keyring and
discard any irrelevant parts. The ugly part is that we also have to
work on a temporary copy of the keyring because gpg wants to persist
the added key material but we don't what that here.
This commit is contained in:
Kai Lüke
2026-04-29 13:57:56 +09:00
parent e50f4b7387
commit 80e12bf88c
2 changed files with 33 additions and 13 deletions

2
README
View File

@@ -270,6 +270,8 @@ REQUIREMENTS:
NOTE: If using dbus < 1.9.18, you should override the default
policy directory (--with-dbuspolicydir=/etc/dbus-1/system.d).
polkit (optional)
gnupg >= 2.4 optional (for signature verification in: importctl,
systemd-sysupdate)
To build in directory build/:
meson setup build/ && ninja -C build/

View File

@@ -5,6 +5,7 @@
#include "sd-id128.h"
#include "alloc-util.h"
#include "copy.h"
#include "dirent-util.h"
#include "escape.h"
#include "fd-util.h"
@@ -409,7 +410,8 @@ static int verify_gpg(
_cleanup_(rm_rf_physical_and_freep) char *gpg_home = NULL;
char sig_file_path[] = "/tmp/sigXXXXXX";
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
const char *keyring_override;
_cleanup_free_ char *keyring_copy = NULL;
const char *keyring_override, *keyring_source;
int r;
assert(iovec_is_valid(payload));
@@ -446,6 +448,31 @@ static int verify_gpg(
goto finish;
}
if (keyring_override)
keyring_source = keyring_override;
else if (access(USER_KEYRING_PATH, F_OK) >= 0)
keyring_source = USER_KEYRING_PATH;
else if (access(USER_KEYRING_PATH_LEGACY, F_OK) >= 0)
keyring_source = USER_KEYRING_PATH_LEGACY;
else
keyring_source = VENDOR_KEYRING_PATH;
/* For whatever reason gpg --auto-key-import writes the key material embedded in the signature
* (the signing subkey) back into the keyring it verifies against instead of just using it
* temporarily. Verify against a throwaway copy in the tmp home so we never write to the
* original keyring. */
keyring_copy = path_join(gpg_home, "keyring.gpg");
if (!keyring_copy) {
r = log_oom();
goto finish;
}
r = copy_file(keyring_source, keyring_copy, 0, 0600, /* copy_flags= */ 0);
if (r < 0) {
log_error_errno(r, "Failed to copy keyring '%s': %m", keyring_source);
goto finish;
}
r = pidref_safe_fork_full(
"(gpg)",
(int[]) { gpg_pipe[0], -EBADF, STDERR_FILENO },
@@ -463,6 +490,8 @@ static int verify_gpg(
"--no-auto-check-trustdb",
"--batch",
"--trust-model=always",
"--auto-key-import",
"--import-options=merge-only,import-clean",
NULL, /* --homedir= */
NULL, /* --keyring= */
NULL, /* --verify */
@@ -475,18 +504,7 @@ static int verify_gpg(
/* Child */
cmd[k++] = strjoina("--homedir=", gpg_home);
if (keyring_override)
cmd[k++] = strjoina("--keyring=", keyring_override);
else if (access(USER_KEYRING_PATH, F_OK) >= 0) /* We add the user keyring only to the
* command line arguments, if it's around
* since gpg fails otherwise. */
cmd[k++] = "--keyring=" USER_KEYRING_PATH;
else if (access(USER_KEYRING_PATH_LEGACY, F_OK) >= 0)
cmd[k++] = "--keyring=" USER_KEYRING_PATH_LEGACY;
else
cmd[k++] = "--keyring=" VENDOR_KEYRING_PATH;
cmd[k++] = strjoina("--keyring=", keyring_copy);
cmd[k++] = "--verify";
if (signature) {
cmd[k++] = sig_file_path;