root/crypto/openssh/sshkey-xmss.c
/* $OpenBSD: sshkey-xmss.c,v 1.12 2022/10/28 00:39:29 djm Exp $ */
/*
 * Copyright (c) 2017 Markus Friedl.  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 "includes.h"
#ifdef WITH_XMSS

#include <sys/types.h>
#include <sys/uio.h>

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#ifdef HAVE_SYS_FILE_H
# include <sys/file.h>
#endif

#include "ssh2.h"
#include "ssherr.h"
#include "sshbuf.h"
#include "cipher.h"
#include "sshkey.h"
#include "sshkey-xmss.h"
#include "atomicio.h"
#include "log.h"

#include "xmss_fast.h"

/* opaque internal XMSS state */
#define XMSS_MAGIC              "xmss-state-v1"
#define XMSS_CIPHERNAME         "aes256-gcm@openssh.com"
struct ssh_xmss_state {
        xmss_params     params;
        u_int32_t       n, w, h, k;

        bds_state       bds;
        u_char          *stack;
        u_int32_t       stackoffset;
        u_char          *stacklevels;
        u_char          *auth;
        u_char          *keep;
        u_char          *th_nodes;
        u_char          *retain;
        treehash_inst   *treehash;

        u_int32_t       idx;            /* state read from file */
        u_int32_t       maxidx;         /* restricted # of signatures */
        int             have_state;     /* .state file exists */
        int             lockfd;         /* locked in sshkey_xmss_get_state() */
        u_char          allow_update;   /* allow sshkey_xmss_update_state() */
        char            *enc_ciphername;/* encrypt state with cipher */
        u_char          *enc_keyiv;     /* encrypt state with key */
        u_int32_t       enc_keyiv_len;  /* length of enc_keyiv */
};

int      sshkey_xmss_init_bds_state(struct sshkey *);
int      sshkey_xmss_init_enc_key(struct sshkey *, const char *);
void     sshkey_xmss_free_bds(struct sshkey *);
int      sshkey_xmss_get_state_from_file(struct sshkey *, const char *,
            int *, int);
int      sshkey_xmss_encrypt_state(const struct sshkey *, struct sshbuf *,
            struct sshbuf **);
int      sshkey_xmss_decrypt_state(const struct sshkey *, struct sshbuf *,
            struct sshbuf **);
int      sshkey_xmss_serialize_enc_key(const struct sshkey *, struct sshbuf *);
int      sshkey_xmss_deserialize_enc_key(struct sshkey *, struct sshbuf *);

#define PRINT(...) do { if (printerror) sshlog(__FILE__, __func__, __LINE__, \
    0, SYSLOG_LEVEL_ERROR, NULL, __VA_ARGS__); } while (0)

int
sshkey_xmss_init(struct sshkey *key, const char *name)
{
        struct ssh_xmss_state *state;

        if (key->xmss_state != NULL)
                return SSH_ERR_INVALID_FORMAT;
        if (name == NULL)
                return SSH_ERR_INVALID_FORMAT;
        state = calloc(sizeof(struct ssh_xmss_state), 1);
        if (state == NULL)
                return SSH_ERR_ALLOC_FAIL;
        if (strcmp(name, XMSS_SHA2_256_W16_H10_NAME) == 0) {
                state->n = 32;
                state->w = 16;
                state->h = 10;
        } else if (strcmp(name, XMSS_SHA2_256_W16_H16_NAME) == 0) {
                state->n = 32;
                state->w = 16;
                state->h = 16;
        } else if (strcmp(name, XMSS_SHA2_256_W16_H20_NAME) == 0) {
                state->n = 32;
                state->w = 16;
                state->h = 20;
        } else {
                free(state);
                return SSH_ERR_KEY_TYPE_UNKNOWN;
        }
        if ((key->xmss_name = strdup(name)) == NULL) {
                free(state);
                return SSH_ERR_ALLOC_FAIL;
        }
        state->k = 2;   /* XXX hardcoded */
        state->lockfd = -1;
        if (xmss_set_params(&state->params, state->n, state->h, state->w,
            state->k) != 0) {
                free(state);
                return SSH_ERR_INVALID_FORMAT;
        }
        key->xmss_state = state;
        return 0;
}

void
sshkey_xmss_free_state(struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;

        sshkey_xmss_free_bds(key);
        if (state) {
                if (state->enc_keyiv) {
                        explicit_bzero(state->enc_keyiv, state->enc_keyiv_len);
                        free(state->enc_keyiv);
                }
                free(state->enc_ciphername);
                free(state);
        }
        key->xmss_state = NULL;
}

#define SSH_XMSS_K2_MAGIC       "k=2"
#define num_stack(x)            ((x->h+1)*(x->n))
#define num_stacklevels(x)      (x->h+1)
#define num_auth(x)             ((x->h)*(x->n))
#define num_keep(x)             ((x->h >> 1)*(x->n))
#define num_th_nodes(x)         ((x->h - x->k)*(x->n))
#define num_retain(x)           (((1ULL << x->k) - x->k - 1) * (x->n))
#define num_treehash(x)         ((x->h) - (x->k))

