commit:     04b4c6834fd83bf0329198c56894bd7dad6f7a6a
Author:     Fabian Groffen <grobian <AT> gentoo <DOT> org>
AuthorDate: Fri May 18 10:12:40 2018 +0000
Commit:     Fabian Groffen <grobian <AT> gentoo <DOT> org>
CommitDate: Fri May 18 10:12:40 2018 +0000
URL:        https://gitweb.gentoo.org/proj/portage-utils.git/commit/?id=04b4c683

qtegrity: new applet by Sam Besselink for use with IMA, bug #619988

Bug: https://bugs.gentoo.org/619988

 applets.h                                      |   7 +-
 man/include/qtegrity-70-relevant-files.include |   9 +
 man/include/qtegrity-authors.include           |   1 +
 man/include/qtegrity.desc                      |   8 +
 man/qtegrity.1                                 |  81 ++++
 qtegrity.c                                     | 509 +++++++++++++++++++++++++
 6 files changed, 614 insertions(+), 1 deletion(-)

diff --git a/applets.h b/applets.h
index 93146c6..daf7047 100644
--- a/applets.h
+++ b/applets.h
@@ -31,6 +31,7 @@ DECLARE_APPLET(qatom)
 DECLARE_APPLET(qmerge)
 DECLARE_APPLET(qcache)
 DECLARE_APPLET(qglsa) /* disable */
+DECLARE_APPLET(qtegrity)
 #undef DECLARE_APPLET
 
 #define DEFINE_APPLET_STUB(applet) \
