keyring-setup: check certificate usage against the VOA purpose

The specification requires the extended key usage of a verifier to be
aligned with its purpose. The kernel has no concept of this. So do it
in systemd-keyring-setup.

An artifact verifier whose extended key usage permits neither code
signing nor any usage is rejected. A trust anchor that is explicitly
not a CA is still enrolled and only logged at notice level because
self-signed leaf certificates doubling as their own anchor are common.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner committed 2026-10-26 18:55:53 +01:00
1 parent b98913f4b8
commit fb6167ca1f
2 files changed
+58 -6

No files matched your search

+9 -2
View File
@@ -113,8 +113,8 @@
<filename><replaceable>OS</replaceable>/kernel-keyring/<replaceable>CONTEXT</replaceable>/x509/<replaceable>NAME</replaceable>-certificate.pem</filename>,
and trust anchors from
<filename><replaceable>OS</replaceable>/trust-anchor-kernel-keyring/<replaceable>CONTEXT</replaceable>/x509/<replaceable>NAME</replaceable>-certificate.pem</filename>
below each load path. Both are enrolled into the same keyring. <replaceable>NAME</replaceable> may consist
of lowercase letters, digits,
below each load path. Both are enrolled into the same keyring, and only differ in how their usage is
checked, see below. <replaceable>NAME</replaceable> may consist of lowercase letters, digits,
<literal>.</literal>, <literal>_</literal> and <literal>-</literal>, 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.</para>
@@ -152,6 +152,13 @@
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.</para>
<para>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.</para>
</refsect1>
<refsect1>
+49 -4
View File
@@ -21,7 +21,7 @@ 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::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};
@@ -227,6 +227,50 @@ impl Keyring {
}
}
/// 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 "<subject>: <hex>", with the subject key identifier or, lacking one,
/// the serial number as hex. Returns the ": <hex>" part, separator included.
fn certificate_description_suffix(x: &X509) -> Result<OwnedCStr> {
@@ -265,7 +309,7 @@ impl core::fmt::Display for Hex<'_> {
}
/// Returns true if the certificate was added, false if the same certificate was found before.
fn load_certificate(k: &mut Keyring, f: &ConfFile) -> Result<bool> {
fn load_certificate(k: &mut Keyring, f: &ConfFile, mode: VoaMode) -> Result<bool> {
let path = f.original_path();
let text = f
@@ -302,7 +346,8 @@ fn load_certificate(k: &mut Keyring, f: &ConfFile) -> Result<bool> {
));
}
// The kernel takes DER
check_usage(path, &x, mode)?;
let Some(der) = x.to_der() else {
return Err(log_openssl_errors!(
LOG_WARNING,
@@ -370,7 +415,7 @@ fn collect_certificates(k: &mut Keyring, root: BorrowedFd<'_>, os: &Strv) -> Res
})?;
for f in files.iter() {
match load_certificate(k, f) {
match load_certificate(k, f, mode) {
Err(e @ Errno::ENOMEM) => return Err(e),
Err(_) => k.data_error = true,
Ok(_) => {}