int
sshkey_xmss_init_bds_state(struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;
        u_int32_t i;

        state->stackoffset = 0;
        if ((state->stack = calloc(num_stack(state), 1)) == NULL ||
            (state->stacklevels = calloc(num_stacklevels(state), 1))== NULL ||
            (state->auth = calloc(num_auth(state), 1)) == NULL ||
            (state->keep = calloc(num_keep(state), 1)) == NULL ||
            (state->th_nodes = calloc(num_th_nodes(state), 1)) == NULL ||
            (state->retain = calloc(num_retain(state), 1)) == NULL ||
            (state->treehash = calloc(num_treehash(state),
            sizeof(treehash_inst))) == NULL) {
                sshkey_xmss_free_bds(key);
                return SSH_ERR_ALLOC_FAIL;
        }
        for (i = 0; i < state->h - state->k; i++)
                state->treehash[i].node = &state->th_nodes[state->n*i];
        xmss_set_bds_state(&state->bds, state->stack, state->stackoffset,
            state->stacklevels, state->auth, state->keep, state->treehash,
            state->retain, 0);
        return 0;
}

void
sshkey_xmss_free_bds(struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;

        if (state == NULL)
                return;
        free(state->stack);
        free(state->stacklevels);
        free(state->auth);
        free(state->keep);
        free(state->th_nodes);
        free(state->retain);
        free(state->treehash);
        state->stack = NULL;
        state->stacklevels = NULL;
        state->auth = NULL;
        state->keep = NULL;
        state->th_nodes = NULL;
        state->retain = NULL;
        state->treehash = NULL;
}

void *
sshkey_xmss_params(const struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;

        if (state == NULL)
                return NULL;
        return &state->params;
}

void *
sshkey_xmss_bds_state(const struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;

        if (state == NULL)
                return NULL;
        return &state->bds;
}

