From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from elastic.org (elastic.org [96.126.110.187]) by sourceware.org (Postfix) with ESMTPS id D45BB3847725 for ; Wed, 3 Apr 2024 21:04:57 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D45BB3847725 Authentication-Results: sourceware.org; dmarc=pass (p=quarantine dis=none) header.from=elastic.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=elastic.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D45BB3847725 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=96.126.110.187 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1712178303; cv=none; b=ReoeZTjq+MVxY3UGCUDR7OrZmEXxmkJszmR2pgveUQ9E6zLDxySLTWdwVSAL5s09+oMYVUriYAgZQN/22gNtwaBXPKrDjIhmkzFyv9uQgNWSJfvkErAQkmoX1oV13mOveggIU9HLZ2OpuvrQbrtNgsr/5eMRhZycjs90fSTrqZ0= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1712178303; c=relaxed/simple; bh=A8IQUnLaN8/ohzF9pS6kQBKsth5x6PptRXGH9rSqPlU=; h=DKIM-Signature:Date:From:To:Subject:Message-ID:MIME-Version; b=D5ZXSmoT8iXgG3fo+M3JyGq3j4voM3u6DNfIa+xMEM1qzTSeAfrv1wuqYG+5GflzigwNRaezVux7gdK28NHnXXCnay5eRXas4YaPUf2F43SipYmnySCXyDj96Q9/56x8wbEgBIv1Uokwpr56a3JBVTNUV3xT6jCcKcK1XBHDsPs= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=elastic.org ; s=default2; h=Content-Type:MIME-Version:Message-ID:Subject:To:From:Date: Sender:Reply-To:Cc:Content-Transfer-Encoding:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=6eMtU+9vc3MKRQ2gtbKMw2ZQ4umj/Ox/wtyp1PCihTQ=; b=cGdJcpJnNyd4gyWKZEOjzTJpuv wK3mAzA34EWZ9vy97cKteAFAxN4WVRJQBtyA0S9MWZ/SrTRvpKtD3I3WtBg4vjnUX1jjxgTT7YfY1 TYYPF48Gu9GWmQheGyUdj3e78oElRXk/D2nVzA7frMoEuflSY3Lj7gpP7WxnqpkXtK/+IUqo6Gu6S vhuK9GGVWudo1NgoX9R5BecoMcW/vWLW7LLaE87QipMA9VlRfK4xAfZANYKpBuu5+x2QP9bnQMXkc JW3p9g2Ew+rW9O6jecYoz8DXzlLZ0uELyXMxqjPOTdnDDaPGNJqjFY0VDzKP3WjPQvPBmtfYG6Sne ZMPk8KBg==; Received: from vpn-home.elastic.org ([10.0.0.2] helo=elastic.org) by elastic.org with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.97.1) (envelope-from ) id 1rs7mz-000000000df-1c7D for elfutils-devel@sourceware.org; Wed, 03 Apr 2024 21:04:57 +0000 Received: from very.elastic.org ([192.168.1.1]) by elastic.org with esmtp (Exim 4.97.1) (envelope-from ) id 1rs7my-00000000FR5-3t4R for elfutils-devel@sourceware.org; Wed, 03 Apr 2024 17:04:56 -0400 Received: from fche by very.elastic.org with local (Exim 4.97.1) (envelope-from ) id 1rs7my-00000008zqP-3dEC for elfutils-devel@sourceware.org; Wed, 03 Apr 2024 17:04:56 -0400 Date: Wed, 3 Apr 2024 17:04:56 -0400 From: "Frank Ch. Eigler" To: elfutils-devel@sourceware.org Subject: [rfc] [patch] PR28204: debuginfod ima signature verification Message-ID: MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline X-Sender-Verification: "" X-Spam-Status: No, score=-107.1 required=5.0 tests=BAYES_00,DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,GIT_PATCH_0,KAM_BADIPHTTP,SPF_HELO_PASS,SPF_PASS,TXREP,URIBL_SBL_A,USER_IN_WELCOMELIST,USER_IN_WHITELIST autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org List-Id: 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 ]) +]) + +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 + + * 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 * 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 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. */ @@ -114,6 +126,8 @@ void debuginfod_end (debuginfod_client *c) { } #include + + 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 + #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,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 + + * 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 * 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 . + +. $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