diff --git a/NEWS b/NEWS
index 44b4bfeced3..ffc0d68fd86 100644
--- a/NEWS
+++ b/NEWS
@@ -209,6 +209,32 @@ CHANGES WITH 263 in spe:
"# [Experimental]" comment line in the interface description, which
sd_varlink_idl_parse() turns back into the flag.
+ Changes in systemd-keyring-setup:
+
+ * A new service systemd-keyring-setup.service has been added that
+ enrolls X.509 certificates into the kernel's .dm-verity, .fs-verity
+ and .bpf keyrings early during boot, and seals the keyrings
+ afterwards. The certificates are read from the File Hierarchy for
+ the Verification of OS Artifacts (VOA), i.e. from /etc/voa/,
+ /run/voa/, /usr/local/share/voa/ and /usr/share/voa/, or may be
+ passed in as keyring-setup.* system credentials. This allows the
+ kernel to verify signed dm-verity volumes, fs-verity files and BPF
+ programs with certificates provided by the OS vendor or the local
+ administrator, without rebuilding the kernel or enrolling keys into
+ the firmware. Note that the kernel seals the .dm-verity and .bpf
+ keyrings while they are still empty, unless
+ dm_verity.keyring_unsealed=1 or bpf.keyring_unsealed=1 is specified
+ on the kernel command line, respectively. The service is enabled
+ statically whenever it is built, and is considered experimental for
+ now. See
+ systemd-keyring-setup.service(8) and
+ https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/
+ for details.
+
+ systemd-keyring-setup is the first systemd component written in
+ Rust, and is hence only built if Rust support is enabled with the
+ new -Drust= build option.
+
CHANGES WITH 262:
Announcements of Future Feature Removals and Incompatible Changes:
diff --git a/README b/README
index 81566028a34..f6b78406657 100644
--- a/README
+++ b/README
@@ -87,6 +87,7 @@ REQUIREMENTS:
PIDFD_INFO_COREDUMP by ioctl(PIDFD_GET_INFO)
≥ 6.17 for coredump socket pattern ('@@' prefix)
≥ 6.19 for PIDFD_INFO_SUPPORTED_MASK and PIDFD_INFO_COREDUMP_SIGNAL
+ ≥ 7.0 for the .dm-verity keyring provisioned by systemd-keyring-setup
≥ 7.1 for PIDFD_INFO_COREDUMP_CODE
✅ systemd utilizes several new kernel APIs, but will fall back gracefully
@@ -182,6 +183,10 @@ REQUIREMENTS:
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG_PLATFORM_KEYRING
CONFIG_IMA_ARCH_POLICY
CONFIG_INTEGRITY_MACHINE_KEYRING
+ Required to verify signed Verity images and fs-verity files with
+ certificates enrolled by systemd-keyring-setup:
+ CONFIG_KEYS
+ CONFIG_FS_VERITY_BUILTIN_SIGNATURES
Required for reading credentials from SMBIOS:
CONFIG_DMI
diff --git a/man/rules/meson.build b/man/rules/meson.build
index e4459c9a2c9..c0869c4b5f3 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1143,6 +1143,10 @@ manpages = [
'systemd-journald@.service',
'systemd-journald@.socket'],
''],
+ ['systemd-keyring-setup.service',
+ '8',
+ ['systemd-keyring-setup'],
+ 'HAVE_OPENSSL HAVE_RUST'],
['systemd-keyutil', '1', [], ''],
['systemd-localed.service', '8', ['systemd-localed'], 'ENABLE_LOCALED'],
['systemd-logind.service',
diff --git a/man/systemd-keyring-setup.service.xml b/man/systemd-keyring-setup.service.xml
new file mode 100644
index 00000000000..ee061d39674
--- /dev/null
+++ b/man/systemd-keyring-setup.service.xml
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+
+ systemd-keyring-setup.service
+ systemd
+
+
+
+ systemd-keyring-setup.service
+ 8
+
+
+
+ systemd-keyring-setup.service
+ systemd-keyring-setup
+ Enroll certificates into kernel keyrings
+
+
+
+ systemd-keyring-setup.service
+
+ /usr/lib/systemd/systemd-keyring-setup
+ OPTIONS
+ KEYRING
+
+
+
+
+
+
+ Description
+
+ systemd-keyring-setup.service is an early boot service that enrolls X.509
+ certificates into the kernel keyrings used to verify signatures on dm-verity volumes, fs-verity files and
+ BPF programs, and then seals these keyrings. The certificates are read from the File
+ Hierarchy for the Verification of OS Artifacts (VOA). This allows the kernel to verify signatures
+ with keys provided by the OS vendor or the local administrator, without rebuilding the kernel and without
+ enrolling the keys into the firmware.
+
+ The following keyrings are provisioned, each from a VOA context of the
+ kernel-keyring purpose:
+
+
+ Keyrings and VOA contexts
+
+
+
+
+
+
+ Keyring
+ VOA context
+ Used to verify
+
+
+
+
+ .dm-verity
+ dm-verity
+ Signatures of the root hashes of dm-verity volumes, such as those of Discoverable Disk Images set up by systemd-veritysetup@.service8, systemd-dissect1 or the RootImage= setting of services
+
+
+ .fs-verity
+ fs-verity
+ Built-in signatures of fs-verity protected files
+
+
+ .bpf
+ bpf
+ Signatures of BPF programs
+
+
+
+
+
+ The kernel only uses the .dm-verity and .bpf keyrings once they
+ have been sealed, and seals them itself while they are still empty, unless
+ dm_verity.keyring_unsealed=1 or bpf.keyring_unsealed=1 is specified on
+ the kernel command line, respectively. Hence, these options are required to provision the two keyrings, see
+ below. The .fs-verity keyring is not sealed by the kernel, and is used as soon as it
+ contains a key.
+
+ For each keyring that is not sealed yet, the service enrolls the certificates it finds and then seals
+ the keyring: it installs a restriction so that only certificates signed by a key in the keyring can be added
+ afterwards, and removes the permission to change this restriction or the permissions of the keyring. The
+ keyring is sealed even if no certificates were found, or if some of them could not be enrolled. Keys that
+ were already in the keyring before the service ran are sealed in as well, and a warning is logged about
+ them.
+
+ Note that kexec will unseal and empty the keyrings.
+
+ The service runs both in the initrd and on the host. Usually, the keyrings are provisioned in the
+ initrd, since the root file system might itself be a dm-verity volume that is verified with one of the
+ enrolled keys. When the service runs again on the host, the keyrings have been sealed already. In this case,
+ only certificates signed by a key that was enrolled earlier can be added, and all other certificates are
+ rejected. Certificates that have been enrolled already are skipped.
+
+
+
+ Certificate Directories and Precedence
+
+ Certificates are read from the VOA load paths /etc/voa/,
+ /run/voa/, /usr/local/share/voa/ and
+ /usr/share/voa/, in order of decreasing priority. For each keyring, artifact verifiers
+ are read from
+ OS/kernel-keyring/CONTEXT/x509/NAME-certificate.pem,
+ and trust anchors from
+ OS/trust-anchor-kernel-keyring/CONTEXT/x509/NAME-certificate.pem
+ below each load path. Both are enrolled into the same keyring, and only differ in how their usage is
+ checked, see below. NAME may consist of lowercase letters, digits,
+ ., _ and -, other files are ignored. Each file must
+ contain a single PEM-encoded certificate. A file that contains more than one certificate is ignored as a
+ whole. CA certificates should be placed in separate files among the trust anchors instead.
+
+ Certificates are looked up for two OS identifiers derived from
+ os-release5: the exact
+ identifier, and the value of ID= alone. The exact identifier consists of the values of
+ ID=, VERSION_ID=, VARIANT_ID=,
+ IMAGE_ID= and IMAGE_VERSION=, separated by :. A
+ field that is not set is left empty, and unset fields at the end are omitted. For example,
+ ID=debian and VERSION_ID=13 result in the identifiers
+ debian:13 and debian. If ID= is not set,
+ linux is used. If one of the other fields contains characters that are not permitted by
+ the VOA specification, only the value of ID= is used. If ID= itself
+ contains such characters, no certificates are looked up. The keyring-setup.os credential
+ may be used to specify the identifiers explicitly, see below.
+
+ Note that the VOA specification requires different semantics than the usual logic for configuration
+ files, where a file in a directory with higher priority replaces a file with the same name in a directory
+ with lower priority. Instead, X.509 certificates are merged: all files in all load paths are used. A
+ certificate that is found in more than one place is only enrolled once. If two different certificates have
+ the same subject and key identifier, only one of them can be added to a keyring. In this case, certificates
+ found for the exact OS identifier take precedence over the ones found for the value of ID=
+ alone, and among those, the certificate from the load path with the highest priority is enrolled.
+
+ A certificate may be masked by placing a symlink to /dev/null with the same file
+ name in any of the load paths, usually in /etc/voa/ or /run/voa/.
+ As required by the VOA specification, a masked certificate is ignored in all load paths, including the
+ ones with a higher priority than the one containing the symlink. Masking is meant as a substitute for
+ revocation: if a certificate is considered compromised, a single symlink ensures that none of its copies is
+ enrolled. Other symlinks are followed as long as they point to a location within the VOA hierarchy, and
+ symlinks to certificate files only if the target has the same file name.
+
+ To make sure that certificates cannot be placed by unprivileged users, files that are executable or
+ world-writable, and directories that are world-writable, are ignored. Once a directory owned by an
+ unprivileged user has been entered, the lookup does not continue into files or directories owned by anyone
+ else.
+
+ As required by the VOA specification, the usage of each certificate is checked against its purpose.
+ An artifact verifier is ignored if it has an extended key usage extension that permits neither code signing
+ nor any extended key usage, or a key usage extension that does not permit digital signatures. A trust anchor
+ that is not a CA certificate is enrolled nonetheless, since self-signed certificates that act as their own
+ trust anchor are common, but a message is logged about it. Certificates with invalid extensions are
+ ignored.
+
+
+
+ Credentials
+
+ systemd-keyring-setup supports the service credentials logic as implemented by
+ ImportCredential=/LoadCredential=/SetCredential=
+ (see systemd.exec5 for
+ details). The following credentials are used when passed in:
+
+
+
+ keyring-setup.dm-verity.NAME
+ keyring-setup.fs-verity.NAME
+ keyring-setup.bpf.NAME
+
+ A PEM-encoded X.509 certificate to enroll into the .dm-verity,
+ .fs-verity or .bpf keyring, respectively.
+ NAME may consist of lowercase letters, digits, .,
+ _ and -. As suggested by the VOA specification for certificates
+ obtained from elsewhere, the certificate is not enrolled directly, but is first written to
+ /run/voa/OS/kernel-keyring/CONTEXT/x509/NAME-certificate.pem,
+ using the first OS identifier. Hence, masking and merging apply to it like to any other file in the
+ hierarchy. Only the certificate itself is written there, so that a private key included in the
+ credential is not made readable to other users. A credential that does not contain exactly one
+ certificate is ignored.
+
+
+
+
+
+ keyring-setup.os
+
+ A whitespace-separated list of up to 64 OS identifiers to use instead of the ones
+ derived from os-release5.
+ This is useful for generic initrds, whose os-release does not match the one of the
+ host. If the list is empty, too long or contains an invalid identifier, the credential is ignored and
+ the identifiers derived from os-release are used.
+
+
+
+
+
+ See
+ systemd.system-credentials7
+ for details about system credentials.
+
+
+
+ Options
+
+ If one or more KEYRING names, such as .dm-verity, are
+ specified, only those keyrings are processed. Otherwise, all keyrings listed above are processed.
+
+ The following options are understood:
+
+
+
+
+
+ Show which certificates would be enrolled into which keyring, and the current state
+ of the keyrings, without changing anything.
+
+
+
+
+
+
+
+
+
+
+ is only supported together with .
+
+
+
+ Exit status
+
+ On success, 0 is returned. This includes the cases where a keyring does not exist, or has been
+ provisioned by an earlier run already. If a certificate or a credential could not be used, for example
+ because it is malformed, or because it is not signed by a key in a keyring that has been sealed already, 65
+ (EX_DATAERR) is returned. systemd-keyring-setup.service treats this
+ exit status as success. On other failures, a different non-zero exit status is returned.
+
+
+
+ Kernel Command Line
+
+
+
+ dm_verity.keyring_unsealed=
+ bpf.keyring_unsealed=
+
+ These options are interpreted by the kernel. If set to 1, the kernel
+ leaves the .dm-verity or .bpf keyring unsealed during boot, so
+ that it can be provisioned by systemd-keyring-setup.service. Otherwise, the kernel
+ seals the keyring while it is still empty, and no certificates can be enrolled into it.
+
+
+
+
+
+
+
+ Security Considerations
+
+ The enrolled certificates can only be trusted as much as the files they were read from. The intended
+ deployment is to provision the keyrings from the initrd of a signed Unified Kernel Image, see
+ systemd-stub7.
+
+ Until a keyring is sealed, any sufficiently privileged process may add arbitrary keys to it. After it
+ has been sealed, privileged processes may still remove keys from it, and add certificates that are signed
+ by a key in the keyring, but they cannot add other keys, or change the restriction or the permissions of
+ the keyring. The keyrings stay sealed until the next reboot. A soft reboot, see
+ systemd-soft-reboot.service8,
+ does not reset them.
+
+ If dm-verity is built as a kernel module, unloading and reloading the module discards the
+ .dm-verity keyring, including its seal, and creates a new, empty one. If
+ dm_verity.keyring_unsealed=1 is set, the new keyring may then be provisioned with
+ arbitrary keys again. systemd-keyring-setup logs a warning if a keyring belongs to a
+ loadable kernel module. For high-security setups, dm-verity should be built into the kernel.
+
+
+
+ See Also
+
+
+ systemd1
+ systemd-veritysetup@.service8
+ systemd-dissect1
+ systemd-stub7
+ systemd.system-credentials7
+ os-release5
+ keyctl1
+
+
+
+
diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml
index 1836b4e71bc..633eaeb884a 100644
--- a/man/systemd.system-credentials.xml
+++ b/man/systemd.system-credentials.xml
@@ -119,6 +119,35 @@
+
+ keyring-setup.dm-verity.NAME
+ keyring-setup.fs-verity.NAME
+ keyring-setup.bpf.NAME
+
+ A PEM-encoded X.509 certificate to enroll into the .dm-verity,
+ .fs-verity or .bpf kernel keyring, respectively (e.g.
+ keyring-setup.dm-verity.vendor). Read by
+ systemd-keyring-setup.service8,
+ which writes the certificate to /run/voa/ before enrolling it, see there for
+ details. NAME may consist of lowercase letters, digits, .,
+ _ and -.
+
+
+
+
+
+
+ keyring-setup.os
+
+ A whitespace-separated list of OS identifiers (e.g. debian:13 debian) under
+ which certificates are looked up in the VOA hierarchy, instead of the ones derived from
+ os-release5. Read by
+ systemd-keyring-setup.service8.
+
+
+
+
+
login.issue
diff --git a/meson.build b/meson.build
index 4bb3bb8047f..d3e24dc868e 100644
--- a/meson.build
+++ b/meson.build
@@ -2419,6 +2419,7 @@ subdir('src/integritysetup')
subdir('src/journal')
subdir('src/journal-remote')
subdir('src/kernel-install')
+subdir('src/keyring-setup')
subdir('src/keyutil')
subdir('src/locale')
subdir('src/login')
diff --git a/mkosi/keyring-setup-test-ca.crt b/mkosi/keyring-setup-test-ca.crt
new file mode 100644
index 00000000000..6c11c0dd7aa
--- /dev/null
+++ b/mkosi/keyring-setup-test-ca.crt
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBnDCCAUOgAwIBAgIURIGL6fLWIcO+/Ako2/UTNpYX3LUwCgYIKoZIzj0EAwIw
+GzEZMBcGA1UEAwwQa2V5cmluZy1zZXR1cC1jYTAgFw0yNjA5MDExOTI0MzZaGA8y
+MTI2MDgwODE5MjQzNlowGzEZMBcGA1UEAwwQa2V5cmluZy1zZXR1cC1jYTBZMBMG
+ByqGSM49AgEGCCqGSM49AwEHA0IABIE9RbLZ1pLUETv15tD8Pu8M7VujIPsLyfbI
+gnnnMt8DuCWBcYpskR7UaXz3KGw8mrHSCXYHcjNr1VH1jKX1u9ijYzBhMB0GA1Ud
+DgQWBBSJJP2p6+SQKwQUKcskhuxFrHPYIDAfBgNVHSMEGDAWgBSJJP2p6+SQKwQU
+KcskhuxFrHPYIDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAKBggq
+hkjOPQQDAgNHADBEAiBbfas+St1VlhTqYskeDmT3ysCEP/Mbnmq9ailCP+7C1wIg
+dU2k8OMygpbLZBJaBWHvkKTXHrvGjYHslqtu7hO4W0A=
+-----END CERTIFICATE-----
diff --git a/mkosi/keyring-setup-test-ca.key b/mkosi/keyring-setup-test-ca.key
new file mode 100644
index 00000000000..f47bd46acdb
--- /dev/null
+++ b/mkosi/keyring-setup-test-ca.key
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3BJkI9JHC9ZZF2dO
+C3y3uBEpZrLp6pPnYniJ1KoJx4ehRANCAASBPUWy2daS1BE79ebQ/D7vDO1boyD7
+C8n2yIJ55zLfA7glgXGKbJEe1Gl89yhsPJqx0gl2B3Iza9VR9Yyl9bvY
+-----END PRIVATE KEY-----
diff --git a/mkosi/keyring-setup-test-ca.py b/mkosi/keyring-setup-test-ca.py
new file mode 100644
index 00000000000..b1420f8f46f
--- /dev/null
+++ b/mkosi/keyring-setup-test-ca.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+"""Mint the extension-less test certificates embedded in TEST-98-KEYRING-SETUP.unsealed.sh.
+
+The subtests for the serial-number fallback of certificate_description_suffix() need certificates without
+a subjectKeyIdentifier extension. OpenSSL's x509 tool adds one to every certificate it signs, no
+matter what, so they cannot be produced with it. python-cryptography only adds what is asked for.
+
+The issuing CA pair next to this script was created with
+ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
+ -subj /CN=keyring-setup-ca -days 36500 \
+ -keyout keyring-setup-test-ca.key -out keyring-setup-test-ca.crt
+and is installed into the trust-anchor hierarchy of the test images by mkosi.finalize. After
+rotating the pair, rerun this script and paste the output over the embedded blobs.
+"""
+
+import pathlib
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.x509.oid import NameOID
+
+here = pathlib.Path(__file__).parent
+ca_cert = x509.load_pem_x509_certificate((here / 'keyring-setup-test-ca.crt').read_bytes())
+ca_key = serialization.load_pem_private_key((here / 'keyring-setup-test-ca.key').read_bytes(), password=None)
+
+for cn, serial in (('noskid', 0x8112131415161718), ('noskid2', 0x11121314)):
+ key = ec.generate_private_key(ec.SECP256R1())
+ cert = (
+ x509.CertificateBuilder()
+ .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]))
+ .issuer_name(ca_cert.subject)
+ .public_key(key.public_key())
+ .serial_number(serial)
+ .not_valid_before(ca_cert.not_valid_before_utc)
+ .not_valid_after(ca_cert.not_valid_after_utc)
+ .sign(ca_key, hashes.SHA256())
+ )
+ print(f'# {cn}, serial {serial:#x}')
+ print(cert.public_bytes(serialization.Encoding.PEM).decode(), end='')
diff --git a/mkosi/mkosi.finalize b/mkosi/mkosi.finalize
index 8db3db1ccf3..87214b92309 100755
--- a/mkosi/mkosi.finalize
+++ b/mkosi/mkosi.finalize
@@ -16,3 +16,14 @@ fi
# Used to sign artifacts verified by kernel platform keyring
cp "$SRCDIR/mkosi/mkosi.crt" "$SRCDIR/mkosi/mkosi.key" "$BUILDROOT/usr/share/"
+
+# The same certificate in the VOA hierarchy, for systemd-keyring-setup to enroll into the .dm-verity and
+# .fs-verity keyrings
+# shellcheck source=/dev/null
+id=$(. "$BUILDROOT/usr/lib/os-release" && echo "$ID")
+for context in dm-verity fs-verity; do
+ install -D -m 0644 "$SRCDIR/mkosi/mkosi.crt" "$BUILDROOT/usr/share/voa/$id/kernel-keyring/$context/x509/mkosi-certificate.pem"
+done
+# A CA certificate among the trust anchors, so that those get enrolled as well
+install -D -m 0644 "$SRCDIR/mkosi/keyring-setup-test-ca.crt" \
+ "$BUILDROOT/usr/share/voa/$id/trust-anchor-kernel-keyring/dm-verity/x509/ca-certificate.pem"
diff --git a/mkosi/mkosi.initrd.conf/mkosi.finalize b/mkosi/mkosi.initrd.conf/mkosi.finalize
new file mode 100755
index 00000000000..5c226926067
--- /dev/null
+++ b/mkosi/mkosi.initrd.conf/mkosi.finalize
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -e
+
+# The certificate mkosi installs for userspace verification, mirrored into the VOA hierarchy so that
+# systemd-keyring-setup enrolls it into the .dm-verity and .fs-verity keyrings while the initrd still can.
+if [ -f "$BUILDROOT/usr/lib/verity.d/mkosi.crt" ]; then
+ # shellcheck source=/dev/null
+ id=$(. "$BUILDROOT/usr/lib/os-release" && echo "$ID")
+ for context in dm-verity fs-verity; do
+ install -D -m 0644 "$BUILDROOT/usr/lib/verity.d/mkosi.crt" "$BUILDROOT/usr/share/voa/$id/kernel-keyring/$context/x509/mkosi-certificate.pem"
+ done
+ install -D -m 0644 "$SRCDIR/mkosi/keyring-setup-test-ca.crt" \
+ "$BUILDROOT/usr/share/voa/$id/trust-anchor-kernel-keyring/dm-verity/x509/ca-certificate.pem"
+fi
diff --git a/presets/90-systemd-initrd.preset b/presets/90-systemd-initrd.preset
index b7b966daca2..53184ae5e68 100644
--- a/presets/90-systemd-initrd.preset
+++ b/presets/90-systemd-initrd.preset
@@ -37,4 +37,5 @@ disable systemd-boot-check-no-failures.service
disable systemd-journal-gatewayd.*
disable systemd-journal-remote.*
disable systemd-journal-upload.*
+disable systemd-keyring-setup.service
disable systemd-time-wait-sync.service
diff --git a/src/include/override/linux/keyctl.h b/src/include/override/linux/keyctl.h
index 1f0988371a2..41fe4b40bab 100644
--- a/src/include/override/linux/keyctl.h
+++ b/src/include/override/linux/keyctl.h
@@ -44,3 +44,8 @@ typedef int32_t key_serial_t;
#else
static_assert(KEY_OTH_ALL == 0x0000003f, "");
#endif
+
+/* TODO: drop once the headers we require carry it */
+#ifndef KEY_SPEC_BPF_KEYRING
+# define KEY_SPEC_BPF_KEYRING -9 /* - key ID for the .bpf keyring */
+#endif
diff --git a/src/keyring-setup/keyring-setup.rs b/src/keyring-setup/keyring-setup.rs
new file mode 100644
index 00000000000..27a56b07dca
--- /dev/null
+++ b/src/keyring-setup/keyring-setup.rs
@@ -0,0 +1,1218 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+//! systemd-keyring-setup: enrolls the X.509 certificates of the VOA hierarchy into the kernel's trust
+//! keyrings and seals them.
+
+#![no_std]
+#![no_main]
+
+use core::ffi::{c_int, CStr};
+use core::ptr;
+use core::sync::atomic::Ordering;
+
+use systemd_shared::chase::{self, CHASE_MAX_MODE, CHASE_MKDIR_0755, CHASE_SAFE};
+use systemd_shared::conf_files::ConfFile;
+use systemd_shared::cstr::{self, display};
+use systemd_shared::fileio::{self, READ_FULL_FILE_FAIL_WHEN_LARGER, READ_FULL_FILE_VERIFY_REGULAR};
+use systemd_shared::keyring::{self, KeySerial};
+use systemd_shared::kmod::Kmod;
+use systemd_shared::prelude::*;
+use systemd_shared::recurse_dir::{self, RECURSE_DIR_IGNORE_DOT, RECURSE_DIR_SORT};
+use systemd_shared::sys::COMMAND_EXPERIMENTAL;
+use systemd_shared::table::{Table, TABLE_ERSATZ_DASH};
+use systemd_shared::tmpfile::{LinkableTmpfile, LINK_TMPFILE_REPLACE};
+use systemd_shared::voa::{self, Lookup, VoaMode};
+use systemd_shared::x509::{self, Der, X509};
+use systemd_shared::{creds, fd, json, sys};
+use systemd_shared::{libcrypto_note, libkmod_note, log_openssl_errors, table_log_add_error};
+
+static ARG_PAGER_FLAGS: AtomicPagerFlags = AtomicPagerFlags::new(0);
+
+verbs! {
+ COMMAND {
+ names: "systemd-keyring-setup\0",
+ abstract_: "Enroll certificates from the VOA hierarchy into the kernel's trust keyrings.",
+ argspec: "[KEYRING…]\0",
+ man_pages: "systemd-keyring-setup.service(8)\0",
+ pager_flags: ARG_PAGER_FLAGS,
+ flags: COMMAND_EXPERIMENTAL,
+ },
+}
+
+/// The permission mask we are left with after we dropped SetAttr.
+const KEYRING_PERM_SEALED: u32 =
+ sys::KEY_POS_SEARCH | sys::KEY_USR_VIEW | sys::KEY_USR_READ | sys::KEY_USR_WRITE;
+
+const CERTIFICATE_SIZE_MAX: usize = 1024 * 1024;
+
+const CREDENTIAL_PREFIX: &str = "keyring-setup.";
+
+/// Cap the number of possible OSes in both the exact and bare form. 64 OSes is plenty and we can always
+/// increase.
+const CREDENTIAL_OS_MAX: usize = 64;
+
+struct KeyringSpec {
+ name: &'static CStr,
+ /// The VOA role its certificates come from.
+ role: &'static CStr,
+ /// The VOA context below the role.
+ context: &'static CStr,
+ /// The module the kernel's keyring_unsealed= parameter is scoped to, i.e. /sys/module//, None if
+ /// the kernel never seals the keyring.
+ param: Option<&'static CStr>,
+ /// The keyring exists once this module is loaded.
+ module: Option<&'static CStr>,
+ /// Fixed KEY_SPEC_* ID, None if the keyring only goes by name.
+ spec_id: Option,
+}
+
+static KEYRING_SPECS: [KeyringSpec; 3] = [
+ KeyringSpec {
+ name: c".dm-verity",
+ role: c"kernel-keyring",
+ context: c"dm-verity",
+ param: Some(c"dm_verity"),
+ module: Some(c"dm_verity"),
+ spec_id: None,
+ },
+ KeyringSpec {
+ name: c".fs-verity",
+ role: c"kernel-keyring",
+ context: c"fs-verity",
+ param: None,
+ module: None,
+ spec_id: None,
+ },
+ KeyringSpec {
+ name: c".bpf",
+ role: c"kernel-keyring",
+ context: c"bpf",
+ param: Some(c"bpf"),
+ module: None,
+ spec_id: Some(sys::KEY_SPEC_BPF_KEYRING),
+ },
+];
+
+fn keyring_spec_from_name(name: &CStr) -> Option<&'static KeyringSpec> {
+ KEYRING_SPECS.iter().find(|s| s.name == name)
+}
+
+/// What the command line asks for.
+struct Args {
+ keyrings: Vec<&'static KeyringSpec>,
+ dry_run: bool,
+ json_format_flags: sys::sd_json_format_flags_t,
+}
+
+impl Args {
+ fn selected(&self, spec: &KeyringSpec) -> bool {
+ self.keyrings.is_empty() || self.keyrings.iter().any(|s| ptr::eq(*s, spec))
+ }
+}
+
+/// What a run amounts to, the exit status: a hard failure outweighs input that could not be used.
+#[derive(Clone, Copy)]
+enum Outcome {
+ Success,
+ DataError,
+ Failed(Errno),
+}
+
+impl Outcome {
+ fn data_error(&mut self) {
+ if let Outcome::Success = self {
+ *self = Outcome::DataError;
+ }
+ }
+
+ fn fail(&mut self, e: Errno) {
+ if !matches!(self, Outcome::Failed(_)) {
+ *self = Outcome::Failed(e);
+ }
+ }
+}
+
+/// Keeps the first error, `RET_GATHER()`.
+fn gather(ret: &mut Option, r: Result<()>) {
+ if let Err(e) = r {
+ ret.get_or_insert(e);
+ }
+}
+
+struct Certificate {
+ path: OwnedCStr,
+ der: Der,
+ /// The ": " tail of the kernel's description.
+ description_suffix: OwnedCStr,
+ /// None until enrolled.
+ serial: Option,
+}
+
+/// A key linked into the keyring.
+struct Member {
+ serial: KeySerial,
+ /// None if the key could not be described.
+ description: Option,
+}
+
+struct Keyring {
+ spec: &'static KeyringSpec,
+ /// 0 while unknown.
+ serial: KeySerial,
+ /// None while unknown, a dry run cannot load the module providing it.
+ exists: Option,
+ /// The boot-time flag, None if unknown.
+ kernel_unsealed: Option,
+ certificates: Vec,
+ /// As last read, None if they could not be read.
+ members: Option>,
+ sealed: bool,
+ /// A certificate could not be used.
+ data_error: bool,
+ /// A hard error that does not stop the sealing.
+ failed: Option,
+}
+
+impl Keyring {
+ fn new(spec: &'static KeyringSpec) -> Keyring {
+ Keyring {
+ spec,
+ serial: 0,
+ exists: None,
+ kernel_unsealed: None,
+ certificates: Vec::new(),
+ members: None,
+ sealed: false,
+ data_error: false,
+ failed: None,
+ }
+ }
+
+ fn name(&self) -> impl core::fmt::Display + '_ {
+ display(self.spec.name)
+ }
+
+ fn members(&self) -> &[Member] {
+ self.members.as_deref().unwrap_or_default()
+ }
+
+ /// The certificates this run enrolled that are still in the keyring.
+ fn n_enrolled(&self) -> usize {
+ self.certificates.iter().filter(|c| c.serial.is_some()).count()
+ }
+
+ /// Reads the members along with their descriptions, they are unknown if that fails.
+ fn read_members(&mut self) -> Result<()> {
+ self.members = None;
+
+ let serials = keyring::list(self.serial)?;
+ let mut members = Vec::with_capacity(serials.len())?;
+ for &serial in &serials {
+ // A key that cannot be described still counts, it just matches nothing
+ let description = keyring::describe_full(serial)
+ .map_err(|e| {
+ log_debug_errno!(
+ e,
+ "Failed to describe key {} of keyring {}, ignoring: {e}",
+ serial,
+ self.name()
+ )
+ })
+ .ok();
+ members.push(Member { serial, description })?;
+ }
+
+ self.members = Some(members);
+ Ok(())
+ }
+}
+
+/// As the VOA specification requires, usage of each certificate is checked against its purpose. An artifact
+/// verifier whose extended key usage permits neither code signing nor any usage is not enrolled. A trust
+/// anchor that is not a CA certificate is still enrolled and only logged about at notice level, since a
+/// self-signed leaf certificate doubling as its own anchor is common.
+fn check_usage(path: &CStr, x: &X509, mode: VoaMode) -> Result<()> {
+ let flags = x.extension_flags();
+ if flags & sys::EXFLAG_INVALID != 0 {
+ return Err(log_warning_errno!(
+ SYNTHETIC_ERRNO(EKEYREJECTED),
+ "'{}' carries an invalid X.509 extension, ignoring.",
+ display(path)
+ ));
+ }
+
+ if mode == voa::VOA_MODE_ARTIFACT_VERIFIER {
+ if x.extended_key_usage()
+ .is_some_and(|u| u & (sys::XKU_CODE_SIGN | sys::XKU_ANYEKU) == 0)
+ {
+ return Err(log_warning_errno!(
+ SYNTHETIC_ERRNO(EKEYREJECTED),
+ "'{}' is not a code signing certificate, ignoring.",
+ display(path)
+ ));
+ }
+ if x.key_usage().is_some_and(|u| u & sys::KU_DIGITAL_SIGNATURE == 0) {
+ return Err(log_warning_errno!(
+ SYNTHETIC_ERRNO(EKEYREJECTED),
+ "'{}' does not permit digital signatures, ignoring.",
+ display(path)
+ ));
+ }
+ } else if (flags & sys::EXFLAG_BCONS != 0 && flags & sys::EXFLAG_CA == 0)
+ || x.key_usage().is_some_and(|u| u & sys::KU_KEY_CERT_SIGN == 0)
+ {
+ // A trust anchor that is explicitly not a CA is common enough to only log a notice about
+ log_notice!(
+ "'{}' is not a CA certificate, but placed among the trust anchors.",
+ display(path)
+ );
+ }
+
+ Ok(())
+}
+
+/// The kernel describes a certificate as ": ", with the subject key identifier or, lacking one,
+/// the serial number as hex. Returns the ": " part, separator included.
+fn certificate_description_suffix(x: &X509) -> Result {
+ let (id, serial) = match x.subject_key_id() {
+ Some(id) => (id, false),
+ None => (x.serial_number(), true),
+ };
+
+ // For a certificate with no subjectKeyIdentifier and a negative serial number certificate_is_enrolled()
+ // will not be able to match the key the kernel created. So on every run we would readd the certificate.
+ // And once the keyring is sealed this would also fail. While such certificates are forbidden per RFC 5280
+ // they can still be created via openssl x509 -req -set_serial -1. For such certificates openssl stores a
+ // negative V_ASN1_NEG_INTEGER plus the absolute value in ->data. Detect such certificates and reject them.
+ if serial && id.type_() == sys::V_ASN1_NEG_INTEGER as c_int {
+ return Err(Errno::EBADMSG);
+ }
+
+ let d = id.data();
+ let Some(first) = d.first() else {
+ return Err(Errno::EBADMSG);
+ };
+
+ // DER pads a serial with the top bit set with a zero byte, which the kernel keeps
+ let pad = if serial && first & 0x80 != 0 { "00" } else { "" };
+
+ cstr::try_format(format_args!(": {pad}{}", Hex(d)))
+}
+
+/// Lowercase hex, formatted without allocating.
+struct Hex<'a>(&'a [u8]);
+
+impl core::fmt::Display for Hex<'_> {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ self.0.iter().try_for_each(|b| write!(f, "{b:02x}"))
+ }
+}
+
+/// Returns true if the certificate was added, false if the same certificate was found before.
+fn load_certificate(k: &mut Keyring, f: &ConfFile, mode: VoaMode) -> Result {
+ let path = f.original_path();
+
+ let text = f
+ .fd()
+ .ok_or(Errno::EBADF)
+ .and_then(|fd| {
+ fileio::read_full_file_full(
+ fd,
+ None,
+ u64::MAX,
+ CERTIFICATE_SIZE_MAX,
+ READ_FULL_FILE_FAIL_WHEN_LARGER | READ_FULL_FILE_VERIFY_REGULAR,
+ )
+ })
+ .map_err(|e| log_warning_errno!(e, "Failed to read '{}', ignoring: {e}", display(path)))?;
+ if text.is_empty() {
+ return Err(log_warning_errno!(
+ SYNTHETIC_ERRNO(EBADMSG),
+ "'{}' is empty, ignoring.",
+ display(path)
+ ));
+ }
+
+ let (x, more) = X509::from_pem(&text)
+ .map_err(|e| log_warning_errno!(e, "Failed to parse '{}', ignoring: {e}", display(path)))?;
+
+ // Don't allow certificate bundles.
+ if more {
+ return Err(log_warning_errno!(
+ SYNTHETIC_ERRNO(EINVAL),
+ "'{}' contains more than one certificate, ignoring. Store one certificate per file, CA certificates below trust-anchor-{}/.",
+ display(path),
+ display(k.spec.role)
+ ));
+ }
+
+ check_usage(path, &x, mode)?;
+
+ let Some(der) = x.to_der() else {
+ return Err(log_openssl_errors!(
+ LOG_WARNING,
+ "Failed to encode '{}', ignoring",
+ display(path)
+ ));
+ };
+
+ // Every copy of a verifier counts, but the same certificate is enrolled once
+ if let Some(c) = k.certificates.iter().find(|c| *c.der == *der) {
+ log_debug!(
+ "'{}' is identical to '{}', skipping.",
+ display(path),
+ display(&c.path)
+ );
+ return Ok(false);
+ }
+
+ let description_suffix = certificate_description_suffix(&x).map_err(|e| {
+ log_warning_errno!(
+ e,
+ "Failed to determine the description of '{}', ignoring: {e}",
+ display(path)
+ )
+ })?;
+
+ let path = OwnedCStr::try_from(path).map_err(|_| log_oom!())?;
+ k.certificates
+ .push(Certificate {
+ path,
+ der,
+ description_suffix,
+ serial: None,
+ })
+ .map_err(|_| log_oom!())?;
+
+ Ok(true)
+}
+
+fn collect_certificates(k: &mut Keyring, root: BorrowedFd<'_>, os: &Strv) -> Result<()> {
+ // Without an identifier there is nothing to look up, the failure was reported once already and the keyring
+ // is still sealed
+ if os.is_empty() {
+ log_debug!("No OS identifier, collecting nothing for keyring {}.", k.name());
+ return Ok(());
+ }
+
+ // Trust anchors and artifact verifiers end up in the same flat keyring, the kernel validates chains itself
+ for mode in [voa::VOA_MODE_ARTIFACT_VERIFIER, voa::VOA_MODE_TRUST_ANCHOR] {
+ let lookup = Lookup {
+ os,
+ role: k.spec.role,
+ context: k.spec.context,
+ mode,
+ technology: voa::VOA_TECHNOLOGY_X509,
+ suffix: voa::VOA_X509_CERTIFICATE_SUFFIX,
+ };
+
+ let files = voa::list_verifiers(root, &lookup, voa::VOA_WARN).map_err(|e| {
+ log_error_errno!(
+ e,
+ "Failed to enumerate certificates for keyring {}: {e}",
+ k.name()
+ )
+ })?;
+
+ for f in files.iter() {
+ match load_certificate(k, f, mode) {
+ Err(e @ Errno::ENOMEM) => return Err(e),
+ Err(_) => k.data_error = true,
+ Ok(_) => {}
+ }
+ }
+ }
+
+ Ok(())
+}
+
+/// /sys/module// below root.
+fn open_module_dir(root: BorrowedFd<'_>, module: &CStr) -> Result {
+ let flags = (sys::O_PATH | sys::O_DIRECTORY | sys::O_CLOEXEC) as c_int;
+ let modules = chase::openat(root, root, c"/sys/module", 0, flags)?;
+ chase::openat(root, modules.as_fd(), module, 0, flags)
+}
+
+/// Returns true if the keyring exists and may be provisioned, false if there is nothing to do.
+fn locate_keyring(
+ k: &mut Keyring,
+ root: BorrowedFd<'_>,
+ kmod: Option<&Kmod>,
+ dry_run: bool,
+) -> Result {
+ let mut module_loaded = true;
+
+ // The keyring exists once the module is loaded. Like modprobe, libkmod applies the parameters from the
+ // kernel command line.
+ if let Some(module) = k.spec.module {
+ if dry_run {
+ // A dry run may not load the module and without it being loaded it has no way of figuring out whether
+ // the required keyring exists.
+ module_loaded = open_module_dir(root, module).is_ok();
+ } else if let Some(kmod) = kmod {
+ let _ = kmod.load_and_warn(module, false);
+ }
+ }
+
+ let r = match k.spec.spec_id {
+ // Kernels without the keyring do not know its ID
+ Some(id) => keyring::resolve(id).map_err(|e| match e {
+ Errno::EINVAL => Errno::ENOKEY,
+ e => e,
+ }),
+ None => keyring::find_by_name_at(root, k.spec.name, 0),
+ };
+ k.serial = match r {
+ Ok(serial) => serial,
+ Err(e @ Errno::ENOKEY) => {
+ // Without the module loaded the keyring cannot exist yet, a dry run cannot tell
+ k.exists = module_loaded.then_some(false);
+
+ if k.certificates.is_empty() {
+ log_debug_errno!(e, "Keyring {} not found, nothing to do: {e}", k.name());
+ } else {
+ log_notice!(
+ "Keyring {} not found, {} certificate(s) cannot be enrolled.",
+ k.name(),
+ k.certificates.len()
+ );
+ }
+ return Ok(false);
+ }
+ Err(e @ Errno::ENOTUNIQ) => {
+ return Err(log_error_errno!(
+ e,
+ "More than one keyring named {}, refusing.",
+ k.name()
+ ));
+ }
+ Err(e @ Errno::ERFKILL) => {
+ return Err(log_error_errno!(
+ e,
+ "Kernel keyrings are not accessible here, /proc/keys is masked."
+ ));
+ }
+ Err(e) => return Err(log_error_errno!(e, "Failed to look up keyring {}: {e}", k.name())),
+ };
+
+ k.exists = Some(true);
+
+ let perm = keyring::perm(k.serial)
+ .map_err(|e| log_error_errno!(e, "Failed to read permissions of keyring {}: {e}", k.name()))?;
+
+ // Restriction state is not observable only the permission mask is. So use that to figure out whether a
+ // keyring is sealed.
+ if perm == KEYRING_PERM_SEALED {
+ log_debug!("Keyring {} is sealed already.", k.name());
+ k.sealed = true;
+ } else if perm & sys::KEY_USR_SETATTR == 0 {
+ // If SetAttr is missing neither the permission mask nor the restrictions can be changed. We don't know
+ // what to do with that. So fail.
+ return Err(log_error_errno!(
+ SYNTHETIC_ERRNO(EPERM),
+ "Keyring {} has permission mask 0x{perm:08x} not set by us and SetAttr is gone, cannot repair it.",
+ k.name()
+ ));
+ } else if perm & sys::KEY_USR_WRITE == 0 {
+ return Err(log_error_errno!(
+ SYNTHETIC_ERRNO(EPERM),
+ "Keyring {} has an unexpected permission mask 0x{perm:08x}, not touching it.",
+ k.name()
+ ));
+ }
+
+ if let Err(e) = k.read_members() {
+ log_debug_errno!(e, "Failed to read keyring {}, ignoring: {e}", k.name());
+ }
+
+ Ok(true)
+}
+
+/// Returns true if the keyring may be provisioned, false if the kernel sealed it at boot.
+fn check_unsealed(k: &mut Keyring, root: BorrowedFd<'_>) -> bool {
+ let Some(param) = k.spec.param else {
+ return true;
+ };
+
+ let unsealed = open_module_dir(root, param)
+ .and_then(|d| chase::open_parent_at(root, d.as_fd(), c"parameters/keyring_unsealed", 0))
+ .and_then(|(dir, name)| fileio::read_boolean_file_at(dir.as_fd(), &name));
+ let unsealed = match unsealed {
+ Ok(unsealed) => unsealed,
+ Err(e) => {
+ // A sealed keyring refuses additions, hence enrollment tells the state, and a keyring the kernel
+ // sealed at boot is then sealed like any other
+ log_debug_errno!(
+ e,
+ "Cannot read keyring_unsealed parameter of {}, ignoring: {e}",
+ display(param)
+ );
+ return true;
+ }
+ };
+ k.kernel_unsealed = Some(unsealed);
+
+ if !unsealed {
+ if k.certificates.is_empty() {
+ log_debug!("Keyring {} was sealed at boot, nothing to do.", k.name());
+ } else {
+ log_notice!(
+ "Keyring {} was sealed at boot, {} certificate(s) cannot be enrolled. Pass {}.keyring_unsealed=1 on the kernel command line to provision it.",
+ k.name(),
+ k.certificates.len(),
+ display(param)
+ );
+ }
+
+ return false;
+ }
+
+ if k.certificates.is_empty() {
+ log_warning!(
+ "Keyring {} was left unsealed at boot, but no certificates are configured for it.",
+ k.name()
+ );
+ }
+
+ true
+}
+
+fn certificate_is_enrolled(members: &[Member], c: &Certificate) -> bool {
+ // For asymmetric keys the kernel creates a description of the form ": " if the key is added
+ // without a description. For other key types the description is whatever the creator of the key added.
+ // Such descriptions must not count as a match.
+ members.iter().any(|m| {
+ m.description.as_ref().is_some_and(|d| {
+ d.type_.as_cstr() == c"asymmetric"
+ && d.description
+ .to_bytes()
+ .ends_with(c.description_suffix.as_cstr().to_bytes())
+ })
+ })
+}
+
+/// Drops only what this invocation enrolled, so the tool may be called multiple times without wasting anyone
+/// else's keys.
+fn unlink_enrolled(k: &mut Keyring) -> Result<()> {
+ let mut r = None;
+
+ for c in &mut k.certificates {
+ let Some(serial) = c.serial else { continue };
+ let q = match keyring::unlink_key(k.serial, serial) {
+ // Superseded by a later certificate, the kernel dropped the link already
+ Err(Errno::ENOENT | Errno::ENOKEY) => Ok(()),
+ q => q,
+ };
+ if q.is_ok() {
+ c.serial = None;
+ }
+ gather(&mut r, q);
+ }
+
+ match r {
+ Some(e) => Err(log_error_errno!(
+ e,
+ "Failed to remove the enrolled keys from keyring {}: {e}",
+ k.name()
+ )),
+ None => Ok(()),
+ }
+}
+
+fn enroll_certificates(k: &mut Keyring) {
+ let hint = k.spec.param.filter(|_| k.kernel_unsealed.is_none());
+
+ // Not enrolled by this run, but sealed in with the rest. Under a seal the restriction vetted them.
+ if !k.sealed {
+ for m in k.members() {
+ let description = m.description.as_ref().map(|d| &*d.description);
+ log_warning!(
+ "Keyring {} already held key {}{}{}, it is sealed in as well.",
+ k.name(),
+ m.serial,
+ if description.is_some() { ": " } else { "" },
+ display(description.unwrap_or_default())
+ );
+ }
+ }
+
+ // In reverse: on a colliding subject and key identifier the kernel replaces the key, so the copy from the
+ // highest-priority load path and the most specific OS identifier is enrolled last and wins.
+ for i in (0..k.certificates.len()).rev() {
+ let c = &k.certificates[i];
+
+ if certificate_is_enrolled(k.members(), c) {
+ log_debug!(
+ "'{}' is in keyring {} already, skipping.",
+ display(&c.path),
+ k.name()
+ );
+ continue;
+ }
+
+ let serial = match keyring::add_asymmetric(k.serial, None, &c.der) {
+ Ok(serial) => serial,
+ Err(Errno::EPERM) => {
+ // Sealed by the kernel at boot. Nothing can be added, but the mask can still be dropped.
+ match hint {
+ Some(param) => log_notice!(
+ "Keyring {} is sealed, cannot enroll '{}'. Pass {}.keyring_unsealed=1 on the kernel command line to provision it.",
+ k.name(),
+ display(&c.path),
+ display(param)
+ ),
+ None => log_notice!(
+ "Keyring {} is sealed, cannot enroll '{}'.",
+ k.name(),
+ display(&c.path)
+ ),
+ }
+ break;
+ }
+ Err(
+ e
+ @ (Errno::EACCES | Errno::EDQUOT | Errno::ENOMEM | Errno::EKEYREVOKED | Errno::EKEYEXPIRED),
+ ) => {
+ // The keyring itself is the problem, sealing it is all that is left. What went in so far is the
+ // low-priority tail, it must not be sealed in alone.
+ log_error_errno!(
+ e,
+ "Failed to enroll '{}' into keyring {}, giving up: {e}",
+ display(&c.path),
+ k.name()
+ );
+ gather(&mut k.failed, Err(e));
+ let _ = unlink_enrolled(k);
+ break;
+ }
+ Err(e) => {
+ match e {
+ Errno::ENOKEY => log_warning_errno!(
+ e,
+ "Keyring {} is sealed and '{}' is not signed by a key enrolled in it: {e}",
+ k.name(),
+ display(&c.path)
+ ),
+ Errno::EKEYREJECTED => log_warning_errno!(
+ e,
+ "Kernel rejected certificate '{}' (blacklisted, or its signature does not verify): {e}",
+ display(&c.path)
+ ),
+ Errno::ENOPKG => log_warning_errno!(
+ e,
+ "Kernel does not support the public key algorithm of '{}': {e}",
+ display(&c.path)
+ ),
+ _ => log_warning_errno!(e, "Failed to enroll '{}': {e}", display(&c.path)),
+ };
+
+ k.data_error = true;
+ continue;
+ }
+ };
+
+ k.certificates[i].serial = Some(serial);
+
+ let description = keyring::description(serial)
+ .map_err(|e| log_debug_errno!(e, "Failed to describe enrolled key, ignoring: {e}"))
+ .ok();
+
+ log_info!(
+ "Enrolled '{}' into keyring {}{}{}.",
+ display(&k.certificates[i].path),
+ k.name(),
+ if description.is_some() { " as " } else { "" },
+ display(description.as_deref().unwrap_or_default())
+ );
+ }
+
+ // A certificate with the same subject and key identifier as a key in the keyring replaces it, which only
+ // reading the members again reveals
+ if let Err(e) = k.read_members() {
+ log_debug_errno!(e, "Failed to read keyring {}, ignoring: {e}", k.name());
+ return;
+ }
+
+ let members = k.members.as_deref().unwrap_or_default();
+ for c in &mut k.certificates {
+ if c.serial.is_some_and(|s| !members.iter().any(|m| m.serial == s)) {
+ log_info!(
+ "'{}' was superseded by a certificate of higher priority with the same subject and key identifier.",
+ display(&c.path)
+ );
+ c.serial = None;
+ }
+ }
+}
+
+fn seal_keyring(k: &mut Keyring, root: BorrowedFd<'_>) -> Result<()> {
+ // Sealing only allows certificates signed by a key already in the keyring to be added. Dropping SetAttr
+ // makes the restriction and mask immutable. The read and write permissions have to stay so keys can still
+ // be enrolled.
+ if k.sealed {
+ return Ok(());
+ }
+
+ // Needs the setattr permission that goes away below
+ match keyring::restrict(k.serial, Some(c"asymmetric"), Some(c"key_or_keyring:0:chain")) {
+ Ok(()) => {}
+ Err(Errno::EEXIST) => {
+ // Someone else already restricted this keyring.
+ log_warning!("Keyring {} carries a restriction not installed by us.", k.name());
+ k.data_error = true;
+ }
+ Err(e) => {
+ // If we fail to enroll certificates drop every certificate we have enrolled in this invocation. We
+ // don't drop anything else so this tool may be called multiple times without wasting anyone else's
+ // keys.
+ let q = unlink_enrolled(k);
+ return Err(log_error_errno!(
+ e,
+ "Failed to restrict keyring {}, leaving it unsealed and writable{}: {e}",
+ k.name(),
+ if q.is_err() {
+ " with keys enrolled"
+ } else {
+ ", the enrolled keys removed"
+ }
+ ));
+ }
+ }
+
+ // Note that if we fail to restrict the keyring above we also skip permission changes. If we were to remove
+ // permissions we would lose the ability to restrict the keyring completely.
+ keyring::set_perm(k.serial, KEYRING_PERM_SEALED)
+ .map_err(|e| log_error_errno!(e, "Failed to drop permissions of keyring {}: {e}", k.name()))?;
+
+ k.sealed = true;
+
+ match &k.members {
+ Some(members) => log_info!(
+ "Sealed keyring {} with {} key(s), {} enrolled now.",
+ k.name(),
+ members.len(),
+ k.n_enrolled()
+ ),
+ None => log_info!("Sealed keyring {}, {} enrolled now.", k.name(), k.n_enrolled()),
+ }
+
+ // Unloading a kernel module may destroy the keyring and circumvent the security model. Warn about that.
+ if let Some(module) = k.spec.module {
+ let loadable = open_module_dir(root, module)
+ .and_then(|d| chase::accessat(root, d.as_fd(), c"refcnt", 0, sys::F_OK as c_int));
+ if loadable.is_ok() {
+ log_warning!(
+ "Keyring {} belongs to the loadable {} module: unloading the module may discard the keyring and its seal, and reloading it creates one that can be provisioned again. Build {} into the kernel to keep the enrolled set for the lifetime of the kernel.",
+ k.name(),
+ display(module),
+ display(module)
+ );
+ }
+ }
+
+ Ok(())
+}
+
+fn add_row(t: &mut Table, k: &Keyring, paths: &Strv) -> Result<()> {
+ t.add_string(k.spec.name)?;
+ t.add_string(k.spec.context)?;
+ match k.exists {
+ Some(exists) => t.add_boolean_checkmark(exists)?,
+ None => t.add_empty()?,
+ }
+ t.add_tristate(k.kernel_unsealed)?;
+ match &k.members {
+ Some(members) => t.add_uint64(members.len() as u64)?,
+ None => t.add_empty()?,
+ }
+ t.add_boolean_checkmark(k.sealed)?;
+ t.add_strv(paths)
+}
+
+fn add_to_table(t: &mut Table, k: &Keyring) -> Result<()> {
+ let mut paths = Strv::new();
+ for c in &k.certificates {
+ paths.push(&c.path).map_err(|_| log_oom!())?;
+ }
+
+ add_row(t, k, &paths).map_err(|e| table_log_add_error!(e))
+}
+
+/// Returns true if a certificate could not be used.
+fn process_keyring(
+ kmod: Option<&Kmod>,
+ spec: &'static KeyringSpec,
+ root: BorrowedFd<'_>,
+ os: &Strv,
+ t: Option<&mut Table>,
+ args: &Args,
+) -> Result {
+ let mut k = Keyring::new(spec);
+
+ // An error enumerating the certificates does not stop the sealing
+ let r = collect_certificates(&mut k, root, os);
+ gather(&mut k.failed, r);
+
+ let mut r = locate_keyring(&mut k, root, kmod, args.dry_run);
+ if matches!(r, Ok(true)) {
+ r = Ok(check_unsealed(&mut k, root));
+ }
+
+ if args.dry_run {
+ if matches!(r, Ok(true)) {
+ let n = k
+ .certificates
+ .iter()
+ .filter(|c| !certificate_is_enrolled(k.members(), c))
+ .count();
+ log_info!("Would enroll {} certificate(s) into keyring {}.", n, k.name());
+ }
+
+ if let Some(t) = t {
+ let q = add_to_table(t, &k);
+ if r.is_ok() {
+ q?;
+ }
+ }
+ } else if matches!(r, Ok(true)) {
+ enroll_certificates(&mut k);
+ r = seal_keyring(&mut k, root).map(|()| true);
+ }
+
+ r?;
+ if let Some(e) = k.failed {
+ return Err(e);
+ }
+
+ Ok(k.data_error)
+}
+
+/// Credentials are the way to hand a certificate to an initrd without rebuilding it. Following the
+/// specification's advice for verifiers retrieved from elsewhere, they are placed into the ephemeral load path
+/// as artifact verifiers. Hence, masking, merging and everything else applies to them like to any other file.
+/// Returns false if the credential is not a single certificate.
+fn materialize_credential(
+ root: BorrowedFd<'_>,
+ creds: BorrowedFd<'_>,
+ cn: &CStr,
+ spec: &KeyringSpec,
+ name: &CStr,
+ os: &Strv,
+ dry_run: bool,
+) -> Result {
+ // The exact OS identifier suffices
+ let os = os.iter().next().ok_or(Errno::EINVAL)?;
+ let path = cstr::try_format(format_args!(
+ "{}/{}/{}/{}/{}/{}{}",
+ display(voa::VOA_EPHEMERAL_LOAD_PATH),
+ display(os),
+ display(spec.role),
+ display(spec.context),
+ display(voa::VOA_TECHNOLOGY_X509),
+ display(name),
+ display(voa::VOA_X509_CERTIFICATE_SUFFIX)
+ ))
+ .map_err(|_| log_oom!())?;
+
+ if dry_run {
+ log_info!("Would place credential '{}' at '{path}'.", display(cn));
+ return Ok(true);
+ }
+
+ let contents = creds::read_credential_at(creds, cn)
+ .map_err(|e| log_error_errno!(e, "Failed to read credential '{}': {e}", display(cn)))?;
+
+ // Only the certificate goes into the world-readable hierarchy, not a private key that came along
+ let x = match X509::from_pem(&contents) {
+ Ok((x, false)) => x,
+ Ok((_, true)) => {
+ log_warning!(
+ "Credential '{}' contains more than one certificate, ignoring.",
+ display(cn)
+ );
+ return Ok(false);
+ }
+ Err(Errno::ENOMEM) => return Err(log_oom!()),
+ Err(e) => {
+ log_warning_errno!(e, "Failed to parse credential '{}', ignoring: {e}", display(cn));
+ return Ok(false);
+ }
+ };
+ let pem = x
+ .to_pem()
+ .map_err(|e| log_error_errno!(e, "Failed to encode credential '{}': {e}", display(cn)))?;
+
+ let (dir, base) =
+ chase::open_parent_at(root, root, &path, CHASE_MKDIR_0755 | CHASE_SAFE | CHASE_MAX_MODE)
+ .map_err(|e| log_error_errno!(e, "Failed to create the directory of '{path}': {e}"))?;
+
+ let tmp = LinkableTmpfile::open_at(dir.as_fd(), &base, (sys::O_WRONLY | sys::O_CLOEXEC) as c_int)
+ .map_err(|e| log_error_errno!(e, "Failed to create '{path}': {e}"))?;
+
+ fd::loop_write(tmp.fd(), pem.as_cstr().to_bytes())
+ .map_err(|e| log_error_errno!(e, "Failed to write '{path}': {e}"))?;
+
+ fd::fchmod(tmp.fd(), 0o644)
+ .map_err(|e| log_error_errno!(e, "Failed to set the mode of '{path}': {e}"))?;
+
+ tmp.link(&base, LINK_TMPFILE_REPLACE)
+ .map_err(|e| log_error_errno!(e, "Failed to link '{path}' into place: {e}"))?;
+
+ log_debug!("Placed credential '{}' at '{path}'.", display(cn));
+ Ok(true)
+}
+
+/// Returns true if a credential could not be used.
+fn materialize_credentials(
+ root: BorrowedFd<'_>,
+ creds_dir: Result>,
+ os: &Strv,
+ args: &Args,
+) -> Result {
+ let creds_dir = match creds_dir {
+ Ok(fd) => fd,
+ Err(Errno::ENXIO) => return Ok(false),
+ Err(e) => return Err(log_error_errno!(e, "Failed to open credentials directory: {e}")),
+ };
+
+ if os.is_empty() {
+ log_warning!("There is no OS identifier to place credentials under, ignoring them.");
+ return Ok(false);
+ }
+
+ let de = recurse_dir::readdir_all(creds_dir, RECURSE_DIR_SORT | RECURSE_DIR_IGNORE_DOT)
+ .map_err(|e| log_error_errno!(e, "Failed to read credentials directory: {e}"))?;
+
+ let mut ret = None;
+ let mut data_error = false;
+ for cn in de.names() {
+ let Some(e) = cstr::strip_prefix(cn, CREDENTIAL_PREFIX.as_bytes()) else {
+ continue;
+ };
+ if e == c"os" {
+ continue;
+ }
+
+ // keyring-setup.., the keyring without its leading dot
+ let found = KEYRING_SPECS.iter().find_map(|s| {
+ let name = cstr::strip_prefix(e, s.name.to_bytes().strip_prefix(b".")?)?;
+ Some((s, cstr::strip_prefix(name, b".")?))
+ });
+ let Some((spec, name)) = found else {
+ log_warning!(
+ "Ignoring unrecognized credential '{}', expected {CREDENTIAL_PREFIX}..",
+ display(cn)
+ );
+ continue;
+ };
+ if !voa::identifier_is_valid(name, false) {
+ log_warning!(
+ "Ignoring credential '{}', the name must consist of lowercase letters, digits, '.', '_' and '-'.",
+ display(cn)
+ );
+ continue;
+ }
+ if !args.selected(spec) {
+ log_debug!(
+ "Skipping credential '{}', keyring {} is not selected.",
+ display(cn),
+ display(spec.name)
+ );
+ continue;
+ }
+
+ match materialize_credential(root, creds_dir, cn, spec, name, os, args.dry_run) {
+ Ok(placed) => data_error |= !placed,
+ Err(e) => gather(&mut ret, Err(e)),
+ }
+ }
+
+ ret.map_or(Ok(data_error), Err)
+}
+
+/// Returns the identifiers if the credential exists.
+fn os_from_credential(creds_dir: Result>) -> Result