root/sys/security/mac_veriexec_parser/mac_veriexec_parser.c
/*-
 * Copyright (c) 2019 Stormshield.
 * Copyright (c) 2019 Semihalf.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/param.h>
#include <sys/ctype.h>
#include <sys/eventhandler.h>
#include <sys/fcntl.h>
#include <sys/lock.h>
#include <sys/module.h>
#include <sys/mutex.h>
#include <sys/namei.h>
#include <sys/proc.h>
#include <sys/systm.h>
#include <sys/vnode.h>

#include <crypto/sha2/sha256.h>
#include <crypto/sha2/sha384.h>
#include <crypto/sha2/sha512.h>

#include <security/mac_veriexec/mac_veriexec.h>
#include <security/mac_veriexec/mac_veriexec_internal.h>

/* The following are based on sbin/veriexec */
struct fingerprint_type {
        const char      *fp_type;
        int             fp_size;
};

struct fp_flag {
        const char      *flag_name;
        int             flag;
};

static const struct fingerprint_type fp_table[] = {
        {"sha256=", SHA256_DIGEST_LENGTH},
#if MAXFINGERPRINTLEN >= SHA384_DIGEST_LENGTH
        {"sha384=", SHA384_DIGEST_LENGTH},
#endif
#if MAXFINGERPRINTLEN >= SHA512_DIGEST_LENGTH
        {"sha512=", SHA512_DIGEST_LENGTH},
#endif
        {NULL, 0}
};

static const struct fp_flag flags_table[] = {
        {"indirect",  VERIEXEC_INDIRECT},
        {"no_ptrace", VERIEXEC_NOTRACE},
        {"trusted",   VERIEXEC_TRUSTED},
        {"no_fips",   VERIEXEC_NOFIPS},
        {NULL, 0}
};

extern struct mtx ve_mutex;

static unsigned char    hexchar_to_byte(unsigned char c);
static int              hexstring_to_bin(unsigned char *buf);

static int      get_flags(const char *entry);
static int      get_fp(const char *entry, char **type,
                    unsigned char **digest, int *flags);
static int      verify_digest(const char *data, size_t len,
                    const unsigned char *expected_hash);

static int      open_file(const char *path, struct nameidata *nid);
static char     *read_manifest(char *path, unsigned char *digest);
static int      parse_entry(char *entry, char *prefix);
static int      parse_manifest(char *path, unsigned char *hash, char *prefix);

static unsigned char
hexchar_to_byte(unsigned char c)
{

        if (isdigit(c))
                return (c - '0');

        return (isupper(c) ? c - 'A' + 10 : c - 'a' + 10);
}

static int
hexstring_to_bin(unsigned char *buf)
{
        size_t          i, len;
        unsigned char   byte;

        len = strlen(buf);
        for (i = 0; i < len / 2; i++) {
                if (!isxdigit(buf[2 * i]) || !isxdigit(buf[2 * i + 1]))
                        return (EINVAL);

                byte = hexchar_to_byte(buf[2 * i]) << 4;
                byte += hexchar_to_byte(buf[2 * i + 1]);
                buf[i] = byte;
        }
        return (0);
}

static int
get_flags(const char *entry)
{
        int     i;
        int     result = 0;

        for (i = 0; flags_table[i].flag_name != NULL; i++)
                if (strstr(entry, flags_table[i].flag_name) != NULL)
                        result |= flags_table[i].flag;

        return (result);
}

/*
 * Parse a single line of manifest looking for a digest and its type.
 * We expect it to be in form of "path shaX=hash".
 * The line will be split into path, hash type and hash value.
 */
