[rfc] [patch] PR28204: debuginfod ima signature verification
Frank Ch. Eigler
fche@elastic.org
Wed Apr 3 21:04:56 GMT 2024
Hi -
The following raw diff reworks this long-blocked patch to overcome
these three objections last fall:
- to drop "permissive" mode
- to stop redistributing published distro ima certificates
- to not use libimaevm.so (due to concurrency / licensing concerns)
This is a raw diff only. I'll be proposing some changes shortly
downthread.
diff --git a/config/Makefile.am b/config/Makefile.am
index ae14e625b726..5a28e66d4408 100644
--- a/config/Makefile.am
+++ b/config/Makefile.am
@@ -46,12 +46,16 @@ pkgconfig_DATA += libdebuginfod.pc
if [ -n "@DEBUGINFOD_URLS@" ]; then \
echo "@DEBUGINFOD_URLS@" > $(DESTDIR)$(sysconfdir)/debuginfod/elfutils.urls; \
fi
+ if [ -n "@DEBUGINFOD_IMA_CERT_PATH@" ]; then \
+ echo "@DEBUGINFOD_IMA_CERT_PATH@" > $(DESTDIR)$(sysconfdir)/debuginfod/elfutils.certpath; \
+ fi
uninstall-local:
rm -f $(DESTDIR)$(sysconfdir)/profile.d/debuginfod.sh
rm -f $(DESTDIR)$(sysconfdir)/profile.d/debuginfod.csh
rm -f $(DESTDIR)$(datadir)/fish/vendor_conf.d/debuginfod.fish
rm -f $(DESTDIR)$(sysconfdir)/debuginfod/elfutils.urls
+ rm -f $(DESTDIR)$(sysconfdir)/debuginfod/elfutils.certpath
-rmdir $(DESTDIR)$(sysconfdir)/debuginfod
endif
diff --git a/config/elfutils.spec.in b/config/elfutils.spec.in
index 4d802a25ad5f..460729972420 100644
--- a/config/elfutils.spec.in
+++ b/config/elfutils.spec.in
@@ -43,6 +43,12 @@ BuildRequires: curl
# For run-debuginfod-response-headers.sh test case
BuildRequires: socat
+# For debuginfod rpm IMA verification
+BuildRequires: rpm-devel
+BuildRequires: ima-evm-utils-devel
+BuildRequires: openssl-devel
+BuildRequires: rpm-sign
+
%define _gnu %{nil}
%define _programprefix eu-
diff --git a/config/profile.csh.in b/config/profile.csh.in
index d962d969c05b..1da9626c711b 100644
--- a/config/profile.csh.in
+++ b/config/profile.csh.in
@@ -4,13 +4,19 @@
# See also [man debuginfod-client-config] for other environment variables
# such as $DEBUGINFOD_MAXSIZE, $DEBUGINFOD_MAXTIME, $DEBUGINFOD_PROGRESS.
+set prefix="@prefix@"
if (! $?DEBUGINFOD_URLS) then
- set prefix="@prefix@"
set DEBUGINFOD_URLS=`sh -c 'cat /dev/null "$0"/*.urls 2>/dev/null; :' "@sysconfdir@/debuginfod" | tr '\n' ' '`
if ( "$DEBUGINFOD_URLS" != "" ) then
setenv DEBUGINFOD_URLS "$DEBUGINFOD_URLS"
else
unset DEBUGINFOD_URLS
endif
- unset prefix
+ set DEBUGINFOD_IMA_CERT_PATH=`sh -c 'cat /dev/null "$0"/*.certpath 2>/dev/null; :' "@sysconfdir@/debuginfod" | tr '\n' ':'`
+ if ( "$DEBUGINFOD_IMA_CERT_PATH" != "" ) then
+ setenv DEBUGINFOD_IMA_CERT_PATH "$DEBUGINFOD_IMA_CERT_PATH"
+ else
+ unset DEBUGINFOD_IMA_CERT_PATH
+ endif
endif
+unset prefix
diff --git a/config/profile.sh.in b/config/profile.sh.in
index 84d3260ddcfc..7db399960915 100644
--- a/config/profile.sh.in
+++ b/config/profile.sh.in
@@ -4,9 +4,15 @@
# See also [man debuginfod-client-config] for other environment variables
# such as $DEBUGINFOD_MAXSIZE, $DEBUGINFOD_MAXTIME, $DEBUGINFOD_PROGRESS.
+prefix="@prefix@"
if [ -z "$DEBUGINFOD_URLS" ]; then
prefix="@prefix@"
DEBUGINFOD_URLS=$(cat /dev/null "@sysconfdir@/debuginfod"/*.urls 2>/dev/null | tr '\n' ' ' || :)
[ -n "$DEBUGINFOD_URLS" ] && export DEBUGINFOD_URLS || unset DEBUGINFOD_URLS
- unset prefix
fi
+
+if [ -z "$DEBUGINFOD_IMA_CERT_PATH" ]; then
+ DEBUGINFOD_IMA_CERT_PATH=$(cat "@sysconfdir@/debuginfod"/*.certpath 2>/dev/null | tr '\n' ':' || :)
+ [ -n "$DEBUGINFOD_IMA_CERT_PATH" ] && export DEBUGINFOD_IMA_CERT_PATH || unset DEBUGINFOD_IMA_CERT_PATH
+fi
+unset prefix
diff --git a/configure.ac b/configure.ac
index a279bb5282c9..19ccf107494b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -667,6 +667,35 @@ case "$ac_cv_search__obstack_free" in
esac
AC_SUBST([obstack_LIBS])
+enable_ima_verification="x"
+AC_CHECK_LIB(rpm, headerGet, [
+ AC_CHECK_DECL(RPMSIGTAG_FILESIGNATURES,
+ [
+ enable_ima_verification=$enable_ima_verification"rpm"
+ AC_SUBST(rpm_LIBS, '-lrpm -lrpmio')
+ ],
+ [], [#include <rpm/rpmlib.h>])
+])
+
+dnl we use only the header, not the code of this library
+AC_CHECK_HEADER(imaevm.h, [
+ enable_ima_verification=$enable_ima_verification"imaevm"
+])
+
+AC_CHECK_LIB(crypto, EVP_MD_CTX_new, [
+ enable_ima_verification=$enable_ima_verification"crypto"
+ AC_SUBST(crypto_LIBS, '-lcrypto')
+])
+
+debuginfod_ima_verification_enabled="no"
+if test "$enable_ima_verification" = "xrpmimaevmcrypto"; then
+ debuginfod_ima_verification_enabled="yes"
+ default_ima_cert_path=`eval echo "/etc/keys/ima:/etc/pki/rpm-ima:$sysconfdir/debuginfod/ima-certs"` # expand $prefix too
+ AC_DEFINE([ENABLE_IMA_VERIFICATION], [1], [Define if the required ima verification libraries are available])
+ AC_DEFINE_UNQUOTED(DEBUGINFOD_IMA_CERT_PATH_DEFAULT, "$default_ima_cert_path", [Default IMA certificate path])
+fi
+AM_CONDITIONAL([ENABLE_IMA_VERIFICATION],[test "$enable_ima_verification" = "xrpmimaevmcrypto"])
+
dnl The directories with content.
dnl Documentation.
@@ -881,6 +910,15 @@ AC_ARG_ENABLE(debuginfod-urls,
fi],
[default_debuginfod_urls=""])
AC_SUBST(DEBUGINFOD_URLS, $default_debuginfod_urls)
+AC_ARG_ENABLE(debuginfod-ima-cert-path,
+ [AS_HELP_STRING([--enable-debuginfod-ima-cert-path@<:@=PATH@:>@],[add PATH to profile.d DEBUGINFOD_IMA_CERT_PATH])],
+ [if test "x${enableval}" = "xyes";
+ then AC_MSG_ERROR([PATH required])
+ elif test "x${enableval}" != "xno"; then
+ default_debuginfod_ima_cert_path="${enableval}";
+ fi],
+ [default_debuginfod_ima_cert_path=""])
+AC_SUBST(DEBUGINFOD_IMA_CERT_PATH, $default_debuginfod_ima_cert_path)
AC_CONFIG_FILES([config/profile.sh config/profile.csh config/profile.fish])
AC_OUTPUT
@@ -920,6 +958,7 @@ AC_MSG_NOTICE([
libdebuginfod client support : ${enable_libdebuginfod}
Debuginfod server support : ${enable_debuginfod}
Default DEBUGINFOD_URLS : ${default_debuginfod_urls}
+ Debuginfod RPM sig checking : ${debuginfod_ima_verification_enabled} ${default_debuginfod_ima_cert_path}
EXTRA TEST FEATURES (used with make check)
have bunzip2 installed (required) : ${HAVE_BUNZIP2}
diff --git a/debuginfod/ChangeLog b/debuginfod/ChangeLog
index 0e4810bba501..f4d98c2e93bc 100644
--- a/debuginfod/ChangeLog
+++ b/debuginfod/ChangeLog
@@ -1,3 +1,17 @@
+2023-08-14 Ryan Goldberg <rgoldber@redhat.com>
+
+ * debuginfod.cxx (handle_buildid_r_match): Added extraction of the
+ per-file IMA signature for the queried file and store in http header.
+ * (find_globbed_koji_filepath): New function.
+ * (parse_opt): New flag --koji-sigcache.
+ * debuginfod-client.c (debuginfod_query_server): Added policy for
+ validating IMA signatures
+ * (debuginfod_validate_imasig): New function.
+ * debuginfod.h.in: Added DEBUGINFOD_IMA_CERT_PATH_ENV_VAR.
+ * Makefile.am: Add linker flags for rpm and imaevm and crypto. Also add install/uninstall
+ ima-certs/ to known location.
+ * ima-certs/: New directory containing known ima verification certificates.
+
2023-04-21 Frank Ch. Eigler <fche@redhat.com>
* debuginfod.cxx (groom): Fix -r / -X logic.
diff --git a/debuginfod/Makefile.am b/debuginfod/Makefile.am
index 125be97bbfcc..5e4f9669d7c1 100644
--- a/debuginfod/Makefile.am
+++ b/debuginfod/Makefile.am
@@ -70,7 +70,7 @@ bin_PROGRAMS += debuginfod-find
endif
debuginfod_SOURCES = debuginfod.cxx
-debuginfod_LDADD = $(libdw) $(libelf) $(libeu) $(libdebuginfod) $(argp_LDADD) $(fts_LIBS) $(libmicrohttpd_LIBS) $(sqlite3_LIBS) $(libarchive_LIBS) -lpthread -ldl
+debuginfod_LDADD = $(libdw) $(libelf) $(libeu) $(libdebuginfod) $(argp_LDADD) $(fts_LIBS) $(libmicrohttpd_LIBS) $(sqlite3_LIBS) $(libarchive_LIBS) $(rpm_LIBS) -lpthread -ldl
debuginfod_find_SOURCES = debuginfod-find.c
debuginfod_find_LDADD = $(libdw) $(libelf) $(libeu) $(libdebuginfod) $(argp_LDADD) $(fts_LIBS)
@@ -97,7 +97,7 @@ libdebuginfod_so_LIBS = libdebuginfod_pic.a
if DUMMY_LIBDEBUGINFOD
libdebuginfod_so_LDLIBS =
else
-libdebuginfod_so_LDLIBS = -lpthread $(libcurl_LIBS) $(fts_LIBS) $(libelf)
+libdebuginfod_so_LDLIBS = -lpthread $(libcurl_LIBS) $(fts_LIBS) $(libelf) $(crypto_LIBS)
endif
$(LIBDEBUGINFOD_SONAME): $(srcdir)/libdebuginfod.map $(libdebuginfod_so_LIBS)
$(AM_V_CCLD)$(LINK) $(dso_LDFLAGS) -o $@ \
@@ -117,7 +117,6 @@ install: install-am libdebuginfod.so
$(DESTDIR)$(libdir)/libdebuginfod-$(PACKAGE_VERSION).so
ln -fs libdebuginfod-$(PACKAGE_VERSION).so $(DESTDIR)$(libdir)/$(LIBDEBUGINFOD_SONAME)
ln -fs libdebuginfod-$(PACKAGE_VERSION).so $(DESTDIR)$(libdir)/libdebuginfod.so
-
uninstall: uninstall-am
rm -f $(DESTDIR)$(libdir)/libdebuginfod-$(PACKAGE_VERSION).so
rm -f $(DESTDIR)$(libdir)/$(LIBDEBUGINFOD_SONAME)
diff --git a/debuginfod/debuginfod-client.c b/debuginfod/debuginfod-client.c
index 0ee7db3d6638..4618234f0718 100644
--- a/debuginfod/debuginfod-client.c
+++ b/debuginfod/debuginfod-client.c
@@ -1,5 +1,5 @@
/* Retrieve ELF / DWARF / source files from the debuginfod.
- Copyright (C) 2019-2021 Red Hat, Inc.
+ Copyright (C) 2019-2024 Red Hat, Inc.
Copyright (C) 2021, 2022 Mark J. Wielaard <mark@klomp.org>
This file is part of elfutils.
@@ -47,6 +47,17 @@
#include <stdlib.h>
#include <gelf.h>
+#ifdef ENABLE_IMA_VERIFICATION
+#include <openssl/sha.h>
+#include <openssl/pem.h>
+#include <openssl/evp.h>
+#include <openssl/x509v3.h>
+#include <arpa/inet.h>
+#include <imaevm.h>
+#endif
+typedef enum {ignore, enforcing, undefined} ima_policy_t;
+
+
/* We might be building a bootstrap dummy library, which is really simple. */
#ifdef DUMMY_LIBDEBUGINFOD
@@ -92,6 +103,7 @@ void debuginfod_end (debuginfod_client *c) { }
#include <sys/stat.h>
#include <sys/utsname.h>
#include <curl/curl.h>
+#include <fnmatch.h>
/* If fts.h is included before config.h, its indirect inclusions may not
give us the right LFS aliases of these functions, so map them manually. */
@@ -114,6 +126,8 @@ void debuginfod_end (debuginfod_client *c) { }
#include <pthread.h>
+
+
static pthread_once_t init_control = PTHREAD_ONCE_INIT;
static void
@@ -122,6 +136,17 @@ libcurl_init(void)
curl_global_init(CURL_GLOBAL_DEFAULT);
}
+
+#ifdef ENABLE_IMA_VERIFICATION
+struct public_key_entry
+{
+ struct public_key_entry *next; /* singly-linked list */
+ uint32_t keyid; /* last 4 bytes of sha1 of public key */
+ EVP_PKEY *key; /* openssl */
+};
+#endif
+
+
struct debuginfod_client
{
/* Progress/interrupt callback function. */
@@ -156,8 +181,14 @@ struct debuginfod_client
handle data, etc. So those don't have to be reparsed and
recreated on each request. */
char * winning_headers;
+
+#ifdef ENABLE_IMA_VERIFICATION
+ /* IMA public keys */
+ struct public_key_entry *ima_public_keys;
+#endif
};
+
/* The cache_clean_interval_s file within the debuginfod cache specifies
how frequently the cache should be cleaned. The file's st_mtime represents
the time of last cleaning. */
@@ -217,6 +248,179 @@ struct handle_data
size_t response_data_size;
};
+
+
+#ifdef ENABLE_IMA_VERIFICATION
+ static inline unsigned char hex2dec(char c)
+ {
+ if (c >= '0' && c <= '9') return (c - '0');
+ if (c >= 'a' && c <= 'f') return (c - 'a') + 10;
+ if (c >= 'A' && c <= 'F') return (c - 'A') + 10;
+ return 0;
+ }
+
+ static inline ima_policy_t ima_policy_str2enum(const char* ima_pol)
+ {
+ if (NULL == ima_pol) return undefined;
+ if (0 == strcmp(ima_pol, "ignore")) return ignore;
+ if (0 == strcmp(ima_pol, "enforcing")) return enforcing;
+ return undefined;
+ }
+
+ static inline const char* ima_policy_enum2str(ima_policy_t ima_pol)
+ {
+ switch (ima_pol)
+ {
+ case ignore:
+ return "ignore";
+ case enforcing:
+ return "enforcing";
+ case undefined:
+ return "undefined";
+ }
+ return "";
+ }
+
+
+static uint32_t extract_skid_pk(EVP_PKEY *pkey) // compute keyid by public key hashing
+{
+ if (!pkey) return 0;
+ uint32_t keyid = 0;
+ X509_PUBKEY *pk = NULL;
+ const unsigned char *public_key = NULL;
+ int len;
+ if (X509_PUBKEY_set(&pk, pkey) &&
+ X509_PUBKEY_get0_param(NULL, &public_key, &len, NULL, pk))
+ {
+ uint8_t sha1[SHA_DIGEST_LENGTH];
+ SHA1(public_key, len, sha1);
+ memcpy(&keyid, sha1 + 16, 4);
+ }
+ X509_PUBKEY_free(pk);
+ return ntohl(keyid);
+}
+
+
+static uint32_t extract_skid(X509* x509) // compute keyid from cert or its public key
+ {
+ if (!x509) return 0;
+ uint32_t keyid = 0;
+ // Attempt to get the skid from the certificate
+ const ASN1_OCTET_STRING *skid_asn1_str = X509_get0_subject_key_id(x509);
+ if (skid_asn1_str)
+ {
+ int skid_len = ASN1_STRING_length(skid_asn1_str);
+ memcpy(&keyid, ASN1_STRING_get0_data(skid_asn1_str) + skid_len - sizeof(keyid), sizeof(keyid));
+ }
+ else // compute keyid ourselves by hashing public key
+ {
+ EVP_PKEY *pkey = X509_get0_pubkey(x509);
+ keyid = htonl(extract_skid_pk(pkey));
+ }
+ return ntohl(keyid);
+ }
+
+
+static void load_ima_public_keys (debuginfod_client *c)
+{
+ /* Iterate over the directories in DEBUGINFOD_IMA_CERT_PATH. */
+ char *cert_paths = strdup (getenv(DEBUGINFOD_IMA_CERT_PATH_ENV_VAR) ?: DEBUGINFOD_IMA_CERT_PATH_DEFAULT);
+ if (!cert_paths)
+ return;
+
+ char* cert_dir_path;
+ DIR *dp;
+ struct dirent *entry;
+ int vfd = c->verbose_fd;
+
+ char *strtok_context = NULL;
+ for(cert_dir_path = strtok_r(cert_paths, ":", &strtok_context);
+ cert_dir_path != NULL;
+ cert_dir_path = strtok_r(NULL, ":", &strtok_context))
+ {
+ dp = opendir(cert_dir_path);
+ if(!dp) continue;
+ while((entry = readdir(dp)))
+ {
+ // Only consider regular files with common x509 cert extensions
+ if(entry->d_type != DT_REG || 0 != fnmatch("*.@(der|pem|crt|cer|cert)", entry->d_name, FNM_EXTMATCH)) continue;
+ char certfile[PATH_MAX];
+ strncpy(certfile, cert_dir_path, PATH_MAX - 1);
+ if(certfile[strlen(certfile)-1] != '/') certfile[strlen(certfile)] = '/';
+ strncat(certfile, entry->d_name, PATH_MAX - strlen(certfile) - 1);
+ certfile[strlen(certfile)] = '\0';
+
+ FILE *cert_fp = fopen(certfile, "r");
+ if(!cert_fp) continue;
+
+ X509 *x509 = NULL;
+ EVP_PKEY *pkey = NULL;
+ char *fmt = "";
+ // Attempt to read the fp as DER
+ if(d2i_X509_fp(cert_fp, &x509))
+ fmt = "der ";
+ // Attempt to read the fp as PEM and assuming the key matches that of the signature add this key to be used
+ // Note we fseek since this is the second time we read from the fp
+ else if(0 == fseek(cert_fp, 0, SEEK_SET) && PEM_read_X509(cert_fp, &x509, NULL, NULL))
+ fmt = "pem "; // PEM with full certificate
+ else if(0 == fseek(cert_fp, 0, SEEK_SET) && PEM_read_PUBKEY(cert_fp, &pkey, NULL, NULL))
+ fmt = "pem "; // some PEM files have just a PUBLIC KEY in them
+ fclose(cert_fp);
+
+ if (x509)
+ {
+ struct public_key_entry *ne = calloc(1, sizeof(struct public_key_entry));
+ if (ne)
+ {
+ ne->key = X509_extract_key(x509);
+ ne->keyid = extract_skid(x509);
+ ne->next = c->ima_public_keys;
+ c->ima_public_keys = ne;
+ if (vfd >= 0)
+ dprintf(vfd, "Loaded %scertificate %s, keyid = %04x\n", fmt, certfile, ne->keyid);
+ }
+ X509_free (x509);
+ }
+ else if (pkey)
+ {
+ struct public_key_entry *ne = calloc(1, sizeof(struct public_key_entry));
+ if (ne)
+ {
+ ne->key = pkey; // preserve refcount
+ ne->keyid = extract_skid_pk(pkey);
+ ne->next = c->ima_public_keys;
+ c->ima_public_keys = ne;
+ if (vfd >= 0)
+ dprintf(vfd, "Loaded %spubkey %s, keyid = %04x\n", fmt, certfile, ne->keyid);
+ }
+ }
+ else
+ {
+ if (vfd >= 0)
+ dprintf(vfd, "Cannot load certificate %s\n", certfile);
+ }
+ } /* for each file in directory */
+ closedir(dp);
+ } /* for each directory */
+
+ free(cert_paths);
+}
+
+
+static void free_ima_public_keys (debuginfod_client *c)
+{
+ while (c->ima_public_keys)
+ {
+ EVP_PKEY_free (c->ima_public_keys->key);
+ struct public_key_entry *oen = c->ima_public_keys->next;
+ free (c->ima_public_keys);
+ c->ima_public_keys = oen;
+ }
+}
+#endif
+
+
+
static size_t
debuginfod_write_callback (char *ptr, size_t size, size_t nmemb, void *data)
{
@@ -853,6 +1057,198 @@ cache_find_section (const char *scn_name, const char *target_cache_dir,
return rc;
}
+
+#ifdef ENABLE_IMA_VERIFICATION
+/* Extract the hash algorithm name from the signature header, of which
+ there are several types. The name will be used for openssl hashing
+ of the file content. The header doesn't need to be super carefully
+ parsed, because if any part of it is wrong, be it the hash
+ algorithm number or hash value or whatever, it will fail
+ computation or verification. Return NULL in case of error. */
+static const char*
+get_signature_params(debuginfod_client *c, unsigned char *bin_sig)
+{
+ int hashalgo = 0;
+
+ switch (bin_sig[0])
+ {
+ case EVM_IMA_XATTR_DIGSIG:
+#ifdef IMA_VERITY_DIGSIG /* missing on debian-i386 trybot */
+ case IMA_VERITY_DIGSIG:
+#endif
+ break;
+ default:
+ if (c->verbose_fd >= 0)
+ dprintf (c->verbose_fd, "Unknown ima digsig %d\n", (int)bin_sig[0]);
+ return NULL;
+ }
+
+ switch (bin_sig[1])
+ {
+ case DIGSIG_VERSION_2:
+ struct signature_v2_hdr hdr_v2;
+ memcpy(& hdr_v2, & bin_sig[1], sizeof(struct signature_v2_hdr));
+ hashalgo = hdr_v2.hash_algo;
+ break;
+ default:
+ if (c->verbose_fd >= 0)
+ dprintf (c->verbose_fd, "Unknown ima signature version %d\n", (int)bin_sig[1]);
+ return NULL;
+ }
+
+ switch (hashalgo)
+ {
+ case PKEY_HASH_SHA1: return "sha1";
+ case PKEY_HASH_SHA256: return "sha256";
+ // (could add many others from enum pkey_hash_algo)
+ default:
+ if (c->verbose_fd >= 0)
+ dprintf (c->verbose_fd, "Unknown ima pkey hash %d\n", hashalgo);
+ return NULL;
+ }
+}
+
+
+/* Verify given hash against given signature blob */
+static int
+debuginfod_verify_hash(debuginfod_client *c, const unsigned char *hash, int size,
+ const char *hash_algo, unsigned char *sig, int siglen)
+{
+ int ret = -EINVAL;
+ struct public_key_entry *pkey;
+ struct signature_v2_hdr hdr;
+ EVP_PKEY_CTX *ctx;
+ const EVP_MD *md;
+
+ memcpy(&hdr, sig, sizeof(struct signature_v2_hdr)); /* avoid just aliasing */
+
+ /* Find the matching public key. */
+ for (pkey = c->ima_public_keys; pkey != NULL; pkey = pkey->next)
+ if (pkey->keyid == ntohl(hdr.keyid)) break;
+ if (!pkey)
+ return -ENOKEY;
+
+ if (!(ctx = EVP_PKEY_CTX_new(pkey->key, NULL)))
+ goto err;
+ if (!EVP_PKEY_verify_init(ctx))
+ goto err;
+ if (!(md = EVP_get_digestbyname(hash_algo)))
+ goto err;
+ if (!EVP_PKEY_CTX_set_signature_md(ctx, md))
+ goto err;
+ ret = EVP_PKEY_verify(ctx, sig + sizeof(hdr),
+ siglen - sizeof(hdr), hash, size);
+ if (ret == 1)
+ ret = 0;
+ else if (ret == 0)
+ ret = -EINVAL;
+err:
+ if (ret < 0 || ret > 1)
+ ret = -EINVAL;
+ EVP_PKEY_CTX_free(ctx);
+ return ret;
+}
+
+
+
+/* Validate an IMA file signature.
+ * Returns 0 on signature validity, -EINVAL on signature invalidity, -ENOSYS on undefined imaevm machinery,
+ * -ENOKEY on key issues, or other -errno.
+ */
+
+static int
+debuginfod_validate_imasig (debuginfod_client *c, int fd)
+{
+ int rc = ENOSYS;
+
+ // int vfd = c->verbose_fd;
+ EVP_MD_CTX *ctx = NULL;
+ if (!c || !c->winning_headers)
+ {
+ rc = -ENODATA;
+ goto exit_validate;
+ }
+ // Extract the HEX IMA-signature from the header
+ char* sig_buf = NULL;
+ char* hdr_ima_sig = strcasestr(c->winning_headers, "x-debuginfod-imasignature");
+ if (!hdr_ima_sig || 1 != sscanf(hdr_ima_sig + strlen("x-debuginfod-imasignature:"), "%ms", &sig_buf))
+ {
+ rc = -ENODATA;
+ goto exit_validate;
+ }
+ if (strlen(sig_buf) > MAX_SIGNATURE_SIZE) // reject if too long
+ {
+ rc = -EBADMSG;
+ goto exit_validate;
+ }
+ // Convert the hex signature to bin
+ size_t bin_sig_len = strlen(sig_buf)/2;
+ unsigned char bin_sig[MAX_SIGNATURE_SIZE/2];
+ for (size_t b = 0; b < bin_sig_len; b++)
+ bin_sig[b] = (hex2dec(sig_buf[2*b]) << 4) | hex2dec(sig_buf[2*b+1]);
+
+ // Compute the binary digest of the cached file (with file descriptor fd)
+ ctx = EVP_MD_CTX_new();
+ const char* sighash_name = get_signature_params(c, bin_sig) ?: "";
+ const EVP_MD *md = EVP_get_digestbyname(sighash_name);
+ if (!ctx || !md || !EVP_DigestInit(ctx, md))
+ {
+ rc = -EBADMSG;
+ goto exit_validate;
+ }
+
+ long data_len;
+ char* hdr_data_len = strcasestr(c->winning_headers, "x-debuginfod-size");
+ if (!hdr_data_len || 1 != sscanf(hdr_data_len + strlen("x-debuginfod-size:") , "%ld", &data_len))
+ {
+ rc = -ENODATA;
+ goto exit_validate;
+ }
+
+ char file_data[DATA_SIZE]; // imaevm.h data chunk hash size
+ ssize_t n;
+ for(off_t k = 0; k < data_len; k += n)
+ {
+ if (-1 == (n = pread(fd, file_data, DATA_SIZE, k)))
+ {
+ rc = -errno;
+ goto exit_validate;
+ }
+
+ if (!EVP_DigestUpdate(ctx, file_data, n))
+ {
+ rc = -EBADMSG;
+ goto exit_validate;
+ }
+ }
+
+ uint8_t bin_dig[MAX_DIGEST_SIZE];
+ unsigned int bin_dig_len;
+ if (!EVP_DigestFinal(ctx, bin_dig, &bin_dig_len))
+ {
+ rc = -EBADMSG;
+ goto exit_validate;
+ }
+
+ // XXX: in case of DIGSIG_VERSION_3, need to hash the file hash, yo dawg
+
+ int res = debuginfod_verify_hash(c,
+ bin_dig, bin_dig_len,
+ sighash_name,
+ & bin_sig[1], bin_sig_len-1); // skip over first byte of signature
+ if (c->verbose_fd >= 0)
+ dprintf (c->verbose_fd, "Computed ima signature verification res=%d\n", res);
+ rc = (res == 1) ? -EINVAL : res;
+
+ exit_validate:
+ free (sig_buf);
+ EVP_MD_CTX_free(ctx);
+ return rc;
+}
+#endif /* ENABLE_IMA_VERIFICATION */
+
+
+
/* Query each of the server URLs found in $DEBUGINFOD_URLS for the file
with the specified build-id and type (debuginfo, executable, source or
section). If type is source, then type_arg should be a filename. If
@@ -1208,12 +1604,39 @@ debuginfod_query_server (debuginfod_client *c,
/* Initialize the memory to zero */
char *strtok_saveptr;
char **server_url_list = NULL;
- char *server_url = strtok_r(server_urls, url_delim, &strtok_saveptr);
+ ima_policy_t* url_ima_policies = NULL;
+ char* server_url;
/* Count number of URLs. */
int num_urls = 0;
- while (server_url != NULL)
+ ima_policy_t verification_mode = ignore; // The default mode
+ for(server_url = strtok_r(server_urls, url_delim, &strtok_saveptr);
+ server_url != NULL; server_url = strtok_r(NULL, url_delim, &strtok_saveptr))
{
+ // When we encounted a (well-formed) token off the form ima:foo, we update the policy
+ // under which results from that server will be ima verified
+ if(startswith(server_url, "ima:"))
+ {
+#ifdef ENABLE_IMA_VERIFICATION
+ ima_policy_t m = ima_policy_str2enum(server_url + strlen("ima:"));
+ if(m != undefined)
+ verification_mode = m;
+ else if (vfd >= 0)
+ dprintf(vfd, "IMA mode not recognized, skipping %s\n", server_url);
+#else
+ if (vfd >= 0)
+ dprintf(vfd, "IMA signature verification is not enabled, skipping %s\n", server_url);
+#endif
+ continue; // Not a url, just a mode change so keep going
+ }
+
+ if (verification_mode==enforcing && 0==strcmp(type,"section"))
+ {
+ if (vfd >= 0)
+ dprintf(vfd, "skipping server %s section query in IMA enforcing mode\n", server_url);
+ continue;
+ }
+
/* PR 27983: If the url is already set to be used use, skip it */
char *slashbuildid;
if (strlen(server_url) > 1 && server_url[strlen(server_url)-1] == '/')
@@ -1245,21 +1668,28 @@ debuginfod_query_server (debuginfod_client *c,
else
{
num_urls++;
- char ** realloc_ptr;
- realloc_ptr = reallocarray(server_url_list, num_urls,
- sizeof(char*));
- if (realloc_ptr == NULL)
+ if (NULL == (server_url_list = reallocarray(server_url_list, num_urls, sizeof(char*)))
+#ifdef ENABLE_IMA_VERIFICATION
+ || NULL == (url_ima_policies = reallocarray(url_ima_policies, num_urls, sizeof(ima_policy_t)))
+#endif
+ )
{
free (tmp_url);
rc = -ENOMEM;
goto out1;
}
- server_url_list = realloc_ptr;
server_url_list[num_urls-1] = tmp_url;
+ if(NULL != url_ima_policies) url_ima_policies[num_urls-1] = verification_mode;
}
- server_url = strtok_r(NULL, url_delim, &strtok_saveptr);
}
+ /* No URLs survived parsing / filtering? Abort abort abort. */
+ if (num_urls == 0)
+ {
+ rc = -ENOSYS;
+ goto out1;
+ }
+
int retry_limit = default_retry_limit;
const char* retry_limit_envvar = getenv(DEBUGINFOD_RETRY_LIMIT_ENV_VAR);
if (retry_limit_envvar != NULL)
@@ -1326,7 +1756,11 @@ debuginfod_query_server (debuginfod_client *c,
if ((server_url = server_url_list[i]) == NULL)
break;
if (vfd >= 0)
- dprintf (vfd, "init server %d %s\n", i, server_url);
+#ifdef ENABLE_IMA_VERIFICATION
+ dprintf (vfd, "init server %d %s [IMA verification policy: %s]\n", i, server_url, ima_policy_enum2str(url_ima_policies[i]));
+#else
+ dprintf (vfd, "init server %d %s\n", i, server_url);
+#endif
data[i].fd = fd;
data[i].target_handle = &target_handle;
@@ -1774,6 +2208,33 @@ debuginfod_query_server (debuginfod_client *c,
/* PR31248: lseek back to beginning */
(void) lseek(fd, 0, SEEK_SET);
+ if(NULL != url_ima_policies && ignore != url_ima_policies[committed_to])
+ {
+#ifdef ENABLE_IMA_VERIFICATION
+ int result = debuginfod_validate_imasig(c, fd);
+#else
+ int result = -ENOSYS;
+#endif
+ if(0 == result)
+ {
+ if (vfd >= 0) dprintf (vfd, "valid signature\n");
+ }
+ else if(EINVAL == result || enforcing == url_ima_policies[committed_to])
+ {
+ // All invalid signatures are rejected.
+ // Additionally in enforcing mode any non-valid signature is rejected, so by reaching
+ // this case we do so since we know it is not valid. Note - this not just invalid signatures
+ // but also signatures that cannot be validated
+ if (vfd >= 0) dprintf (vfd, "error: invalid or missing signature (%d)\n", result);
+ rc = -EBADMSG;
+ goto out2;
+ }
+ else
+ {
+ // NOTREACHED
+ }
+ }
+
/* rename tmp->real */
rc = rename (target_cache_tmppath, target_cache_path);
if (rc < 0)
@@ -1794,6 +2255,7 @@ debuginfod_query_server (debuginfod_client *c,
for (int i = 0; i < num_urls; ++i)
free(server_url_list[i]);
free(server_url_list);
+ free(url_ima_policies);
free (data);
free (server_urls);
@@ -1827,6 +2289,7 @@ debuginfod_query_server (debuginfod_client *c,
for (int i = 0; i < num_urls; ++i)
free(server_url_list[i]);
free(server_url_list);
+ free(url_ima_policies);
out0:
free (server_urls);
@@ -1859,7 +2322,11 @@ debuginfod_query_server (debuginfod_client *c,
free (cache_miss_path);
free (target_cache_dir);
free (target_cache_path);
+ if (rc < 0 && target_cache_tmppath != NULL)
+ (void)unlink (target_cache_tmppath);
free (target_cache_tmppath);
+
+
return rc;
}
@@ -1891,6 +2358,10 @@ debuginfod_begin (void)
goto out1;
}
+#ifdef ENABLE_IMA_VERIFICATION
+ load_ima_public_keys (client);
+#endif
+
// extra future initialization
goto out;
@@ -1938,6 +2409,9 @@ debuginfod_end (debuginfod_client *client)
curl_slist_free_all (client->headers);
free (client->winning_headers);
free (client->url);
+#ifdef ENABLE_IMA_VERIFICATION
+ free_ima_public_keys (client);
+#endif
free (client);
}
@@ -1977,9 +2451,11 @@ debuginfod_find_section (debuginfod_client *client,
{
int rc = debuginfod_query_server(client, build_id, build_id_len,
"section", section, path);
- if (rc != -EINVAL)
+ if (rc != -EINVAL && rc != -ENOSYS)
return rc;
-
+ /* NB: we fall through in case of ima:enforcing-filtered DEBUGINFOD_URLS servers,
+ so we can download the entire file, verify it locally, then slice it. */
+
/* The servers may have lacked support for section queries. Attempt to
download the debuginfo or executable containing the section in order
to extract it. */
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index ece5031f02f9..30c818dd24bf 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -122,6 +122,13 @@ using namespace std;
#define MHD_RESULT int
#endif
+#ifdef ENABLE_IMA_VERIFICATION
+ #include <rpm/rpmlib.h>
+ #include <rpm/rpmfi.h>
+ #include <rpm/header.h>
+ #include <glob.h>
+#endif
+
#include <curl/curl.h>
#include <archive.h>
#include <archive_entry.h>
@@ -443,6 +450,10 @@ static const struct argp_option options[] =
{ "disable-source-scan", ARGP_KEY_DISABLE_SOURCE_SCAN, NULL, 0, "Do not scan dwarf source info.", 0 },
#define ARGP_SCAN_CHECKPOINT 0x100A
{ "scan-checkpoint", ARGP_SCAN_CHECKPOINT, "NUM", 0, "Number of files scanned before a WAL checkpoint.", 0 },
+#ifdef ENABLE_IMA_VERIFICATION
+#define ARGP_KEY_KOJI_SIGCACHE 0x100B
+ { "koji-sigcache", ARGP_KEY_KOJI_SIGCACHE, NULL, 0, "Do a koji specific mapping of rpm paths to get IMA signatures.", 0 },
+#endif
{ NULL, 0, NULL, 0, NULL, 0 },
};
@@ -495,6 +506,9 @@ static bool scan_source_info = true;
static string tmpdir;
static bool passive_p = false;
static long scan_checkpoint = 256;
+#ifdef ENABLE_IMA_VERIFICATION
+static bool requires_koji_sigcache_mapping = false;
+#endif
static void set_metric(const string& key, double value);
static void inc_metric(const string& key);
@@ -699,6 +713,11 @@ parse_opt (int key, char *arg,
if (scan_checkpoint < 0)
argp_failure(state, 1, EINVAL, "scan checkpoint");
break;
+#ifdef ENABLE_IMA_VERIFICATION
+ case ARGP_KEY_KOJI_SIGCACHE:
+ requires_koji_sigcache_mapping = true;
+ break;
+#endif
// case 'h': argp_state_help (state, stderr, ARGP_HELP_LONG|ARGP_HELP_EXIT_OK);
default: return ARGP_ERR_UNKNOWN;
}
@@ -1959,6 +1978,146 @@ handle_buildid_r_match (bool internal_req_p,
return 0;
}
+ // Extract the IMA per-file signature (if it exists)
+ string ima_sig = "";
+ #ifdef ENABLE_IMA_VERIFICATION
+ do
+ {
+ FD_t rpm_fd;
+ if(!(rpm_fd = Fopen(b_source0.c_str(), "r.ufdio"))) // read, uncompressed, rpm/rpmio.h
+ {
+ if (verbose) obatched(clog) << "There was an error while opening " << b_source0 << endl;
+ break; // Exit IMA extraction
+ }
+
+ Header rpm_hdr;
+ if(RPMRC_FAIL == rpmReadPackageFile(NULL, rpm_fd, b_source0.c_str(), &rpm_hdr))
+ {
+ if (verbose) obatched(clog) << "There was an error while reading the header of " << b_source0 << endl;
+ Fclose(rpm_fd);
+ break; // Exit IMA extraction
+ }
+
+ // Fill sig_tag_data with an alloc'd copy of the array of IMA signatures (if they exist)
+ struct rpmtd_s sig_tag_data;
+ rpmtdReset(&sig_tag_data);
+ do{ /* A do-while so we can break out of the koji sigcache checking on failure */
+ if(requires_koji_sigcache_mapping)
+ {
+ /* NB: Koji builds result in a directory structure like the following
+ - PACKAGE/VERSION/RELEASE
+ - ARCH1
+ - foo.rpm // The rpm known by debuginfod
+ - ...
+ - ARCHN
+ - data
+ - signed // Periodically purged (and not scanned by debuginfod)
+ - sigcache
+ - ARCH1
+ - foo.rpm.sig // An empty rpm header
+ - ...
+ - ARCHN
+ - PACKAGE_KEYID1
+ - ARCH1
+ - foo.rpm.sig // The header of the signed rpm. This is the file we need to extract the IMA signatures
+ - ...
+ - ARCHN
+ - ...
+ - PACKAGE_KEYIDn
+
+ We therefore need to do a mapping:
+
+ P/V/R/A/N-V-R.A.rpm ->
+ P/V/R/data/sigcache/KEYID/A/N-V-R.A.rpm.sig
+
+ There are 2 key insights here
+
+ 1. We need to go 2 directories down from sigcache to get to the
+ rpm header. So to distinguish ARCH1/foo.rpm.sig and
+ PACKAGE_KEYID1/ARCH1/foo.rpm.sig we can look 2 directories down
+
+ 2. It's safe to assume that the user will have all of the
+ required verification certs. So we can pick from any of the
+ PACKAGE_KEYID* directories. For simplicity we choose first we
+ match against
+
+ See: https://pagure.io/koji/issue/3670
+ */
+
+ // Do the mapping from b_source0 to the koji path for the signed rpm header
+ string signed_rpm_path = b_source0;
+ size_t insert_pos = string::npos;
+ for(int i = 0; i < 2; i++) insert_pos = signed_rpm_path.rfind("/", insert_pos) - 1;
+ string globbed_path = signed_rpm_path.insert(insert_pos + 1, "/data/sigcache/*").append(".sig"); // The globbed path we're seeking
+ glob_t pglob;
+ int grc;
+ if(0 != (grc = glob(globbed_path.c_str(), GLOB_NOSORT, NULL, &pglob)))
+ {
+ // Break out, but only report real errors
+ if (verbose && grc != GLOB_NOMATCH) obatched(clog) << "There was an error (" << strerror(errno) << ") globbing " << globbed_path << endl;
+ break; // Exit koji sigcache check
+ }
+ signed_rpm_path = pglob.gl_pathv[0]; // See insight 2 above
+ globfree(&pglob);
+
+ if (verbose > 2) obatched(clog) << "attempting IMA signature extraction from koji header " << signed_rpm_path << endl;
+
+ FD_t sig_rpm_fd;
+ if(NULL == (sig_rpm_fd = Fopen(signed_rpm_path.c_str(), "r")))
+ {
+ if (verbose) obatched(clog) << "There was an error while opening " << signed_rpm_path << endl;
+ break; // Exit koji sigcache check
+ }
+
+ Header sig_hdr = headerRead(sig_rpm_fd, HEADER_MAGIC_YES /* Validate magic too */ );
+ if (!sig_hdr || 1 != headerGet(sig_hdr, RPMSIGTAG_FILESIGNATURES, &sig_tag_data, HEADERGET_ALLOC))
+ {
+ if (verbose) obatched(clog) << "Unable to extract RPMSIGTAG_FILESIGNATURES from " << signed_rpm_path << endl;
+ }
+ headerFree(sig_hdr); // We can free here since sig_tag_data has an alloc'd copy of the data
+ Fclose(sig_rpm_fd);
+ }
+ }while(false);
+
+ if(0 == sig_tag_data.count)
+ {
+ // In the general case (or a fallback from the koji sigcache mapping not finding signatures)
+ // we can just (try) extract the signatures from the rpm header
+ if (1 != headerGet(rpm_hdr, RPMTAG_FILESIGNATURES, &sig_tag_data, HEADERGET_ALLOC))
+ {
+ if (verbose) obatched(clog) << "Unable to extract RPMTAG_FILESIGNATURES from " << b_source0 << endl;
+ }
+ }
+ // Search the array for the signature coresponding to b_source1
+ int idx = -1;
+ char *sig = NULL;
+ rpmfi hdr_fi = rpmfiNew(NULL, rpm_hdr, RPMTAG_BASENAMES, RPMFI_FLAGS_QUERY);
+ do
+ {
+ sig = (char*)rpmtdNextString(&sig_tag_data);
+ idx = rpmfiNext(hdr_fi);
+ }
+ while (idx != -1 && 0 != strcmp(b_source1.c_str(), rpmfiFN(hdr_fi)));
+ rpmfiFree(hdr_fi);
+
+ if(sig && 0 != strlen(sig) && idx != -1)
+ {
+ if (verbose > 2) obatched(clog) << "Found IMA signature for " << b_source1 << ":\n" << sig << endl;
+ ima_sig = sig;
+ inc_metric("http_responses_total","extra","ima-sigs-extracted");
+ }
+ else
+ {
+ if (verbose > 2) obatched(clog) << "Could not find IMA signature for " << b_source1 << endl;
+ }
+
+ rpmtdFreeData (&sig_tag_data);
+ headerFree(rpm_hdr);
+ Fclose(rpm_fd);
+ }
+ while(false);
+ #endif
+
// check for a match in the fdcache first
int fd = fdcache.lookup(b_source0, b_source1);
while (fd >= 0) // got one!; NB: this is really an if() with a possible branch out to the end
@@ -2016,11 +2175,13 @@ handle_buildid_r_match (bool internal_req_p,
to_string(fs.st_size).c_str());
add_mhd_response_header (r, "X-DEBUGINFOD-ARCHIVE", b_source0.c_str());
add_mhd_response_header (r, "X-DEBUGINFOD-FILE", b_source1.c_str());
+ if(!ima_sig.empty()) add_mhd_response_header(r, "X-DEBUGINFOD-IMASIGNATURE", ima_sig.c_str());
add_mhd_last_modified (r, fs.st_mtime);
if (verbose > 1)
obatched(clog) << "serving fdcache archive " << b_source0
<< " file " << b_source1
- << " section=" << section << endl;
+ << " section=" << section
+ << " IMA signature=" << ima_sig << endl;
/* libmicrohttpd will close it. */
if (result_fd)
*result_fd = fd;
@@ -2204,11 +2365,13 @@ handle_buildid_r_match (bool internal_req_p,
to_string(archive_entry_size(e)).c_str());
add_mhd_response_header (r, "X-DEBUGINFOD-ARCHIVE", b_source0.c_str());
add_mhd_response_header (r, "X-DEBUGINFOD-FILE", b_source1.c_str());
+ if(!ima_sig.empty()) add_mhd_response_header(r, "X-DEBUGINFOD-IMASIGNATURE", ima_sig.c_str());
add_mhd_last_modified (r, archive_entry_mtime(e));
if (verbose > 1)
obatched(clog) << "serving archive " << b_source0
<< " file " << b_source1
- << " section=" << section << endl;
+ << " section=" << section
+ << " IMA signature=" << ima_sig << endl;
/* libmicrohttpd will close it. */
if (result_fd)
*result_fd = fd;
diff --git a/debuginfod/debuginfod.h.in b/debuginfod/debuginfod.h.in
index 4a256ba9af1f..73f633f0b8e9 100644
--- a/debuginfod/debuginfod.h.in
+++ b/debuginfod/debuginfod.h.in
@@ -39,6 +39,7 @@
#define DEBUGINFOD_MAXSIZE_ENV_VAR "DEBUGINFOD_MAXSIZE"
#define DEBUGINFOD_MAXTIME_ENV_VAR "DEBUGINFOD_MAXTIME"
#define DEBUGINFOD_HEADERS_FILE_ENV_VAR "DEBUGINFOD_HEADERS_FILE"
+#define DEBUGINFOD_IMA_CERT_PATH_ENV_VAR "DEBUGINFOD_IMA_CERT_PATH"
/* The libdebuginfod soname. */
#define DEBUGINFOD_SONAME "@LIBDEBUGINFOD_SONAME@"
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 7f2d6ff4fd31..914f8f649511 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -1,3 +1,10 @@
+2023-08-14 Ryan Goldberg <rgoldber@redhat.com>
+
+ * debuginfod-client-config.7: Document DEBUGINFOD_IMA_CERT_PATH,
+ update DEBUGINFOD_URLS.
+ * debuginfod.8: Document --koji-sigcache
+ * debuginfod-find.1, debuginfod_find_debuginfo.3: Update SECURITY
+
2023-02-14 Mark Wielaard <mark@klomp.org>
* debuginfod.8: Add .TP before -g.
diff --git a/doc/debuginfod-client-config.7 b/doc/debuginfod-client-config.7
index 53d82806d395..f16612084e9b 100644
--- a/doc/debuginfod-client-config.7
+++ b/doc/debuginfod-client-config.7
@@ -27,6 +27,33 @@ debuginfod instances. Alternate URL prefixes are separated by space.
This environment variable may be set by /etc/profile.d scripts
reading /etc/debuginfod/*.urls files.
+This environment variable can also contain policy defining tags which
+dictate the response policy for verifying per-file IMA signatures in
+RPMs. As the space seperated list is read left to right, upon
+encountering a tag, subsequent URLs up to the next tag will be handled
+using that specified policy. All URLs before the first tag will use
+the default policy, \fIima:ignore\fP. For example:
+
+.in +4n
+.EX
+DEBUGINFOD_URLS="https://foo.com ima:enforcing https://bar.ca http://localhost:8002/ ima:ignore https://baz.org"
+.EE
+.in
+
+Where foo.com and baz.org use the default \fIignore\fP policy and
+bar.ca and localhost use an \fIenforcing\fP policy. The policy tag
+may be one of the following:
+.IP
+\fIima:enforcing\fP Every downloaded file requires a valid signature,
+fully protecting integrity.
+.IP
+\fIima:ignore\fP Skips verification altogether, providing no
+protection.
+.IP
+
+Alerts of validation failure will be directed as specified
+in $DEBUGINFOD_VERBOSE.
+
.TP
.B $DEBUGINFOD_CACHE_PATH
This environment variable governs the location of the cache where
@@ -82,6 +109,12 @@ outbound HTTP requests, one per line. The header lines shouldn't end with
CRLF, unless that's the system newline convention. Whitespace-only lines
are skipped.
+.TP
+.B $DEBUGINFOD_IMA_CERT_PATH
+This environment variable contains a list of absolute directory paths
+holding X.509 certificates for RPM per-file IMA-verification.
+Alternate paths are separated by colons.
+
.SH CACHE
Before each query, the debuginfod client library checks for a need to
diff --git a/doc/debuginfod-find.1 b/doc/debuginfod-find.1
index 7d577babeb89..d7db1bfdd838 100644
--- a/doc/debuginfod-find.1
+++ b/doc/debuginfod-find.1
@@ -129,10 +129,18 @@ and printing the http response headers from the server.
.SH "SECURITY"
-debuginfod-find \fBdoes not\fP include any particular security
-features. It trusts that the binaries returned by the debuginfod(s)
-are accurate. Therefore, the list of servers should include only
-trustworthy ones. If accessed across HTTP rather than HTTPS, the
+If IMA signature(s) are available from the RPMs that contain
+requested files, then
+.BR debuginfod
+will extract those signatures into response headers, and
+.BR debuginfod-find
+will perform verification upon the files.
+Validation policy is controlled via tags inserted into
+$DEBUGINFOD_URLS. By default,
+.BR debuginfod-find
+acts in ignore mode.
+
+If accessed across HTTP rather than HTTPS, the
network should be trustworthy. Authentication information through
the internal \fIlibcurl\fP library is not currently enabled, except
for the basic plaintext \%\fIhttp[s]://userid:password@hostname/\fP style.
diff --git a/doc/debuginfod.8 b/doc/debuginfod.8
index 42e0fc9fbb34..577f58b6ee2e 100644
--- a/doc/debuginfod.8
+++ b/doc/debuginfod.8
@@ -285,6 +285,14 @@ completed archive or file scans. This may slow down parallel scanning
phase somewhat, but generate much smaller "-wal" temporary files on
busy servers. The default is 256. Disabled if 0.
+.TP
+.B "\-\-koji\-sigcache"
+Enable an additional step of RPM path mapping when extracting signatures for use
+in RPM per-file IMA verification on koji repositories. The signatures are retrieved
+from the Fedora koji sigcache rpm.sig files as opposed to the original RPM header.
+If a signature cannot be found in the sigcache rpm.sig file, the RPM will be
+tried as a fallback.
+
.TP
.B "\-v"
Increase verbosity of logging to the standard error file descriptor.
@@ -300,8 +308,15 @@ Unknown buildid / request combinations result in HTTP error codes.
This file service resemblance is intentional, so that an installation
can take advantage of standard HTTP management infrastructure.
-For most queries, some custom http headers are added to the response,
-providing additional metadata about the buildid-related response. For example:
+Upon finding a file in an archive or simply in the database, some
+custom http headers are added to the response. For files in the
+database X-DEBUGINFOD-FILE and X-DEBUGINFOD-SIZE are added.
+X-DEBUGINFOD-FILE is simply the unescaped filename and
+X-DEBUGINFOD-SIZE is the size of the file. For files found in archives,
+in addition to X-DEBUGINFOD-FILE and X-DEBUGINFOD-SIZE,
+X-DEBUGINFOD-ARCHIVE is added. X-DEBUGINFOD-ARCHIVE is the name of the
+archive the file was found in. X-DEBUGINFOD-IMA-SIGNATURE contains the
+per-file IMA signature as a hexadecimal blob.
.SAMPLE
% debuginfod-find -v debuginfo /bin/ls |& grep -i x-debuginfo
diff --git a/doc/debuginfod_find_debuginfo.3 b/doc/debuginfod_find_debuginfo.3
index 0d553665f42b..cb49eb83d779 100644
--- a/doc/debuginfod_find_debuginfo.3
+++ b/doc/debuginfod_find_debuginfo.3
@@ -251,13 +251,21 @@ void *debuginfod_so = dlopen(DEBUGINFOD_SONAME, RTLD_LAZY);
.in
.SH "SECURITY"
+
+If IMA signature(s) are available from the RPMs that contain
+requested files, then
+.BR debuginfod
+will extract those signatures into response headers, and
+.BR debuginfod_find_* ()
+will perform verification upon the files.
+Validation policy is controlled via tags inserted into
+$DEBUGINFOD_URLS. By default,
.BR debuginfod_find_* ()
-functions \fBdo not\fP include any particular security
-features. They trust that the binaries returned by the debuginfod(s)
-are accurate. Therefore, the list of servers should include only
-trustworthy ones. If accessed across HTTP rather than HTTPS, the
-network should be trustworthy. Passing user authentication information
-through the internal \fIlibcurl\fP library is not currently enabled, except
+acts in ignore mode.
+
+If accessed across HTTP rather than HTTPS, the
+network should be trustworthy. Authentication information through
+the internal \fIlibcurl\fP library is not currently enabled, except
for the basic plaintext \%\fIhttp[s]://userid:password@hostname/\fP style.
(The debuginfod server does not perform authentication, but a front-end
proxy server could.)
@@ -325,6 +333,10 @@ Query failed due to timeout. \fB$DEBUGINFOD_TIMEOUT\fP and
Query aborted due to the file requested being too big. The
\fB$DEBUGINFOD_MAXSIZE\fP controls this.
+.TP
+.BR EBADMSG
+File content failed IMA verification.
+
.nr zZ 1
.so man7/debuginfod-client-config.7
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 40e0eaa5a368..c1bd52cf4fe8 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -278,6 +278,9 @@ if !OLD_LIBMICROHTTPD
# Too many open file descriptors confuses libmicrohttpd < 0.9.51
TESTS += run-debuginfod-federation-metrics.sh
endif
+if ENABLE_IMA_VERIFICATION
+TESTS += run-debuginfod-ima-verification.sh
+endif
endif
if HAVE_CXX11
@@ -600,6 +603,7 @@ EXTRA_DIST = run-arextract.sh run-arsymtest.sh run-ar.sh \
run-debuginfod-webapi-concurrency.sh \
run-debuginfod-section.sh \
run-debuginfod-IXr.sh \
+ run-debuginfod-ima-verification.sh \
debuginfod-rpms/fedora30/hello2-1.0-2.src.rpm \
debuginfod-rpms/fedora30/hello2-1.0-2.x86_64.rpm \
debuginfod-rpms/fedora30/hello2-debuginfo-1.0-2.x86_64.rpm \
@@ -623,6 +627,11 @@ EXTRA_DIST = run-arextract.sh run-arsymtest.sh run-ar.sh \
debuginfod-rpms/rhel7/hello2-debuginfo-1.0-2.x86_64.rpm \
debuginfod-rpms/rhel7/hello2-two-1.0-2.x86_64.rpm \
debuginfod-rpms/rhel7/hello2-two-1.0-2.x86_64.rpm \
+ debuginfod-ima/koji/arch/hello-2.10-9.fc38.x86_64.rpm \
+ debuginfod-ima/koji/data/sigcache/keyid/arch/hello-2.10-9.fc38.x86_64.rpm.sig \
+ debuginfod-ima/koji/fedora-38-ima.pem \
+ debuginfod-ima/rhel9/hello2-1.0-1.x86_64.rpm \
+ debuginfod-ima/rhel9/imacert.der \
debuginfod-debs/hithere-dbgsym_1.0-1_amd64.ddeb \
debuginfod-debs/hithere_1.0-1.debian.tar.xz \
debuginfod-debs/hithere_1.0-1.dsc \
diff --git a/tests/debuginfod-ima/koji/arch/hello-2.10-9.fc38.x86_64.rpm b/tests/debuginfod-ima/koji/arch/hello-2.10-9.fc38.x86_64.rpm
new file mode 100644
index 000000000000..b04ad8c2af39
Binary files /dev/null and b/tests/debuginfod-ima/koji/arch/hello-2.10-9.fc38.x86_64.rpm differ
diff --git a/tests/debuginfod-ima/koji/data/sigcache/keyid/arch/hello-2.10-9.fc38.x86_64.rpm.sig b/tests/debuginfod-ima/koji/data/sigcache/keyid/arch/hello-2.10-9.fc38.x86_64.rpm.sig
new file mode 100644
index 000000000000..ee7eb8e467b4
Binary files /dev/null and b/tests/debuginfod-ima/koji/data/sigcache/keyid/arch/hello-2.10-9.fc38.x86_64.rpm.sig differ
diff --git a/tests/debuginfod-ima/koji/fedora-38-ima.pem b/tests/debuginfod-ima/koji/fedora-38-ima.pem
new file mode 100644
index 000000000000..e323fa24a6fd
--- /dev/null
+++ b/tests/debuginfod-ima/koji/fedora-38-ima.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEj5EVzjUa4PW3I3Y/RTkLgfjP3Elu
+4AyKdXXxIldW6VVi3QMEpP5eZ7lZmlB2892QFpbWMLNJ4jXlPehMgqNgvg==
+-----END PUBLIC KEY-----
diff --git a/tests/debuginfod-ima/rhel9/hello2-1.0-1.x86_64.rpm b/tests/debuginfod-ima/rhel9/hello2-1.0-1.x86_64.rpm
new file mode 100644
index 000000000000..0262ae2f0c4c
Binary files /dev/null and b/tests/debuginfod-ima/rhel9/hello2-1.0-1.x86_64.rpm differ
diff --git a/tests/debuginfod-ima/rhel9/imacert.der b/tests/debuginfod-ima/rhel9/imacert.der
new file mode 100644
index 000000000000..b0250b6c30d5
Binary files /dev/null and b/tests/debuginfod-ima/rhel9/imacert.der differ
diff --git a/tests/run-debuginfod-ima-verification.sh b/tests/run-debuginfod-ima-verification.sh
new file mode 100755
index 000000000000..d582af5f6a9d
--- /dev/null
+++ b/tests/run-debuginfod-ima-verification.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2023-2024 Red Hat, Inc.
+# This file is part of elfutils.
+#
+# This file is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# elfutils is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+. $srcdir/debuginfod-subr.sh
+
+type rpmsign 2>/dev/null || { echo "need rpmsign"; exit 77; }
+cat << EoF > include.c
+#include <rpm/rpmlib.h>
+#include <rpm/rpmfi.h>
+#include <rpm/header.h>
+#include <imaevm.h>
+#include <openssl/evp.h>
+EoF
+tempfiles include.c
+gcc -H -fsyntax-only include.c 2> /dev/null || { echo "one or more devel packages are missing (rpm-devel, ima-evm-utils-devel, openssl-devel)"; exit 77; }
+
+set -x
+export DEBUGINFOD_VERBOSE=1
+
+DB=${PWD}/.debuginfod_tmp.sqlite
+tempfiles $DB
+export DEBUGINFOD_CACHE_PATH=${PWD}/.client_cache
+IMA_POLICY="enforcing"
+
+# This variable is essential and ensures no time-race for claiming ports occurs
+# set base to a unique multiple of 100 not used in any other 'run-debuginfod-*' test
+base=14000
+get_ports
+mkdir R
+env LD_LIBRARY_PATH=$ldpath DEBUGINFOD_URLS= ${abs_builddir}/../debuginfod/debuginfod $VERBOSE -R \
+ -d $DB -p $PORT1 -t0 -g0 R > vlog$PORT1 2>&1 &
+PID1=$!
+tempfiles vlog$PORT1
+errfiles vlog$PORT1
+
+########################################################################
+cp -pv ${abs_srcdir}/debuginfod-ima/rhel9/hello2-1.0-1.x86_64.rpm signed.rpm
+tempfiles signed.rpm
+RPM_BUILDID=460912dbc989106ec7325d243384df20c5ccec0c # /usr/local/bin/hello
+
+MIN_IMAEVM_MAJ_VERSION=3
+MIN_RPM_MAJ_VERSION=4
+# If the correct programs (and versions) exist sign the rpm in the test
+if false && \
+ (command -v openssl &> /dev/null) && \
+ (command -v rpmsign &> /dev/null) && \
+ (command -v gpg &> /dev/null) && \
+ [ $(ldd `which rpmsign` | grep libimaevm | awk -F'[^0-9]+' '{ print $2 }') -ge $MIN_IMAEVM_MAJ_VERSION ] && \
+ [ $(rpm --version | awk -F'[^0-9]+' '{ print $2 }') -ge $MIN_RPM_MAJ_VERSION ]
+then
+ # SIGN THE RPM
+ # First remove any old signatures
+ rpmsign --delsign signed.rpm &> /dev/null
+ rpmsign --delfilesign signed.rpm &> /dev/null
+
+ # Make a gpg keypair (with $PWD as the homedir)
+ mkdir -m 700 openpgp-revocs.d private-keys-v1.d
+ gpg --quick-gen-key --yes --homedir ${PWD} --batch --passphrase '' --no-default-keyring --keyring "${PWD}/pubring.kbx" example@elfutils.org 2> /dev/null
+
+ # Create a private DER signing key and a public X509 DER format verification key pair
+ openssl genrsa | openssl pkcs8 -topk8 -nocrypt -outform PEM -out signing.pem
+ openssl req -x509 -key signing.pem -out imacert.pem -days 365 -keyform PEM \
+ -subj "/C=CA/ST=ON/L=TO/O=Elfutils/CN=www.sourceware.org\/elfutils"
+
+ tempfiles openpgp-revocs.d/* private-keys-v1.d/* * openpgp-revocs.d private-keys-v1.d
+
+ rpmsign --addsign --signfiles --fskpath=signing.pem -D "_gpg_name example@elfutils.org" -D "_gpg_path ${PWD}" signed.rpm
+ cp signed.rpm R/signed.rpm
+ VERIFICATION_CERT_DIR=${PWD}
+
+ # Cleanup
+ rm -rf openpgp-revocs.d private-keys-v1.d
+else
+ # USE A PRESIGNED RPM
+ cp signed.rpm R/signed.rpm
+ # Note we test with no trailing /
+ VERIFICATION_CERT_DIR=${abs_srcdir}/debuginfod-ima/rhel9
+fi
+
+########################################################################
+# Server must become ready with R fully scanned and indexed
+wait_ready $PORT1 'ready' 1
+wait_ready $PORT1 'thread_work_total{role="traverse"}' 1
+wait_ready $PORT1 'thread_work_pending{role="scan"}' 0
+wait_ready $PORT1 'thread_busy{role="scan"}' 0
+
+export DEBUGINFOD_URLS="ima:$IMA_POLICY http://127.0.0.1:$PORT1"
+
+echo Test 1: Without a certificate the verification should fail
+export DEBUGINFOD_IMA_CERT_PATH=
+RC=0
+testrun ${abs_top_builddir}/debuginfod/debuginfod-find -vv executable $RPM_BUILDID || RC=1
+test $RC -ne 0
+
+echo Test 2: It should pass once the certificate is added to the path
+export DEBUGINFOD_IMA_CERT_PATH=$VERIFICATION_CERT_DIR
+rm -rf $DEBUGINFOD_CACHE_PATH # clean it from previous tests
+kill -USR1 $PID1
+wait_ready $PORT1 'thread_work_total{role="traverse"}' 2
+wait_ready $PORT1 'thread_work_pending{role="scan"}' 0
+wait_ready $PORT1 'thread_busy{role="scan"}' 0
+testrun ${abs_top_builddir}/debuginfod/debuginfod-find -vv executable $RPM_BUILDID
+
+echo Test 3: Corrupt the data and it should fail
+dd if=/dev/zero of=R/signed.rpm bs=1 count=128 seek=1024 conv=notrunc
+rm -rf $DEBUGINFOD_CACHE_PATH # clean it from previous tests
+kill -USR1 $PID1
+wait_ready $PORT1 'thread_work_total{role="traverse"}' 3
+wait_ready $PORT1 'thread_work_pending{role="scan"}' 0
+wait_ready $PORT1 'thread_busy{role="scan"}' 0
+RC=0
+testrun ${abs_top_builddir}/debuginfod/debuginfod-find executable $RPM_BUILDID || RC=1
+test $RC -ne 0
+
+echo Test 4: A rpm without a signature will fail
+cp signed.rpm R/signed.rpm
+rpmsign --delfilesign R/signed.rpm
+rm -rf $DEBUGINFOD_CACHE_PATH # clean it from previous tests
+kill -USR1 $PID1
+wait_ready $PORT1 'thread_work_total{role="traverse"}' 4
+wait_ready $PORT1 'thread_work_pending{role="scan"}' 0
+wait_ready $PORT1 'thread_busy{role="scan"}' 0
+RC=0
+testrun ${abs_top_builddir}/debuginfod/debuginfod-find executable $RPM_BUILDID || RC=1
+test $RC -ne 0
+
+echo Test 5: Only tests 1,2 will result in extracted signature
+[[ $(curl -s http://127.0.0.1:$PORT1/metrics | grep 'http_responses_total{extra="ima-sigs-extracted"}' | awk '{print $NF}') -eq 2 ]]
+
+kill $PID1
+wait $PID1
+PID1=0
+
+#######################################################################
+# We also test the --koji-sigcache
+cp -pR ${abs_srcdir}/debuginfod-ima/koji R/koji
+
+rm -rf $DEBUGINFOD_CACHE_PATH # clean it from previous tests
+env LD_LIBRARY_PATH=$ldpath DEBUGINFOD_URLS= ${abs_builddir}/../debuginfod/debuginfod $VERBOSE -R \
+ -d $DB -p $PORT2 -t0 -g0 -X /data/ --koji-sigcache R/koji > vlog$PORT1 2>&1 &
+#reuse PID1
+PID1=$!
+tempfiles vlog$PORT2
+errfiles vlog$PORT2
+
+RPM_BUILDID=c592a95e45625d7891b90f6b86e63373d540461d #/usr/bin/hello
+# Note we test with a trailing slash
+VERIFICATION_CERT_DIR=/not/a/dir:${abs_srcdir}/debuginfod-ima/koji/
+
+########################################################################
+# Server must become ready with koji fully scanned and indexed
+wait_ready $PORT2 'ready' 1
+wait_ready $PORT2 'thread_work_total{role="traverse"}' 1
+wait_ready $PORT2 'thread_work_pending{role="scan"}' 0
+wait_ready $PORT2 'thread_busy{role="scan"}' 0
+
+echo Test 6: The path should be properly mapped and verified using the actual fedora 38 cert
+export DEBUGINFOD_URLS="ima:$IMA_POLICY http://127.0.0.1:$PORT2"
+export DEBUGINFOD_IMA_CERT_PATH=$VERIFICATION_CERT_DIR
+testrun ${abs_top_builddir}/debuginfod/debuginfod-find -vv executable $RPM_BUILDID
+
+kill $PID1
+wait $PID1
+PID1=0
+
+exit 0
More information about the Elfutils-devel
mailing list