[RFC][PATCH v2 02/13] integrity: Introduce a digest cache

From: Roberto Sassu
Date: Sat Aug 12 2023 - 06:48:06 EST


From: Roberto Sassu <roberto.sassu@xxxxxxxxxx>

Introduce the digest cache, a structure holding a hash table of digests,
extracted from a digest list. Its pointer is stored in the iint of the
digest list the digest cache was created from (dig_owner field), and in
the iint of the inodes for which the digest cache is used (dig_user field).
The digest cache has also a reference count to track how many iints are
referencing it.

For simplicity, the digest cache is created only once, from the first read.
Further modifications of the digest lists, if they are ever allowed, are
ignored.

Introduce two methods to manage the digest cache: digest_cache_get() and
digest_cache_free(). The first creates and returns a digest cache created
from the digest list whose path is stored in the security.digest_list xattr
of the inode being measured/appraised.

The second is called at the time an iint is freed. When the digest cache
reference count reaches zero, the digest cache is also freed.

Each digest cache pointer in the iint is protected by the respective
mutex. Dig_owner_mutex ensures that the first caller of digest_cache_get()
creates and initializes dig_owner, and ensures that the other callers wait
until the creation and initialization is complete.

Dig_user_mutex serializes calls to digest_cache_get() by processes
accessing the same inode, and ensures that only the first assigns dig_user.

The digest cache also stores which IMA actions have been done to the
digest list, and is usable for the same actions done to an inode.

Signed-off-by: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
---
include/uapi/linux/xattr.h | 3 +
security/integrity/Kconfig | 12 ++
security/integrity/Makefile | 1 +
security/integrity/digest_cache.c | 317 ++++++++++++++++++++++++++++++
security/integrity/digest_cache.h | 81 ++++++++
security/integrity/iint.c | 12 ++
security/integrity/integrity.h | 8 +
7 files changed, 434 insertions(+)
create mode 100644 security/integrity/digest_cache.c
create mode 100644 security/integrity/digest_cache.h

diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h
index 9463db2dfa9..8a58cf4bce6 100644
--- a/include/uapi/linux/xattr.h
+++ b/include/uapi/linux/xattr.h
@@ -54,6 +54,9 @@
#define XATTR_IMA_SUFFIX "ima"
#define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX

+#define XATTR_DIGEST_LIST_SUFFIX "digest_list"
+#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
+
#define XATTR_SELINUX_SUFFIX "selinux"
#define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX

diff --git a/security/integrity/Kconfig b/security/integrity/Kconfig
index ec6e0d789da..df8a1f7e6e2 100644
--- a/security/integrity/Kconfig
+++ b/security/integrity/Kconfig
@@ -130,6 +130,18 @@ config INTEGRITY_AUDIT
be enabled by specifying 'integrity_audit=1' on the kernel
command line.

+config INTEGRITY_DIGEST_CACHE
+ bool "Enable the integrity digest cache"
+ depends on INTEGRITY
+ default n
+ help
+ This option enables a cache of digests from a digest list, possibly
+ authenticated with a signature.
+
+ The digest cache can be used to make a TPM PCR predictable
+ (by skipping the measurement of cached digests), or for appraisal
+ with already available sources (e.g. RPM packages).
+
source "security/integrity/ima/Kconfig"
source "security/integrity/evm/Kconfig"

