diff options
author | Frank Ch. Eigler <fche@redhat.com> | 2024-03-30 13:04:14 -0400 |
---|---|---|
committer | Frank Ch. Eigler <fche@redhat.com> | 2024-04-03 16:45:56 -0400 |
commit | 56dbea31bd0306d18f813b75beaf592cef029dd4 (patch) | |
tree | 0ba73a7082377d8b3aab1c261a2286fb8514c7ee | |
parent | 1a1c02ebf257b496becddd9f788660945aba3731 (diff) |
PR28204 debuginfod ima verification review changes cont'dupstream/users/fche/try-bz28204e
- Drop use of libimaevm.so. Instead, reimplement the core signature
computation functions based on openssl functions directly, using
only imaevm.h for struct decls.
- Add an --enable-debuginfod-ima-cert-path=PATH configury option to
populate /etc/debuginfod/*.imacert files, which profile.d scripts
consume
-rw-r--r-- | config/Makefile.am | 4 | ||||
-rw-r--r-- | config/profile.csh.in | 10 | ||||
-rw-r--r-- | config/profile.sh.in | 8 | ||||
-rw-r--r-- | configure.ac | 15 | ||||
-rw-r--r-- | debuginfod/Makefile.am | 2 | ||||
-rw-r--r-- | debuginfod/debuginfod-client.c | 510 | ||||
-rw-r--r-- | debuginfod/debuginfod.cxx | 2 | ||||
-rw-r--r-- | doc/debuginfod_find_debuginfo.3 | 4 | ||||
-rw-r--r-- | tests/Makefile.am | 1 | ||||
-rw-r--r-- | tests/debuginfod-ima/koji/fedora-38-ima.pem | 4 | ||||
-rwxr-xr-x | tests/run-debuginfod-ima-verification.sh | 24 |
11 files changed, 395 insertions, 189 deletions
diff --git a/config/Makefile.am b/config/Makefile.am index ae14e625..5a28e66d 100644 --- a/config/Makefile.am +++ b/config/Makefile.am @@ -46,12 +46,16 @@ install-data-local: 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/profile.csh.in b/config/profile.csh.in index 2a2ecacb..1da9626c 100644 --- a/config/profile.csh.in +++ b/config/profile.csh.in @@ -9,12 +9,14 @@ if (! $?DEBUGINFOD_URLS) then 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" - if (! $?DEBUGINFOD_IMA_CERT_PATH) then - set DEBUGINFOD_IMA_CERT_PATH="@sysconfdir@/debuginfod/ima-certs/:@DEBUGINFOD_IMA_CERT_PATH@" - setenv DEBUGINFOD_IMA_CERT_PATH "$DEBUGINFOD_IMA_CERT_PATH" - endif else unset DEBUGINFOD_URLS endif + 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 16926382..7db39996 100644 --- a/config/profile.sh.in +++ b/config/profile.sh.in @@ -9,10 +9,10 @@ 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 +fi - if [ -z "$DEBUGINFOD_IMA_CERT_PATH" ]; then - DEBUGINFOD_IMA_CERT_PATH="@sysconfdir@/debuginfod/ima-certs/:@DEBUGINFOD_IMA_CERT_PATH@" - export DEBUGINFOD_IMA_CERT_PATH - 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 0324c5c0..19ccf107 100644 --- a/configure.ac +++ b/configure.ac @@ -677,9 +677,9 @@ AC_CHECK_LIB(rpm, headerGet, [ [], [#include <rpm/rpmlib.h>]) ]) -AC_CHECK_LIB(imaevm, imaevm_hash_algo_from_sig, [ +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_SUBST(imaevm_LIBS, '-limaevm') ]) AC_CHECK_LIB(crypto, EVP_MD_CTX_new, [ @@ -910,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 @@ -949,7 +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} + Debuginfod RPM sig checking : ${debuginfod_ima_verification_enabled} ${default_debuginfod_ima_cert_path} EXTRA TEST FEATURES (used with make check) have bunzip2 installed (required) : ${HAVE_BUNZIP2} diff --git a/debuginfod/Makefile.am b/debuginfod/Makefile.am index 467f3530..5e4f9669 100644 --- a/debuginfod/Makefile.am +++ b/debuginfod/Makefile.am @@ -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) $(imaevm_LIBS) $(crypto_LIBS) +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 $@ \ diff --git a/debuginfod/debuginfod-client.c b/debuginfod/debuginfod-client.c index 67e5e84b..4618234f 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 @@ -115,65 +126,7 @@ void debuginfod_end (debuginfod_client *c) { } #include <pthread.h> -typedef enum {ignore, enforcing, undefined} ima_policy_t; -#ifdef ENABLE_IMA_VERIFICATION - #include <imaevm.h> - #include <openssl/pem.h> - #include <openssl/evp.h> - #include <openssl/x509v3.h> - #include <arpa/inet.h> - 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(X509* x509) - { - 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 - { - // If it is not there fallback to trying to extract it from the - // public key itself - EVP_PKEY * pkey = X509_get0_pubkey(x509); - char name[PATH_MAX]; - calc_keyid_v2(&keyid, name, pkey); - } - return ntohl(keyid); - } -#endif static pthread_once_t init_control = PTHREAD_ONCE_INIT; @@ -183,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. */ @@ -217,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. */ @@ -278,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) { @@ -914,27 +1057,111 @@ 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 and -errno on error + * Returns 0 on signature validity, -EINVAL on signature invalidity, -ENOSYS on undefined imaevm machinery, + * -ENOKEY on key issues, or other -errno. */ -/* libimaevm is not threadsafe [2023-10], so lock around calls */ -static pthread_mutex_t imaevm_lock = PTHREAD_MUTEX_INITIALIZER; - static int -debuginfod_validate_imasig (debuginfod_client *c, const char* tmp_path, int fd) +debuginfod_validate_imasig (debuginfod_client *c, int fd) { - (void) c; - (void) tmp_path; - (void) fd; int rc = ENOSYS; - (void) pthread_mutex_lock(& imaevm_lock); - - #ifdef ENABLE_IMA_VERIFICATION - char *cert_paths = NULL; // need to copy because of strtok - int vfd = c->verbose_fd; + // int vfd = c->verbose_fd; EVP_MD_CTX *ctx = NULL; if (!c || !c->winning_headers) { @@ -962,8 +1189,8 @@ debuginfod_validate_imasig (debuginfod_client *c, const char* tmp_path, int fd) // Compute the binary digest of the cached file (with file descriptor fd) ctx = EVP_MD_CTX_new(); - int hash_algo = imaevm_hash_algo_from_sig(bin_sig + 1); - const EVP_MD *md = EVP_get_digestbyname(imaevm_hash_algo_by_id(hash_algo)); + 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; @@ -978,23 +1205,23 @@ debuginfod_validate_imasig (debuginfod_client *c, const char* tmp_path, int fd) goto exit_validate; } - char file_data[DATA_SIZE]; + char file_data[DATA_SIZE]; // imaevm.h data chunk hash size ssize_t n; - for(long k = 0; k < data_len; k += n) - { - if (-1 == (n = pread(fd, file_data, DATA_SIZE, k))) + for(off_t k = 0; k < data_len; k += n) { - rc = -errno; - goto exit_validate; - } - - if (!EVP_DigestUpdate(ctx, file_data, n)) - { - rc = -EBADMSG; - goto exit_validate; + 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)) @@ -1003,89 +1230,24 @@ debuginfod_validate_imasig (debuginfod_client *c, const char* tmp_path, int fd) goto exit_validate; } - /* Iterate over the directories in DEBUGINFOD_IMA_CERT_PATH. Only - need this done once per process, persistently updating libimaevm's - public_keys */ - static int certs_loaded_p = 0; - if (! certs_loaded_p) - { - certs_loaded_p = 1; - - imaevm_params.verbose = 0; - cert_paths = strdup (getenv(DEBUGINFOD_IMA_CERT_PATH_ENV_VAR) ?: DEBUGINFOD_IMA_CERT_PATH_DEFAULT); - rc = ENOKEY; // This is updated iff a good cert is found - if (!cert_paths) - goto exit_validate; - - char* cert_dir_path; - DIR *dp; - struct dirent *entry; - 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)", 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; - // Attempt to read the fp as DER and assuming the key matches that of the signature add this key to be used - if(d2i_X509_fp(cert_fp, &x509)) - { - init_public_keys(certfile); - if (vfd >= 0) dprintf(vfd, "Loaded certificate %s (keyid = %04x, encoding = der)\n", certfile, extract_skid(x509)); - } - // 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)) - { - /* init_public_keys requires the path to a DER format x509 cert, so when we encounter a PEM - * format cert we first need to encode x509 to DER and write it to a new temp file. - * We can then pass this to init_public_keys and remove it afterwards - */ - char tmp_file[] = "/tmp/debuginfod_tempcert_XXXXXX"; - int tmp_fd = mkstemp(tmp_file); - if(-1 == tmp_fd) break; - FILE* der_cert_fp = fopen(tmp_file, "w"); - i2d_X509_fp(der_cert_fp, x509); - fclose(der_cert_fp); - init_public_keys(tmp_file); - if (vfd >= 0) dprintf(vfd, "Loaded certificate %s (keyid = %04x, encoding = pem)\n", certfile, extract_skid(x509)); - unlink(tmp_file); - close(tmp_fd); - } - fclose(cert_fp); - if(x509) X509_free(x509); - } /* for each file in directory */ - closedir(dp); - } /* for each directory */ - free(cert_paths); - } - - int res = ima_verify_signature(tmp_path, bin_sig, bin_sig_len, bin_dig, bin_dig_len); + // 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; // We update rc such that res = 1 is mapped to EINVAL, res = 0 is considered valid, res = -1 is an error + rc = (res == 1) ? -EINVAL : res; exit_validate: free (sig_buf); EVP_MD_CTX_free(ctx); - #endif - (void) pthread_mutex_unlock(& imaevm_lock); 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 @@ -1457,7 +1619,10 @@ debuginfod_query_server (debuginfod_client *c, { #ifdef ENABLE_IMA_VERIFICATION ima_policy_t m = ima_policy_str2enum(server_url + strlen("ima:")); - if(m != undefined) verification_mode = m; + 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); @@ -2045,7 +2210,11 @@ debuginfod_query_server (debuginfod_client *c, if(NULL != url_ima_policies && ignore != url_ima_policies[committed_to]) { - int result = debuginfod_validate_imasig(c, target_cache_tmppath, fd); +#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"); @@ -2057,7 +2226,7 @@ debuginfod_query_server (debuginfod_client *c, // 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 = -EPERM; + rc = -EBADMSG; goto out2; } else @@ -2189,6 +2358,10 @@ debuginfod_begin (void) goto out1; } +#ifdef ENABLE_IMA_VERIFICATION + load_ima_public_keys (client); +#endif + // extra future initialization goto out; @@ -2236,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); } diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx index 366cea25..30c818dd 100644 --- a/debuginfod/debuginfod.cxx +++ b/debuginfod/debuginfod.cxx @@ -506,7 +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); diff --git a/doc/debuginfod_find_debuginfo.3 b/doc/debuginfod_find_debuginfo.3 index 9be385a1..cb49eb83 100644 --- a/doc/debuginfod_find_debuginfo.3 +++ b/doc/debuginfod_find_debuginfo.3 @@ -333,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 f18939f9..c1bd52cf 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -629,6 +629,7 @@ EXTRA_DIST = run-arextract.sh run-arsymtest.sh run-ar.sh \ 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 \ 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 00000000..e323fa24 --- /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/run-debuginfod-ima-verification.sh b/tests/run-debuginfod-ima-verification.sh index 45f3e7d0..d582af5f 100755 --- a/tests/run-debuginfod-ima-verification.sh +++ b/tests/run-debuginfod-ima-verification.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Copyright (C) 2019-2023 Red Hat, Inc. +# 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 @@ -29,6 +29,9 @@ 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 @@ -53,7 +56,8 @@ 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 (command -v openssl &> /dev/null) && \ +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 ] && \ @@ -97,13 +101,13 @@ wait_ready $PORT1 'thread_busy{role="scan"}' 0 export DEBUGINFOD_URLS="ima:$IMA_POLICY http://127.0.0.1:$PORT1" -# Test 1: Without a certificate the verification should fail +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 -# Test 2: It should pass once the certificate is added to the path +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 @@ -112,7 +116,7 @@ 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 -# Test 3: Corrupt the data and it should fail +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 @@ -123,7 +127,7 @@ RC=0 testrun ${abs_top_builddir}/debuginfod/debuginfod-find executable $RPM_BUILDID || RC=1 test $RC -ne 0 -# Test 4: A rpm without a signature will fail +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 @@ -135,7 +139,7 @@ RC=0 testrun ${abs_top_builddir}/debuginfod/debuginfod-find executable $RPM_BUILDID || RC=1 test $RC -ne 0 -# Test 5: Only tests 1,2 will result in extracted signature +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 @@ -155,8 +159,8 @@ tempfiles vlog$PORT2 errfiles vlog$PORT2 RPM_BUILDID=c592a95e45625d7891b90f6b86e63373d540461d #/usr/bin/hello -# Note we test with a trailing slash and with the required directory as the third in the PATH -VERIFICATION_CERT_DIR=/not/a/dir:${abs_srcdir}/debuginfod-ima/rhel9:${abs_srcdir}/../debuginfod/ima-certs/ +# 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 @@ -165,7 +169,7 @@ 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 -# Test 1: The path should be properly mapped and verified using the actual fedora 38 cert +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 |