int
sshkey_xmss_siglen(const struct sshkey *key, size_t *lenp)
{
        struct ssh_xmss_state *state = key->xmss_state;

        if (lenp == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if (state == NULL)
                return SSH_ERR_INVALID_FORMAT;
        *lenp = 4 + state->n +
            state->params.wots_par.keysize +
            state->h * state->n;
        return 0;
}

size_t
sshkey_xmss_pklen(const struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;

        if (state == NULL)
                return 0;
        return state->n * 2;
}

size_t
sshkey_xmss_sklen(const struct sshkey *key)
{
        struct ssh_xmss_state *state = key->xmss_state;

        if (state == NULL)
                return 0;
        return state->n * 4 + 4;
}

int
sshkey_xmss_init_enc_key(struct sshkey *k, const char *ciphername)
{
        struct ssh_xmss_state *state = k->xmss_state;
        const struct sshcipher *cipher;
        size_t keylen = 0, ivlen = 0;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((cipher = cipher_by_name(ciphername)) == NULL)
                return SSH_ERR_INTERNAL_ERROR;
        if ((state->enc_ciphername = strdup(ciphername)) == NULL)
                return SSH_ERR_ALLOC_FAIL;
        keylen = cipher_keylen(cipher);
        ivlen = cipher_ivlen(cipher);
        state->enc_keyiv_len = keylen + ivlen;
        if ((state->enc_keyiv = calloc(state->enc_keyiv_len, 1)) == NULL) {
                free(state->enc_ciphername);
                state->enc_ciphername = NULL;
                return SSH_ERR_ALLOC_FAIL;
        }
        arc4random_buf(state->enc_keyiv, state->enc_keyiv_len);
        return 0;
}

int
sshkey_xmss_serialize_enc_key(const struct sshkey *k, struct sshbuf *b)
{
        struct ssh_xmss_state *state = k->xmss_state;
        int r;

        if (state == NULL || state->enc_keyiv == NULL ||
            state->enc_ciphername == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((r = sshbuf_put_cstring(b, state->enc_ciphername)) != 0 ||
            (r = sshbuf_put_string(b, state->enc_keyiv,
            state->enc_keyiv_len)) != 0)
                return r;
        return 0;
}

int
sshkey_xmss_deserialize_enc_key(struct sshkey *k, struct sshbuf *b)
{
        struct ssh_xmss_state *state = k->xmss_state;
        size_t len;
        int r;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((r = sshbuf_get_cstring(b, &state->enc_ciphername, NULL)) != 0 ||
            (r = sshbuf_get_string(b, &state->enc_keyiv, &len)) != 0)
                return r;
        state->enc_keyiv_len = len;
        return 0;
}

int
sshkey_xmss_serialize_pk_info(const struct sshkey *k, struct sshbuf *b,
    enum sshkey_serialize_rep opts)
{
        struct ssh_xmss_state *state = k->xmss_state;
        u_char have_info = 1;
        u_int32_t idx;
        int r;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if (opts != SSHKEY_SERIALIZE_INFO)
                return 0;
        idx = k->xmss_sk ? PEEK_U32(k->xmss_sk) : state->idx;
        if ((r = sshbuf_put_u8(b, have_info)) != 0 ||
            (r = sshbuf_put_u32(b, idx)) != 0 ||
            (r = sshbuf_put_u32(b, state->maxidx)) != 0)
                return r;
        return 0;
}

int
sshkey_xmss_deserialize_pk_info(struct sshkey *k, struct sshbuf *b)
{
        struct ssh_xmss_state *state = k->xmss_state;
        u_char have_info;
        int r;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        /* optional */
        if (sshbuf_len(b) == 0)
                return 0;
        if ((r = sshbuf_get_u8(b, &have_info)) != 0)
                return r;
        if (have_info != 1)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((r = sshbuf_get_u32(b, &state->idx)) != 0 ||
            (r = sshbuf_get_u32(b, &state->maxidx)) != 0)
                return r;
        return 0;
}

int
sshkey_xmss_generate_private_key(struct sshkey *k, int bits)
{
        int r;
        const char *name;

        if (bits == 10) {
                name = XMSS_SHA2_256_W16_H10_NAME;
        } else if (bits == 16) {
                name = XMSS_SHA2_256_W16_H16_NAME;
        } else if (bits == 20) {
                name = XMSS_SHA2_256_W16_H20_NAME;
        } else {
                name = XMSS_DEFAULT_NAME;
        }
        if ((r = sshkey_xmss_init(k, name)) != 0 ||
            (r = sshkey_xmss_init_bds_state(k)) != 0 ||
            (r = sshkey_xmss_init_enc_key(k, XMSS_CIPHERNAME)) != 0)
                return r;
        if ((k->xmss_pk = malloc(sshkey_xmss_pklen(k))) == NULL ||
            (k->xmss_sk = malloc(sshkey_xmss_sklen(k))) == NULL) {
                return SSH_ERR_ALLOC_FAIL;
        }
        xmss_keypair(k->xmss_pk, k->xmss_sk, sshkey_xmss_bds_state(k),
            sshkey_xmss_params(k));
        return 0;
}

int
sshkey_xmss_get_state_from_file(struct sshkey *k, const char *filename,
    int *have_file, int printerror)
{
        struct sshbuf *b = NULL, *enc = NULL;
        int ret = SSH_ERR_SYSTEM_ERROR, r, fd = -1;
        u_int32_t len;
        unsigned char buf[4], *data = NULL;

        *have_file = 0;
        if ((fd = open(filename, O_RDONLY)) >= 0) {
                *have_file = 1;
                if (atomicio(read, fd, buf, sizeof(buf)) != sizeof(buf)) {
                        PRINT("corrupt state file: %s", filename);
                        goto done;
                }
                len = PEEK_U32(buf);
                if ((data = calloc(len, 1)) == NULL) {
                        ret = SSH_ERR_ALLOC_FAIL;
                        goto done;
                }
                if (atomicio(read, fd, data, len) != len) {
                        PRINT("cannot read blob: %s", filename);
                        goto done;
                }
                if ((enc = sshbuf_from(data, len)) == NULL) {
                        ret = SSH_ERR_ALLOC_FAIL;
                        goto done;
                }
                sshkey_xmss_free_bds(k);
                if ((r = sshkey_xmss_decrypt_state(k, enc, &b)) != 0) {
                        ret = r;
                        goto done;
                }
                if ((r = sshkey_xmss_deserialize_state(k, b)) != 0) {
                        ret = r;
                        goto done;
                }
                ret = 0;
        }
done:
        if (fd != -1)
                close(fd);
        free(data);
        sshbuf_free(enc);
        sshbuf_free(b);
        return ret;
}

int
sshkey_xmss_get_state(const struct sshkey *k, int printerror)
{
        struct ssh_xmss_state *state = k->xmss_state;
        u_int32_t idx = 0;
        char *filename = NULL;
        char *statefile = NULL, *ostatefile = NULL, *lockfile = NULL;
        int lockfd = -1, have_state = 0, have_ostate, tries = 0;
        int ret = SSH_ERR_INVALID_ARGUMENT, r;

        if (state == NULL)
                goto done;
        /*
         * If maxidx is set, then we are allowed a limited number
         * of signatures, but don't need to access the disk.
         * Otherwise we need to deal with the on-disk state.
         */
        if (state->maxidx) {
                /* xmss_sk always contains the current state */
                idx = PEEK_U32(k->xmss_sk);
                if (idx < state->maxidx) {
                        state->allow_update = 1;
                        return 0;
                }
                return SSH_ERR_INVALID_ARGUMENT;
        }
        if ((filename = k->xmss_filename) == NULL)
                goto done;
        if (asprintf(&lockfile, "%s.lock", filename) == -1 ||
            asprintf(&statefile, "%s.state", filename) == -1 ||
            asprintf(&ostatefile, "%s.ostate", filename) == -1) {
                ret = SSH_ERR_ALLOC_FAIL;
                goto done;
        }
        if ((lockfd = open(lockfile, O_CREAT|O_RDONLY, 0600)) == -1) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("cannot open/create: %s", lockfile);
                goto done;
        }
        while (flock(lockfd, LOCK_EX|LOCK_NB) == -1) {
                if (errno != EWOULDBLOCK) {
                        ret = SSH_ERR_SYSTEM_ERROR;
                        PRINT("cannot lock: %s", lockfile);
                        goto done;
                }
                if (++tries > 10) {
                        ret = SSH_ERR_SYSTEM_ERROR;
                        PRINT("giving up on: %s", lockfile);
                        goto done;
                }
                usleep(1000*100*tries);
        }
        /* XXX no longer const */
        if ((r = sshkey_xmss_get_state_from_file((struct sshkey *)k,
            statefile, &have_state, printerror)) != 0) {
                if ((r = sshkey_xmss_get_state_from_file((struct sshkey *)k,
                    ostatefile, &have_ostate, printerror)) == 0) {
                        state->allow_update = 1;
                        r = sshkey_xmss_forward_state(k, 1);
                        state->idx = PEEK_U32(k->xmss_sk);
                        state->allow_update = 0;
                }
        }
        if (!have_state && !have_ostate) {
                /* check that bds state is initialized */
                if (state->bds.auth == NULL)
                        goto done;
                PRINT("start from scratch idx 0: %u", state->idx);
        } else if (r != 0) {
                ret = r;
                goto done;
        }
        if (state->idx + 1 < state->idx) {
                PRINT("state wrap: %u", state->idx);
                goto done;
        }
        state->have_state = have_state;
        state->lockfd = lockfd;
        state->allow_update = 1;
        lockfd = -1;
        ret = 0;
done:
        if (lockfd != -1)
                close(lockfd);
        free(lockfile);
        free(statefile);
        free(ostatefile);
        return ret;
}

int
sshkey_xmss_forward_state(const struct sshkey *k, u_int32_t reserve)
{
        struct ssh_xmss_state *state = k->xmss_state;
        u_char *sig = NULL;
        size_t required_siglen;
        unsigned long long smlen;
        u_char data;
        int ret, r;

        if (state == NULL || !state->allow_update)
                return SSH_ERR_INVALID_ARGUMENT;
        if (reserve == 0)
                return SSH_ERR_INVALID_ARGUMENT;
        if (state->idx + reserve <= state->idx)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((r = sshkey_xmss_siglen(k, &required_siglen)) != 0)
                return r;
        if ((sig = malloc(required_siglen)) == NULL)
                return SSH_ERR_ALLOC_FAIL;
        while (reserve-- > 0) {
                state->idx = PEEK_U32(k->xmss_sk);
                smlen = required_siglen;
                if ((ret = xmss_sign(k->xmss_sk, sshkey_xmss_bds_state(k),
                    sig, &smlen, &data, 0, sshkey_xmss_params(k))) != 0) {
                        r = SSH_ERR_INVALID_ARGUMENT;
                        break;
                }
        }
        free(sig);
        return r;
}

int
sshkey_xmss_update_state(const struct sshkey *k, int printerror)
{
        struct ssh_xmss_state *state = k->xmss_state;
        struct sshbuf *b = NULL, *enc = NULL;
        u_int32_t idx = 0;
        unsigned char buf[4];
        char *filename = NULL;
        char *statefile = NULL, *ostatefile = NULL, *nstatefile = NULL;
        int fd = -1;
        int ret = SSH_ERR_INVALID_ARGUMENT;

        if (state == NULL || !state->allow_update)
                return ret;
        if (state->maxidx) {
                /* no update since the number of signatures is limited */
                ret = 0;
                goto done;
        }
        idx = PEEK_U32(k->xmss_sk);
        if (idx == state->idx) {
                /* no signature happened, no need to update */
                ret = 0;
                goto done;
        } else if (idx != state->idx + 1) {
                PRINT("more than one signature happened: idx %u state %u",
                    idx, state->idx);
                goto done;
        }
        state->idx = idx;
        if ((filename = k->xmss_filename) == NULL)
                goto done;
        if (asprintf(&statefile, "%s.state", filename) == -1 ||
            asprintf(&ostatefile, "%s.ostate", filename) == -1 ||
            asprintf(&nstatefile, "%s.nstate", filename) == -1) {
                ret = SSH_ERR_ALLOC_FAIL;
                goto done;
        }
        unlink(nstatefile);
        if ((b = sshbuf_new()) == NULL) {
                ret = SSH_ERR_ALLOC_FAIL;
                goto done;
        }
        if ((ret = sshkey_xmss_serialize_state(k, b)) != 0) {
                PRINT("SERLIALIZE FAILED: %d", ret);
                goto done;
        }
        if ((ret = sshkey_xmss_encrypt_state(k, b, &enc)) != 0) {
                PRINT("ENCRYPT FAILED: %d", ret);
                goto done;
        }
        if ((fd = open(nstatefile, O_CREAT|O_WRONLY|O_EXCL, 0600)) == -1) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("open new state file: %s", nstatefile);
                goto done;
        }
        POKE_U32(buf, sshbuf_len(enc));
        if (atomicio(vwrite, fd, buf, sizeof(buf)) != sizeof(buf)) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("write new state file hdr: %s", nstatefile);
                close(fd);
                goto done;
        }
        if (atomicio(vwrite, fd, sshbuf_mutable_ptr(enc), sshbuf_len(enc)) !=
            sshbuf_len(enc)) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("write new state file data: %s", nstatefile);
                close(fd);
                goto done;
        }
        if (fsync(fd) == -1) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("sync new state file: %s", nstatefile);
                close(fd);
                goto done;
        }
        if (close(fd) == -1) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("close new state file: %s", nstatefile);
                goto done;
        }
        if (state->have_state) {
                unlink(ostatefile);
                if (link(statefile, ostatefile)) {
                        ret = SSH_ERR_SYSTEM_ERROR;
                        PRINT("backup state %s to %s", statefile, ostatefile);
                        goto done;
                }
        }
        if (rename(nstatefile, statefile) == -1) {
                ret = SSH_ERR_SYSTEM_ERROR;
                PRINT("rename %s to %s", nstatefile, statefile);
                goto done;
        }
        ret = 0;
