root/lib/libfido2/src/bio.c
/*
 * Copyright (c) 2019 Yubico AB. All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file.
 */

#include "fido.h"
#include "fido/bio.h"
#include "fido/es256.h"

#define CMD_ENROLL_BEGIN        0x01
#define CMD_ENROLL_NEXT         0x02
#define CMD_ENROLL_CANCEL       0x03
#define CMD_ENUM                0x04
#define CMD_SET_NAME            0x05
#define CMD_ENROLL_REMOVE       0x06
#define CMD_GET_INFO            0x07

static int
bio_prepare_hmac(uint8_t cmd, cbor_item_t **argv, size_t argc,
    cbor_item_t **param, fido_blob_t *hmac_data)
{
        const uint8_t    prefix[2] = { 0x01 /* modality */, cmd };
        int              ok = -1;
        size_t           cbor_alloc_len;
        size_t           cbor_len;
        unsigned char   *cbor = NULL;

        if (argv == NULL || param == NULL)
                return (fido_blob_set(hmac_data, prefix, sizeof(prefix)));

        if ((*param = cbor_flatten_vector(argv, argc)) == NULL) {
                fido_log_debug("%s: cbor_flatten_vector", __func__);
                goto fail;
        }

        if ((cbor_len = cbor_serialize_alloc(*param, &cbor,
            &cbor_alloc_len)) == 0 || cbor_len > SIZE_MAX - sizeof(prefix)) {
                fido_log_debug("%s: cbor_serialize_alloc", __func__);
                goto fail;
        }

        if ((hmac_data->ptr = malloc(cbor_len + sizeof(prefix))) == NULL) {
                fido_log_debug("%s: malloc", __func__);
                goto fail;
        }

        memcpy(hmac_data->ptr, prefix, sizeof(prefix));
        memcpy(hmac_data->ptr + sizeof(prefix), cbor, cbor_len);
        hmac_data->len = cbor_len + sizeof(prefix);

        ok = 0;
fail:
        free(cbor);

        return (ok);
}

static int
bio_tx(fido_dev_t *dev, uint8_t subcmd, cbor_item_t **sub_argv, size_t sub_argc,
    const char *pin, const fido_blob_t *token, int *ms)
{
        cbor_item_t     *argv[5];
        es256_pk_t      *pk = NULL;
        fido_blob_t     *ecdh = NULL;
        fido_blob_t      f;
        fido_blob_t      hmac;
        const uint8_t    cmd = CTAP_CBOR_BIO_ENROLL_PRE;
        int              r = FIDO_ERR_INTERNAL;

        memset(&f, 0, sizeof(f));
        memset(&hmac, 0, sizeof(hmac));
        memset(&argv, 0, sizeof(argv));

        /* modality, subCommand */
        if ((argv[0] = cbor_build_uint8(1)) == NULL ||
            (argv[1] = cbor_build_uint8(subcmd)) == NULL) {
                fido_log_debug("%s: cbor encode", __func__);
                goto fail;
        }

        /* subParams */
        if (pin || token) {
                if (bio_prepare_hmac(subcmd, sub_argv, sub_argc, &argv[2],
                    &hmac) < 0) {
                        fido_log_debug("%s: bio_prepare_hmac", __func__);
                        goto fail;
                }
        }

        /* pinProtocol, pinAuth */
        if (pin) {
                if ((r = fido_do_ecdh(dev, &pk, &ecdh, ms)) != FIDO_OK) {
                        fido_log_debug("%s: fido_do_ecdh", __func__);
                        goto fail;
                }
                if ((r = cbor_add_uv_params(dev, cmd, &hmac, pk, ecdh, pin,
                    NULL, &argv[4], &argv[3], ms)) != FIDO_OK) {
                        fido_log_debug("%s: cbor_add_uv_params", __func__);
                        goto fail;
                }
        } else if (token) {
                if ((argv[3] = cbor_encode_pin_opt(dev)) == NULL ||
                    (argv[4] = cbor_encode_pin_auth(dev, token, &hmac)) == NULL) {
                        fido_log_debug("%s: encode pin", __func__);
                        goto fail;
                }
        }

        /* framing and transmission */
        if (cbor_build_frame(cmd, argv, nitems(argv), &f) < 0 ||
            fido_tx(dev, CTAP_CMD_CBOR, f.ptr, f.len, ms) < 0) {
                fido_log_debug("%s: fido_tx", __func__);
                r = FIDO_ERR_TX;
                goto fail;
        }

        r = FIDO_OK;
fail:
        cbor_vector_free(argv, nitems(argv));
        es256_pk_free(&pk);
        fido_blob_free(&ecdh);
        free(f.ptr);
        free(hmac.ptr);

        return (r);
}

