root/sys/dev/tpm/tpm20.c
/*-
 * Copyright (c) 2018 Stormshield.
 * Copyright (c) 2018 Semihalf.
 * All rights reserved.
 *
 * 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/random.h>
#include <dev/random/randomdev.h>

#include "tpm20.h"

#define TPM_HARVEST_SIZE     16
/*
 * Perform a harvest every 10 seconds.
 * Since discrete TPMs are painfully slow
 * we don't want to execute this too often
 * as the chip is likely to be used by others too.
 */
#define TPM_HARVEST_INTERVAL 10

MALLOC_DEFINE(M_TPM20, "tpm_buffer", "buffer for tpm 2.0 driver");

#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
static void tpm20_harvest(void *arg, int unused);
#endif
static int  tpm20_restart(device_t dev, bool clear);
static int  tpm20_save_state(device_t dev, bool suspend);

static d_open_t         tpm20_open;
static d_close_t        tpm20_close;
static d_read_t         tpm20_read;
static d_write_t        tpm20_write;
static d_ioctl_t        tpm20_ioctl;

static struct cdevsw tpm20_cdevsw = {
        .d_version = D_VERSION,
        .d_open = tpm20_open,
        .d_close = tpm20_close,
        .d_read = tpm20_read,
        .d_write = tpm20_write,
        .d_ioctl = tpm20_ioctl,
        .d_name = "tpm20",
};

int
tpm20_read(struct cdev *dev, struct uio *uio, int flags)
{
        struct tpm_sc *sc;
        struct tpm_priv *priv;
        size_t bytes_to_transfer;
        size_t offset;
        int result = 0;

        sc = (struct tpm_sc *)dev->si_drv1;
        devfs_get_cdevpriv((void **)&priv);

        sx_xlock(&sc->dev_lock);
        offset = priv->offset;
        bytes_to_transfer = MIN(priv->len, uio->uio_resid);
        if (bytes_to_transfer > 0) {
                result = uiomove((caddr_t) priv->buf + offset, bytes_to_transfer, uio);
                priv->offset += bytes_to_transfer;
                priv->len -= bytes_to_transfer;
        } else {
                result = 0;
        }

        sx_xunlock(&sc->dev_lock);

        return (result);
}

int
tpm20_write(struct cdev *dev, struct uio *uio, int flags)
{
        struct tpm_sc *sc;
        struct tpm_priv *priv;
        size_t byte_count;
        int result = 0;

        sc = (struct tpm_sc *)dev->si_drv1;
        devfs_get_cdevpriv((void **)&priv);

        byte_count = uio->uio_resid;
        if (byte_count < TPM_HEADER_SIZE) {
                device_printf(sc->dev,
                    "Requested transfer is too small\n");
                return (EINVAL);
        }

        if (byte_count > TPM_BUFSIZE) {
                device_printf(sc->dev,
                    "Requested transfer is too large\n");
                return (E2BIG);
        }

        sx_xlock(&sc->dev_lock);

        result = uiomove(priv->buf, byte_count, uio);
        if (result != 0) {
                sx_xunlock(&sc->dev_lock);
                return (result);
        }

        result = TPM_TRANSMIT(sc->dev, priv, byte_count);

        sx_xunlock(&sc->dev_lock);
        return (result);
}

static struct tpm_priv *
tpm20_priv_alloc(void)
{
        struct tpm_priv *priv;

        priv = malloc(sizeof (*priv), M_TPM20, M_WAITOK | M_ZERO);
        return (priv);
}

static void
tpm20_priv_dtor(void *data)
{
        struct tpm_priv *priv = data;

        free(priv->buf, M_TPM20);
}

int
tpm20_open(struct cdev *dev, int flag, int mode, struct thread *td)
{
        struct tpm_priv *priv;

        priv = tpm20_priv_alloc();
        devfs_set_cdevpriv(priv, tpm20_priv_dtor);

        return (0);
}

int
tpm20_close(struct cdev *dev, int flag, int mode, struct thread *td)
{

        return (0);
}

int
tpm20_ioctl(struct cdev *dev, u_long cmd, caddr_t data,
    int flags, struct thread *td)
{

        return (ENOTTY);
}

#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
static const struct random_source random_tpm = {
        .rs_ident = "TPM",
        .rs_source = RANDOM_PURE_TPM,
};
#endif

int
tpm20_init(struct tpm_sc *sc)
{
        struct make_dev_args args;
        int result;

        sc->internal_priv = tpm20_priv_alloc();

        make_dev_args_init(&args);
        args.mda_devsw = &tpm20_cdevsw;
        args.mda_uid = UID_ROOT;
        args.mda_gid = GID_WHEEL;
        args.mda_mode = TPM_CDEV_PERM_FLAG;
        args.mda_si_drv1 = sc;
        result = make_dev_s(&args, &sc->sc_cdev, TPM_CDEV_NAME);
        if (result != 0)
                tpm20_release(sc);

#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
        random_source_register(&random_tpm);
        TIMEOUT_TASK_INIT(taskqueue_thread, &sc->harvest_task, 0,
            tpm20_harvest, sc);
        taskqueue_enqueue_timeout(taskqueue_thread, &sc->harvest_task, 0);
#endif

        return (result);

}

void
tpm20_release(struct tpm_sc *sc)
{

#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
        if (device_is_attached(sc->dev))
                taskqueue_drain_timeout(taskqueue_thread, &sc->harvest_task);
        random_source_deregister(&random_tpm);
#endif

        tpm20_priv_dtor(sc->internal_priv);
        sx_destroy(&sc->dev_lock);
        if (sc->sc_cdev != NULL)
                destroy_dev(sc->sc_cdev);
}