done:
        if (state->lockfd != -1) {
                close(state->lockfd);
                state->lockfd = -1;
        }
        if (nstatefile)
                unlink(nstatefile);
        free(statefile);
        free(ostatefile);
        free(nstatefile);
        sshbuf_free(b);
        sshbuf_free(enc);
        return ret;
}

int
sshkey_xmss_serialize_state(const struct sshkey *k, struct sshbuf *b)
{
        struct ssh_xmss_state *state = k->xmss_state;
        treehash_inst *th;
        u_int32_t i, node;
        int r;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if (state->stack == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        state->stackoffset = state->bds.stackoffset;    /* copy back */
        if ((r = sshbuf_put_cstring(b, SSH_XMSS_K2_MAGIC)) != 0 ||
            (r = sshbuf_put_u32(b, state->idx)) != 0 ||
            (r = sshbuf_put_string(b, state->stack, num_stack(state))) != 0 ||
            (r = sshbuf_put_u32(b, state->stackoffset)) != 0 ||
            (r = sshbuf_put_string(b, state->stacklevels, num_stacklevels(state))) != 0 ||
            (r = sshbuf_put_string(b, state->auth, num_auth(state))) != 0 ||
            (r = sshbuf_put_string(b, state->keep, num_keep(state))) != 0 ||
            (r = sshbuf_put_string(b, state->th_nodes, num_th_nodes(state))) != 0 ||
            (r = sshbuf_put_string(b, state->retain, num_retain(state))) != 0 ||
            (r = sshbuf_put_u32(b, num_treehash(state))) != 0)
                return r;
        for (i = 0; i < num_treehash(state); i++) {
                th = &state->treehash[i];
                node = th->node - state->th_nodes;
                if ((r = sshbuf_put_u32(b, th->h)) != 0 ||
                    (r = sshbuf_put_u32(b, th->next_idx)) != 0 ||
                    (r = sshbuf_put_u32(b, th->stackusage)) != 0 ||
                    (r = sshbuf_put_u8(b, th->completed)) != 0 ||
                    (r = sshbuf_put_u32(b, node)) != 0)
                        return r;
        }
        return 0;
}

int
sshkey_xmss_serialize_state_opt(const struct sshkey *k, struct sshbuf *b,
    enum sshkey_serialize_rep opts)
{
        struct ssh_xmss_state *state = k->xmss_state;
        int r = SSH_ERR_INVALID_ARGUMENT;
        u_char have_stack, have_filename, have_enc;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((r = sshbuf_put_u8(b, opts)) != 0)
                return r;
        switch (opts) {
        case SSHKEY_SERIALIZE_STATE:
                r = sshkey_xmss_serialize_state(k, b);
                break;
        case SSHKEY_SERIALIZE_FULL:
                if ((r = sshkey_xmss_serialize_enc_key(k, b)) != 0)
                        return r;
                r = sshkey_xmss_serialize_state(k, b);
                break;
        case SSHKEY_SERIALIZE_SHIELD:
                /* all of stack/filename/enc are optional */
                have_stack = state->stack != NULL;
                if ((r = sshbuf_put_u8(b, have_stack)) != 0)
                        return r;
                if (have_stack) {
                        state->idx = PEEK_U32(k->xmss_sk);      /* update */
                        if ((r = sshkey_xmss_serialize_state(k, b)) != 0)
                                return r;
                }
                have_filename = k->xmss_filename != NULL;
                if ((r = sshbuf_put_u8(b, have_filename)) != 0)
                        return r;
                if (have_filename &&
                    (r = sshbuf_put_cstring(b, k->xmss_filename)) != 0)
                        return r;
                have_enc = state->enc_keyiv != NULL;
                if ((r = sshbuf_put_u8(b, have_enc)) != 0)
                        return r;
                if (have_enc &&
                    (r = sshkey_xmss_serialize_enc_key(k, b)) != 0)
                        return r;
                if ((r = sshbuf_put_u32(b, state->maxidx)) != 0 ||
                    (r = sshbuf_put_u8(b, state->allow_update)) != 0)
                        return r;
                break;
        case SSHKEY_SERIALIZE_DEFAULT:
                r = 0;
                break;
        default:
                r = SSH_ERR_INVALID_ARGUMENT;
                break;
        }
        return r;
}

int
sshkey_xmss_deserialize_state(struct sshkey *k, struct sshbuf *b)
{
        struct ssh_xmss_state *state = k->xmss_state;
        treehash_inst *th;
        u_int32_t i, lh, node;
        size_t ls, lsl, la, lk, ln, lr;
        char *magic;
        int r = SSH_ERR_INTERNAL_ERROR;

        if (state == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if (k->xmss_sk == NULL)
                return SSH_ERR_INVALID_ARGUMENT;
        if ((state->treehash = calloc(num_treehash(state),
            sizeof(treehash_inst))) == NULL)
                return SSH_ERR_ALLOC_FAIL;
        if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0 ||
            (r = sshbuf_get_u32(b, &state->idx)) != 0 ||
            (r = sshbuf_get_string(b, &state->stack, &ls)) != 0 ||
            (r = sshbuf_get_u32(b, &state->stackoffset)) != 0 ||
            (r = sshbuf_get_string(b, &state->stacklevels, &lsl)) != 0 ||
            (r = sshbuf_get_string(b, &state->auth, &la)) != 0 ||
            (r = sshbuf_get_string(b, &state->keep, &lk)) != 0 ||
            (r = sshbuf_get_string(b, &state->th_nodes, &ln)) != 0 ||
            (r = sshbuf_get_string(b, &state->retain, &lr)) != 0 ||
            (r = sshbuf_get_u32(b, &lh)) != 0)
                goto out;
        if (strcmp(magic, SSH_XMSS_K2_MAGIC) != 0) {
                r = SSH_ERR_INVALID_ARGUMENT;
                goto out;
        }
        /* XXX check stackoffset */
        if (ls != num_stack(state) ||
            lsl != num_stacklevels(state) ||
            la != num_auth(state) ||
            lk != num_keep(state) ||
            ln != num_th_nodes(state) ||
            lr != num_retain(state) ||
            lh != num_treehash(state)) {
                r = SSH_ERR_INVALID_ARGUMENT;
                goto out;
        }
        for (i = 0; i < num_treehash(state); i++) {
                th = &state->treehash[i];
                if ((r = sshbuf_get_u32(b, &th->h)) != 0 ||
                    (r = sshbuf_get_u32(b, &th->next_idx)) != 0 ||
                    (r = sshbuf_get_u32(b, &th->stackusage)) != 0 ||
                    (r = sshbuf_get_u8(b, &th->completed)) != 0 ||
                    (r = sshbuf_get_u32(b, &node)) != 0)
                        goto out;
                if (node < num_th_nodes(state))
                        th->node = &state->th_nodes[node];
        }
        POKE_U32(k->xmss_sk, state->idx);
        xmss_set_bds_state(&state->bds, state->stack, state->stackoffset,
            state->stacklevels, state->auth, state->keep, state->treehash,
            state->retain, 0);
        /* success */
        r = 0;
 out:
        free(magic);
        return r;
}

int
sshkey_xmss_deserialize_state_opt(struct sshkey *k, struct sshbuf *b)
{
        struct ssh_xmss_state *state = k->xmss_state;
        enum sshkey_serialize_rep opts;
        u_char have_state, have_stack, have_filename, have_enc;
        int r;

        if ((r = sshbuf_get_u8(b, &have_state)) != 0)
                return r;

        opts = have_state;
        switch (opts) {
        case SSHKEY_SERIALIZE_DEFAULT:
                r = 0;
                break;
        case SSHKEY_SERIALIZE_SHIELD:
                if ((r = sshbuf_get_u8(b, &have_stack)) != 0)
                        return r;
                if (have_stack &&
                    (r = sshkey_xmss_deserialize_state(k, b)) != 0)
                        return r;
                if ((r = sshbuf_get_u8(b, &have_filename)) != 0)
                        return r;
                if (have_filename &&
                    (r = sshbuf_get_cstring(b, &k->xmss_filename, NULL)) != 0)
                        return r;
                if ((r = sshbuf_get_u8(b, &have_enc)) != 0)
                        return r;
                if (have_enc &&
                    (r = sshkey_xmss_deserialize_enc_key(k, b)) != 0)
                        return r;
                if ((r = sshbuf_get_u32(b, &state->maxidx)) != 0 ||
                    (r = sshbuf_get_u8(b, &state->allow_update)) != 0)
                        return r;
                break;
        case SSHKEY_SERIALIZE_STATE:
                if ((r = sshkey_xmss_deserialize_state(k, b)) != 0)
                        return r;
                break;
        case SSHKEY_SERIALIZE_FULL:
                if ((r = sshkey_xmss_deserialize_enc_key(k, b)) != 0 ||
                    (r = sshkey_xmss_deserialize_state(k, b)) != 0)
                        return r;
                break;
        default:
                r = SSH_ERR_INVALID_FORMAT;
                break;
        }
        return r;
}

int
sshkey_xmss_encrypt_state(const struct sshkey *k, struct sshbuf *b,
   struct sshbuf **retp)
{
        struct ssh_xmss_state *state = k->xmss_state;
        struct sshbuf *encrypted = NULL, *encoded = NULL, *padded = NULL;
        struct sshcipher_ctx *ciphercontext = NULL;
        const struct sshcipher *cipher;
        u_char *cp, *key, *iv = NULL;
        size_t i, keylen, ivlen, blocksize, authlen, encrypted_len, aadlen;
        int r = SSH_ERR_INTERNAL_ERROR;

        if (retp != NULL)
                *retp = NULL;
        if (state == NULL ||
            state->enc_keyiv == NULL ||
            state->enc_ciphername == NULL)
                return SSH_ERR_INTERNAL_ERROR;
        if ((cipher = cipher_by_name(state->enc_ciphername)) == NULL) {
                r = SSH_ERR_INTERNAL_ERROR;
                goto out;
        }
        blocksize = cipher_blocksize(cipher);
        keylen = cipher_keylen(cipher);
        ivlen = cipher_ivlen(cipher);
        authlen = cipher_authlen(cipher);
        if (state->enc_keyiv_len != keylen + ivlen) {
                r = SSH_ERR_INVALID_FORMAT;
                goto out;
        }
        key = state->enc_keyiv;
        if ((encrypted = sshbuf_new()) == NULL ||
            (encoded = sshbuf_new()) == NULL ||
            (padded = sshbuf_new()) == NULL ||
            (iv = malloc(ivlen)) == NULL) {
                r = SSH_ERR_ALLOC_FAIL;
                goto out;
        }

        /* replace first 4 bytes of IV with index to ensure uniqueness */
        memcpy(iv, key + keylen, ivlen);
        POKE_U32(iv, state->idx);

        if ((r = sshbuf_put(encoded, XMSS_MAGIC, sizeof(XMSS_MAGIC))) != 0 ||
            (r = sshbuf_put_u32(encoded, state->idx)) != 0)
                goto out;

        /* padded state will be encrypted */
        if ((r = sshbuf_putb(padded, b)) != 0)
                goto out;
        i = 0;
        while (sshbuf_len(padded) % blocksize) {
                if ((r = sshbuf_put_u8(padded, ++i & 0xff)) != 0)
                        goto out;
        }
        encrypted_len = sshbuf_len(padded);

        /* header including the length of state is used as AAD */
        if ((r = sshbuf_put_u32(encoded, encrypted_len)) != 0)
                goto out;
        aadlen = sshbuf_len(encoded);

        /* concat header and state */
        if ((r = sshbuf_putb(encoded, padded)) != 0)
                goto out;

        /* reserve space for encryption of encoded data plus auth tag */
        /* encrypt at offset addlen */
        if ((r = sshbuf_reserve(encrypted,
            encrypted_len + aadlen + authlen, &cp)) != 0 ||
            (r = cipher_init(&ciphercontext, cipher, key, keylen,
            iv, ivlen, 1)) != 0 ||
            (r = cipher_crypt(ciphercontext, 0, cp, sshbuf_ptr(encoded),
            encrypted_len, aadlen, authlen)) != 0)
                goto out;

        /* success */
        r = 0;
 out:
        if (retp != NULL) {
                *retp = encrypted;
                encrypted = NULL;
        }
        sshbuf_free(padded);
        sshbuf_free(encoded);
        sshbuf_free(encrypted);
        cipher_free(ciphercontext);
        free(iv);
        return r;
}

int
sshkey_xmss_decrypt_state(const struct sshkey *k, struct sshbuf *encoded,
   struct sshbuf **retp)
{
        struct ssh_xmss_state *state = k->xmss_state;
        struct sshbuf *copy = NULL, *decrypted = NULL;
        struct sshcipher_ctx *ciphercontext = NULL;
        const struct sshcipher *cipher = NULL;
        u_char *key, *iv = NULL, *dp;
        size_t keylen, ivlen, authlen, aadlen;
        u_int blocksize, encrypted_len, index;
        int r = SSH_ERR_INTERNAL_ERROR;

        if (retp != NULL)
                *retp = NULL;
        if (state == NULL ||
            state->enc_keyiv == NULL ||
            state->enc_ciphername == NULL)
                return SSH_ERR_INTERNAL_ERROR;
        if ((cipher = cipher_by_name(state->enc_ciphername)) == NULL) {
                r = SSH_ERR_INVALID_FORMAT;
                goto out;
        }
        blocksize = cipher_blocksize(cipher);
        keylen = cipher_keylen(cipher);
        ivlen = cipher_ivlen(cipher);
        authlen = cipher_authlen(cipher);
        if (state->enc_keyiv_len != keylen + ivlen) {
                r = SSH_ERR_INTERNAL_ERROR;
                goto out;
        }
        key = state->enc_keyiv;

        if ((copy = sshbuf_fromb(encoded)) == NULL ||
            (decrypted = sshbuf_new()) == NULL ||
            (iv = malloc(ivlen)) == NULL) {
                r = SSH_ERR_ALLOC_FAIL;
                goto out;
        }

        /* check magic */
        if (sshbuf_len(encoded) < sizeof(XMSS_MAGIC) ||
            memcmp(sshbuf_ptr(encoded), XMSS_MAGIC, sizeof(XMSS_MAGIC))) {
                r = SSH_ERR_INVALID_FORMAT;
                goto out;
        }
        /* parse public portion */
        if ((r = sshbuf_consume(encoded, sizeof(XMSS_MAGIC))) != 0 ||
            (r = sshbuf_get_u32(encoded, &index)) != 0 ||
            (r = sshbuf_get_u32(encoded, &encrypted_len)) != 0)
                goto out;

        /* check size of encrypted key blob */
        if (encrypted_len < blocksize || (encrypted_len % blocksize) != 0) {
                r = SSH_ERR_INVALID_FORMAT;
                goto out;
        }
        /* check that an appropriate amount of auth data is present */
        if (sshbuf_len(encoded) < authlen ||
            sshbuf_len(encoded) - authlen < encrypted_len) {
                r = SSH_ERR_INVALID_FORMAT;
                goto out;
        }

        aadlen = sshbuf_len(copy) - sshbuf_len(encoded);

        /* replace first 4 bytes of IV with index to ensure uniqueness */
        memcpy(iv, key + keylen, ivlen);
        POKE_U32(iv, index);

        /* decrypt private state of key */
        if ((r = sshbuf_reserve(decrypted, aadlen + encrypted_len, &dp)) != 0 ||
            (r = cipher_init(&ciphercontext, cipher, key, keylen,
            iv, ivlen, 0)) != 0 ||
            (r = cipher_crypt(ciphercontext, 0, dp, sshbuf_ptr(copy),
            encrypted_len, aadlen, authlen)) != 0)
                goto out;

        /* there should be no trailing data */
        if ((r = sshbuf_consume(encoded, encrypted_len + authlen)) != 0)
                goto out;
        if (sshbuf_len(encoded) != 0) {
                r = SSH_ERR_INVALID_FORMAT;
                goto out;
        }

        /* remove AAD */
        if ((r = sshbuf_consume(decrypted, aadlen)) != 0)
                goto out;
        /* XXX encrypted includes unchecked padding */

        /* success */
        r = 0;
        if (retp != NULL) {
                *retp = decrypted;
                decrypted = NULL;
        }
 out:
        cipher_free(ciphercontext);
        sshbuf_free(copy);
        sshbuf_free(decrypted);
        free(iv);
        return r;
}

u_int32_t
sshkey_xmss_signatures_left(const struct sshkey *k)
{
        struct ssh_xmss_state *state = k->xmss_state;
        u_int32_t idx;

        if (sshkey_type_plain(k->type) == KEY_XMSS && state &&
            state->maxidx) {
                idx = k->xmss_sk ? PEEK_U32(k->xmss_sk) : state->idx;
                if (idx < state->maxidx)
                        return state->maxidx - idx;
        }
        return 0;
}

int
sshkey_xmss_enable_maxsign(struct sshkey *k, u_int32_t maxsign)
{
        struct ssh_xmss_state *state = k->xmss_state;

        if (sshkey_type_plain(k->type) != KEY_XMSS)
                return SSH_ERR_INVALID_ARGUMENT;
        if (maxsign == 0)
                return 0;
        if (state->idx + maxsign < state->idx)
                return SSH_ERR_INVALID_ARGUMENT;
        state->maxidx = state->idx + maxsign;
        return 0;
}
#endif /* WITH_XMSS */