static void
bio_reset_template(fido_bio_template_t *t)
{
        free(t->name);
        t->name = NULL;
        fido_blob_reset(&t->id);
}

static void
bio_reset_template_array(fido_bio_template_array_t *ta)
{
        for (size_t i = 0; i < ta->n_alloc; i++)
                bio_reset_template(&ta->ptr[i]);

        free(ta->ptr);
        ta->ptr = NULL;
        memset(ta, 0, sizeof(*ta));
}

static int
decode_template(const cbor_item_t *key, const cbor_item_t *val, void *arg)
{
        fido_bio_template_t *t = arg;

        if (cbor_isa_uint(key) == false ||
            cbor_int_get_width(key) != CBOR_INT_8) {
                fido_log_debug("%s: cbor type", __func__);
                return (0); /* ignore */
        }

        switch (cbor_get_uint8(key)) {
        case 1: /* id */
                return (fido_blob_decode(val, &t->id));
        case 2: /* name */
                return (cbor_string_copy(val, &t->name));
        }

        return (0); /* ignore */
}

static int
decode_template_array(const cbor_item_t *item, void *arg)
{
        fido_bio_template_array_t *ta = arg;

        if (cbor_isa_map(item) == false ||
            cbor_map_is_definite(item) == false) {
                fido_log_debug("%s: cbor type", __func__);
                return (-1);
        }

        if (ta->n_rx >= ta->n_alloc) {
                fido_log_debug("%s: n_rx >= n_alloc", __func__);
                return (-1);
        }

        if (cbor_map_iter(item, &ta->ptr[ta->n_rx], decode_template) < 0) {
                fido_log_debug("%s: decode_template", __func__);
                return (-1);
        }

        ta->n_rx++;

        return (0);
}

static int
bio_parse_template_array(const cbor_item_t *key, const cbor_item_t *val,
    void *arg)
{
        fido_bio_template_array_t *ta = arg;

        if (cbor_isa_uint(key) == false ||
            cbor_int_get_width(key) != CBOR_INT_8 ||
            cbor_get_uint8(key) != 7) {
                fido_log_debug("%s: cbor type", __func__);
                return (0); /* ignore */
        }

        if (cbor_isa_array(val) == false ||
            cbor_array_is_definite(val) == false) {
                fido_log_debug("%s: cbor type", __func__);
                return (-1);
        }

        if (ta->ptr != NULL || ta->n_alloc != 0 || ta->n_rx != 0) {
                fido_log_debug("%s: ptr != NULL || n_alloc != 0 || n_rx != 0",
                    __func__);
                return (-1);
        }

        if ((ta->ptr = calloc(cbor_array_size(val), sizeof(*ta->ptr))) == NULL)
                return (-1);

        ta->n_alloc = cbor_array_size(val);

        if (cbor_array_iter(val, ta, decode_template_array) < 0) {
                fido_log_debug("%s: decode_template_array", __func__);
                return (-1);
        }

        return (0);
}

static int
bio_rx_template_array(fido_dev_t *dev, fido_bio_template_array_t *ta, int *ms)
{
        unsigned char   reply[FIDO_MAXMSG];
        int             reply_len;
        int             r;

        bio_reset_template_array(ta);

        if ((reply_len = fido_rx(dev, CTAP_CMD_CBOR, &reply, sizeof(reply),
            ms)) < 0) {
                fido_log_debug("%s: fido_rx", __func__);
                return (FIDO_ERR_RX);
        }

        if ((r = cbor_parse_reply(reply, (size_t)reply_len, ta,
            bio_parse_template_array)) != FIDO_OK) {
                fido_log_debug("%s: bio_parse_template_array" , __func__);
                return (r);
        }

        return (FIDO_OK);
}