int
tpm20_resume(device_t dev)
{

        tpm20_restart(dev, false);

#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
        struct tpm_sc *sc;

        sc = device_get_softc(dev);
        taskqueue_enqueue_timeout(taskqueue_thread, &sc->harvest_task,
            hz * TPM_HARVEST_INTERVAL);
#endif
        return (0);
}

int
tpm20_suspend(device_t dev)
{
#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
        struct tpm_sc *sc;

        sc = device_get_softc(dev);
        taskqueue_drain_timeout(taskqueue_thread, &sc->harvest_task);
#endif
        return (tpm20_save_state(dev, true));
}

int
tpm20_shutdown(device_t dev)
{
        return (tpm20_save_state(dev, false));
}

#if defined TPM_HARVEST || defined RANDOM_ENABLE_TPM
/*
 * Get TPM_HARVEST_SIZE random bytes and add them
 * into system entropy pool.
 */
static void
tpm20_harvest(void *arg, int unused)
{
        struct tpm_sc *sc;
        struct tpm_priv *priv;
        unsigned char entropy[TPM_HARVEST_SIZE];
        uint16_t entropy_size;
        int result;
        uint8_t cmd[] = {
                0x80, 0x01,             /* TPM_ST_NO_SESSIONS tag*/
                0x00, 0x00, 0x00, 0x0c, /* cmd length */
                0x00, 0x00, 0x01, 0x7b, /* cmd TPM_CC_GetRandom */
                0x00, TPM_HARVEST_SIZE  /* number of bytes requested */
        };

        sc = arg;
        sx_xlock(&sc->dev_lock);

        priv = sc->internal_priv;
        memcpy(priv->buf, cmd, sizeof(cmd));

        result = TPM_TRANSMIT(sc->dev, priv, sizeof(cmd));
        if (result != 0) {
                sx_xunlock(&sc->dev_lock);
                return;
        }

        /* The number of random bytes we got is placed right after the header */
        entropy_size = (uint16_t) priv->buf[TPM_HEADER_SIZE + 1];
        if (entropy_size > 0) {
                entropy_size = MIN(entropy_size, TPM_HARVEST_SIZE);
                memcpy(entropy,
                        priv->buf + TPM_HEADER_SIZE + sizeof(uint16_t),
                        entropy_size);
        }

        sx_xunlock(&sc->dev_lock);
        if (entropy_size > 0)
                random_harvest_queue(entropy, entropy_size, RANDOM_PURE_TPM);

        taskqueue_enqueue_timeout(taskqueue_thread, &sc->harvest_task,
            hz * TPM_HARVEST_INTERVAL);
}
#endif  /* TPM_HARVEST */

static int
tpm20_restart(device_t dev, bool clear)
{
        struct tpm_sc *sc;
        struct tpm_priv *priv;
        uint8_t startup_cmd[] = {
                0x80, 0x01,             /* TPM_ST_NO_SESSIONS tag*/
                0x00, 0x00, 0x00, 0x0C, /* cmd length */
                0x00, 0x00, 0x01, 0x44, /* cmd TPM_CC_Startup */
                0x00, 0x01              /* TPM_SU_STATE */
        };

        sc = device_get_softc(dev);

        /*
         * Inform the TPM whether we are resetting or resuming.
         */
        if (clear)
                startup_cmd[11] = 0; /* TPM_SU_CLEAR */

        if (sc == NULL)
                return (0);

        sx_xlock(&sc->dev_lock);

        priv = sc->internal_priv;
        memcpy(priv->buf, startup_cmd, sizeof(startup_cmd));

        /* XXX Ignoring both TPM_TRANSMIT return and tpm's response */
        TPM_TRANSMIT(sc->dev, priv, sizeof(startup_cmd));

        sx_xunlock(&sc->dev_lock);

        return (0);
}

static int
tpm20_save_state(device_t dev, bool suspend)
{
        struct tpm_sc *sc;
        struct tpm_priv *priv;
        uint8_t save_cmd[] = {
                0x80, 0x01,             /* TPM_ST_NO_SESSIONS tag*/
                0x00, 0x00, 0x00, 0x0C, /* cmd length */
                0x00, 0x00, 0x01, 0x45, /* cmd TPM_CC_Shutdown */
                0x00, 0x00              /* TPM_SU_STATE */
        };

        sc = device_get_softc(dev);

        /*
         * Inform the TPM whether we are going to suspend or reboot/shutdown.
         */
        if (suspend)
                save_cmd[11] = 1; /* TPM_SU_STATE */

        if (sc == NULL)
                return (0);

        sx_xlock(&sc->dev_lock);

        priv = sc->internal_priv;
        memcpy(priv->buf, save_cmd, sizeof(save_cmd));

        /* XXX Ignoring both TPM_TRANSMIT return and tpm's response */
        TPM_TRANSMIT(sc->dev, priv, sizeof(save_cmd));

        sx_xunlock(&sc->dev_lock);

        return (0);
}

int32_t
tpm20_get_timeout(uint32_t command)
{
        int32_t timeout;

        switch (command) {
                case TPM_CC_CreatePrimary:
                case TPM_CC_Create:
                case TPM_CC_CreateLoaded:
                        timeout = TPM_TIMEOUT_LONG;
                        break;
                case TPM_CC_SequenceComplete:
                case TPM_CC_Startup:
                case TPM_CC_SequenceUpdate:
                case TPM_CC_GetCapability:
                case TPM_CC_PCR_Extend:
                case TPM_CC_EventSequenceComplete:
                case TPM_CC_HashSequenceStart:
                        timeout = TPM_TIMEOUT_C;
                        break;
                default:
                        timeout = TPM_TIMEOUT_B;
                        break;
        }
        return timeout;
}