Home | History | Annotate | Line # | Download | only in dist
      1 /*	$NetBSD: ssh-pkcs11-client.c,v 1.22 2026/04/08 18:58:41 christos Exp $	*/
      2 /* $OpenBSD: ssh-pkcs11-client.c,v 1.26 2026/02/09 22:11:39 dtucker Exp $ */
      3 
      4 /*
      5  * Copyright (c) 2010 Markus Friedl.  All rights reserved.
      6  * Copyright (c) 2014 Pedro Martelletto. All rights reserved.
      7  *
      8  * Permission to use, copy, modify, and distribute this software for any
      9  * purpose with or without fee is hereby granted, provided that the above
     10  * copyright notice and this permission notice appear in all copies.
     11  *
     12  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     13  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     14  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     15  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     16  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     17  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     18  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     19  */
     20 #include "includes.h"
     21 __RCSID("$NetBSD: ssh-pkcs11-client.c,v 1.22 2026/04/08 18:58:41 christos Exp $");
     22 
     23 #include <sys/types.h>
     24 #include <sys/time.h>
     25 #include <sys/socket.h>
     26 
     27 #include <stdarg.h>
     28 #include <stdlib.h>
     29 #include <string.h>
     30 #include <unistd.h>
     31 #include <errno.h>
     32 #include <limits.h>
     33 
     34 #include "pathnames.h"
     35 #include "xmalloc.h"
     36 #include "sshbuf.h"
     37 #include "log.h"
     38 #include "misc.h"
     39 #include "sshkey.h"
     40 #include "authfd.h"
     41 #include "atomicio.h"
     42 #include "ssh-pkcs11.h"
     43 #include "ssherr.h"
     44 
     45 /* borrows code from sftp-server and ssh-agent */
     46 
     47 /*
     48  * Maintain a list of ssh-pkcs11-helper subprocesses. These may be looked up
     49  * by provider path or their unique keyblobs.
     50  */
     51 struct helper {
     52 	char *path;
     53 	pid_t pid;
     54 	int fd;
     55 	size_t nkeyblobs;
     56 	struct sshbuf **keyblobs; /* XXX use a tree or something faster */
     57 };
     58 static struct helper **helpers;
     59 static size_t nhelpers;
     60 
     61 static struct helper *
     62 helper_by_provider(const char *path)
     63 {
     64 	size_t i;
     65 
     66 	for (i = 0; i < nhelpers; i++) {
     67 		if (helpers[i] == NULL || helpers[i]->path == NULL ||
     68 		    helpers[i]->fd == -1)
     69 			continue;
     70 		if (strcmp(helpers[i]->path, path) == 0)
     71 			return helpers[i];
     72 	}
     73 	return NULL;
     74 }
     75 
     76 static struct helper *
     77 helper_by_key(const struct sshkey *key)
     78 {
     79 	size_t i, j;
     80 	struct sshbuf *keyblob = NULL;
     81 	int r;
     82 
     83 	if ((keyblob = sshbuf_new()) == NULL)
     84 		fatal_f("sshbuf_new failed");
     85 	if ((r = sshkey_putb(key, keyblob)) != 0)
     86 		fatal_fr(r, "serialise key");
     87 
     88 	for (i = 0; i < nhelpers; i++) {
     89 		if (helpers[i] == NULL)
     90 			continue;
     91 		for (j = 0; j < helpers[i]->nkeyblobs; j++) {
     92 			if (sshbuf_equals(keyblob,
     93 			    helpers[i]->keyblobs[j]) == 0) {
     94 				sshbuf_free(keyblob);
     95 				return helpers[i];
     96 			}
     97 		}
     98 	}
     99 	sshbuf_free(keyblob);
    100 	return NULL;
    101 
    102 }
    103 
    104 static void
    105 helper_add_key(struct helper *helper, struct sshkey *key)
    106 {
    107 	int r;
    108 
    109 	helper->keyblobs = xrecallocarray(helper->keyblobs, helper->nkeyblobs,
    110 	    helper->nkeyblobs + 1, sizeof(*helper->keyblobs));
    111 	if ((helper->keyblobs[helper->nkeyblobs] = sshbuf_new()) == NULL)
    112 		fatal_f("sshbuf_new failed");
    113 	if ((r = sshkey_putb(key, helper->keyblobs[helper->nkeyblobs])) != 0)
    114 		fatal_fr(r, "shkey_putb failed");
    115 	helper->nkeyblobs++;
    116 	debug3_f("added %s key for provider %s, now has %zu keys",
    117 	    sshkey_type(key), helper->path, helper->nkeyblobs);
    118 }
    119 
    120 static void
    121 helper_terminate(struct helper *helper)
    122 {
    123 	size_t i;
    124 	int found = 0;
    125 
    126 	if (helper == NULL)
    127 		return;
    128 	if (helper->path == NULL)
    129 		fatal_f("inconsistent helper");
    130 
    131 	debug3_f("terminating helper for %s; remaining %zu keys",
    132 	    helper->path, helper->nkeyblobs);
    133 
    134 	close(helper->fd);
    135 	/* XXX waitpid() */
    136 	helper->fd = -1;
    137 	helper->pid = -1;
    138 
    139 	/* repack helpers */
    140 	for (i = 0; i < nhelpers; i++) {
    141 		if (helpers[i] == helper) {
    142 			if (found)
    143 				fatal_f("helper recorded more than once");
    144 			found = 1;
    145 		} else if (found)
    146 			helpers[i - 1] = helpers[i];
    147 	}
    148 	if (found) {
    149 		helpers = xrecallocarray(helpers, nhelpers,
    150 		    nhelpers - 1, sizeof(*helpers));
    151 		nhelpers--;
    152 	}
    153 	for (i = 0; i < helper->nkeyblobs; i++)
    154 		sshbuf_free(helper->keyblobs[i]);
    155 	free(helper->path);
    156 	free(helper);
    157 }
    158 
    159 static void
    160 send_msg(int fd, struct sshbuf *m)
    161 {
    162 	u_char buf[4];
    163 	size_t mlen = sshbuf_len(m);
    164 	int r;
    165 
    166 	if (fd == -1)
    167 		return;
    168 	POKE_U32(buf, mlen);
    169 	if (atomicio(vwrite, fd, buf, 4) != 4 ||
    170 	    atomicio(vwrite, fd, sshbuf_mutable_ptr(m),
    171 	    sshbuf_len(m)) != sshbuf_len(m))
    172 		error("write to helper failed");
    173 	if ((r = sshbuf_consume(m, mlen)) != 0)
    174 		fatal_fr(r, "consume");
    175 }
    176 
    177 static int
    178 recv_msg(int fd, struct sshbuf *m)
    179 {
    180 	u_int l, len;
    181 	u_char c, buf[1024];
    182 	int r;
    183 
    184 	sshbuf_reset(m);
    185 	if (fd == -1)
    186 		return 0; /* XXX */
    187 	if ((len = atomicio(read, fd, buf, 4)) != 4) {
    188 		error("read from helper failed: %u", len);
    189 		return (0); /* XXX */
    190 	}
    191 	len = PEEK_U32(buf);
    192 	if (len > 256 * 1024)
    193 		fatal("response too long: %u", len);
    194 	/* read len bytes into m */
    195 	while (len > 0) {
    196 		l = len;
    197 		if (l > sizeof(buf))
    198 			l = sizeof(buf);
    199 		if (atomicio(read, fd, buf, l) != l) {
    200 			error("response from helper failed.");
    201 			return (0); /* XXX */
    202 		}
    203 		if ((r = sshbuf_put(m, buf, l)) != 0)
    204 			fatal_fr(r, "sshbuf_put");
    205 		len -= l;
    206 	}
    207 	if ((r = sshbuf_get_u8(m, &c)) != 0)
    208 		fatal_fr(r, "parse type");
    209 	return c;
    210 }
    211 
    212 int
    213 pkcs11_init(int interactive)
    214 {
    215 	return 0;
    216 }
    217 
    218 void
    219 pkcs11_terminate(void)
    220 {
    221 	size_t i;
    222 
    223 	debug3_f("terminating %zu helpers", nhelpers);
    224 	for (i = 0; i < nhelpers; i++)
    225 		helper_terminate(helpers[i]);
    226 }
    227 
    228 int
    229 pkcs11_sign(struct sshkey *key,
    230     u_char **sigp, size_t *lenp,
    231     const u_char *data, size_t datalen,
    232     const char *alg, const char *sk_provider,
    233     const char *sk_pin, u_int compat)
    234 {
    235 	struct sshbuf *msg = NULL;
    236 	struct helper *helper;
    237 	int status, r;
    238 	u_char *signature = NULL;
    239 	size_t signature_len = 0;
    240 	int ret = SSH_ERR_INTERNAL_ERROR;
    241 
    242 	if (sigp != NULL)
    243 		*sigp = NULL;
    244 	if (lenp != NULL)
    245 		*lenp = 0;
    246 
    247 	if ((helper = helper_by_key(key)) == NULL || helper->fd == -1)
    248 		fatal_f("no helper for %s key", sshkey_type(key));
    249 
    250 	if ((msg = sshbuf_new()) == NULL)
    251 		return SSH_ERR_ALLOC_FAIL;
    252 	if ((r = sshbuf_put_u8(msg, SSH2_AGENTC_SIGN_REQUEST)) != 0 ||
    253 	    (r = sshkey_puts_plain(key, msg)) != 0 ||
    254 	    (r = sshbuf_put_string(msg, data, datalen)) != 0 ||
    255 	    (r = sshbuf_put_cstring(msg, alg == NULL ? "" : alg)) != 0 ||
    256 	    (r = sshbuf_put_u32(msg, compat)) != 0)
    257 		fatal_fr(r, "compose");
    258 	send_msg(helper->fd, msg);
    259 	sshbuf_reset(msg);
    260 
    261 	if ((status = recv_msg(helper->fd, msg)) != SSH2_AGENT_SIGN_RESPONSE) {
    262 		/* XXX translate status to something useful */
    263 		debug_fr(r, "recv_msg");
    264 		ret = SSH_ERR_AGENT_FAILURE;
    265 		goto fail;
    266 	}
    267 
    268 	if ((r = sshbuf_get_string(msg, &signature, &signature_len)) != 0)
    269 		fatal_fr(r, "parse");
    270 
    271 	/* success */
    272 	if (sigp != NULL) {
    273 		*sigp = signature;
    274 		signature = NULL;
    275 	}
    276 	if (lenp != NULL)
    277 		*lenp = signature_len;
    278 	ret = 0;
    279 
    280  fail:
    281 	free(signature);
    282 	sshbuf_free(msg);
    283 	return ret;
    284 }
    285 
    286 /*
    287  * Make a private PKCS#11-backed certificate by grafting a previously-loaded
    288  * PKCS#11 private key and a public certificate key.
    289  */
    290 int
    291 pkcs11_make_cert(const struct sshkey *priv,
    292     const struct sshkey *certpub, struct sshkey **certprivp)
    293 {
    294 	struct helper *helper = NULL;
    295 	struct sshkey *ret;
    296 	int r;
    297 
    298 	if ((helper = helper_by_key(priv)) == NULL || helper->fd == -1)
    299 		fatal_f("no helper for %s key", sshkey_type(priv));
    300 
    301 	debug3_f("private key type %s cert type %s on provider %s",
    302 	    sshkey_type(priv), sshkey_type(certpub), helper->path);
    303 
    304 	*certprivp = NULL;
    305 	if (!sshkey_is_cert(certpub) || sshkey_is_cert(priv) ||
    306 	    !sshkey_equal_public(priv, certpub)) {
    307 		error_f("private key %s doesn't match cert %s",
    308 		    sshkey_type(priv), sshkey_type(certpub));
    309 		return SSH_ERR_INVALID_ARGUMENT;
    310 	}
    311 	*certprivp = NULL;
    312 	if ((r = sshkey_from_private(priv, &ret)) != 0)
    313 		fatal_fr(r, "copy key");
    314 
    315 	ret->flags |= SSHKEY_FLAG_EXT;
    316 	if ((r = sshkey_to_certified(ret)) != 0 ||
    317 	    (r = sshkey_cert_copy(certpub, ret)) != 0)
    318 		fatal_fr(r, "graft certificate");
    319 
    320 	helper_add_key(helper, ret);
    321 
    322 	debug3_f("provider %s: %zu remaining keys",
    323 	    helper->path, helper->nkeyblobs);
    324 
    325 	/* success */
    326 	*certprivp = ret;
    327 	return 0;
    328 }
    329 
    330 static struct helper *
    331 pkcs11_start_helper(const char *path)
    332 {
    333 	int pair[2];
    334 	const char *prog, *verbosity = NULL;
    335 	struct helper *helper;
    336 	pid_t pid;
    337 
    338 	if (nhelpers >= INT_MAX)
    339 		fatal_f("too many helpers");
    340 	debug3_f("start helper for %s", path);
    341 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == -1) {
    342 		error_f("socketpair: %s", strerror(errno));
    343 		return NULL;
    344 	}
    345 	helper = xcalloc(1, sizeof(*helper));
    346 	if ((pid = fork()) == -1) {
    347 		error_f("fork: %s", strerror(errno));
    348 		close(pair[0]);
    349 		close(pair[1]);
    350 		free(helper);
    351 		return NULL;
    352 	} else if (pid == 0) {
    353 		if ((dup2(pair[1], STDIN_FILENO) == -1) ||
    354 		    (dup2(pair[1], STDOUT_FILENO) == -1)) {
    355 			fprintf(stderr, "dup2: %s\n", strerror(errno));
    356 			_exit(1);
    357 		}
    358 		close(pair[0]);
    359 		close(pair[1]);
    360 		closefrom(STDERR_FILENO + 1);
    361 		prog = getenv("SSH_PKCS11_HELPER");
    362 		if (prog == NULL || strlen(prog) == 0)
    363 			prog = _PATH_SSH_PKCS11_HELPER;
    364 		if (log_level_get() >= SYSLOG_LEVEL_DEBUG1)
    365 			verbosity = "-vvv";
    366 		debug_f("starting %s %s", prog,
    367 		    verbosity == NULL ? "" : verbosity);
    368 		execlp(prog, prog, verbosity, (char *)NULL);
    369 		fprintf(stderr, "exec: %s: %s\n", prog, strerror(errno));
    370 		_exit(1);
    371 	}
    372 	close(pair[1]);
    373 	helper->fd = pair[0];
    374 	helper->path = xstrdup(path);
    375 	helper->pid = pid;
    376 	debug3_f("helper %zu for \"%s\" on fd %d pid %ld", nhelpers,
    377 	    helper->path, helper->fd, (long)helper->pid);
    378 	helpers = xrecallocarray(helpers, nhelpers,
    379 	    nhelpers + 1, sizeof(*helpers));
    380 	helpers[nhelpers++] = helper;
    381 	return helper;
    382 }
    383 
    384 int
    385 pkcs11_add_provider(char *name, char *pin, struct sshkey ***keysp,
    386     char ***labelsp)
    387 {
    388 	struct sshkey *k;
    389 	int r, type;
    390 	char *label;
    391 	u_int ret = -1, nkeys, i;
    392 	struct sshbuf *msg;
    393 	struct helper *helper;
    394 
    395 	if ((helper = helper_by_provider(name)) == NULL &&
    396 	    (helper = pkcs11_start_helper(name)) == NULL)
    397 		return -1;
    398 
    399 	debug3_f("add %s", helper->path);
    400 
    401 	if ((msg = sshbuf_new()) == NULL)
    402 		fatal_f("sshbuf_new failed");
    403 	if ((r = sshbuf_put_u8(msg, SSH_AGENTC_ADD_SMARTCARD_KEY)) != 0 ||
    404 	    (r = sshbuf_put_cstring(msg, name)) != 0 ||
    405 	    (r = sshbuf_put_cstring(msg, pin)) != 0)
    406 		fatal_fr(r, "compose");
    407 	send_msg(helper->fd, msg);
    408 	sshbuf_reset(msg);
    409 
    410 	type = recv_msg(helper->fd, msg);
    411 	debug3_f("response %d", type);
    412 	if (type == SSH2_AGENT_IDENTITIES_ANSWER) {
    413 		if ((r = sshbuf_get_u32(msg, &nkeys)) != 0)
    414 			fatal_fr(r, "parse nkeys");
    415 		debug3_f("helper return %u keys", nkeys);
    416 		*keysp = xcalloc(nkeys, sizeof(struct sshkey *));
    417 		if (labelsp)
    418 			*labelsp = xcalloc(nkeys, sizeof(char *));
    419 		for (i = 0; i < nkeys; i++) {
    420 			/* XXX clean up properly instead of fatal() */
    421 			if ((r = sshkey_froms(msg, &k)) != 0 ||
    422 			    (r = sshbuf_get_cstring(msg, &label, NULL)) != 0)
    423 				fatal_fr(r, "parse key");
    424 			k->flags |= SSHKEY_FLAG_EXT;
    425 			helper_add_key(helper, k);
    426 			(*keysp)[i] = k;
    427 			if (labelsp)
    428 				(*labelsp)[i] = label;
    429 			else
    430 				free(label);
    431 		}
    432 		/* success */
    433 		ret = 0;
    434 	} else if (type == SSH2_AGENT_FAILURE) {
    435 		if ((r = sshbuf_get_u32(msg, &nkeys)) != 0)
    436 			error_fr(r, "failed to parse failure response");
    437 	}
    438 	if (ret != 0) {
    439 		debug_f("no keys; terminate helper");
    440 		helper_terminate(helper);
    441 	}
    442 	sshbuf_free(msg);
    443 	return ret == 0 ? (int)nkeys : -1;
    444 }
    445 
    446 int
    447 pkcs11_del_provider(char *name)
    448 {
    449 	struct helper *helper;
    450 
    451 	/*
    452 	 * ssh-agent deletes keys before calling this, so the helper entry
    453 	 * should be gone before we get here.
    454 	 */
    455 	debug3_f("delete %s", name ? name : "(null)");
    456 	if ((helper = helper_by_provider(name)) != NULL)
    457 		helper_terminate(helper);
    458 	return 0;
    459 }
    460 
    461 void
    462 pkcs11_key_free(struct sshkey *key)
    463 {
    464 	struct helper *helper;
    465 	struct sshbuf *keyblob = NULL;
    466 	size_t i;
    467 	int r, found = 0;
    468 
    469 	debug3_f("free %s key", sshkey_type(key));
    470 
    471 	if ((helper = helper_by_key(key)) == NULL || helper->fd == -1)
    472 		fatal_f("no helper for %s key", sshkey_type(key));
    473 	if ((keyblob = sshbuf_new()) == NULL)
    474 		fatal_f("sshbuf_new failed");
    475 	if ((r = sshkey_putb(key, keyblob)) != 0)
    476 		fatal_fr(r, "serialise key");
    477 
    478 	/* repack keys */
    479 	for (i = 0; i < helper->nkeyblobs; i++) {
    480 		if (sshbuf_equals(keyblob, helper->keyblobs[i]) == 0) {
    481 			if (found)
    482 				fatal_f("key recorded more than once");
    483 			found = 1;
    484 		} else if (found)
    485 			helper->keyblobs[i - 1] = helper->keyblobs[i];
    486 	}
    487 	if (found) {
    488 		helper->keyblobs = xrecallocarray(helper->keyblobs,
    489 		    helper->nkeyblobs, helper->nkeyblobs - 1,
    490 		    sizeof(*helper->keyblobs));
    491 		helper->nkeyblobs--;
    492 	}
    493 	if (helper->nkeyblobs == 0)
    494 		helper_terminate(helper);
    495 }
    496