diff --git a/security/integrity/Makefile b/security/integrity/Makefile
index d0ffe37dc1d..0c175a567ac 100644
--- a/security/integrity/Makefile
+++ b/security/integrity/Makefile
@@ -11,6 +11,7 @@ integrity-$(CONFIG_INTEGRITY_SIGNATURE) += digsig.o
integrity-$(CONFIG_INTEGRITY_ASYMMETRIC_KEYS) += digsig_asymmetric.o
integrity-$(CONFIG_INTEGRITY_PLATFORM_KEYRING) += platform_certs/platform_keyring.o
integrity-$(CONFIG_INTEGRITY_MACHINE_KEYRING) += platform_certs/machine_keyring.o
+integrity-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
integrity-$(CONFIG_LOAD_UEFI_KEYS) += platform_certs/efi_parser.o \
platform_certs/load_uefi.o \
platform_certs/keyring_handler.o
diff --git a/security/integrity/digest_cache.c b/security/integrity/digest_cache.c
new file mode 100644
index 00000000000..4201c68171a
--- /dev/null
+++ b/security/integrity/digest_cache.c
@@ -0,0 +1,317 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2019 IBM Corporation
+ * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+ *
+ * Implement the integrity digest cache.
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/init_task.h>
+#include <linux/fs.h>
+#include <linux/namei.h>
+#include <linux/xattr.h>
+#include <linux/kernel_read_file.h>
+#include <linux/module_signature.h>
+
+#include "integrity.h"
+
+#ifdef pr_fmt
+#undef pr_fmt
+#endif
+#define pr_fmt(fmt) "DIGEST CACHE: "fmt
+
+/**
+ * digest_cache_alloc - Allocate and initialize a new digest cache
+ * @path_str: Path of the digest list
+ * @digest_cache_mask: Actions done by IMA on the digest list
+ *
+ * This function allocates a new digest cache and initializes all fields of
+ * the digest_cache structure.
+ *
+ * Return: A digest_cache structure on success, NULL on error.
+ */
+static struct digest_cache *digest_cache_alloc(char *path_str,
+ u64 digest_cache_mask)
+{
+ struct digest_cache *digest_cache;
+
+ digest_cache = kmalloc(sizeof(*digest_cache), GFP_KERNEL);
+ if (!digest_cache)
+ return digest_cache;
+
+ digest_cache->algo = HASH_ALGO__LAST;
+ digest_cache->path_str = path_str;
+ digest_cache->mask = digest_cache_mask;
+ digest_cache->slots = NULL;
+ digest_cache->num_slots = 0;
+ /*
+ * One for dig_owner of the digest list iint, one for dig_user of the
+ * iint of the inode for which the digest cache is used.
+ */
+ atomic_set(&digest_cache->ref_count, 2);
+ return digest_cache;
+}
+
+/**
+ * digest_cache_free - Free all memory occupied by a digest cache
+ * @digest_cache: Digest cache
+ *
+ * This function frees the digests associated to the digest cache and the
+ * digest cache itself.
+ */
+void digest_cache_free(struct digest_cache *digest_cache)
+{
+ struct digest_cache_entry *p;
+ struct hlist_node *q;
+ int digest_len, i;
+
+ if (!digest_cache)
+ return;
+
+ pr_debug("Free cache (ref count: %d), algo: %s, digest list: %s",
+ atomic_read(&digest_cache->ref_count),
+ hash_algo_name[digest_cache->algo], digest_cache->path_str);
+
+ if (!atomic_dec_and_test(&digest_cache->ref_count))
+ return;
+
+ digest_len = hash_digest_size[digest_cache->algo];
+
+ for (i = 0; i < digest_cache->num_slots; i++) {
+ hlist_for_each_entry_safe(p, q, &digest_cache->slots[i],
+ hnext) {
+ hlist_del(&p->hnext);
+ pr_debug("Remove digest %s:%*phN from digest list %s\n",
+ hash_algo_name[digest_cache->algo],
+ digest_len, p->digest, digest_cache->path_str);
+ kfree(p);
+ }
+ }
+
+ pr_debug("Freed cache (ref count: %d), algo: %s, digest list: %s",
+ atomic_read(&digest_cache->ref_count),
+ hash_algo_name[digest_cache->algo], digest_cache->path_str);
+
+ kfree(digest_cache->path_str);
+ kfree(digest_cache->slots);
+ kfree(digest_cache);
+}
+
+/**
+ * digest_cache_parse_digest_list - Parse a digest list
+ * @digest_cache: Digest cache
+ * @digest_list_path: Path of the digest list
+ * @data: Data to parse
+ * @data_len: Length of @data
+ *
+ * This function parses a digest list. First, it strips the module-style
+ * appended signature, if present. Then, it selects the parser to call from
+ * the beginning of the file name, which is expected to be in the format:
+ * <digest list format>-<digest list file name>.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+static int digest_cache_parse_digest_list(struct digest_cache *digest_cache,
+ struct path *digest_list_path,
+ void *data, size_t data_len)
+{
+ const size_t marker_len = strlen(MODULE_SIG_STRING);
+ const struct module_signature *sig;
+ size_t sig_len;
+ const void *p;
+ int ret = -EINVAL;
+
+ /* From ima_modsig.c */
+ if (data_len <= marker_len + sizeof(*sig))
+ goto parse;
+
+ p = data + data_len - marker_len;
+ if (memcmp(p, MODULE_SIG_STRING, marker_len))
+ goto parse;
+
+ data_len -= marker_len;
+ sig = (const struct module_signature *)(p - sizeof(*sig));
+
+ sig_len = be32_to_cpu(sig->sig_len);
+ data_len -= sig_len + sizeof(*sig);
+parse:
+ pr_debug("Parsing %s, size: %ld\n", digest_cache->path_str, data_len);
+
+ return ret;
+}
+
+/**
+ * digest_cache_new - Create a new digest cache
+ * @dentry: Dentry of the file being measured/appraised
+ *
+ * This function retrieves the path of the digest list from the
+ * security.digest_list xattr of the file being measured/appraised. It then
+ * opens and parses the digest list, and finally instantiates a new digest
+ * cache.
+ *
+ * After read, the IMA actions done on the digest list are recorded in the
+ * digest cache. The use of the digest cache is allowed for measuring/appraising
+ * a file, only if the same action has been done on the digest list itself.
+ *
+ * The invoked parser will in turn set the digest algorithm, initialize the
+ * hash table and add the extracted digests to the digest cache.
+ *
+ * Return: A new digest cache on success, NULL on error.
+ */
+static struct digest_cache *digest_cache_new(struct dentry *dentry)
+{
+ struct integrity_iint_cache *digest_list_iint;
+ struct digest_cache *digest_cache = NULL;
+ struct path digest_list_path;
+ char *path_str = NULL;
+ struct file *file;
+ void *data = NULL;
+ size_t data_len = 0;
+ struct inode *inode;
+ u64 digest_cache_mask = 0;
+ int ret;
+
+ ret = vfs_getxattr_alloc(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST,
+ &path_str, 0, GFP_NOFS);
+ if (ret <= 0) {
+ pr_debug("%s xattr not found in %s\n", XATTR_NAME_DIGEST_LIST,
+ dentry->d_name.name);
+ return digest_cache;
+ }
+
+ pr_debug("Found %s xattr in %s, digest list: %s\n",
+ XATTR_NAME_DIGEST_LIST, dentry->d_name.name, path_str);
+
+ ret = kern_path(path_str, 0, &digest_list_path);
+ if (ret < 0) {
+ pr_debug("Cannot open digest list %s\n", path_str);
+ goto out;
+ }
+
+ inode = d_backing_inode(digest_list_path.dentry);
+
+ digest_list_iint = integrity_inode_get(inode);
+ if (!digest_list_iint) {
+ pr_debug("Cannot get integrity metadata for digest list %s\n",
+ path_str);
+ goto out_path;
+ }
+
+ if (digest_list_iint->dig_owner) {
+ pr_debug("Cache for digest list %s exists\n", path_str);
+ digest_cache = digest_list_iint->dig_owner;
+ atomic_inc(&digest_cache->ref_count);
+ goto out_path;
+ }
+
+ mutex_lock(&digest_list_iint->dig_owner_mutex);
+
+ if (digest_list_iint->dig_owner) {
+ pr_debug("Cache for digest list %s exists\n", path_str);
+ digest_cache = digest_list_iint->dig_owner;
+ atomic_inc(&digest_cache->ref_count);
+ goto out_unlock;
+ }
+
+ file = dentry_open(&digest_list_path, O_RDONLY, &init_cred);
+ if (IS_ERR(file)) {
+ pr_debug("Unable to open digest list %s\n", path_str);
+ goto out_unlock;
+ }
+
+ /* Write-lock the file to avoid getting outdated iint->flags. */
+ ret = deny_write_access(file);
+ if (ret < 0) {
+ pr_err("Unable to write-lock digest list %s", path_str);
+ goto out_fput;
+ }
+
+ ret = kernel_read_file(file, 0, &data, INT_MAX, NULL,
+ READING_DIGEST_LIST);
+ if (ret < 0) {
+ pr_debug("Unable to read digest list %s\n", path_str);
+ goto out_allow;
+ }
+
+ if (digest_list_iint->flags & IMA_MEASURED)
+ digest_cache_mask |= DIGEST_CACHE_MEASURE;
+ if (digest_list_iint->flags & IMA_APPRAISED_SUBMASK)
+ digest_cache_mask |= DIGEST_CACHE_APPRAISE_CONTENT;
+
+ if (!digest_cache_mask) {
+ pr_debug("No actions done on digest list %s\n", path_str);
+ ret = -ENOENT;
+ goto out_vfree;
+ }
+
+ data_len = ret;
+
+ digest_cache = digest_cache_alloc(path_str, digest_cache_mask);
+ if (!digest_cache)
+ goto out_vfree;
+
+ /* Freed by digest_cache_free(). */
+ path_str = NULL;
+
+ /*
+ * Digest list parsers must set the digest algorithm, initialize the
+ * hash table and add the digests.
+ */
+ ret = digest_cache_parse_digest_list(digest_cache, &digest_list_path,
+ data, data_len);
+ if (ret < 0) {
+ pr_debug("Error parsing digest list %s, ret: %d\n",
+ digest_cache->path_str, ret);
+ digest_cache_free(digest_cache);
+ digest_cache = NULL;
+ goto out_vfree;
+ }
+
+ digest_list_iint->dig_owner = digest_cache;
+
+ pr_debug("New cache (ref count: %d), algo: %s, digest list: %s, mask: %llu\n",
+ atomic_read(&digest_cache->ref_count),
+ hash_algo_name[digest_cache->algo], digest_cache->path_str,
+ digest_cache->mask);
+out_vfree:
+ vfree(data);
+out_allow:
+ allow_write_access(file);
+out_fput:
+ fput(file);
+out_unlock:
+ mutex_unlock(&digest_list_iint->dig_owner_mutex);
+out_path:
+ path_put(&digest_list_path);
+out:
+ kfree(path_str);
+ return digest_cache;
+}
+
+/**
+ * digest_cache_get - Obtain a digest cache and set it in the iint
+ * @dentry: Dentry of the file being measured/appraised
+ * @iint: Integrity inode cache of the file being measured/appraised
+ *
+ * Obtain a digest cache, and set it in the dig_user field of the passed iint,
+ * if not already done.
+ *
+ * Return: A digest cache on success, NULL otherwise.
+ */
+struct digest_cache *digest_cache_get(struct dentry *dentry,
+ struct integrity_iint_cache *iint)
+{
+ if (iint->dig_user)
+ return iint->dig_user;
+
+ mutex_lock(&iint->dig_user_mutex);
+ if (!iint->dig_user)
+ iint->dig_user = digest_cache_new(dentry);
+ mutex_unlock(&iint->dig_user_mutex);
+
+ return iint->dig_user;
+}
diff --git a/security/integrity/digest_cache.h b/security/integrity/digest_cache.h
new file mode 100644
index 00000000000..ff88e8593c6
--- /dev/null
+++ b/security/integrity/digest_cache.h
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+ *
+ * Header of the integrity digest cache.
+ */
+
+#ifndef _DIGEST_CACHE_H
+#define _DIGEST_CACHE_H
+
+#include <linux/types.h>
+#include <linux/list.h>
+#include <crypto/hash_info.h>
+
+/* Depth if elements were uniformly distributed in the hash table slots. */
+#define DIGEST_CACHE_HTABLE_DEPTH 30
+
+/* There is no explicit concept of metadata measurement in IMA. */
+#define DIGEST_CACHE_MEASURE 0x01
+#define DIGEST_CACHE_APPRAISE_CONTENT 0x02
+
+struct integrity_iint_cache;
+
+/**
+ * struct digest_cache - Digest cache
+ * @slots: Hash table slots
+ * @num_slots: Number of slots
+ * @ref_count: Number of references to the digest cache
+ * @algo: Algorithm of digests stored in the cache
+ * @path_str: Path of the digest list the cache was created from
+ * @mask: For which IMA actions and purpose the digest cache can be used
+ *
+ * This structure represents a cache of digests extracted from a file, to be
+ * primarily used for IMA measurement and appraisal.
+ */
+struct digest_cache {
+ struct hlist_head *slots;
+ unsigned int num_slots;
+ atomic_t ref_count;
+ enum hash_algo algo;
+ char *path_str;
+ u64 mask;
+};
+
+/**
+ * struct digest_cache_entry - Entry of a digest cache
+ * @hnext: Pointer to the next element in the collision list
+ * @digest: Stored digest
+ *
+ * This structure represents an entry of a digest cache, storing a digest.
+ */
+struct digest_cache_entry {
+ struct hlist_node hnext;
+ u8 digest[];
+} __packed;
+
+static inline unsigned int digest_cache_hash_key(u8 *digest,
+ unsigned int num_slots)
+{
+ return (digest[0] | digest[1] << 8) % num_slots;
+}
+
+#ifdef CONFIG_INTEGRITY_DIGEST_CACHE
+void digest_cache_free(struct digest_cache *digest_cache);
+struct digest_cache *digest_cache_get(struct dentry *dentry,
+ struct integrity_iint_cache *iint);
+#else
+static inline void digest_cache_free(struct digest_cache *digest_cache)
+{
+}
+
+static inline struct digest_cache *
+digest_cache_get(struct dentry *dentry, struct integrity_iint_cache *iint)
+{
+ return NULL;
+}
+
+#endif /* CONFIG_INTEGRITY_DIGEST_CACHE */
+#endif /* _DIGEST_CACHE_H */
diff --git a/security/integrity/iint.c b/security/integrity/iint.c
index a462df827de..68ec73172e3 100644
--- a/security/integrity/iint.c
+++ b/security/integrity/iint.c
@@ -80,6 +80,12 @@ static void iint_free(struct integrity_iint_cache *iint)
iint->ima_creds_status = INTEGRITY_UNKNOWN;
iint->evm_status = INTEGRITY_UNKNOWN;
iint->measured_pcrs = 0;
+#ifdef CONFIG_INTEGRITY_DIGEST_CACHE
+ digest_cache_free(iint->dig_owner);
+ digest_cache_free(iint->dig_user);
+ iint->dig_owner = NULL;
+ iint->dig_user = NULL;
+#endif
kmem_cache_free(iint_cache, iint);
}