static int
bio_get_template_array_wait(fido_dev_t *dev, fido_bio_template_array_t *ta,
    const char *pin, int *ms)
{
        int r;

        if ((r = bio_tx(dev, CMD_ENUM, NULL, 0, pin, NULL, ms)) != FIDO_OK ||
            (r = bio_rx_template_array(dev, ta, ms)) != FIDO_OK)
                return (r);

        return (FIDO_OK);
}

int
fido_bio_dev_get_template_array(fido_dev_t *dev, fido_bio_template_array_t *ta,
    const char *pin)
{
        int ms = dev->timeout_ms;

        if (pin == NULL)
                return (FIDO_ERR_INVALID_ARGUMENT);

        return (bio_get_template_array_wait(dev, ta, pin, &ms));
}

static int
bio_set_template_name_wait(fido_dev_t *dev, const fido_bio_template_t *t,
    const char *pin, int *ms)
{
        cbor_item_t     *argv[2];
        int              r = FIDO_ERR_INTERNAL;

        memset(&argv, 0, sizeof(argv));

        if ((argv[0] = fido_blob_encode(&t->id)) == NULL ||
            (argv[1] = cbor_build_string(t->name)) == NULL) {
                fido_log_debug("%s: cbor encode", __func__);
                goto fail;
        }

        if ((r = bio_tx(dev, CMD_SET_NAME, argv, 2, pin, NULL,
            ms)) != FIDO_OK ||
            (r = fido_rx_cbor_status(dev, ms)) != FIDO_OK) {
                fido_log_debug("%s: tx/rx", __func__);
                goto fail;
        }

        r = FIDO_OK;
fail:
        cbor_vector_free(argv, nitems(argv));

        return (r);
}

int
fido_bio_dev_set_template_name(fido_dev_t *dev, const fido_bio_template_t *t,
    const char *pin)
{
        int ms = dev->timeout_ms;

        if (pin == NULL || t->name == NULL)
                return (FIDO_ERR_INVALID_ARGUMENT);

        return (bio_set_template_name_wait(dev, t, pin, &ms));
}

static void
bio_reset_enroll(fido_bio_enroll_t *e)
{
        e->remaining_samples = 0;
        e->last_status = 0;

        if (e->token)
                fido_blob_free(&e->token);
}

static int
bio_parse_enroll_status(const cbor_item_t *key, const cbor_item_t *val,
    void *arg)
{
        fido_bio_enroll_t *e = arg;
        uint64_t x;

        if (cbor_isa_uint(key) == false ||
            cbor_int_get_width(key) != CBOR_INT_8) {
                fido_log_debug("%s: cbor type", __func__);
                return (0); /* ignore */
        }

        switch (cbor_get_uint8(key)) {
        case 5:
                if (cbor_decode_uint64(val, &x) < 0 || x > UINT8_MAX) {
                        fido_log_debug("%s: cbor_decode_uint64", __func__);
                        return (-1);
                }
                e->last_status = (uint8_t)x;
                break;
        case 6:
                if (cbor_decode_uint64(val, &x) < 0 || x > UINT8_MAX) {
                        fido_log_debug("%s: cbor_decode_uint64", __func__);
                        return (-1);
                }
                e->remaining_samples = (uint8_t)x;
                break;
        default:
                return (0); /* ignore */
        }

        return (0);
}

static int
bio_parse_template_id(const cbor_item_t *key, const cbor_item_t *val,
    void *arg)
{
        fido_blob_t *id = arg;

        if (cbor_isa_uint(key) == false ||
            cbor_int_get_width(key) != CBOR_INT_8 ||
            cbor_get_uint8(key) != 4) {
                fido_log_debug("%s: cbor type", __func__);
                return (0); /* ignore */
        }

        return (fido_blob_decode(val, id));
}

static int
bio_rx_enroll_begin(fido_dev_t *dev, fido_bio_template_t *t,
    fido_bio_enroll_t *e, int *ms)
{
        unsigned char   reply[FIDO_MAXMSG];
        int             reply_len;
        int             r;

        bio_reset_template(t);

        e->remaining_samples = 0;
        e->last_status = 0;

        if ((reply_len = fido_rx(dev, CTAP_CMD_CBOR, &reply, sizeof(reply),
            ms)) < 0) {
                fido_log_debug("%s: fido_rx", __func__);
                return (FIDO_ERR_RX);
        }

        if ((r = cbor_parse_reply(reply, (size_t)reply_len, e,
            bio_parse_enroll_status)) != FIDO_OK) {
                fido_log_debug("%s: bio_parse_enroll_status", __func__);
                return (r);
        }
        if ((r = cbor_parse_reply(reply, (size_t)reply_len, &t->id,
            bio_parse_template_id)) != FIDO_OK) {
                fido_log_debug("%s: bio_parse_template_id", __func__);
                return (r);
        }

        return (FIDO_OK);
}

