mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
keyring-setup: accept certificates via system credentials
Allow the use of credentials (SMBIOS, ESP credential drop-ins, --set-credential for the boot loader) to ship keys to enroll into keyrings. This lets users avoid having to ship keys as part of the initrd. So accept keyring-setup.<keyring>.<name> credentials holding one PEM certificate each. We don't enroll them directly but follow the VOA spec instead. Before enrolling they are placed into the ephemeral load path as: /run/voa/<os>/kernel-keyring/<context>/x509/<name>-certificate.pem This also makes masking, merging and deduplication apply to them like to any other file. Only the certificate is written there, encoded anew, so a private key that came along in the credential doesn't end up world-readable. A credential that isn't a single certificate is bad input. A keyring-setup.os credential overrides the OS identifiers derived from os-release. This is useful for generic initrds whose identity differs from the host's. Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
1 parent
24548be0bf
commit
b98913f4b8
5 files changed
+309
-18
No files matched your search
@@ -129,7 +129,8 @@
|
||||
<literal>debian:13</literal> and <literal>debian</literal>. If <varname>ID=</varname> is not set,
|
||||
<literal>linux</literal> is used. If one of the other fields contains characters that are not permitted by
|
||||
the VOA specification, only the value of <varname>ID=</varname> is used. If <varname>ID=</varname> itself
|
||||
contains such characters, no certificates are looked up.</para>
|
||||
contains such characters, no certificates are looked up. The <varname>keyring-setup.os</varname> credential
|
||||
may be used to specify the identifiers explicitly, see below.</para>
|
||||
|
||||
<para>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
|
||||
@@ -153,6 +154,52 @@
|
||||
else.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Credentials</title>
|
||||
|
||||
<para><command>systemd-keyring-setup</command> supports the service credentials logic as implemented by
|
||||
<varname>ImportCredential=</varname>/<varname>LoadCredential=</varname>/<varname>SetCredential=</varname>
|
||||
(see <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
|
||||
details). The following credentials are used when passed in:</para>
|
||||
|
||||
<variablelist class='system-credentials'>
|
||||
<varlistentry>
|
||||
<term><varname>keyring-setup.dm-verity.<replaceable>NAME</replaceable></varname></term>
|
||||
<term><varname>keyring-setup.fs-verity.<replaceable>NAME</replaceable></varname></term>
|
||||
<term><varname>keyring-setup.bpf.<replaceable>NAME</replaceable></varname></term>
|
||||
|
||||
<listitem><para>A PEM-encoded X.509 certificate to enroll into the <literal>.dm-verity</literal>,
|
||||
<literal>.fs-verity</literal> or <literal>.bpf</literal> keyring, respectively.
|
||||
<replaceable>NAME</replaceable> may consist of lowercase letters, digits, <literal>.</literal>,
|
||||
<literal>_</literal> and <literal>-</literal>. As suggested by the VOA specification for certificates
|
||||
obtained from elsewhere, the certificate is not enrolled directly, but is first written to
|
||||
<filename>/run/voa/<replaceable>OS</replaceable>/kernel-keyring/<replaceable>CONTEXT</replaceable>/x509/<replaceable>NAME</replaceable>-certificate.pem</filename>,
|
||||
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.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>keyring-setup.os</varname></term>
|
||||
|
||||
<listitem><para>A whitespace-separated list of up to 64 OS identifiers to use instead of the ones
|
||||
derived from <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
|
||||
This is useful for generic initrds, whose <filename>os-release</filename> 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 <filename>os-release</filename> are used.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<para>See
|
||||
<citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry>
|
||||
for details about system credentials.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Options</title>
|
||||
|
||||
@@ -184,7 +231,7 @@
|
||||
<title>Exit status</title>
|
||||
|
||||
<para>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 could not be used, for example
|
||||
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
|
||||
(<constant>EX_DATAERR</constant>) is returned. <filename>systemd-keyring-setup.service</filename> treats this
|
||||
exit status as success. On other failures, a different non-zero exit status is returned.</para>
|
||||
@@ -237,6 +284,7 @@
|
||||
<member><citerefentry><refentrytitle>systemd-veritysetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-dissect</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
|
||||
<member><citerefentry project='man-pages'><refentrytitle>keyctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
|
||||
@@ -119,6 +119,35 @@
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>keyring-setup.dm-verity.<replaceable>NAME</replaceable></varname></term>
|
||||
<term><varname>keyring-setup.fs-verity.<replaceable>NAME</replaceable></varname></term>
|
||||
<term><varname>keyring-setup.bpf.<replaceable>NAME</replaceable></varname></term>
|
||||
<listitem>
|
||||
<para>A PEM-encoded X.509 certificate to enroll into the <literal>.dm-verity</literal>,
|
||||
<literal>.fs-verity</literal> or <literal>.bpf</literal> kernel keyring, respectively (e.g.
|
||||
<varname>keyring-setup.dm-verity.vendor</varname>). Read by
|
||||
<citerefentry><refentrytitle>systemd-keyring-setup.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
|
||||
which writes the certificate to <filename>/run/voa/</filename> before enrolling it, see there for
|
||||
details. <replaceable>NAME</replaceable> may consist of lowercase letters, digits, <literal>.</literal>,
|
||||
<literal>_</literal> and <literal>-</literal>.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>keyring-setup.os</varname></term>
|
||||
<listitem>
|
||||
<para>A whitespace-separated list of OS identifiers (e.g. <literal>debian:13 debian</literal>) under
|
||||
which certificates are looked up in the VOA hierarchy, instead of the ones derived from
|
||||
<citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>. Read by
|
||||
<citerefentry><refentrytitle>systemd-keyring-setup.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>login.issue</varname></term>
|
||||
<listitem>
|
||||
|
||||
@@ -10,18 +10,20 @@ use core::ffi::{c_int, CStr};
|
||||
use core::ptr;
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
use systemd_shared::chase;
|
||||
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};
|
||||
use systemd_shared::x509::{self, Der, X509};
|
||||
use systemd_shared::{fd, json, sys};
|
||||
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);
|
||||
@@ -43,6 +45,12 @@ const KEYRING_PERM_SEALED: u32 =
|
||||
|
||||
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.
|
||||
@@ -834,6 +842,187 @@ fn process_keyring(
|
||||
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<bool> {
|
||||
// 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<BorrowedFd<'_>>,
|
||||
os: &Strv,
|
||||
args: &Args,
|
||||
) -> Result<bool> {
|
||||
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.<keyring>.<name>, 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}<keyring>.<name>.",
|
||||
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<BorrowedFd<'_>>) -> Result<Option<Strv>> {
|
||||
let v = match creds_dir.and_then(|d| creds::read_credential_string_at(d, c"keyring-setup.os")) {
|
||||
Ok(v) => v,
|
||||
Err(Errno::ENXIO | Errno::ENOENT) => return Ok(None),
|
||||
Err(e) => {
|
||||
return Err(log_warning_errno!(
|
||||
e,
|
||||
"Failed to read credential {CREDENTIAL_PREFIX}os, ignoring: {e}"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let l = Strv::split(v.as_cstr(), sys::WHITESPACE, sys::EXTRACT_RETAIN_ESCAPE).map_err(|_| log_oom!())?;
|
||||
if l.is_empty() {
|
||||
return Err(log_warning_errno!(
|
||||
SYNTHETIC_ERRNO(EINVAL),
|
||||
"Credential {CREDENTIAL_PREFIX}os is empty, ignoring."
|
||||
));
|
||||
}
|
||||
if l.len() > CREDENTIAL_OS_MAX {
|
||||
return Err(log_warning_errno!(
|
||||
SYNTHETIC_ERRNO(EINVAL),
|
||||
"Credential {CREDENTIAL_PREFIX}os lists more than {CREDENTIAL_OS_MAX} OS identifiers, ignoring it."
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(i) = l.iter().find(|i| !voa::os_is_valid(i)) {
|
||||
return Err(log_warning_errno!(
|
||||
SYNTHETIC_ERRNO(EINVAL),
|
||||
"Invalid OS identifier '{}' in credential {CREDENTIAL_PREFIX}os, ignoring it.",
|
||||
display(i)
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(l))
|
||||
}
|
||||
|
||||
fn parse_argv(opts: &mut OptionParser<'_>, args: &mut Args) -> Result<c_int> {
|
||||
foreach_option! { opts,
|
||||
OPTION_COMMON_HELP => return command_print_help!(),
|
||||
@@ -903,24 +1092,44 @@ fn run(argv: Argv<'_>) -> Result<c_int> {
|
||||
|
||||
let mut outcome = Outcome::Success;
|
||||
|
||||
let os = match voa::os_identifiers(root.as_fd()) {
|
||||
Ok((l, bare)) => {
|
||||
if bare {
|
||||
log_warning!(
|
||||
"os-release contains characters the VOA specification does not permit, looking up the bare ID only."
|
||||
);
|
||||
// Its users treat its absence differently
|
||||
let creds = creds::open_credentials_dir_at(root.as_fd());
|
||||
let creds_dir = creds.as_ref().map(OwnedFd::as_fd).map_err(|&e| e);
|
||||
|
||||
let os = match os_from_credential(creds_dir) {
|
||||
Ok(Some(l)) => l,
|
||||
r => {
|
||||
if r.is_err() {
|
||||
// Bad input, but os-release is still there
|
||||
outcome.data_error();
|
||||
}
|
||||
match voa::os_identifiers(root.as_fd()) {
|
||||
Ok((l, bare)) => {
|
||||
if bare {
|
||||
log_warning!(
|
||||
"os-release contains characters the VOA specification does not permit, looking up the bare ID only. Pass the {CREDENTIAL_PREFIX}os credential to specify identifiers explicitly."
|
||||
);
|
||||
}
|
||||
l
|
||||
}
|
||||
Err(e) => {
|
||||
outcome.fail(log_error_errno!(
|
||||
e,
|
||||
"Failed to determine the OS identifier from os-release, enrolling nothing: {e}"
|
||||
));
|
||||
Strv::new()
|
||||
}
|
||||
}
|
||||
l
|
||||
}
|
||||
Err(e) => {
|
||||
outcome.fail(log_error_errno!(
|
||||
e,
|
||||
"Failed to determine the OS identifier from os-release, enrolling nothing: {e}"
|
||||
));
|
||||
Strv::new()
|
||||
}
|
||||
};
|
||||
|
||||
// Sealing does not depend on it either
|
||||
match materialize_credentials(root.as_fd(), creds_dir, &os, &args) {
|
||||
Ok(true) => outcome.data_error(),
|
||||
Ok(false) => {}
|
||||
Err(e) => outcome.fail(e),
|
||||
}
|
||||
|
||||
let mut t = if args.dry_run {
|
||||
let mut t = Table::new(&[
|
||||
c"keyring",
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
#include "process-util.h"
|
||||
#include "recurse-dir.h"
|
||||
#include "rm-rf.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "tests.h"
|
||||
|
||||
@@ -26,6 +26,7 @@ ConditionVirtualization=!container
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
SuccessExitStatus=DATAERR
|
||||
ImportCredential=keyring-setup.*
|
||||
ExecStart={{LIBEXECDIR}}/systemd-keyring-setup
|
||||
|
||||
# Loading dm_verity is the only privileged operation, the keyrings belong to root anyway
|
||||
@@ -46,6 +47,9 @@ RestrictAddressFamilies=AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
# The ephemeral load path credentials are placed into, kept for the consumers of the hierarchy
|
||||
RuntimeDirectory=voa
|
||||
RuntimeDirectoryPreserve=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallErrorNumber=EPERM
|
||||
SystemCallFilter=@system-service @module
|
||||
Reference in new issue
Block a user