@@ -62,8 +63,9 @@ static const struct applet_t {
        {"qtbz2",     qtbz2_main,     "<misc args>",     "manipulate tbz2 
packages"},
        {"quse",      quse_main,      "<useflag>",       "find pkgs using 
useflags"},
        {"qxpak",     qxpak_main,     "<misc args>",     "manipulate xpak 
archives"},
+       {"qtegrity",  qtegrity_main,  "<misc args>",     "verify files with 
IMA"},
 
-       /* aliases for equery capatability */
+       /* aliases for equery compatibility */
        {"belongs",   qfile_main,     NULL, NULL},
        /*"changes"*/
        {"check",     qcheck_main,    NULL, NULL},
@@ -82,6 +84,9 @@ static const struct applet_t {
        {"uickpkg",   qpkg_main,      NULL, NULL},
        /* {"glsa",      qglsa_main,     NULL, NULL}, */
 
+       /* alias for qtegrity */
+       {"integrity", qtegrity_main,  NULL, NULL},
+
        {NULL, NULL, NULL, NULL}
 };
 

diff --git a/man/include/qtegrity-70-relevant-files.include 
b/man/include/qtegrity-70-relevant-files.include
new file mode 100644
index 0000000..742658f
--- /dev/null
+++ b/man/include/qtegrity-70-relevant-files.include
@@ -0,0 +1,9 @@
+.SH RELEVANT FILES
+.PP
+Central list of known good digests
+.nf\fI
+       /var/db/QTEGRITY\fi
+.PP
+Linux kernel's recorded digests
+.nf\fI
+       /sys/kernel/security/ima/ascii_runtime_measurements\fi

diff --git a/man/include/qtegrity-authors.include 
b/man/include/qtegrity-authors.include
new file mode 100644
index 0000000..160ea6a
--- /dev/null
+++ b/man/include/qtegrity-authors.include
@@ -0,0 +1 @@
+Sam Besselink

diff --git a/man/include/qtegrity.desc b/man/include/qtegrity.desc
new file mode 100644
index 0000000..5f9029b
--- /dev/null
+++ b/man/include/qtegrity.desc
@@ -0,0 +1,8 @@
+The default behavior of \fBqtegrity\fP is to verify digests of performed
+executables to a list of known good digests. This requires an IMA-enabled
+linux kernel, which records digests of performed executables and exports them
+through securityfs. Using \fB\-\-ignore-non-existent\fP suppresses messages
+about recorded files that can't be accessed (assuming they got removed).
+By using \fB\-\-add\fP, the program behaves differently. No verification is
+performed, instead a digest is made of the provided file and appended to
+the list of known good digests.

diff --git a/man/qtegrity.1 b/man/qtegrity.1
new file mode 100644
index 0000000..76ed731
--- /dev/null
+++ b/man/qtegrity.1
@@ -0,0 +1,81 @@
+.\" generated by mkman.py, please do NOT edit!
+.TH qtegrity "1" "May 2018" "Gentoo Foundation" "qtegrity"
+.SH NAME
+qtegrity \- verify files with IMA
+.SH SYNOPSIS
+.B qtegrity
+\fI[opts] <misc args>\fR
+.SH DESCRIPTION
+The default behavior of \fBqtegrity\fP is to verify digests of performed
+executables to a list of known good digests. This requires an IMA-enabled
+linux kernel, which records digests of performed executables and exports them
+through securityfs. Using \fB\-\-ignore-non-existent\fP suppresses messages
+about recorded files that can't be accessed (assuming they got removed).
+By using \fB\-\-add\fP, the program behaves differently. No verification is
+performed, instead a digest is made of the provided file and appended to
+the list of known good digests.
+.SH OPTIONS
+.TP
+\fB\-a\fR \fI<arg>\fR, \fB\-\-add\fR \fI<arg>\fR
+Add file to store of known-good digests.
+.TP
+\fB\-i\fR, \fB\-\-ignore\-non\-existent\fR
+Be silent if recorded file no longer exists.
+.TP
+\fB\-s\fR, \fB\-\-show\-matches\fR
+Show recorded digests that match with known-good digests.
+.TP
+\fB\-\-root\fR \fI<arg>\fR
+Set the ROOT env var.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Make a lot of noise.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+Tighter output; suppress warnings.
+.TP
+\fB\-C\fR, \fB\-\-nocolor\fR
+Don't output color.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Print this help and exit.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+Print version and exit.
+.SH RELEVANT FILES
+.PP
+Central list of known good digests
+.nf\fI
+       /var/db/QTEGRITY\fi
+.PP
+Linux kernel's recorded digests
+.nf\fI
+       /sys/kernel/security/ima/ascii_runtime_measurements\fi
+.SH "REPORTING BUGS"
+Please report bugs via http://bugs.gentoo.org/
+.br
+Product: Portage Development; Component: Tools, Assignee:
[email protected]
+.SH AUTHORS
+.nf
+Ned Ludd <[email protected]>
+Mike Frysinger <[email protected]>
+Sam Besselink
+.fi
+.SH "SEE ALSO"
+.BR q (1),
+.BR qatom (1),
+.BR qcache (1),
+.BR qcheck (1),
+.BR qdepends (1),
+.BR qfile (1),
+.BR qgrep (1),
+.BR qlist (1),
+.BR qlop (1),
+.BR qmerge (1),
+.BR qpkg (1),
+.BR qsearch (1),
+.BR qsize (1),
+.BR qtbz2 (1),
+.BR quse (1),
+.BR qxpak (1)

diff --git a/qtegrity.c b/qtegrity.c
new file mode 100644
index 0000000..8af5b08
--- /dev/null
+++ b/qtegrity.c
@@ -0,0 +1,509 @@
+/*
+ * Copyright 2005-2018 Gentoo Foundation
+ * Distributed under the terms of the GNU General Public License v2
+ *
+ * Copyright 2005-2010 Ned Ludd        - <[email protected]>
+ * Copyright 2005-2014 Mike Frysinger  - <[email protected]>
+ * Copyright 2017-2018 Sam Besselink
+ */
+
+#ifdef APPLET_qtegrity
+
+#define QTEGRITY_FLAGS "a:is" COMMON_FLAGS
+static struct option const qtegrity_long_opts[] = {
+       {"add",                 a_argument, NULL, 'a'},
+       {"ignore-non-existent", no_argument, NULL, 'i'},
+       {"show-matches", no_argument, NULL, 's'},
+/* TODO, add this functionality
+       {"convert", a_argument, NULL, 'c'}
+*/
+       COMMON_LONG_OPTS
+};
+static const char * const qtegrity_opts_help[] = {
+       "Add file to store of known-good digests",
+       "Be silent if recorded file no longer exists",
+       "Show recorded digests that match with known-good digests",
+/* TODO
+       "Convert known good digests to different hash function",
+*/
+       COMMON_OPTS_HELP
+};
+#define qtegrity_usage(ret) usage(ret, QTEGRITY_FLAGS, qtegrity_long_opts, 
qtegrity_opts_help, NULL, lookup_applet_idx("qtegrity"))
+
+struct qtegrity_opt_state {
+       bool ima;
+       bool add;
+       char* add_file;
+       bool ignore_non_exist;
+       bool show_matches;
+/* TODO
+       bool convert;
+*/
+};
+
+#define FILE_SUCCESS 1
+#define FILE_EMPTY 2
+#define FILE_RELATIVE 3
+
+#define SHA1_DIGEST_LENGTH 40
+#define SHA256_PREFIX_LENGTH 8
+#define SHA256_DIGEST_LENGTH 64
+#define SHA256_LENGTH (SHA256_PREFIX_LENGTH + SHA256_DIGEST_LENGTH)
+#define SHA512_DIGEST_LENGTH 128
+
+static void external_check_sha(char * ret_digest, char * filepath, char * 
algo) {
+       size_t size_digest = 1;
+
+       if (strcmp(algo, "sha256") == 0) {
+               size_digest = 64;
+       } else if (strcmp(algo, "sha512") == 0) {
+               size_digest = 128;
+       }
+
+       if ((strcmp(algo, "sha256") != 0) && (strcmp(algo, "sha512") != 0)) {
+               return;
+       }
+
+       char cmd[11];
+       snprintf(cmd, 10, "%ssum", algo);
+
+       int pipefd[2];
+       pid_t pid;
+
+       if (pipe(pipefd) == -1) {
+               perror("Couldn't create pipe to shasum\n");
+               exit(1);
+       }
+       if ((pid = fork()) == -1) {
+               perror("Couldn't fork to shasum\n");
+               exit(1);
+       }
+       if (pid == 0)
+       {
+               /* Child. Redirect stdout and stderr to pipe, replace execution
+                * environment */
+               close(pipefd[0]);
+               dup2(pipefd[1], STDOUT_FILENO);
+               dup2(pipefd[1], STDERR_FILENO);
+               execlp(cmd, cmd, filepath, NULL);
+               perror("Executing shasum failed\n");
+               exit(1);
+       }
+
+       /* Only parent gets here. Listen to pipe */
+       close(pipefd[1]);
+       FILE* output = fdopen(pipefd[0], "r");
+       if (output == NULL) {
+               printf("Failed to run command '%s'\n", cmd);
+               exit(1);
+       }
+
+       /* Read pipe line for line */
+       while (fgets(ret_digest, size_digest+1, output))
+       {
+               if (strlen(ret_digest) == 64) /* Found what we need, can stop */
+               {
+                       kill(pid, SIGKILL);
+                       break;
+               }
+       }
+
+       pclose(output);
+       return;
+}
+
+static void get_fname_from_line(char * line, char **ret, int digest_size, int 
offset)
+{
+       size_t dlenstr = strlen(line);
+       char *p;
+       /* Skip first 123 chars to get to file depends on digest_func in IMA */
+       size_t skip = ((digest_size == SHA256_DIGEST_LENGTH) ||
+                       (digest_size == SHA512_DIGEST_LENGTH)) ?
+               digest_size+offset+8 : digest_size+offset+6;
+
+       if (dlenstr > skip) { /* assume file is at least two chars long */
+               int segment_size = dlenstr - skip - 1;
+               p = xmalloc(segment_size+1);
+               memcpy(p, line + skip, segment_size);
+               p[segment_size] = '\0';
+       } else {
+               /* E.g. digest used wrong hash algo, or malformed input */
+               p = NULL;
+       }
+
+       *ret = p;
+}
+
+static void get_digest_from_line(char * line, char * ret, int digest_size, int 
offset)
+{
+       size_t dlenstr = strlen(line);
+       /* Skip first chars to get to digest depends on digest_func in IMA */
+       int skip = ((digest_size == SHA256_DIGEST_LENGTH) ||
+                       (digest_size == SHA512_DIGEST_LENGTH)) ?
+               offset+8 : offset+6;
+
+       if (dlenstr > (digest_size+skip+1)) {
+               memcpy(ret, line+skip, digest_size);
+               ret[digest_size] = '\0';
+       }
+}
+
+static void get_known_good_digest(const char * fn_store, char * 
recorded_fname, char * ret, int recorded_digest_size)
+{
+       /* Open file with known good hashes */
+       int fd_store;
+       FILE *fp_store;
+
+       fd_store = open(fn_store, O_RDONLY|O_CLOEXEC, 0);
+       if (fd_store == -1) {
+               warnp("unable to open(%s)", fn_store);
+               exit(0);
+       }
+       if ((fp_store = fdopen(fd_store, "r")) == NULL) {
+               warnp("unable to fopen(%s, r)", fn_store);
+               close(fd_store);
+               exit(0);
+       }
+
+       char *buffered_line, *line, *fname;
+       size_t linelen;
+
+       /* Iterate over lines in known-good-hashes-file; per line: if fname
+        * matches, grab hash. */
+       buffered_line = line = fname = NULL;
+       while (getline(&line, &linelen, fp_store) != -1) {
+               free(buffered_line);
+               buffered_line = xstrdup(line);
+
+               get_fname_from_line(line, &fname, recorded_digest_size, 15);
+
+               if (fname == NULL) {
+                       /* probably line without digest (e.g. symlink) */
+                       continue;
+               }
+
+               if (strcmp(recorded_fname, fname) == 0) {
+                       get_digest_from_line(line, ret, recorded_digest_size, 
9);
+
+                       free(fname);
+                       break;
+               }
+
+               free(fname);
+       }
+
+       free(line);
+       free(buffered_line);
+
+       close(fd_store);
+       fclose(fp_store);
+}
+
+static int get_size_digest(char * line)
+{
+       int ret = 0;
+
+       char *pfound;
+       /* find colon; it is boundary between end of hash func & begin of
+        * digest */
+       pfound = strchr(line, ':');
+       if (pfound != NULL) {
+               int dpfound = pfound - line;
+               int cutoff_prefix = 0;
+
+               if (dpfound == 55 || dpfound == 6) {
+                       ret = SHA1_DIGEST_LENGTH;
+               } else if (dpfound == 57) {
+                       cutoff_prefix = 51;
+               } else if (dpfound == 8) {
+                       cutoff_prefix = 0;
+               }
+
+               int dsegment = dpfound - cutoff_prefix;
+
+               char *line_segment;
+               line_segment = xmalloc(dsegment + 1);
+               /* chop off the first chars to get to the hash func */
+               memcpy(line_segment, line + cutoff_prefix, dsegment);
+               line_segment[dsegment] = '\0';
+
+               /* If line segment equals name of hash func, then return
+                * relevant const. */
+               if (strcmp(line_segment, "sha512") == 0) {
+                       ret = SHA512_DIGEST_LENGTH;
+               } else if (strcmp(line_segment, "sha256") == 0) {
+                       ret = SHA256_DIGEST_LENGTH;
+               } else {
+                       printf("Expected sha algo, got %s", line_segment);
+               }
+
+               free(line_segment);
+       }
+
+       return ret;
+}
+
+static int check_file(char * filename)
+{
+       /* TODO, this is 4096 too low, because this variable also holds
+        * path; for linux path is max 4096 chars */
+       if (strlen(filename) > 255)
+               err("Filename too long");
+
+       if (filename[0] != '/') {
+               return FILE_RELATIVE;
+       }
+
+       return FILE_SUCCESS;
+}
+
+int qtegrity_main(int argc, char **argv)
+{
+       int i;
+
+       struct qtegrity_opt_state state = {
+               .ima = true,
+               .add = false,
+               .ignore_non_exist = false,
+               .show_matches = false,
+/* TODO
+               .convert = false;
+*/
+       };
+
+       while ((i = GETOPT_LONG(QTEGRITY, qtegrity, "")) != -1) {
+               switch (i) {
+                       COMMON_GETOPTS_CASES(qtegrity)
+                       case 'a':
+                               state.ima = false;
+                               state.add = true;
+                               if (check_file(optarg) == FILE_SUCCESS) {
+                                       state.add_file = xstrdup(optarg);
+                               } else {
+                                       err("Expected absolute file as 
argument, got '%s'", optarg);
+                               }
+                               break;
+                       case 'i': state.ignore_non_exist = true; break;
+                       case 's': state.show_matches = true; break;
+               }
+       }
+
+       if (state.ima) {
+               const char *fn_ima =
+                       "/sys/kernel/security/ima/ascii_runtime_measurements";
+               int fd_ima;
+               FILE *fp_ima;
+               struct stat st;
+
+               fd_ima = open(fn_ima, O_RDONLY|O_CLOEXEC, 0);
+               if (fd_ima == -1) {
+                       /* TODO, shouldn't we explicitly remind user 
IMA/securityfs
+                        * is needed? */
+                       warnp("Unable to open(%s)", fn_ima);
+                       exit(0);
+               }
+               if ((fp_ima = fdopen(fd_ima, "r")) == NULL) {
+                       warnp("Unable to fopen(%s, r)", fn_ima);
+                       close(fd_ima);
+                       exit(0);
+               }
+
+               char *buffered_line, *line, *recorded_fname;
+               int recorded_digest_size = 0;
+               size_t linelen;
+
+               /* Iterate over IMA file, grab fname and digest, get known good
+                * digest for fname and compare */
+               buffered_line = line = recorded_fname = NULL;
+               while (getline(&line, &linelen, fp_ima) != -1) {
+                       char *recorded_digest;
+                       char *digest;
+
+                       free(buffered_line);
+                       buffered_line = xstrdup(line);
+
+                       if (buffered_line[0] != '1' || buffered_line[1] != '0')
+                               continue;
+
+                       recorded_digest_size = get_size_digest(buffered_line);
+                       recorded_digest = xmalloc(recorded_digest_size+1);
+                       recorded_digest[0] = '\0';
+
+                       /* grab fname from IMA file line */
+                       get_fname_from_line(buffered_line, &recorded_fname,
+                                       recorded_digest_size, 51);
+                       /* grab digest from IMA file line, @TODO, check whether
+                        * digest == 000etc */
+                       get_digest_from_line(buffered_line, recorded_digest,
+                                       recorded_digest_size, 50);
+
+                       if (recorded_fname == NULL || recorded_digest == NULL) {
+                               printf("Empty recorded filename: %s\n", line);
+
+                               if (recorded_fname != NULL)
+                                       free(recorded_fname);
+
+                               if (recorded_digest != NULL)
+                                       free(recorded_digest);
+
+                               continue;
+                       }
+
+                       if (check_file(recorded_fname) == FILE_RELATIVE) {
+                               printf("Seems like a kernel process: %s\n", 
recorded_fname);
+
+                               free(recorded_fname);
+                               free(recorded_digest);
+                               continue;
+                       }
+
+                       if (stat(recorded_fname, &st) < 0) {
+                               if (!state.ignore_non_exist)
+                                       printf("Couldn't access recorded file 
'%s'\n",
+                                                       recorded_fname);
+
+                               free(recorded_fname);
+                               free(recorded_digest);
+                               continue;
+                       }
+
+                       if (!(st.st_mode & S_IXUSR ||
+                                               st.st_mode & S_IXGRP ||
+                                               st.st_mode & S_IXOTH))
+                       {
+                               free(recorded_fname);
+                               free(recorded_digest);
+                               continue;
+                       }
+
+                       digest = xmalloc(recorded_digest_size+1);
+                       digest[0] = '\0';
+
+                       /* first try custom known good digests for fname */
+                       get_known_good_digest("/var/db/QTEGRITY_custom",
+                                       recorded_fname, digest, 
recorded_digest_size);
+
+                       if (digest[0] == '\0') {
+                               digest[0] = '\0';
+                               /* then try from OS source */
+                               get_known_good_digest("/var/db/QTEGRITY",
+                                               recorded_fname, digest, 
recorded_digest_size);
+
+                               if (digest[0] == '\0') {
+                                       printf("No digest found for: %s\n", 
line);
+
+                                       free(recorded_fname);
+                                       free(recorded_digest);
+                                       free(digest);
+                                       continue;
+                               }
+                       }
+
+                       if (strcmp(recorded_digest, digest) != 0) {
+                               printf("Digest didn't match for %s\n", 
recorded_fname);
+                               printf("Known-good: '%s'...\nRecorded: 
'%s'\n\n",
+                                               digest, recorded_digest);
+                       } else if (state.show_matches) {
+                               printf("Success! Digest matched for %s\n", 
recorded_fname);
+                       }
+
+                       free(recorded_fname);
+                       free(recorded_digest);
+                       free(digest);
+               }
+
+               free(line);
+               free(buffered_line);
+
+               close(fd_ima);
+               fclose(fp_ima);
+       } else if (state.add) {
+               /* Add a single executable file+digest to the custom digest 
store */
+               const char *fn_qtegrity_custom = "/var/db/QTEGRITY_custom";
+               int fd_qtegrity_custom;
+               FILE *fp_qtegrity_custom;
+               struct stat st;
+               int flush_status;
+
+               fd_qtegrity_custom =
+                       open(fn_qtegrity_custom, O_RDWR|O_CREAT|O_CLOEXEC, 0);
+               if (fd_qtegrity_custom == -1) {
+                       warnp("Unable to open(%s)", fn_qtegrity_custom);
+                       exit(0);
+               }
+               if ((fp_qtegrity_custom = fdopen(fd_qtegrity_custom, "w+")) == 
NULL) {
+                       warnp("Unable to fopen(%s, r)", fn_qtegrity_custom);
+                       close(fd_qtegrity_custom);
+                       exit(0);
+               }
+
+               printf("Adding %s to %s\n", state.add_file, fn_qtegrity_custom);
+
+               if (stat(state.add_file, &st) < 0)
+                       err("Couldn't access file '%s'\n", state.add_file);
+
+               if (!(st.st_mode & S_IXUSR ||
+                                       st.st_mode & S_IXGRP ||
+                                       st.st_mode & S_IXOTH))
+                       err("File '%s' is not executable\n", state.add_file);
+
+               /* add digest */
+               char *hash_algo = (char *)"sha256";
+               char *file_digest;
+               file_digest = xmalloc(SHA256_DIGEST_LENGTH+1);
+               file_digest[0] = '\0';
+               external_check_sha(file_digest, state.add_file, hash_algo);
+
+               /* Iterate over lines; if fname matches, exit-loop */
+               char *line, *fname;
+               size_t linelen;
+               int recorded_digest_size = 0;
+               int skip = 0;
+               line = fname = NULL;
+               while (getline(&line, &linelen, fp_qtegrity_custom) != -1) {
+                       recorded_digest_size = get_size_digest(line);
+                       get_fname_from_line(line, &fname, recorded_digest_size, 
5);
+
+                       /* probably line without digest (e.g. symlink) */
+                       if (fname == NULL)
+                               continue;
+
+                       if (strcmp(state.add_file, fname) == 0) {
+                               printf("Executable already recorded, "
+                                               "replacing digest with %s\n", 
file_digest);
+                               skip = ((recorded_digest_size == 
SHA256_DIGEST_LENGTH) ||
+                                               (recorded_digest_size == 
SHA512_DIGEST_LENGTH)) ?
+                                       recorded_digest_size+6+8 : 
recorded_digest_size+6+6;
+                               fseek(fp_qtegrity_custom, -skip-strlen(fname), 
SEEK_CUR);
+                               free(fname);
+                               break;
+                       }
+
+                       free(fname);
+               }
+
+               free(line);
+
+               fputs(hash_algo, fp_qtegrity_custom);
+               fputs(":", fp_qtegrity_custom);
+               fputs(file_digest, fp_qtegrity_custom);
+               fputs(" file:", fp_qtegrity_custom);
+               fputs(state.add_file, fp_qtegrity_custom);
+               fputs("\n", fp_qtegrity_custom);
+
+               flush_status = fflush(fp_qtegrity_custom);
+               if (flush_status != 0)
+                       puts("Error flushing stream!");
+
+               free(file_digest);
+       }
+
+       if (state.add)
+               free(state.add_file);
+
+       return EXIT_SUCCESS;
+}
+
+#else
+DEFINE_APPLET_STUB(qtegrity)
+#endif

Reply via email to