diff mbox series

[RFC,8/9] diglim: RPM parser

Message ID 20210915163145.1046505-9-roberto.sassu@huawei.com
State New
Headers show
Series integrity: Introduce DIGLIM advanced features | expand

Commit Message

Roberto Sassu Sept. 15, 2021, 4:31 p.m. UTC
Introduce a parser of the RPM digest list, which converts the digest lists
to the compact format and uploads the converted digest lists to the kernel.

It takes as input the type of operation to perform, add or delete, and the
file or directory with the files to process.

Also the RPM parser is intentionally compiled as a static binary to avoid
to generate a compact digest list also for its dependencies.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 MAINTAINERS               |   1 +
 tools/diglim/Makefile     |   5 +-
 tools/diglim/rpm_parser.c | 483 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 488 insertions(+), 1 deletion(-)
 create mode 100644 tools/diglim/rpm_parser.c
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 148a2a7957b7..1efc1724376e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -5527,6 +5527,7 @@  F:	tools/diglim/common.c
 F:	tools/diglim/common.h
 F:	tools/diglim/compact_gen.c
 F:	tools/diglim/rpm_gen.c
+F:	tools/diglim/rpm_parser.c
 F:	tools/diglim/upload_digest_lists.c
 F:	tools/testing/selftests/diglim/
 
diff --git a/tools/diglim/Makefile b/tools/diglim/Makefile
index a22125ad0281..7019c5b9fad9 100644
--- a/tools/diglim/Makefile
+++ b/tools/diglim/Makefile
@@ -3,7 +3,7 @@ 
 CC := $(CROSS_COMPILE)gcc
 CFLAGS += -O2 -Wall -g -I./ -I../../usr/include/ -ggdb
 
-PROGS := compact_gen rpm_gen upload_digest_lists
+PROGS := compact_gen rpm_gen upload_digest_lists rpm_parser
 PROGS_EXTENDED := common.o
 
 all: $(PROGS)
@@ -22,3 +22,6 @@  rpm_gen: rpm_gen.c $(PROGS_EXTENDED)
 
 upload_digest_lists: upload_digest_lists.c
 	$(CC) $(CFLAGS) -static $< -o $@ $(LDFLAGS)