static int
bio_enroll_begin_wait(fido_dev_t *dev, fido_bio_template_t *t,
    fido_bio_enroll_t *e, uint32_t timo_ms, int *ms)
{
        cbor_item_t     *argv[3];
        const uint8_t    cmd = CMD_ENROLL_BEGIN;
        int              r = FIDO_ERR_INTERNAL;

        memset(&argv, 0, sizeof(argv));

        if ((argv[2] = cbor_build_uint(timo_ms)) == NULL) {
                fido_log_debug("%s: cbor encode", __func__);
                goto fail;
        }

        if ((r = bio_tx(dev, cmd, argv, 3, NULL, e->token, ms)) != FIDO_OK ||
            (r = bio_rx_enroll_begin(dev, t, e, ms)) != FIDO_OK) {
                fido_log_debug("%s: tx/rx", __func__);
                goto fail;
        }

        r = FIDO_OK;
fail:
        cbor_vector_free(argv, nitems(argv));

        return (r);
}

int
fido_bio_dev_enroll_begin(fido_dev_t *dev, fido_bio_template_t *t,
    fido_bio_enroll_t *e, uint32_t timo_ms, const char *pin)
{
        es256_pk_t      *pk = NULL;
        fido_blob_t     *ecdh = NULL;
        fido_blob_t     *token = NULL;
        int              ms = dev->timeout_ms;
        int              r;

        if (pin == NULL || e->token != NULL)
                return (FIDO_ERR_INVALID_ARGUMENT);

        if ((token = fido_blob_new()) == NULL) {
                r = FIDO_ERR_INTERNAL;
                goto fail;
        }

        if ((r = fido_do_ecdh(dev, &pk, &ecdh, &ms)) != FIDO_OK) {
                fido_log_debug("%s: fido_do_ecdh", __func__);
                goto fail;
        }

        if ((r = fido_dev_get_uv_token(dev, CTAP_CBOR_BIO_ENROLL_PRE, pin, ecdh,
            pk, NULL, token, &ms)) != FIDO_OK) {
                fido_log_debug("%s: fido_dev_get_uv_token", __func__);
                goto fail;
        }

        e->token = token;
        token = NULL;
fail:
        es256_pk_free(&pk);
        fido_blob_free(&ecdh);
        fido_blob_free(&token);

        if (r != FIDO_OK)
                return (r);

        return (bio_enroll_begin_wait(dev, t, e, timo_ms, &ms));
}

static int
bio_rx_enroll_continue(fido_dev_t *dev, fido_bio_enroll_t *e, int *ms)
{
        unsigned char   reply[FIDO_MAXMSG];
        int             reply_len;
        int             r;

        e->remaining_samples = 0;
        e->last_status = 0;

        if ((reply_len = fido_rx(dev, CTAP_CMD_CBOR, &reply, sizeof(reply),
            ms)) < 0) {
                fido_log_debug("%s: fido_rx", __func__);
                return (FIDO_ERR_RX);
        }

        if ((r = cbor_parse_reply(reply, (size_t)reply_len, e,
            bio_parse_enroll_status)) != FIDO_OK) {
                fido_log_debug("%s: bio_parse_enroll_status", __func__);
                return (r);
        }

        return (FIDO_OK);
}

static int
bio_enroll_continue_wait(fido_dev_t *dev, const fido_bio_template_t *t,
    fido_bio_enroll_t *e, uint32_t timo_ms, int *ms)
{
        cbor_item_t     *argv[3];
        const uint8_t    cmd = CMD_ENROLL_NEXT;
        int              r = FIDO_ERR_INTERNAL;

        memset(&argv, 0, sizeof(argv));

        if ((argv[0] = fido_blob_encode(&t->id)) == NULL ||
            (argv[2] = cbor_build_uint(timo_ms)) == NULL) {
                fido_log_debug("%s: cbor encode", __func__);
                goto fail;
        }

        if ((r = bio_tx(dev, cmd, argv, 3, NULL, e->token, ms)) != FIDO_OK ||
            (r = bio_rx_enroll_continue(dev, e, ms)) != FIDO_OK) {
                fido_log_debug("%s: tx/rx", __func__);
                goto fail;
        }

        r = FIDO_OK;
fail:
        cbor_vector_free(argv, nitems(argv));

        return (r);
}

