Hi Frank, I've pointed out a couple nits below, but otherwise the patch LGTM. I've also attached a diff for handling DEBUGINFOD_IMA_CERT_PATH in profile.fish.in that should apply on top of this patch. I know there's already been a lot of discussion re. ima:permissive and I'm weighing in rather late, but FWIW I do support including it. Currently individual ELF sections cannot be downloaded when ima:enforcing is active. With ima:permissive we could support proper section queries while also being able to perform some amount of ima verification. On Tue, Apr 16, 2024 at 6:15 PM Frank Ch. Eigler wrote: > > Hi - > > The following is the candidate patch for the basic functionality. > It's been corrected for whitespace & error codes, given more complete > docs and commit message. See also the users/fche/try-bz2824f branch. > > > debuginfod: PR28204 - RPM IMA per-file signature verification > > Recent versions of Fedora/RHEL include per-file cryptographic > signatures in RPMs, not just an overall RPM signature. This work > extends debuginfod client & server to extract, transfer, and verify > those signatures. These allow clients to assure users that the > downloaded files have not been corrupted since their original > packaging. Downloads that fail the test are rejected. > > Clients may select a desired level of enforcement for sets of URLs in > the DEBUGINFOD_URLS by inserting special markers ahead of them: > > ima:ignore pay no attention to absence or presence of signatures > ima:enforcing require every file to be correctly signed > > The default is ima:ignore mode. In ima:enforcing mode, section > queries are forced to be entire-file downloads, as it is not > possible to crypto-verify just sections. > > IMA signatures are verified against a set of signing certificates. > These are normally published by distributions. The environment > variable $DEBUGINFOD_IMA_CERT_PATH contains a colon-separated path for > finding DER or PEM formatted certificates / public keys. These > certificates are assumed trusted. The profile.d scripts transcribe > /etc/debuginfod/*.certdir files into that variable. > > As for implementation: > > * configure.ac: Add --enable-default-ima-cert-path=PATH parameter. > Check for libimaevm (using headers only). > > * config/Makefile.am: Install defaults into /etc files. > * config/profile.{csh,sh}.in: Process defaults into env variables. > * config/elfutils.spec.in: Add more buildrequires. > > * debuginfod/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/debuginfod-client.c (debuginfod_query_server): Added policy for > validating IMA signatures > (debuginfod_validate_imasig): New function, with friends. > * debuginfod/debuginfod.h.in: Added DEBUGINFOD_IMA_CERT_PATH_ENV_VAR. > * debuginfod/Makefile.am: Add linker flags for rpm and crypto. > > * doc/debuginfod-client-config.7: Document DEBUGINFOD_IMA_CERT_PATH, > update DEBUGINFOD_URLS. > * doc/debuginfod.8: Document --koji-sigcache. > * doc/debuginfod-find.1, doc/debuginfod_find_debuginfo.3: Update SECURITY. > > * tests/run-debuginfod-ima-verification.sh: New test. > * tests/debuginfod-ima: Some new files for the tests. > * tests/Makefile.am: run/distribute them. > > Signed-off-by: Ryan Goldberg > Signed-off-by: Frank Ch. Eigler > > 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@" This second definition of prefix can be removed. > 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..d75d9ba02e79 100644 > --- a/configure.ac > +++ b/configure.ac > @@ -667,6 +667,33 @@ 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 ]) > +]) > + > +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" > + AC_DEFINE([ENABLE_IMA_VERIFICATION], [1], [Define if the required ima verification libraries are available]) > +fi > +AM_CONDITIONAL([ENABLE_IMA_VERIFICATION],[test "$enable_ima_verification" = "xrpmimaevmcrypto"]) > + > dnl The directories with content. > > dnl Documentation. > @@ -881,6 +908,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) It might be helpful to add AC_ARG_ENABLE for debuginfod_ima_verification. Then a configure error can alert us to any missing libraries if --enable-debuginfod-ima-verification=yes. > AC_CONFIG_FILES([config/profile.sh config/profile.csh config/profile.fish]) > > AC_OUTPUT > @@ -920,6 +956,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/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 4e7a8a2ad9ff..4dc6b4411eb2 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 > This file is part of elfutils. > > @@ -47,6 +47,17 @@ > #include > #include > > +#ifdef ENABLE_IMA_VERIFICATION > +#include > +#include > +#include > +#include > +#include > +#include > +#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 > #include > #include > +#include > > /* 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. */ > @@ -130,6 +142,17 @@ libcurl_init(void) > } > } > > + > +#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. */ > @@ -164,8 +187,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. */ > @@ -225,6 +254,182 @@ 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 = getenv(DEBUGINFOD_IMA_CERT_PATH_ENV_VAR); > + if (cert_paths == NULL || cert_paths[0] == '\0') > + return; > + cert_paths = strdup(cert_paths); // Modified during tokenization > + if (cert_paths == NULL) > + 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) > { > @@ -861,6 +1066,199 @@ 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. Return 0 on ok, -errno otherwise. */ > +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 = -EBADMSG; > + 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 */ > + > + if (c->verbose_fd >= 0) > + dprintf (c->verbose_fd, "Searching for ima keyid %04x\n", ntohl(hdr.keyid)); > + > + /* 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; // success! > + else if (ret == 0) > + ret = -EBADMSG; > + err: > + 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; This line can be removed. > + 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; > + > + 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 > @@ -1216,12 +1614,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] == '/') > @@ -1253,21 +1678,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) > @@ -1334,7 +1766,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; > @@ -1784,6 +2220,29 @@ 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 (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 = result; > + goto out2; > + } > + } > + > /* rename tmp->real */ > rc = rename (target_cache_tmppath, target_cache_path); > if (rc < 0) > @@ -1804,6 +2263,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); > > @@ -1837,6 +2297,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); > @@ -1869,7 +2330,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; > } > > @@ -1901,6 +2366,10 @@ debuginfod_begin (void) > goto out1; > } > > +#ifdef ENABLE_IMA_VERIFICATION > + load_ima_public_keys (client); > +#endif > + > // extra future initialization > > goto out; > @@ -1948,6 +2417,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); > } > > @@ -1987,9 +2459,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..d9259ad26bb8 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 > + #include > + #include > + #include > +#endif > + > #include > #include > #include > @@ -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,145 @@ 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 +2174,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 +2364,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/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..4e359c8c4bd4 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,18 @@ 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 against a known signer certificate. > + > +.TP > +.BR ENOKEY > +File content failed IMA verification due to missing signer certificate. > + > +.TP > +.BR ENODATA > +File content failed IMA verification because of a missing signature. > + > .nr zZ 1 > .so man7/debuginfod-client-config.7 > > diff --git a/tests/Makefile.am b/tests/Makefile.am > index 7aae3d8aa0e5..db071186c533 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 . > + > +. $srcdir/debuginfod-subr.sh > + > +type rpmsign 2>/dev/null || { echo "need rpmsign"; exit 77; } > +cat << EoF > include.c > +#include > +#include > +#include > +#include > +#include > +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 > Aaron