1 1.3 christos /* $NetBSD: ssh-sk-client.c,v 1.8 2025/04/09 15:49:33 christos Exp $ */ 2 1.7 christos /* $OpenBSD: ssh-sk-client.c,v 1.13 2025/02/18 08:02:48 djm Exp $ */ 3 1.1 christos /* 4 1.1 christos * Copyright (c) 2019 Google LLC 5 1.1 christos * 6 1.1 christos * Permission to use, copy, modify, and distribute this software for any 7 1.1 christos * purpose with or without fee is hereby granted, provided that the above 8 1.1 christos * copyright notice and this permission notice appear in all copies. 9 1.1 christos * 10 1.1 christos * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 1.1 christos * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 1.1 christos * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 1.1 christos * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 1.1 christos * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 1.1 christos * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 1.1 christos * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 1.1 christos */ 18 1.2 christos #include "includes.h" 19 1.3 christos __RCSID("$NetBSD: ssh-sk-client.c,v 1.8 2025/04/09 15:49:33 christos Exp $"); 20 1.1 christos 21 1.1 christos #include <sys/types.h> 22 1.1 christos #include <sys/socket.h> 23 1.1 christos #include <sys/wait.h> 24 1.1 christos 25 1.1 christos #include <fcntl.h> 26 1.1 christos #include <limits.h> 27 1.1 christos #include <errno.h> 28 1.1 christos #include <signal.h> 29 1.1 christos #include <stdarg.h> 30 1.1 christos #include <stdio.h> 31 1.1 christos #include <stdlib.h> 32 1.1 christos #include <string.h> 33 1.1 christos #include <unistd.h> 34 1.1 christos 35 1.1 christos #include "log.h" 36 1.1 christos #include "ssherr.h" 37 1.1 christos #include "sshbuf.h" 38 1.1 christos #include "sshkey.h" 39 1.1 christos #include "msg.h" 40 1.1 christos #include "digest.h" 41 1.1 christos #include "pathnames.h" 42 1.1 christos #include "ssh-sk.h" 43 1.1 christos #include "misc.h" 44 1.1 christos 45 1.1 christos /* #define DEBUG_SK 1 */ 46 1.1 christos 47 1.1 christos static int 48 1.1 christos start_helper(int *fdp, pid_t *pidp, void (**osigchldp)(int)) 49 1.1 christos { 50 1.1 christos void (*osigchld)(int); 51 1.4 christos int oerrno, pair[2]; 52 1.1 christos pid_t pid; 53 1.2 christos const char *helper, *verbosity = NULL; 54 1.1 christos 55 1.1 christos *fdp = -1; 56 1.1 christos *pidp = 0; 57 1.1 christos *osigchldp = SIG_DFL; 58 1.1 christos 59 1.1 christos helper = getenv("SSH_SK_HELPER"); 60 1.1 christos if (helper == NULL || strlen(helper) == 0) 61 1.1 christos helper = _PATH_SSH_SK_HELPER; 62 1.1 christos if (access(helper, X_OK) != 0) { 63 1.1 christos oerrno = errno; 64 1.4 christos error_f("helper \"%s\" unusable: %s", helper, strerror(errno)); 65 1.1 christos errno = oerrno; 66 1.1 christos return SSH_ERR_SYSTEM_ERROR; 67 1.1 christos } 68 1.1 christos #ifdef DEBUG_SK 69 1.1 christos verbosity = "-vvv"; 70 1.1 christos #endif 71 1.1 christos 72 1.1 christos /* Start helper */ 73 1.1 christos if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == -1) { 74 1.1 christos error("socketpair: %s", strerror(errno)); 75 1.1 christos return SSH_ERR_SYSTEM_ERROR; 76 1.1 christos } 77 1.1 christos osigchld = ssh_signal(SIGCHLD, SIG_DFL); 78 1.1 christos if ((pid = fork()) == -1) { 79 1.1 christos oerrno = errno; 80 1.1 christos error("fork: %s", strerror(errno)); 81 1.1 christos close(pair[0]); 82 1.1 christos close(pair[1]); 83 1.1 christos ssh_signal(SIGCHLD, osigchld); 84 1.1 christos errno = oerrno; 85 1.1 christos return SSH_ERR_SYSTEM_ERROR; 86 1.1 christos } 87 1.1 christos if (pid == 0) { 88 1.1 christos if ((dup2(pair[1], STDIN_FILENO) == -1) || 89 1.1 christos (dup2(pair[1], STDOUT_FILENO) == -1)) { 90 1.4 christos error_f("dup2: %s", strerror(errno)); 91 1.1 christos _exit(1); 92 1.1 christos } 93 1.1 christos close(pair[0]); 94 1.1 christos close(pair[1]); 95 1.1 christos closefrom(STDERR_FILENO + 1); 96 1.4 christos debug_f("starting %s %s", helper, 97 1.1 christos verbosity == NULL ? "" : verbosity); 98 1.1 christos execlp(helper, helper, verbosity, (char *)NULL); 99 1.4 christos error_f("execlp: %s", strerror(errno)); 100 1.1 christos _exit(1); 101 1.1 christos } 102 1.1 christos close(pair[1]); 103 1.1 christos 104 1.1 christos /* success */ 105 1.4 christos debug3_f("started pid=%ld", (long)pid); 106 1.1 christos *fdp = pair[0]; 107 1.1 christos *pidp = pid; 108 1.1 christos *osigchldp = osigchld; 109 1.1 christos return 0; 110 1.1 christos } 111 1.1 christos 112 1.1 christos static int 113 1.1 christos reap_helper(pid_t pid) 114 1.1 christos { 115 1.1 christos int status, oerrno; 116 1.1 christos 117 1.4 christos debug3_f("pid=%ld", (long)pid); 118 1.1 christos 119 1.1 christos errno = 0; 120 1.1 christos while (waitpid(pid, &status, 0) == -1) { 121 1.1 christos if (errno == EINTR) { 122 1.1 christos errno = 0; 123 1.1 christos continue; 124 1.1 christos } 125 1.1 christos oerrno = errno; 126 1.4 christos error_f("waitpid: %s", strerror(errno)); 127 1.1 christos errno = oerrno; 128 1.1 christos return SSH_ERR_SYSTEM_ERROR; 129 1.1 christos } 130 1.1 christos if (!WIFEXITED(status)) { 131 1.4 christos error_f("helper exited abnormally"); 132 1.1 christos return SSH_ERR_AGENT_FAILURE; 133 1.1 christos } else if (WEXITSTATUS(status) != 0) { 134 1.4 christos error_f("helper exited with non-zero exit status"); 135 1.1 christos return SSH_ERR_AGENT_FAILURE; 136 1.1 christos } 137 1.1 christos return 0; 138 1.1 christos } 139 1.1 christos 140 1.1 christos static int 141 1.1 christos client_converse(struct sshbuf *msg, struct sshbuf **respp, u_int type) 142 1.1 christos { 143 1.1 christos int oerrno, fd, r2, ll, r = SSH_ERR_INTERNAL_ERROR; 144 1.1 christos u_int rtype, rerr; 145 1.1 christos pid_t pid; 146 1.1 christos u_char version; 147 1.1 christos void (*osigchld)(int); 148 1.1 christos struct sshbuf *req = NULL, *resp = NULL; 149 1.1 christos *respp = NULL; 150 1.1 christos 151 1.1 christos if ((r = start_helper(&fd, &pid, &osigchld)) != 0) 152 1.1 christos return r; 153 1.1 christos 154 1.1 christos if ((req = sshbuf_new()) == NULL || (resp = sshbuf_new()) == NULL) { 155 1.1 christos r = SSH_ERR_ALLOC_FAIL; 156 1.1 christos goto out; 157 1.1 christos } 158 1.1 christos /* Request preamble: type, log_on_stderr, log_level */ 159 1.1 christos ll = log_level_get(); 160 1.1 christos if ((r = sshbuf_put_u32(req, type)) != 0 || 161 1.5 christos (r = sshbuf_put_u8(req, log_is_on_stderr() != 0)) != 0 || 162 1.5 christos (r = sshbuf_put_u32(req, (uint32_t)(ll < 0 ? 0 : ll))) != 0 || 163 1.5 christos (r = sshbuf_putb(req, msg)) != 0) { 164 1.4 christos error_fr(r, "compose"); 165 1.1 christos goto out; 166 1.1 christos } 167 1.1 christos if ((r = ssh_msg_send(fd, SSH_SK_HELPER_VERSION, req)) != 0) { 168 1.4 christos error_fr(r, "send"); 169 1.1 christos goto out; 170 1.1 christos } 171 1.1 christos if ((r = ssh_msg_recv(fd, resp)) != 0) { 172 1.4 christos error_fr(r, "receive"); 173 1.1 christos goto out; 174 1.1 christos } 175 1.1 christos if ((r = sshbuf_get_u8(resp, &version)) != 0) { 176 1.4 christos error_fr(r, "parse version"); 177 1.1 christos goto out; 178 1.1 christos } 179 1.1 christos if (version != SSH_SK_HELPER_VERSION) { 180 1.4 christos error_f("unsupported version: got %u, expected %u", 181 1.4 christos version, SSH_SK_HELPER_VERSION); 182 1.1 christos r = SSH_ERR_INVALID_FORMAT; 183 1.1 christos goto out; 184 1.1 christos } 185 1.1 christos if ((r = sshbuf_get_u32(resp, &rtype)) != 0) { 186 1.4 christos error_fr(r, "parse message type"); 187 1.1 christos goto out; 188 1.1 christos } 189 1.1 christos if (rtype == SSH_SK_HELPER_ERROR) { 190 1.1 christos if ((r = sshbuf_get_u32(resp, &rerr)) != 0) { 191 1.4 christos error_fr(r, "parse"); 192 1.1 christos goto out; 193 1.1 christos } 194 1.4 christos debug_f("helper returned error -%u", rerr); 195 1.1 christos /* OpenSSH error values are negative; encoded as -err on wire */ 196 1.1 christos if (rerr == 0 || rerr >= INT_MAX) 197 1.1 christos r = SSH_ERR_INTERNAL_ERROR; 198 1.1 christos else 199 1.1 christos r = -(int)rerr; 200 1.1 christos goto out; 201 1.1 christos } else if (rtype != type) { 202 1.4 christos error_f("helper returned incorrect message type %u, " 203 1.4 christos "expecting %u", rtype, type); 204 1.1 christos r = SSH_ERR_INTERNAL_ERROR; 205 1.1 christos goto out; 206 1.1 christos } 207 1.1 christos /* success */ 208 1.1 christos r = 0; 209 1.1 christos out: 210 1.1 christos oerrno = errno; 211 1.1 christos close(fd); 212 1.1 christos if ((r2 = reap_helper(pid)) != 0) { 213 1.1 christos if (r == 0) { 214 1.1 christos r = r2; 215 1.1 christos oerrno = errno; 216 1.1 christos } 217 1.1 christos } 218 1.1 christos if (r == 0) { 219 1.1 christos *respp = resp; 220 1.1 christos resp = NULL; 221 1.1 christos } 222 1.1 christos sshbuf_free(req); 223 1.1 christos sshbuf_free(resp); 224 1.1 christos ssh_signal(SIGCHLD, osigchld); 225 1.1 christos errno = oerrno; 226 1.1 christos return r; 227 1.1 christos 228 1.1 christos } 229 1.1 christos 230 1.1 christos int 231 1.1 christos sshsk_sign(const char *provider, struct sshkey *key, 232 1.1 christos u_char **sigp, size_t *lenp, const u_char *data, size_t datalen, 233 1.1 christos u_int compat, const char *pin) 234 1.1 christos { 235 1.1 christos int oerrno, r = SSH_ERR_INTERNAL_ERROR; 236 1.1 christos struct sshbuf *kbuf = NULL, *req = NULL, *resp = NULL; 237 1.1 christos 238 1.1 christos *sigp = NULL; 239 1.1 christos *lenp = 0; 240 1.1 christos 241 1.1 christos if ((kbuf = sshbuf_new()) == NULL || 242 1.1 christos (req = sshbuf_new()) == NULL) { 243 1.1 christos r = SSH_ERR_ALLOC_FAIL; 244 1.1 christos goto out; 245 1.1 christos } 246 1.1 christos 247 1.1 christos if ((r = sshkey_private_serialize(key, kbuf)) != 0) { 248 1.4 christos error_fr(r, "encode key"); 249 1.1 christos goto out; 250 1.1 christos } 251 1.1 christos if ((r = sshbuf_put_stringb(req, kbuf)) != 0 || 252 1.1 christos (r = sshbuf_put_cstring(req, provider)) != 0 || 253 1.1 christos (r = sshbuf_put_string(req, data, datalen)) != 0 || 254 1.1 christos (r = sshbuf_put_cstring(req, NULL)) != 0 || /* alg */ 255 1.1 christos (r = sshbuf_put_u32(req, compat)) != 0 || 256 1.1 christos (r = sshbuf_put_cstring(req, pin)) != 0) { 257 1.4 christos error_fr(r, "compose"); 258 1.1 christos goto out; 259 1.1 christos } 260 1.1 christos 261 1.1 christos if ((r = client_converse(req, &resp, SSH_SK_HELPER_SIGN)) != 0) 262 1.1 christos goto out; 263 1.1 christos 264 1.1 christos if ((r = sshbuf_get_string(resp, sigp, lenp)) != 0) { 265 1.4 christos error_fr(r, "parse signature"); 266 1.1 christos r = SSH_ERR_INVALID_FORMAT; 267 1.1 christos goto out; 268 1.1 christos } 269 1.1 christos if (sshbuf_len(resp) != 0) { 270 1.4 christos error_f("trailing data in response"); 271 1.1 christos r = SSH_ERR_INVALID_FORMAT; 272 1.1 christos goto out; 273 1.1 christos } 274 1.1 christos /* success */ 275 1.1 christos r = 0; 276 1.1 christos out: 277 1.1 christos oerrno = errno; 278 1.1 christos if (r != 0) { 279 1.1 christos freezero(*sigp, *lenp); 280 1.1 christos *sigp = NULL; 281 1.1 christos *lenp = 0; 282 1.1 christos } 283 1.1 christos sshbuf_free(kbuf); 284 1.1 christos sshbuf_free(req); 285 1.1 christos sshbuf_free(resp); 286 1.1 christos errno = oerrno; 287 1.1 christos return r; 288 1.1 christos } 289 1.1 christos 290 1.1 christos int 291 1.1 christos sshsk_enroll(int type, const char *provider_path, const char *device, 292 1.1 christos const char *application, const char *userid, uint8_t flags, 293 1.1 christos const char *pin, struct sshbuf *challenge_buf, 294 1.1 christos struct sshkey **keyp, struct sshbuf *attest) 295 1.1 christos { 296 1.1 christos int oerrno, r = SSH_ERR_INTERNAL_ERROR; 297 1.1 christos struct sshbuf *kbuf = NULL, *abuf = NULL, *req = NULL, *resp = NULL; 298 1.1 christos struct sshkey *key = NULL; 299 1.1 christos 300 1.1 christos *keyp = NULL; 301 1.1 christos if (attest != NULL) 302 1.1 christos sshbuf_reset(attest); 303 1.1 christos 304 1.1 christos if (type < 0) 305 1.1 christos return SSH_ERR_INVALID_ARGUMENT; 306 1.1 christos 307 1.1 christos if ((abuf = sshbuf_new()) == NULL || 308 1.1 christos (kbuf = sshbuf_new()) == NULL || 309 1.1 christos (req = sshbuf_new()) == NULL) { 310 1.1 christos r = SSH_ERR_ALLOC_FAIL; 311 1.1 christos goto out; 312 1.1 christos } 313 1.1 christos 314 1.1 christos if ((r = sshbuf_put_u32(req, (u_int)type)) != 0 || 315 1.1 christos (r = sshbuf_put_cstring(req, provider_path)) != 0 || 316 1.1 christos (r = sshbuf_put_cstring(req, device)) != 0 || 317 1.1 christos (r = sshbuf_put_cstring(req, application)) != 0 || 318 1.1 christos (r = sshbuf_put_cstring(req, userid)) != 0 || 319 1.1 christos (r = sshbuf_put_u8(req, flags)) != 0 || 320 1.1 christos (r = sshbuf_put_cstring(req, pin)) != 0 || 321 1.1 christos (r = sshbuf_put_stringb(req, challenge_buf)) != 0) { 322 1.4 christos error_fr(r, "compose"); 323 1.1 christos goto out; 324 1.1 christos } 325 1.1 christos 326 1.1 christos if ((r = client_converse(req, &resp, SSH_SK_HELPER_ENROLL)) != 0) 327 1.1 christos goto out; 328 1.1 christos 329 1.1 christos if ((r = sshbuf_get_stringb(resp, kbuf)) != 0 || 330 1.1 christos (r = sshbuf_get_stringb(resp, abuf)) != 0) { 331 1.4 christos error_fr(r, "parse"); 332 1.1 christos r = SSH_ERR_INVALID_FORMAT; 333 1.1 christos goto out; 334 1.1 christos } 335 1.1 christos if (sshbuf_len(resp) != 0) { 336 1.4 christos error_f("trailing data in response"); 337 1.1 christos r = SSH_ERR_INVALID_FORMAT; 338 1.1 christos goto out; 339 1.1 christos } 340 1.1 christos if ((r = sshkey_private_deserialize(kbuf, &key)) != 0) { 341 1.4 christos error_fr(r, "encode"); 342 1.1 christos goto out; 343 1.1 christos } 344 1.1 christos if (attest != NULL && (r = sshbuf_putb(attest, abuf)) != 0) { 345 1.4 christos error_fr(r, "encode attestation information"); 346 1.1 christos goto out; 347 1.1 christos } 348 1.1 christos 349 1.1 christos /* success */ 350 1.1 christos r = 0; 351 1.1 christos *keyp = key; 352 1.1 christos key = NULL; 353 1.1 christos out: 354 1.1 christos oerrno = errno; 355 1.1 christos sshkey_free(key); 356 1.1 christos sshbuf_free(kbuf); 357 1.1 christos sshbuf_free(abuf); 358 1.1 christos sshbuf_free(req); 359 1.1 christos sshbuf_free(resp); 360 1.1 christos errno = oerrno; 361 1.1 christos return r; 362 1.1 christos } 363 1.1 christos 364 1.6 christos static void 365 1.6 christos sshsk_free_resident_key(struct sshsk_resident_key *srk) 366 1.6 christos { 367 1.6 christos if (srk == NULL) 368 1.6 christos return; 369 1.6 christos sshkey_free(srk->key); 370 1.6 christos freezero(srk->user_id, srk->user_id_len); 371 1.6 christos free(srk); 372 1.6 christos } 373 1.6 christos 374 1.6 christos 375 1.6 christos void 376 1.6 christos sshsk_free_resident_keys(struct sshsk_resident_key **srks, size_t nsrks) 377 1.6 christos { 378 1.6 christos size_t i; 379 1.6 christos 380 1.6 christos if (srks == NULL || nsrks == 0) 381 1.6 christos return; 382 1.6 christos 383 1.6 christos for (i = 0; i < nsrks; i++) 384 1.6 christos sshsk_free_resident_key(srks[i]); 385 1.6 christos free(srks); 386 1.6 christos } 387 1.6 christos 388 1.1 christos int 389 1.1 christos sshsk_load_resident(const char *provider_path, const char *device, 390 1.6 christos const char *pin, u_int flags, struct sshsk_resident_key ***srksp, 391 1.6 christos size_t *nsrksp) 392 1.1 christos { 393 1.1 christos int oerrno, r = SSH_ERR_INTERNAL_ERROR; 394 1.1 christos struct sshbuf *kbuf = NULL, *req = NULL, *resp = NULL; 395 1.6 christos struct sshkey *key = NULL; 396 1.6 christos struct sshsk_resident_key *srk = NULL, **srks = NULL, **tmp; 397 1.6 christos u_char *userid = NULL; 398 1.6 christos size_t userid_len = 0, nsrks = 0; 399 1.1 christos 400 1.6 christos *srksp = NULL; 401 1.6 christos *nsrksp = 0; 402 1.1 christos 403 1.6 christos if ((kbuf = sshbuf_new()) == NULL || 404 1.1 christos (req = sshbuf_new()) == NULL) { 405 1.1 christos r = SSH_ERR_ALLOC_FAIL; 406 1.1 christos goto out; 407 1.1 christos } 408 1.1 christos 409 1.1 christos if ((r = sshbuf_put_cstring(req, provider_path)) != 0 || 410 1.1 christos (r = sshbuf_put_cstring(req, device)) != 0 || 411 1.6 christos (r = sshbuf_put_cstring(req, pin)) != 0 || 412 1.6 christos (r = sshbuf_put_u32(req, flags)) != 0) { 413 1.4 christos error_fr(r, "compose"); 414 1.1 christos goto out; 415 1.1 christos } 416 1.1 christos 417 1.1 christos if ((r = client_converse(req, &resp, SSH_SK_HELPER_LOAD_RESIDENT)) != 0) 418 1.1 christos goto out; 419 1.1 christos 420 1.1 christos while (sshbuf_len(resp) != 0) { 421 1.6 christos /* key, comment, user_id */ 422 1.1 christos if ((r = sshbuf_get_stringb(resp, kbuf)) != 0 || 423 1.6 christos (r = sshbuf_get_cstring(resp, NULL, NULL)) != 0 || 424 1.6 christos (r = sshbuf_get_string(resp, &userid, &userid_len)) != 0) { 425 1.6 christos error_fr(r, "parse"); 426 1.1 christos r = SSH_ERR_INVALID_FORMAT; 427 1.1 christos goto out; 428 1.1 christos } 429 1.1 christos if ((r = sshkey_private_deserialize(kbuf, &key)) != 0) { 430 1.4 christos error_fr(r, "decode key"); 431 1.1 christos goto out; 432 1.1 christos } 433 1.6 christos if ((srk = calloc(1, sizeof(*srk))) == NULL) { 434 1.6 christos error_f("calloc failed"); 435 1.7 christos r = SSH_ERR_ALLOC_FAIL; 436 1.6 christos goto out; 437 1.6 christos } 438 1.6 christos srk->key = key; 439 1.6 christos key = NULL; 440 1.6 christos srk->user_id = userid; 441 1.6 christos srk->user_id_len = userid_len; 442 1.6 christos userid = NULL; 443 1.6 christos userid_len = 0; 444 1.6 christos if ((tmp = recallocarray(srks, nsrks, nsrks + 1, 445 1.6 christos sizeof(*srks))) == NULL) { 446 1.4 christos error_f("recallocarray keys failed"); 447 1.7 christos r = SSH_ERR_ALLOC_FAIL; 448 1.1 christos goto out; 449 1.1 christos } 450 1.6 christos debug_f("srks[%zu]: %s %s uidlen %zu", nsrks, 451 1.6 christos sshkey_type(srk->key), srk->key->sk_application, 452 1.6 christos srk->user_id_len); 453 1.6 christos srks = tmp; 454 1.6 christos srks[nsrks++] = srk; 455 1.6 christos srk = NULL; 456 1.1 christos } 457 1.1 christos 458 1.1 christos /* success */ 459 1.1 christos r = 0; 460 1.6 christos *srksp = srks; 461 1.6 christos *nsrksp = nsrks; 462 1.6 christos srks = NULL; 463 1.6 christos nsrks = 0; 464 1.1 christos out: 465 1.1 christos oerrno = errno; 466 1.6 christos sshsk_free_resident_key(srk); 467 1.6 christos sshsk_free_resident_keys(srks, nsrks); 468 1.6 christos freezero(userid, userid_len); 469 1.1 christos sshkey_free(key); 470 1.1 christos sshbuf_free(kbuf); 471 1.1 christos sshbuf_free(req); 472 1.1 christos sshbuf_free(resp); 473 1.1 christos errno = oerrno; 474 1.1 christos return r; 475 1.1 christos } 476