/********************************************************************************/
/*                                                                              */
/*                           TPM Main Program                                   */
/*                 Written by Ken Goldman, Stefan Berger                        */
/*                     IBM Thomas J. Watson Research Center                     */
/*                                                                              */
/* (c) Copyright IBM Corporation 2006, 2010, 2015, 2016, 2019			*/
/*										*/
/* All rights reserved.								*/
/* 										*/
/* Redistribution and use in source and binary forms, with or without		*/
/* modification, are permitted provided that the following conditions are	*/
/* met:										*/
/* 										*/
/* Redistributions of source code must retain the above copyright notice,	*/
/* this list of conditions and the following disclaimer.			*/
/* 										*/
/* 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.		*/
/* 										*/
/* Neither the names of the IBM Corporation nor the names of its		*/
/* contributors may be used to endorse or promote products derived from		*/
/* this software without specific prior written permission.			*/
/* 										*/
/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS		*/
/* "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 COPYRIGHT		*/
/* HOLDER OR CONTRIBUTORS 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 <config.h>

#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <getopt.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#include <libtpms/tpm_error.h>
#include <libtpms/tpm_library.h>
#include <libtpms/tpm_memory.h>

#include "main.h"
#include "swtpm_debug.h"
#include "swtpm_io.h"
#include "swtpm_nvstore.h"
#include "common.h"
#include "locality.h"
#include "logging.h"
#include "pidfile.h"
#include "tpmlib.h"
#include "utils.h"
#include "ctrlchannel.h"
#include "mainloop.h"
#ifdef WITH_VTPM_PROXY
#include "vtpm_proxy.h"
#endif
#include "tpmstate.h"
#include "daemonize.h"
#include "seccomp_profile.h"
#include "options.h"
#include "capabilities.h"

/* local variables */
static int notify_fd[2] = {-1, -1};

static struct libtpms_callbacks callbacks = {
    .sizeOfStruct            = sizeof(struct libtpms_callbacks),
    .tpm_nvram_init          = SWTPM_NVRAM_Init,
    .tpm_nvram_loaddata      = SWTPM_NVRAM_LoadData,
    .tpm_nvram_storedata     = SWTPM_NVRAM_StoreData,
    .tpm_nvram_deletename    = SWTPM_NVRAM_DeleteName,
    .tpm_io_init             = NULL,
    .tpm_io_getlocality      = mainloop_cb_get_locality,
};

static void sigterm_handler(int sig __attribute__((unused)))
{
    TPM_DEBUG("Terminating...\n");
    if (write(notify_fd[1], "T", 1) < 0) {
        logprintf(STDERR_FILENO, "Error: sigterm notification failed: %s\n",
                  strerror(errno));
    }
    mainloop_terminate = true;
}

#ifdef WITH_VTPM_PROXY
static int create_vtpm_proxy(struct vtpm_proxy_new_dev *vtpm_new_dev,
                             int *_errno)
{
    int fd, n, ret = 0;

    fd = open("/dev/vtpmx", O_RDWR);
    if (fd < 0) {
        logprintf(STDERR_FILENO, "Could not open /dev/vtpmx: %s\n",
                  strerror(errno));
        *_errno = errno;
        return -1;
    }

    n = ioctl(fd, VTPM_PROXY_IOC_NEW_DEV, vtpm_new_dev);
    if (n) {
        logprintf(STDERR_FILENO, "Ioctl to create vtpm proxy failed: %s\n",
                  strerror(errno));
        *_errno = errno;
        ret = -1;
    }
    close(fd);

    return ret;
}
#endif

