/* $NetBSD: sshkey-xmss.c,v 1.4.2.2 2023/12/25 12:31:09 martin Exp $ */ /* $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" __RCSID("$NetBSD: sshkey-xmss.c,v 1.4.2.2 2023/12/25 12:31:09 martin Exp $"); #include #include #include #include #include #include #include #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 = 0, 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(__UNCONST(k), statefile, &have_state, printerror)) != 0) { if ((r = sshkey_xmss_get_state_from_file(__UNCONST(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; }