int
fido_bio_dev_enroll_continue(fido_dev_t *dev, const fido_bio_template_t *t,
    fido_bio_enroll_t *e, uint32_t timo_ms)
{
        int ms = dev->timeout_ms;

        if (e->token == NULL)
                return (FIDO_ERR_INVALID_ARGUMENT);

        return (bio_enroll_continue_wait(dev, t, e, timo_ms, &ms));
}

static int
bio_enroll_cancel_wait(fido_dev_t *dev, int *ms)
{
        const uint8_t   cmd = CMD_ENROLL_CANCEL;
        int             r;

        if ((r = bio_tx(dev, cmd, NULL, 0, NULL, NULL, ms)) != FIDO_OK ||
            (r = fido_rx_cbor_status(dev, ms)) != FIDO_OK) {
                fido_log_debug("%s: tx/rx", __func__);
                return (r);
        }

        return (FIDO_OK);
}

int
fido_bio_dev_enroll_cancel(fido_dev_t *dev)
{
        int ms = dev->timeout_ms;

        return (bio_enroll_cancel_wait(dev, &ms));
}

static int
bio_enroll_remove_wait(fido_dev_t *dev, const fido_bio_template_t *t,
    const char *pin, int *ms)
{
        cbor_item_t     *argv[1];
        const uint8_t    cmd = CMD_ENROLL_REMOVE;
        int              r = FIDO_ERR_INTERNAL;

        memset(&argv, 0, sizeof(argv));

        if ((argv[0] = fido_blob_encode(&t->id)) == NULL) {
                fido_log_debug("%s: cbor encode", __func__);
                goto fail;
        }

        if ((r = bio_tx(dev, cmd, argv, 1, pin, NULL, ms)) != FIDO_OK ||
            (r = fido_rx_cbor_status(dev, ms)) != FIDO_OK) {
                fido_log_debug("%s: tx/rx", __func__);
                goto fail;
        }

        r = FIDO_OK;
fail:
        cbor_vector_free(argv, nitems(argv));

        return (r);
}

int
fido_bio_dev_enroll_remove(fido_dev_t *dev, const fido_bio_template_t *t,
    const char *pin)
{
        int ms = dev->timeout_ms;

        return (bio_enroll_remove_wait(dev, t, pin, &ms));
}

static void
bio_reset_info(fido_bio_info_t *i)
{
        i->type = 0;
        i->max_samples = 0;
}

static int
bio_parse_info(const cbor_item_t *key, const cbor_item_t *val, void *arg)
{
        fido_bio_info_t *i = arg;
        uint64_t         x;

        if (cbor_isa_uint(key) == false ||
            cbor_int_get_width(key) != CBOR_INT_8) {
                fido_log_debug("%s: cbor type", __func__);
                return (0); /* ignore */
        }

        switch (cbor_get_uint8(key)) {
        case 2:
                if (cbor_decode_uint64(val, &x) < 0 || x > UINT8_MAX) {
                        fido_log_debug("%s: cbor_decode_uint64", __func__);
                        return (-1);
                }
                i->type = (uint8_t)x;
                break;
        case 3:
                if (cbor_decode_uint64(val, &x) < 0 || x > UINT8_MAX) {
                        fido_log_debug("%s: cbor_decode_uint64", __func__);
                        return (-1);
                }
                i->max_samples = (uint8_t)x;
                break;
        default:
                return (0); /* ignore */
        }

        return (0);
}

static int
bio_rx_info(fido_dev_t *dev, fido_bio_info_t *i, int *ms)
{
        unsigned char   reply[FIDO_MAXMSG];
        int             reply_len;
        int             r;

        bio_reset_info(i);

        if ((reply_len = fido_rx(dev, CTAP_CMD_CBOR, &reply, sizeof(reply),
            ms)) < 0) {
                fido_log_debug("%s: fido_rx", __func__);
                return (FIDO_ERR_RX);
        }

        if ((r = cbor_parse_reply(reply, (size_t)reply_len, i,
            bio_parse_info)) != FIDO_OK) {
                fido_log_debug("%s: bio_parse_info" , __func__);
                return (r);
        }

        return (FIDO_OK);
}