static void usage(FILE *file, const char *prgname, const char *iface)
{
    fprintf(file,
    "Usage: %s %s [options]\n"
    "\n"
    "The following options are supported:\n"
    "\n"
    "-c|--chardev <device>\n"
    "                 : use the given character device\n"
    "-f|--fd <fd>     : use the given character device file descriptor\n"
    "-d|--daemon      : daemonize the TPM\n"
    "--ctrl type=[unixio|tcp][,path=<path>][,port=<port>[,bindaddr=address[,ifname=ifname]]][,fd=<filedescriptor|clientfd=<filedescriptor>][,mode=0...][,uid=uid][,gid=gid]\n"
    "                 : TPM control channel using either UnixIO or TCP sockets;\n"
    "                   the path is only valid for Unixio channels; the port must\n"
    "                   be given in case the type is TCP; the TCP socket is bound\n"
    "                   to 127.0.0.1 by default and other bind addresses can be\n"
    "                   given with the bindaddr parameter; if fd is provided,\n"
    "                   it will be treated as a server socket and used for \n"
    "                   accepting client connections; if clientfd is provided,\n"
    "                   it will be treaded as client connection;\n"
    "                   NOTE: fd and clientfd are mutually exclusive and \n"
    "                   clientfd is only valid for UnixIO channels\n"
    "                   mode allows a user to set the file mode bits of a Unixio socket;\n"
    "                   the value must be given in octal number format\n"
    "                   uid and gid set the ownership of the Unixio socket's file;\n"
    "--migration-key file=<path>|fd=<fd>[,mode=aes-cbc|aes-256-cbc][,format=hex|binary][,remove=[true|false]]\n"
    "                 : use an AES key for the encryption of the TPM's state\n"
    "                   when it is retrieved from the TPM via ioctls;\n"
    "                   Setting this key ensures that the TPM's state will always\n"
    "                   be encrypted when migrated\n"
    "--migration-key pwdfile=<path>|pwdfd=<fd>[,mode=aes-cbc|aes-256-cbc][,remove=[true|false]][,kdf=sha512|pbkdf2]\n"
    "                 : provide a passphrase in a file; the AES key will be\n"
    "                   derived from this passphrase; default kdf is PBKDF2\n"
    "--log file=<path>|fd=<filedescriptor>[,level=n][,prefix=<prefix>][,truncate]\n"
    "                 : write the TPM's log into the given file rather than\n"
    "                   to the console; provide '-' for path to avoid logging\n"
    "                   log level 5 and higher will enable libtpms logging;\n"
    "                   all logged output will be prefixed with prefix;\n"
    "                   the log file can be reset (truncate)\n"
    "--key file=<path>|fd=<fd>[,mode=aes-cbc|aes-256-cbc][,format=hex|binary][,remove=[true|false]]\n"
    "                 : use an AES key for the encryption of the TPM's state\n"
    "                   files; use the given mode for the block encryption;\n"
    "                   the key is to be provided as a hex string or in binary\n"
    "                   format; the keyfile can be automatically removed using\n"
    "                   the remove parameter\n"
    "--key pwdfile=<path>|pwdfd=<fd>[,mode=aes-cbc|aes-256-cbc][,remove=[true|false]][,kdf=sha512|pbkdf2]\n"
    "                 : provide a passphrase in a file; the AES key will be\n"
    "                   derived from this passphrase; default kdf is PBKDF2\n"
    "--pid file=<path>|fd=<filedescriptor>\n"
    "                 : write the process ID into the given file\n"
    "--tpmstate dir=<dir>[,mode=0...]|backend-uri=<uri>\n"
    "                 : set the directory or uri where the TPM's state will be written\n"
    "                   into; the TPM_PATH environment variable can be used\n"
    "                   instead of dir option;\n"
    "                   mode allows a user to set the file mode bits of the state files;\n"
    "                   the default mode is 0640;\n"
    "-r|--runas <user>: change to the given user\n"
#ifdef WITH_VTPM_PROXY
    "--vtpm-proxy     : spawn a Linux vTPM proxy driver device and read TPM\n"
#endif
    "                   command from its anonymous file descriptor\n"
    "--locality [reject-locality-4][,allow-set-locality]\n"
    "                 : reject-locality-4: reject any command in locality 4\n"
    "                   allow-set-locality: accept SetLocality command\n"
    "--flags [not-need-init][,startup-clear|startup-state|startup-deactivated|startup-none]\n"
    "                 : not-need-init: commands can be sent without needing to\n"
    "                   send an INIT via control channel; not needed when using\n"
    "                   --vtpm-proxy\n"
    "                   startup-...: send Startup command with this type;\n"
    "                   when --vtpm-proxy is used, startup-clear is used\n"
    "--tpm2           : choose TPM2 functionality\n"
#ifdef WITH_SECCOMP
# ifndef SCMP_ACT_LOG
    "--seccomp action=none|kill\n"
# else
    "--seccomp action=none|kill|log\n"
# endif
    "                 : Choose the action of the seccomp profile when a\n"
    "                   blacklisted syscall is executed; default is kill\n"
#endif
    "--print-capabilites\n"
    "                 : print capabilities and terminate\n"
    "--print-states\n"
    "                 : print existing TPM states and terminate\n"
    "-h|--help        : display this help screen and terminate\n"
    "\n",
    prgname, iface);
}