+
+rpm_parser: rpm_parser.c $(PROGS_EXTENDED)
+	$(CC) $(CFLAGS) -static $< $(PROGS_EXTENDED) -o $@ $(LDFLAGS)
diff --git a/tools/diglim/rpm_parser.c b/tools/diglim/rpm_parser.c
new file mode 100644
index 000000000000..f8a4b63b2fa8
--- /dev/null
+++ b/tools/diglim/rpm_parser.c
@@ -0,0 +1,483 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2017-2021 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Parse RPM header and upload digest list to the kernel.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <fts.h>
+#include <rpm/rpmtag.h>
+#include <rpm/rpmpgp.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/xattr.h>
+#include <sys/mount.h>
+#include <linux/diglim.h>
+#include <linux/hash_info.h>
+#include <bits/endianness.h>
+
+#include "common.h"
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#include <linux/byteorder/big_endian.h>
+#else
+#include <linux/byteorder/little_endian.h>
+#endif
+
+#define SYSFS_MNTPOINT "/sys"
+#define SECURITYFS_MNTPOINT SYSFS_MNTPOINT "/kernel/security"
+#define DIGLIM_DIR SECURITYFS_MNTPOINT "/integrity/diglim"
+#define DIGEST_LIST_ADD DIGLIM_DIR "/digest_list_add"
+#define DIGEST_LIST_DEL DIGLIM_DIR "/digest_list_del"
+#define DIGEST_LIST_LABEL DIGLIM_DIR "/digest_list_label"
+#define DIGEST_LIST_DIR "/etc/digest_lists"
+
+#define MOUNT_FLAGS (MS_NOSUID | MS_NODEV | MS_NOEXEC | MS_RELATIME)
+
+enum hash_algo pgp_algo_mapping[PGPHASHALGO_SHA224 + 1] = {
+	[PGPHASHALGO_MD5] = HASH_ALGO_MD5,
+	[PGPHASHALGO_SHA1] = HASH_ALGO_SHA1,
+	[PGPHASHALGO_SHA224] = HASH_ALGO_SHA224,
+	[PGPHASHALGO_SHA256] = HASH_ALGO_SHA256,
+	[PGPHASHALGO_SHA384] = HASH_ALGO_SHA384,
+	[PGPHASHALGO_SHA512] = HASH_ALGO_SHA512,
+};
+
+struct rpm_hdr {
+	int32_t magic;
+	int32_t reserved;
+	int32_t tags;
+	int32_t datasize;
+} __attribute__((packed));
+
+struct rpm_entryinfo {
+	int32_t tag;
+	int32_t type;
+	int32_t offset;
+	int32_t count;
+} __attribute__((packed));
+
+/* from lib/hexdump.c (Linux kernel) */
+static int hex_to_bin(char ch)
+{
+	if ((ch >= '0') && (ch <= '9'))
+		return ch - '0';
+	ch = tolower(ch);
+	if ((ch >= 'a') && (ch <= 'f'))
+		return ch - 'a' + 10;
+	return -1;
+}
+
+int _hex2bin(unsigned char *dst, const char *src, size_t count)
+{
+	while (count--) {
+		int hi = hex_to_bin(*src++);
+		int lo = hex_to_bin(*src++);
+
+		if ((hi < 0) || (lo < 0))
+			return -1;
+
+		*dst++ = (hi << 4) | lo;
+	}
+	return 0;
+}
+
+static u8 *new_digest_list(enum hash_algo algo, enum compact_types type,
+			   u16 modifiers, u32 count)
+{
+	u8 *digest_list;
+	struct compact_list_hdr *hdr;
+
+	digest_list = calloc(1, sizeof(struct compact_list_hdr) +
+			     count * hash_digest_size[algo]);
+	if (!digest_list)
+		return NULL;
+
+	hdr = (struct compact_list_hdr *)digest_list;
+	hdr->version = 1;
+	hdr->type = __cpu_to_le16(type);
+	hdr->modifiers = __cpu_to_le16(modifiers);
+	hdr->algo = __cpu_to_le16(algo);
+	return digest_list;
+}
+
+static int upload_digest_list(int add_del_fd, int label_fd, u8 *digest_list,
+			      char *label)
+{
+	struct compact_list_hdr *hdr;
+	u32 datalen;
+	ssize_t ret;
+
+	hdr = (struct compact_list_hdr *)digest_list;
+	if (!hdr->count)
+		return 0;
+
+	datalen = hdr->datalen;
+	hdr->count = __cpu_to_le32(hdr->count);
+	hdr->datalen = __cpu_to_le32(hdr->datalen);
+
+	ret = write(label_fd, label, strlen(label) + 1);
+	if (ret < 0 || ret != strlen(label) + 1)
+		return -EIO;
+
+	ret = write(add_del_fd, digest_list, sizeof(*hdr) + datalen);
+	if (ret < 0) {
+		if (errno == EEXIST || errno == ENOENT)
+			return 0;
+
+		return ret;
+	} else if (ret != sizeof(*hdr) + datalen) {
+		return -EIO;
+	}
+
+	return ret;
+}
+
+static int parse_rpm(int add_del_fd, int label_fd, char *path,
+		     struct stat *st, bool only_immutable)
+{
+	void *buf, *bufp, *bufendp, *datap;
+	struct rpm_hdr *hdr;
+	int32_t tags;
+	struct rpm_entryinfo *entry;
+	void *digests = NULL, *algo_buf = NULL, *modes = NULL, *sizes = NULL;
+	void *dirnames = NULL, *basenames = NULL, *dirindexes = NULL;
+	char **dirnames_ptr = NULL;
+	u8 *digest_list = NULL, *digest_list_immutable = NULL;
+	u32 digests_count = 0, dirnames_count = 0;
+	u16 algo = HASH_ALGO_MD5;
+	char *label = strrchr(path, '/') + 1;
+	int ret = 0, fd_rpm, i, dirname_len;
+
+	const unsigned char rpm_header_magic[8] = {
+		0x8e, 0xad, 0xe8, 0x01, 0x00, 0x00, 0x00, 0x00
+	};
+
+	if (st->st_size < sizeof(*hdr)) {
+		printf("Missing RPM header\n");
+		return -EINVAL;
+	}
+
+	fd_rpm = open(path, O_RDONLY);
+	if (fd_rpm < 0) {
+		printf("Cannot access %s (%d)\n", path, -errno);
+		return -EACCES;
+	}
+
+	buf = bufp = mmap(NULL, st->st_size, PROT_READ | PROT_WRITE,
+			  MAP_PRIVATE, fd_rpm, 0);
+
+	close(fd_rpm);
+
+	if (bufp == MAP_FAILED)
+		return -ENOMEM;
+
+	if (memcmp(bufp, rpm_header_magic, sizeof(rpm_header_magic))) {
+		printf("Invalid RPM header\n");
+		ret = -EINVAL;
+		goto out;
+	}
+
+	hdr = (struct rpm_hdr *)bufp;
+	tags = __be32_to_cpu(hdr->tags);
+	datap = bufp + sizeof(*hdr) + tags * sizeof(struct rpm_entryinfo);
+	bufendp = bufp + st->st_size;
+	bufp += sizeof(*hdr);
+
+	for (i = 0; i < tags && (bufp + sizeof(*entry)) <= bufendp;
+	     i++, bufp += sizeof(*entry)) {
+		entry = bufp;
+
+		switch (__be32_to_cpu(entry->tag)) {
+		case RPMTAG_FILEDIGESTS:
+			digests = datap + __be32_to_cpu(entry->offset);
+			digests_count = __be32_to_cpu(entry->count);
+			break;
+		case RPMTAG_FILEDIGESTALGO:
+			algo_buf = datap + __be32_to_cpu(entry->offset);
+			break;
+		case RPMTAG_DIRNAMES:
+			dirnames = datap + __be32_to_cpu(entry->offset);
+			dirnames_count = __be32_to_cpu(entry->count);
+			break;
+		case RPMTAG_BASENAMES:
+			basenames = datap + __be32_to_cpu(entry->offset);
+			break;
+		case RPMTAG_DIRINDEXES:
+			dirindexes = datap + __be32_to_cpu(entry->offset);
+			break;
+		case RPMTAG_FILEMODES:
+			modes = datap + __be32_to_cpu(entry->offset);
+			break;
+		case RPMTAG_FILESIZES:
+			sizes = datap + __be32_to_cpu(entry->offset);
+			break;
+
+		if (digests && algo_buf && modes && dirnames && basenames &&
+		    dirindexes)
+			break;
+		}
+	}
+
+	if (!digests || !modes || !sizes || !dirnames || !basenames ||
+	    !dirindexes) {
+		ret = 0;
+		goto out;
+	}
+
+	if (algo_buf && algo_buf + sizeof(u32) <= bufendp)
+		algo = pgp_algo_mapping[__be32_to_cpu(*(u32 *)algo_buf)];
+
+	digest_list = new_digest_list(algo, COMPACT_FILE, 0, digests_count);
+	if (!digest_list) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	digest_list_immutable = new_digest_list(algo, COMPACT_FILE,
+						(1 << COMPACT_MOD_IMMUTABLE),
+						digests_count);
+	if (!digest_list_immutable) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	dirnames_ptr = calloc(dirnames_count, sizeof(*dirnames_ptr));
+	if (!dirnames_ptr) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	for (i = 0; i < dirnames_count; i++) {
+		dirname_len = strlen(dirnames) + 1;
+
+		if (dirnames + dirname_len > bufendp) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		dirnames_ptr[i] = dirnames;
+		dirnames += dirname_len;
+	}
+
+	for (i = 0; i < digests_count; i++) {
+		int digest_str_len = strlen(digests);
+		int basename_str_len = strlen(basenames);
+		u8 *digest_list_ptr = digest_list;
+		struct compact_list_hdr *cur_hdr;
+		u32 dirindex;
+		u32 size;
+		u16 mode;
+
+		if (digests + digest_str_len * 2 + 1 > bufendp) {
+			printf("RPM header read at invalid offset\n");
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (basenames + basename_str_len + 1 > bufendp) {
+			printf("RPM header read at invalid offset\n");
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (dirindexes + sizeof(u32) > bufendp) {
+			printf("RPM header read at invalid offset\n");
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (sizes + sizeof(u32) > bufendp) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (modes + sizeof(u16) > bufendp) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (!digest_str_len) {
+			digests += digest_str_len + 1;
+			basenames += basename_str_len + 1;
+			dirindexes += sizeof(u32);
+			sizes += sizeof(u32);
+			modes += sizeof(u16);
+			continue;
+		}
+
+		dirindex = __be32_to_cpu(*(u32 *)dirindexes);
+		size = __be32_to_cpu(*(u32 *)sizes);
+		mode = __be16_to_cpu(*(u16 *)modes);
+
+		if (((mode & 0111) || !(mode & 0222)) && size)
+			digest_list_ptr = digest_list_immutable;
+
+		if ((strstr(dirnames_ptr[dirindex], "/lib/modules") &&
+		    strncmp(basenames, "modules.", 8)) ||
+		    strstr(dirnames_ptr[dirindex], "/lib/firmware") ||
+		    strstr(dirnames_ptr[dirindex], "/usr/libexec/sudo"))
+			digest_list_ptr = digest_list_immutable;
+
+		if (only_immutable && digest_list_ptr != digest_list_immutable)
+			continue;
+
+		cur_hdr = (struct compact_list_hdr *)digest_list_ptr;
+
+		ret = _hex2bin(digest_list_ptr + sizeof(*cur_hdr) +
+			       cur_hdr->count * hash_digest_size[algo],
+			       digests, digest_str_len / 2);
+		if (ret < 0)
+			goto out;
+
+		digests += digest_str_len + 1;
+		basenames += basename_str_len + 1;
+		dirindexes += sizeof(u32);
+		sizes += sizeof(u32);
+		modes += sizeof(u16);
+
+		cur_hdr->count++;
+		cur_hdr->datalen += hash_digest_size[algo];
+	}
+
+	ret = upload_digest_list(add_del_fd, label_fd, digest_list, label);
+	if (ret < 0) {
+		printf("Failed to upload digest list\n");
+		goto out;
+	}
+
+	ret = upload_digest_list(add_del_fd, label_fd, digest_list_immutable,
+				 label);
+	if (ret < 0)
+		printf("Failed to upload digest list\n");
+out:
+	munmap(buf, st->st_size);
+	free(digest_list);
+	free(digest_list_immutable);
+	free(dirnames_ptr);
+	return ret;
+}
+
+int parse_digest_list(char *name)
+{
+	char *type_start, *format_start, *format_end;
+
+	type_start = strchr(name, '-');
+	if (!type_start++)
+		return 0;
+
+	format_start = strchr(type_start, '-');
+	if (!format_start++)
+		return 0;
+
+	format_end = strchr(format_start, '-');
+	if (!format_end)
+		return 0;
+
+	if (format_end - format_start != 3 ||
+	    strncmp(format_start, "rpm", 3))
+		return 0;
+
+	return 1;
+}
+
+int main(int argc, char *argv[])
+{
+	FTS *fts = NULL;
+	FTSENT *ftsent;
+	int fts_flags = (FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR | FTS_XDEV);
+	char *paths[2] = { NULL, NULL };
+	char *add_del_path = DIGEST_LIST_ADD;
+	char path[PATH_MAX], *path_ptr = path;
+	bool only_immutable = false;
+	struct stat st;
+	int ret, add_del_fd = -1, label_fd = -1;
+
+	if (argc < 3) {
+		printf("Usage: %s add|del <digest list path>\n",
+		       argv[0]);
+		return -EINVAL;
+	}
+
+	if (stat(argv[2], &st) == -1)
+		return -ENOENT;
+
+	strncpy(path, argv[2], sizeof(path) - 1);
+
+	if (!S_ISDIR(st.st_mode)) {
+		path_ptr = strrchr(path, '/');
+		if (!path_ptr)
+			path_ptr = path;
+		else
+			path_ptr++;
+	} else {
+		path_ptr = path + strlen(path);
+	}
+
+	strncpy(path_ptr, "/.immutable", sizeof(path) - (path_ptr - path));
+	if (!stat(path, &st))
+		only_immutable = true;
+
+	paths[0] = argv[2];
+
+	if (!strcmp(argv[1], "del"))
+		add_del_path = DIGEST_LIST_DEL;
+
+	add_del_fd = open(add_del_path, O_WRONLY);
+	if (add_del_fd < 0) {
+		printf("Unable to open %s\n", add_del_path);
+		return -EACCES;
+	}
+
+	label_fd = open(DIGEST_LIST_LABEL, O_WRONLY);
+	if (label_fd < 0) {
+		printf("Unable to open %s\n", DIGEST_LIST_LABEL);
+		ret = -EACCES;
+		goto out;
+	}
+
+	fts = fts_open(paths, fts_flags, NULL);
+	if (!fts) {
+		printf("Unable to open %s\n", argv[2]);
+		ret = -EACCES;
+		goto out;
+	}
+
+	while ((ftsent = fts_read(fts)) != NULL) {
+		switch (ftsent->fts_info) {
+		case FTS_F:
+			if (!parse_digest_list(ftsent->fts_name))
+				break;
+
+			ret = parse_rpm(add_del_fd, label_fd,
+					ftsent->fts_path, ftsent->fts_statp,
+					only_immutable);
+			if (ret < 0)
+				printf("Cannot parse %s\n", ftsent->fts_path);
+			break;
+		default:
+			break;
+		}
+	}
+
+out:
+	if (add_del_fd >= 0)
+		close(add_del_fd);
+	if (label_fd >= 0)
+		close(label_fd);
+	if (fts)
+		fts_close(fts);
+
+	return 0;
+}