diff --git a/man/systemd-keyring-setup.service.xml b/man/systemd-keyring-setup.service.xml
index ad674204258..ee061d39674 100644
--- a/man/systemd-keyring-setup.service.xml
+++ b/man/systemd-keyring-setup.service.xml
@@ -113,8 +113,8 @@
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. NAME 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. 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.
@@ -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.
+
+ 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.
diff --git a/src/keyring-setup/keyring-setup.rs b/src/keyring-setup/keyring-setup.rs
index e352cf55012..27a56b07dca 100644
--- a/src/keyring-setup/keyring-setup.rs
+++ b/src/keyring-setup/keyring-setup.rs
@@ -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 ": ", 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 {
@@ -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 {
+fn load_certificate(k: &mut Keyring, f: &ConfFile, mode: VoaMode) -> Result {
let path = f.original_path();
let text = f
@@ -302,7 +346,8 @@ fn load_certificate(k: &mut Keyring, f: &ConfFile) -> Result {
));
}
- // 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(_) => {}