static void swtpm_cleanup(struct ctrlchannel *cc)
{
    pidfile_remove();
    ctrlchannel_free(cc);
    log_global_free();
    tpmstate_global_free();
    SWTPM_NVRAM_Shutdown();
}

#ifdef WITH_VTPM_PROXY
static int swtpm_chardev_create_vtpm_proxy(struct mainLoopParams *mlp,
                                           bool *need_init_cmd) {
    struct vtpm_proxy_new_dev vtpm_new_dev = {
        .flags = 0,
    };
    int _errno;

    if (mlp->tpmversion == TPMLIB_TPM_VERSION_2)
        vtpm_new_dev.flags = VTPM_PROXY_FLAG_TPM2;

    /* Will be adjusted for TPM 2 */
    mlp->startupType = TPM_ST_CLEAR;
    *need_init_cmd = false;

    if (mlp->fd >= 0) {
        logprintf(STDERR_FILENO,
                  "Cannot use vTPM proxy with a provided device.\n");
        return -1;
    }

    if (create_vtpm_proxy(&vtpm_new_dev, &_errno))
        return -1;

    mlp->fd = vtpm_new_dev.fd;

    mlp->flags |= MAIN_LOOP_FLAG_TERMINATE | MAIN_LOOP_FLAG_USE_FD;
    SWTPM_IO_SetSocketFD(mlp->fd);

    fprintf(stdout, "New TPM device: /dev/tpm%u (major/minor = %u/%u)\n",
            vtpm_new_dev.tpm_num,
            vtpm_new_dev.major, vtpm_new_dev.minor);
    mlp->locality_flags |= LOCALITY_FLAG_ALLOW_SETLOCALITY;

    return 0;
}
#endif