static int
get_fp(const char *entry, char **type, unsigned char **digest, int *flags)
{
        char    *delimiter;
        char    *local_digest;
        char    *fp_type;
        char    *prev_fp_type;
        size_t  min_len;
        int     i;

        delimiter = NULL;
        fp_type = NULL;
        prev_fp_type = NULL;

        for (i = 0; fp_table[i].fp_type != NULL; i++) {
                fp_type = strstr(entry, fp_table[i].fp_type);
                /* Look for the last "shaX=hash" in line */
                while (fp_type != NULL) {
                        prev_fp_type = fp_type;
                        fp_type++;
                        fp_type = strstr(fp_type, fp_table[i].fp_type);
                }
                fp_type = prev_fp_type;
                if (fp_type != NULL) {
                        if (fp_type == entry || fp_type[-1] != ' ')
                                return (EINVAL);

                        /*
                         * The entry should contain at least
                         * fp_type and digest in hexadecimal form.
                         */
                        min_len = strlen(fp_table[i].fp_type) +
                                2 * fp_table[i].fp_size;

                        if (strnlen(fp_type, min_len) < min_len)
                                return (EINVAL);

                        local_digest = &fp_type[strlen(fp_table[i].fp_type)];
                        delimiter = &local_digest[2 * fp_table[i].fp_size];

                        /*
                         * Make sure that digest is followed by
                         * some kind of delimiter.
                         */
                        if (*delimiter != '\n' &&
                            *delimiter != '\0' &&
                            *delimiter != ' ')
                                return (EINVAL);

                        /*
                         * Does the entry contain flags we need to parse?
                         */
                        if (*delimiter == ' ' && flags != NULL)
                                *flags = get_flags(delimiter);

                        /*
                         * Split entry into three parts:
                         * path, fp_type and digest.
                         */
                        local_digest[-1] = '\0';
                        *delimiter = '\0';
                        fp_type[-1] = '\0';
                        break;
                }
        }

        if (fp_type == NULL)
                return (EINVAL);

        if (type != NULL)
                *type = fp_type;

        if (digest != NULL)
                *digest = local_digest;

        return (0);
}

/*
 * Currently we verify manifest using sha256.
 * In future another env with hash type could be introduced.
 */
static int
verify_digest(const char *data, size_t len, const unsigned char *expected_hash)
{
        SHA256_CTX      ctx;
        unsigned char   hash[SHA256_DIGEST_LENGTH];

        SHA256_Init(&ctx);
        SHA256_Update(&ctx, data, len);
        SHA256_Final(hash, &ctx);

        return (memcmp(expected_hash, hash, SHA256_DIGEST_LENGTH));
}

static int
open_file(const char *path, struct nameidata *nid)
{
        int flags, rc;

        flags = FREAD;

        pwd_ensure_dirs();

        NDINIT(nid, LOOKUP, 0, UIO_SYSSPACE, path);
        rc = vn_open(nid, &flags, 0, NULL);
        if (rc != 0)
                return (rc);
        NDFREE_PNBUF(nid);

        return (0);
}

/*
 * Read the manifest from location specified in path and verify its digest.
 */
static char*
read_manifest(char *path, unsigned char *digest)
{
        struct nameidata        nid;
        struct vattr            va;
        char                    *data;
        ssize_t                 bytes_read, resid;
        int                     rc;

        data = NULL;
        bytes_read = 0;

        rc = open_file(path, &nid);
        if (rc != 0)
                goto fail;

        rc = VOP_GETATTR(nid.ni_vp, &va, curthread->td_ucred);
        if (rc != 0)
                goto fail;

        data = (char *)malloc(va.va_size + 1, M_VERIEXEC, M_WAITOK);

        while (bytes_read < va.va_size) {
                rc = vn_rdwr(
                    UIO_READ, nid.ni_vp, data,
                    va.va_size - bytes_read, bytes_read,
                    UIO_SYSSPACE, IO_NODELOCKED,
                    curthread->td_ucred, NOCRED, &resid, curthread);
                if (rc != 0)
                        goto fail;

                bytes_read = va.va_size - resid;
        }

        data[bytes_read] = '\0';

        VOP_UNLOCK(nid.ni_vp);
        (void)vn_close(nid.ni_vp, FREAD, curthread->td_ucred, curthread);

        /*
         * If digest is wrong someone might be trying to fool us.
         */
        if (verify_digest(data, va.va_size, digest))
                panic("Manifest hash doesn't match expected value!");

        return (data);

fail:
        if (data != NULL)
                free(data, M_VERIEXEC);

        return (NULL);
}

/*
 * Process single line.
 * First split it into path, digest_type and digest.
 * Then try to open the file and insert its fingerprint into metadata store.
 */