static int
bio_get_info_wait(fido_dev_t *dev, fido_bio_info_t *i, int *ms)
{
        int r;

        if ((r = bio_tx(dev, CMD_GET_INFO, NULL, 0, NULL, NULL,
            ms)) != FIDO_OK ||
            (r = bio_rx_info(dev, i, ms)) != FIDO_OK) {
                fido_log_debug("%s: tx/rx", __func__);
                return (r);
        }

        return (FIDO_OK);
}

int
fido_bio_dev_get_info(fido_dev_t *dev, fido_bio_info_t *i)
{
        int ms = dev->timeout_ms;

        return (bio_get_info_wait(dev, i, &ms));
}

const char *
fido_bio_template_name(const fido_bio_template_t *t)
{
        return (t->name);
}

const unsigned char *
fido_bio_template_id_ptr(const fido_bio_template_t *t)
{
        return (t->id.ptr);
}

size_t
fido_bio_template_id_len(const fido_bio_template_t *t)
{
        return (t->id.len);
}

size_t
fido_bio_template_array_count(const fido_bio_template_array_t *ta)
{
        return (ta->n_rx);
}

fido_bio_template_array_t *
fido_bio_template_array_new(void)
{
        return (calloc(1, sizeof(fido_bio_template_array_t)));
}

fido_bio_template_t *
fido_bio_template_new(void)
{
        return (calloc(1, sizeof(fido_bio_template_t)));
}

void
fido_bio_template_array_free(fido_bio_template_array_t **tap)
{
        fido_bio_template_array_t *ta;

        if (tap == NULL || (ta = *tap) == NULL)
                return;

        bio_reset_template_array(ta);
        free(ta);
        *tap = NULL;
}

void
fido_bio_template_free(fido_bio_template_t **tp)
{
        fido_bio_template_t *t;

        if (tp == NULL || (t = *tp) == NULL)
                return;

        bio_reset_template(t);
        free(t);
        *tp = NULL;
}

int
fido_bio_template_set_name(fido_bio_template_t *t, const char *name)
{
        free(t->name);
        t->name = NULL;

        if (name && (t->name = strdup(name)) == NULL)
                return (FIDO_ERR_INTERNAL);

        return (FIDO_OK);
}

int
fido_bio_template_set_id(fido_bio_template_t *t, const unsigned char *ptr,
    size_t len)
{
        fido_blob_reset(&t->id);

        if (ptr && fido_blob_set(&t->id, ptr, len) < 0)
                return (FIDO_ERR_INTERNAL);

        return (FIDO_OK);
}

const fido_bio_template_t *
fido_bio_template(const fido_bio_template_array_t *ta, size_t idx)
{
        if (idx >= ta->n_alloc)
                return (NULL);

        return (&ta->ptr[idx]);
}

fido_bio_enroll_t *
fido_bio_enroll_new(void)
{
        return (calloc(1, sizeof(fido_bio_enroll_t)));
}

fido_bio_info_t *
fido_bio_info_new(void)
{
        return (calloc(1, sizeof(fido_bio_info_t)));
}

uint8_t
fido_bio_info_type(const fido_bio_info_t *i)
{
        return (i->type);
}

uint8_t
fido_bio_info_max_samples(const fido_bio_info_t *i)
{
        return (i->max_samples);
}

void
fido_bio_enroll_free(fido_bio_enroll_t **ep)
{
        fido_bio_enroll_t *e;

        if (ep == NULL || (e = *ep) == NULL)
                return;

        bio_reset_enroll(e);

        free(e);
        *ep = NULL;
}

void
fido_bio_info_free(fido_bio_info_t **ip)
{
        fido_bio_info_t *i;

        if (ip == NULL || (i = *ip) == NULL)
                return;

        free(i);
        *ip = NULL;
}

uint8_t
fido_bio_enroll_remaining_samples(const fido_bio_enroll_t *e)
{
        return (e->remaining_samples);
}

uint8_t
fido_bio_enroll_last_status(const fido_bio_enroll_t *e)
{
        return (e->last_status);
}