public inbox for elfutils@sourceware.org
 help / color / mirror / Atom feed
* [rfc] [patch] PR28204: debuginfod ima signature verification
@ 2024-04-03 21:04 Frank Ch. Eigler
  2024-04-09 12:31 ` Mark Wielaard
  0 siblings, 1 reply; 9+ messages in thread
From: Frank Ch. Eigler @ 2024-04-03 21:04 UTC (permalink / raw)
  To: elfutils-devel

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

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2024-05-14 15:18 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-04-03 21:04 [rfc] [patch] PR28204: debuginfod ima signature verification Frank Ch. Eigler
2024-04-09 12:31 ` Mark Wielaard
2024-04-10 21:01   ` Frank Ch. Eigler
2024-04-11 10:14     ` Mark Wielaard
2024-04-11 14:09       ` Frank Ch. Eigler
2024-04-16 22:15   ` Frank Ch. Eigler
2024-05-05  1:30     ` Frank Ch. Eigler
2024-05-09 17:56       ` Aaron Merey
2024-05-14 15:18         ` Mark Wielaard

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).