int swtpm_chardev_main(int argc, char **argv, const char *prgname, const char *iface)
{
    TPM_RESULT rc = 0;
    int daemonize = FALSE;
    int opt, longindex, ret;
    struct stat statbuf;
    struct mainLoopParams mlp = {
        .cc = NULL,
        .fd = -1,
        .flags = 0,
        .locality_flags = 0,
        .tpmversion = TPMLIB_TPM_VERSION_1_2,
        .startupType = _TPM_ST_NONE,
    };
    unsigned long val;
    char *end_ptr;
    char *keydata = NULL;
    char *migkeydata = NULL;
    char *logdata = NULL;
    char *piddata = NULL;
    char *localitydata = NULL;
    char *tpmstatedata = NULL;
    char *ctrlchdata = NULL;
    char *flagsdata = NULL;
    char *seccompdata = NULL;
    char *runas = NULL;
#ifdef WITH_VTPM_PROXY
    bool use_vtpm_proxy = false;
#endif
#ifdef DEBUG
    time_t              start_time;
#endif
    bool need_init_cmd = true;
    unsigned int seccomp_action;
    bool printcapabilities = false;
    bool printstates = false;
    static struct option longopts[] = {
        {"daemon"    ,       no_argument, 0, 'd'},
        {"help"      ,       no_argument, 0, 'h'},
        {"chardev"   , required_argument, 0, 'c'},
        {"fd"        , required_argument, 0, 'f'},
        {"runas"     , required_argument, 0, 'r'},
        {"locality"  , required_argument, 0, 'L'},
        {"log"       , required_argument, 0, 'l'},
        {"key"       , required_argument, 0, 'k'},
        {"migration-key", required_argument, 0, 'K'},
        {"pid"       , required_argument, 0, 'P'},
        {"tpmstate"  , required_argument, 0, 's'},
        {"ctrl"      , required_argument, 0, 'C'},
        {"flags"     , required_argument, 0, 'F'},
#ifdef WITH_VTPM_PROXY
        {"vtpm-proxy",       no_argument, 0, 'v'},
#endif
        {"tpm2"      ,       no_argument, 0, '2'},
#ifdef WITH_SECCOMP
        {"seccomp"   , required_argument, 0, 'S'},
#endif
        {"print-capabilities"
                     ,       no_argument, 0, 'a'},
        {"print-states",     no_argument, 0, 'e'},
        {NULL        , 0                , 0, 0  },
    };

    log_set_prefix("swtpm: ");

    while (TRUE) {
        opt = getopt_long(argc, argv, "dhc:f:r:", longopts, &longindex);

        if (opt == -1)
            break;

        switch (opt) {
        case 'd':
            daemonize = TRUE;
            if (daemonize_prep() == -1) {
                logprintf(STDERR_FILENO,
                          "Could not prepare to daemonize: %s\n", strerror(errno));
                exit(EXIT_FAILURE);
            }
            break;

        case 'c':
            if (mlp.fd >= 0)
                continue;

            mlp.fd = open(optarg, O_RDWR);
            if (mlp.fd < 0) {
                logprintf(STDERR_FILENO, "Cannot open %s: %s\n",
                          optarg, strerror(errno));
                exit(EXIT_FAILURE);
            }
            mlp.flags |= MAIN_LOOP_FLAG_TERMINATE | MAIN_LOOP_FLAG_USE_FD;
            SWTPM_IO_SetSocketFD(mlp.fd);
            break;

        case 'f':
            if (mlp.fd >= 0)
                continue;

            errno = 0;
            val = strtoul(optarg, &end_ptr, 10);
            if (val != (unsigned int)val || errno || end_ptr[0] != '\0') {
                logprintf(STDERR_FILENO,
                          "Cannot parse character device file descriptor.\n");
                exit(EXIT_FAILURE);
            }
            mlp.fd = val;
            if (fstat(mlp.fd, &statbuf) != 0) {
                logprintf(STDERR_FILENO, "Cannot stat file descriptor: %s\n",
                          strerror(errno));
                exit(EXIT_FAILURE);
            }
            /*
             * test for wrong file types; anonymous fd's do not seem to be any of the wrong
             * ones but are also not character devices
             */
            if (S_ISREG(statbuf.st_mode) || S_ISDIR(statbuf.st_mode) || S_ISBLK(statbuf.st_mode)
                || S_ISLNK(statbuf.st_mode)) {
                logprintf(STDERR_FILENO,
                          "Given file descriptor type is not supported.\n");
                exit(EXIT_FAILURE);
            }
            mlp.flags |= MAIN_LOOP_FLAG_TERMINATE | MAIN_LOOP_FLAG_USE_FD;
            SWTPM_IO_SetSocketFD(mlp.fd);

            break;

        case 'k':
            keydata = optarg;
            break;

        case 'K':
            migkeydata = optarg;
            break;

        case 'l':
            logdata = optarg;
            break;

        case 'P':
            piddata = optarg;
            break;

        case 's':
            tpmstatedata = optarg;
            break;

        case 'C':
            ctrlchdata = optarg;
            break;

        case 'L':
            localitydata = optarg;
            break;

        case 'F':
            flagsdata = optarg;
            break;

        case '2':
            mlp.tpmversion = TPMLIB_TPM_VERSION_2;
            break;

        case 'h':
            usage(stdout, prgname, iface);
            exit(EXIT_SUCCESS);

        case 'a':
            printcapabilities = true;
            break;

        case 'e':
            printstates = true;
            break;

        case 'r':
            runas = optarg;
            break;

#ifdef WITH_VTPM_PROXY
        case 'v':
            use_vtpm_proxy = true;
            break;
#endif

        case 'S':
            seccompdata = optarg;
            break;

        default:
            usage(stderr, prgname, iface);
            exit(EXIT_FAILURE);
        }
    }

    if (optind < argc) {
        logprintf(STDERR_FILENO,
                  "Unknown parameter '%s'\n", argv[optind]);
        exit(EXIT_FAILURE);
    }

    /* change process ownership before accessing files */
    if (runas) {
        if (change_process_owner(runas) < 0)
            exit(EXIT_FAILURE);
    }

    if (handle_log_options(logdata) < 0)
        exit(EXIT_FAILURE);

    if (handle_locality_options(localitydata, &mlp.locality_flags) < 0)
        exit(EXIT_FAILURE);

#ifdef WITH_VTPM_PROXY
    if (use_vtpm_proxy) {
        if (swtpm_chardev_create_vtpm_proxy(&mlp, &need_init_cmd) < 0)
            exit(EXIT_FAILURE);
    }
#endif

    if (printcapabilities) {
        /*
         * Choose the TPM version so that getting/setting buffer size works.
         * Ignore failure, for backward compatibility when TPM 1.2 is disabled.
         */
        TPMLIB_ChooseTPMVersion(mlp.tpmversion);
        ret = capabilities_print_json(false);
        exit(ret ? EXIT_FAILURE : EXIT_SUCCESS);
    }

    if (TPMLIB_ChooseTPMVersion(mlp.tpmversion) != TPM_SUCCESS) {
        logprintf(STDERR_FILENO,
                  "Error: Could not choose TPM version.\n");
        exit(EXIT_FAILURE);
    }

    tpmstate_set_version(mlp.tpmversion);

    if (printstates) {
        if (handle_tpmstate_options(tpmstatedata) < 0)
            exit(EXIT_FAILURE);
        if (tpmstatedata == NULL) {
            logprintf(STDERR_FILENO,
                      "Error: --tpmstate option is required for --print-states\n");
            exit(EXIT_FAILURE);
        }
        ret = SWTPM_NVRAM_PrintJson();
        if (ret)
            goto exit_failure;
        else
            goto exit_success;
    }

    if (mlp.fd < 0) {
        logprintf(STDERR_FILENO,
                  "Error: Missing character device or file descriptor\n");
        exit(EXIT_FAILURE);
    } else if (mlp.fd < 3) {
        /* no std{in,out,err} */
        logprintf(STDERR_FILENO,
            "Error: Cannot accept file descriptors with values 0, 1, or 2\n");
        exit(EXIT_FAILURE);
    }

    if (handle_ctrlchannel_options(ctrlchdata, &mlp.cc) < 0) {
        goto exit_failure;
    }

    if (handle_key_options(keydata) < 0 ||
        handle_migration_key_options(migkeydata) < 0 ||
        handle_pid_options(piddata) < 0 ||
        handle_tpmstate_options(tpmstatedata) < 0 ||
        handle_seccomp_options(seccompdata, &seccomp_action) < 0 ||
        handle_flags_options(flagsdata, &need_init_cmd,
                             &mlp.startupType) < 0) {
        goto exit_failure;
    }

    if (pidfile_write(getpid()) < 0) {
        goto exit_failure;
    }

    setvbuf(stdout, 0, _IONBF, 0);      /* output may be going through pipe */

#ifdef DEBUG
    /* initialization */
    start_time = time(NULL);
#endif

    TPM_DEBUG("main: Initializing TPM at %s", ctime(&start_time));

    tpmlib_debug_libtpms_parameters(mlp.tpmversion);

    if ((rc = tpmlib_register_callbacks(&callbacks)))
        goto error_no_tpm;

    if (!need_init_cmd) {
        if ((rc = tpmlib_start(0, mlp.tpmversion)))
            goto error_no_tpm;
        tpm_running = true;
    }

    if (install_sighandlers(notify_fd, sigterm_handler) < 0)
        goto error_no_sighandlers;

    if (create_seccomp_profile(false, seccomp_action) < 0)
        goto error_seccomp_profile;

    mlp.flags |= MAIN_LOOP_FLAG_USE_FD | MAIN_LOOP_FLAG_KEEP_CONNECTION | \
      MAIN_LOOP_FLAG_END_ON_HUP;

    if (daemonize) {
        daemonize_finish();
    }

    rc = mainLoop(&mlp, notify_fd[0]);

error_seccomp_profile:
    uninstall_sighandlers();

error_no_sighandlers:
    TPMLIB_Terminate();

error_no_tpm:
    close(notify_fd[0]);
    notify_fd[0] = -1;
    close(notify_fd[1]);
    notify_fd[1] = -1;

    swtpm_cleanup(mlp.cc);

    /* Fatal initialization errors cause the program to abort */
    if (rc == 0) {
        exit(EXIT_SUCCESS);
    }
    else {
        TPM_DEBUG("main: TPM initialization failure %08x, exiting\n", rc);
        exit(EXIT_FAILURE);
    }

exit_failure:
    swtpm_cleanup(mlp.cc);

    exit(EXIT_FAILURE);

exit_success:
    swtpm_cleanup(mlp.cc);

    exit(EXIT_SUCCESS);
}