@@ -165,6 +171,12 @@ static void init_once(void *foo)
iint->ima_creds_status = INTEGRITY_UNKNOWN;
iint->evm_status = INTEGRITY_UNKNOWN;
mutex_init(&iint->mutex);
+#ifdef CONFIG_INTEGRITY_DIGEST_CACHE
+ iint->dig_owner = NULL;
+ iint->dig_user = NULL;
+ mutex_init(&iint->dig_owner_mutex);
+ mutex_init(&iint->dig_user_mutex);
+#endif
}

static int __init integrity_iintcache_init(void)
diff --git a/security/integrity/integrity.h b/security/integrity/integrity.h
index 7167a6e99bd..0192f81c67f 100644
--- a/security/integrity/integrity.h
+++ b/security/integrity/integrity.h
@@ -19,6 +19,8 @@
#include <linux/key.h>
#include <linux/audit.h>

+#include "digest_cache.h"
+
/* iint action cache flags */
#define IMA_MEASURE 0x00000001
#define IMA_MEASURED 0x00000002
@@ -171,6 +173,12 @@ struct integrity_iint_cache {
enum integrity_status ima_creds_status:4;
enum integrity_status evm_status:4;
struct ima_digest_data *ima_hash;
+#ifdef CONFIG_INTEGRITY_DIGEST_CACHE
+ struct digest_cache *dig_owner; /* created from this inode */
+ struct mutex dig_owner_mutex; /* protects dig_owner */
+ struct digest_cache *dig_user; /* user of the digest cache */
+ struct mutex dig_user_mutex; /* protects dig_user */
+#endif
};

/* rbtree tree calls to lookup, insert, delete
--
2.34.1