static int
parse_entry(char *entry, char *prefix)
{
        struct nameidata        nid;
        struct vattr            va;
        char                    path[MAXPATHLEN];
        char                    *fp_type;
        unsigned char           *digest;
        int                     rc, is_exec, flags;

        fp_type = NULL;
        digest = NULL;
        flags = 0;

        rc = get_fp(entry, &fp_type, &digest, &flags);
        if (rc != 0)
                return (rc);

        rc = hexstring_to_bin(digest);
        if (rc != 0)
                return (rc);

        if (strnlen(entry, MAXPATHLEN) == MAXPATHLEN)
                return (EINVAL);

        /* If the path is not absolute prepend it with a prefix */
        if (prefix != NULL && entry[0] != '/') {
                rc = snprintf(path, MAXPATHLEN, "%s/%s",
                            prefix, entry);
                if (rc < 0)
                        return (-rc);
        } else {
                strcpy(path, entry);
        }

        rc = open_file(path, &nid);
        if (rc != 0)
                return (rc);

        rc = VOP_GETATTR(nid.ni_vp, &va, curthread->td_ucred);
        if (rc != 0)
                goto out;

        is_exec = (va.va_mode & VEXEC);

        mtx_lock(&ve_mutex);
        rc = mac_veriexec_metadata_add_file(
            is_exec == 0,
            va.va_fsid, va.va_fileid, va.va_gen,
            digest,
            NULL, 0,
            flags, fp_type, 1);
        mtx_unlock(&ve_mutex);

out:
        VOP_UNLOCK(nid.ni_vp);
        vn_close(nid.ni_vp, FREAD, curthread->td_ucred, curthread);
        return (rc);
}

/*
 * Look for manifest in env that have beed passed by loader.
 * This routine should be called right after the rootfs is mounted.
 */
static int
parse_manifest(char *path, unsigned char *hash, char *prefix)
{
        char    *data;
        char    *entry;
        char    *next_entry;
        int     rc, success_count;

        data = NULL;
        success_count = 0;
        rc = 0;

        data = read_manifest(path, hash);
        if (data == NULL) {
                rc = EIO;
                goto out;
        }

        entry = data;
        while (entry != NULL) {
                next_entry = strchr(entry, '\n');
                if (next_entry != NULL) {
                        *next_entry = '\0';
                        next_entry++;
                }
                if (entry[0] == '\n' || entry[0] == '\0') {
                        entry = next_entry;
                        continue;
                }
                if ((rc = parse_entry(entry, prefix)))
                        printf("mac_veriexec_parser: Warning: Failed to parse"
                               " entry with rc:%d, entry:\"%s\"\n", rc, entry);
                else
                        success_count++;

                entry = next_entry;
        }
        rc = 0;

out:
        if (data != NULL)
                free(data, M_VERIEXEC);

        if (success_count == 0)
                rc = EINVAL;

        return (rc);
}

static void
parse_manifest_event(void *dummy)
{
        char            *manifest_path;
        char            *manifest_prefix;
        unsigned char   *manifest_hash;
        int             rc;

        /* If the envs are not set fail silently */
        manifest_path = kern_getenv("veriexec.manifest_path");
        if (manifest_path == NULL)
                return;

        manifest_hash = kern_getenv("veriexec.manifest_hash");
        if (manifest_hash == NULL) {
                freeenv(manifest_path);
                return;
        }

        manifest_prefix = kern_getenv("veriexec.manifest_prefix");

        if (strlen(manifest_hash) != 2 * SHA256_DIGEST_LENGTH)
                panic("veriexec.manifest_hash has incorrect size");

        rc = hexstring_to_bin(manifest_hash);
        if (rc != 0)
                panic("mac_veriexec: veriexec.loader.manifest_hash"
                    " doesn't contain a hash in hexadecimal form");

        rc = parse_manifest(manifest_path, manifest_hash, manifest_prefix);
        if (rc != 0)
                panic("mac_veriexec: Failed to parse manifest err=%d", rc);

        mtx_lock(&ve_mutex);
        mac_veriexec_set_state(
            VERIEXEC_STATE_LOADED | VERIEXEC_STATE_ACTIVE |
            VERIEXEC_STATE_LOCKED | VERIEXEC_STATE_ENFORCE);
        mtx_unlock(&ve_mutex);

        freeenv(manifest_path);
        freeenv(manifest_hash);
        if (manifest_prefix != NULL)
                freeenv(manifest_prefix);
}

EVENTHANDLER_DEFINE(mountroot, parse_manifest_event, NULL, 0);