1 /* $NetBSD: auth2-pubkeyfile.c,v 1.5 2026/04/08 18:58:40 christos Exp $ */ 2 /* $OpenBSD: auth2-pubkeyfile.c,v 1.8 2026/04/02 07:48:13 djm Exp $ */ 3 4 /* 5 * Copyright (c) 2000 Markus Friedl. All rights reserved. 6 * Copyright (c) 2010 Damien Miller. All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 #include "includes.h" 30 __RCSID("$NetBSD: auth2-pubkeyfile.c,v 1.5 2026/04/08 18:58:40 christos Exp $"); 31 32 #include <sys/types.h> 33 #include <sys/stat.h> 34 35 #include <stdlib.h> 36 #include <errno.h> 37 #include <fcntl.h> 38 #include <pwd.h> 39 #include <stdio.h> 40 #include <stdarg.h> 41 #include <string.h> 42 #include <time.h> 43 #include <unistd.h> 44 45 #include "ssh.h" 46 #include "log.h" 47 #include "misc.h" 48 #include "sshkey.h" 49 #include "digest.h" 50 #include "hostfile.h" 51 #include "auth.h" 52 #include "auth-options.h" 53 #include "authfile.h" 54 #include "match.h" 55 #include "ssherr.h" 56 #include "xmalloc.h" 57 58 #ifdef WITH_LDAP_PUBKEY 59 #include "servconf.h" 60 #include "uidswap.h" 61 #include "ldapauth.h" 62 63 extern ServerOptions options; 64 #endif 65 66 int 67 auth_authorise_keyopts(struct passwd *pw, struct sshauthopt *opts, 68 int allow_cert_authority, const char *remote_ip, const char *remote_host, 69 const char *loc) 70 { 71 time_t now = time(NULL); 72 char buf[64]; 73 74 /* 75 * Check keys/principals file expiry time. 76 * NB. validity interval in certificate is handled elsewhere. 77 */ 78 if (opts->valid_before && now > 0 && 79 opts->valid_before < (uint64_t)now) { 80 format_absolute_time(opts->valid_before, buf, sizeof(buf)); 81 debug("%s: entry expired at %s", loc, buf); 82 auth_debug_add("%s: entry expired at %s", loc, buf); 83 return -1; 84 } 85 /* Consistency checks */ 86 if (opts->cert_principals != NULL && !opts->cert_authority) { 87 debug("%s: principals on non-CA key", loc); 88 auth_debug_add("%s: principals on non-CA key", loc); 89 /* deny access */ 90 return -1; 91 } 92 /* cert-authority flag isn't valid in authorized_principals files */ 93 if (!allow_cert_authority && opts->cert_authority) { 94 debug("%s: cert-authority flag invalid here", loc); 95 auth_debug_add("%s: cert-authority flag invalid here", loc); 96 /* deny access */ 97 return -1; 98 } 99 100 /* Perform from= checks */ 101 if (opts->required_from_host_keys != NULL) { 102 switch (match_host_and_ip(remote_host, remote_ip, 103 opts->required_from_host_keys )) { 104 case 1: 105 /* Host name matches. */ 106 break; 107 case -1: 108 default: 109 debug("%s: invalid from criteria", loc); 110 auth_debug_add("%s: invalid from criteria", loc); 111 /* FALLTHROUGH */ 112 case 0: 113 logit("%s: Authentication tried for %.100s with " 114 "correct key but not from a permitted " 115 "host (host=%.200s, ip=%.200s, required=%.200s).", 116 loc, pw->pw_name, remote_host, remote_ip, 117 opts->required_from_host_keys); 118 auth_debug_add("%s: Your host '%.200s' is not " 119 "permitted to use this key for login.", 120 loc, remote_host); 121 /* deny access */ 122 return -1; 123 } 124 } 125 /* Check source-address restriction from certificate */ 126 if (opts->required_from_host_cert != NULL) { 127 switch (addr_match_cidr_list(remote_ip, 128 opts->required_from_host_cert)) { 129 case 1: 130 /* accepted */ 131 break; 132 case -1: 133 default: 134 /* invalid */ 135 error("%s: Certificate source-address invalid", loc); 136 /* FALLTHROUGH */ 137 case 0: 138 logit("%s: Authentication tried for %.100s with valid " 139 "certificate but not from a permitted source " 140 "address (%.200s).", loc, pw->pw_name, remote_ip); 141 auth_debug_add("%s: Your address '%.200s' is not " 142 "permitted to use this certificate for login.", 143 loc, remote_ip); 144 return -1; 145 } 146 } 147 /* 148 * 149 * XXX this is spammy. We should report remotely only for keys 150 * that are successful in actual auth attempts, and not PK_OK 151 * tests. 152 */ 153 auth_log_authopts(loc, opts, 1); 154 155 return 0; 156 } 157 158 static int 159 match_principals_option(const char *principal_list, struct sshkey_cert *cert) 160 { 161 char *list, *olist, *entry; 162 u_int i; 163 164 olist = list = xstrdup(principal_list); 165 for (;;) { 166 if ((entry = strsep(&list, ",")) == NULL || *entry == '\0') 167 break; 168 for (i = 0; i < cert->nprincipals; i++) { 169 if (strcmp(entry, cert->principals[i]) == 0) { 170 debug3("matched principal from key i" 171 "options \"%.100s\"", entry); 172 free(olist); 173 return 1; 174 } 175 } 176 } 177 free(olist); 178 return 0; 179 } 180 181 /* 182 * Process a single authorized_principals format line. Returns 0 and sets 183 * authoptsp is principal is authorised, -1 otherwise. "loc" is used as a 184 * log preamble for file/line information. 185 */ 186 int 187 auth_check_principals_line(char *cp, const struct sshkey_cert *cert, 188 const char *loc, struct sshauthopt **authoptsp) 189 { 190 u_int i, found = 0; 191 char *ep, *line_opts; 192 const char *reason = NULL; 193 struct sshauthopt *opts = NULL; 194 195 if (authoptsp != NULL) 196 *authoptsp = NULL; 197 198 /* Trim trailing whitespace. */ 199 ep = cp + strlen(cp) - 1; 200 while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t')) 201 *ep-- = '\0'; 202 203 /* 204 * If the line has internal whitespace then assume it has 205 * key options. 206 */ 207 line_opts = NULL; 208 if ((ep = strrchr(cp, ' ')) != NULL || 209 (ep = strrchr(cp, '\t')) != NULL) { 210 for (; *ep == ' ' || *ep == '\t'; ep++) 211 ; 212 line_opts = cp; 213 cp = ep; 214 } 215 if ((opts = sshauthopt_parse(line_opts, &reason)) == NULL) { 216 debug("%s: bad principals options: %s", loc, reason); 217 auth_debug_add("%s: bad principals options: %s", loc, reason); 218 return -1; 219 } 220 /* Check principals in cert against those on line */ 221 for (i = 0; i < cert->nprincipals; i++) { 222 if (strcmp(cp, cert->principals[i]) != 0) 223 continue; 224 debug3("%s: matched principal \"%.100s\"", 225 loc, cert->principals[i]); 226 found = 1; 227 } 228 if (found && authoptsp != NULL) { 229 *authoptsp = opts; 230 opts = NULL; 231 } 232 sshauthopt_free(opts); 233 return found ? 0 : -1; 234 } 235 236 int 237 auth_process_principals(FILE *f, const char *file, 238 const struct sshkey_cert *cert, struct sshauthopt **authoptsp) 239 { 240 char loc[256], *line = NULL, *cp, *ep; 241 size_t linesize = 0; 242 u_long linenum = 0, nonblank = 0; 243 u_int found_principal = 0; 244 245 if (authoptsp != NULL) 246 *authoptsp = NULL; 247 248 while (getline(&line, &linesize, f) != -1) { 249 linenum++; 250 /* Always consume entire input */ 251 if (found_principal) 252 continue; 253 254 /* Skip leading whitespace. */ 255 for (cp = line; *cp == ' ' || *cp == '\t'; cp++) 256 ; 257 /* Skip blank and comment lines. */ 258 if ((ep = strchr(cp, '#')) != NULL) 259 *ep = '\0'; 260 if (!*cp || *cp == '\n') 261 continue; 262 263 nonblank++; 264 snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum); 265 if (auth_check_principals_line(cp, cert, loc, authoptsp) == 0) 266 found_principal = 1; 267 } 268 debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum); 269 free(line); 270 return found_principal; 271 } 272 273 /* 274 * Check a single line of an authorized_keys-format file. Returns 0 if key 275 * matches, -1 otherwise. Will return key/cert options via *authoptsp 276 * on success. "loc" is used as file/line location in log messages. 277 */ 278 int 279 auth_check_authkey_line(struct passwd *pw, struct sshkey *key, 280 char *cp, const char *remote_ip, const char *remote_host, const char *loc, 281 struct sshauthopt **authoptsp) 282 { 283 int want_keytype = sshkey_is_cert(key) ? KEY_UNSPEC : key->type; 284 struct sshkey *found = NULL; 285 struct sshauthopt *keyopts = NULL, *certopts = NULL, *finalopts = NULL; 286 char *key_options = NULL, *fp = NULL; 287 const char *reason = NULL; 288 int ret = -1; 289 290 if (authoptsp != NULL) 291 *authoptsp = NULL; 292 293 if ((found = sshkey_new(want_keytype)) == NULL) { 294 debug3_f("keytype %d failed", want_keytype); 295 goto out; 296 } 297 298 /* XXX djm: peek at key type in line and skip if unwanted */ 299 300 if (sshkey_read(found, &cp) != 0) { 301 /* no key? check for options */ 302 debug2("%s: check options: '%s'", loc, cp); 303 key_options = cp; 304 if (sshkey_advance_past_options(&cp) != 0) { 305 reason = "invalid key option string"; 306 goto fail_reason; 307 } 308 skip_space(&cp); 309 if (sshkey_read(found, &cp) != 0) { 310 /* still no key? advance to next line*/ 311 debug2("%s: advance: '%s'", loc, cp); 312 goto out; 313 } 314 } 315 /* Parse key options now; we need to know if this is a CA key */ 316 if ((keyopts = sshauthopt_parse(key_options, &reason)) == NULL) { 317 debug("%s: bad key options: %s", loc, reason); 318 auth_debug_add("%s: bad key options: %s", loc, reason); 319 goto out; 320 } 321 /* Ignore keys that don't match or incorrectly marked as CAs */ 322 if (sshkey_is_cert(key)) { 323 /* Certificate; check signature key against CA */ 324 if (!sshkey_equal(found, key->cert->signature_key) || 325 !keyopts->cert_authority) 326 goto out; 327 } else { 328 /* Plain key: check it against key found in file */ 329 if (!sshkey_equal(found, key) || keyopts->cert_authority) 330 goto out; 331 } 332 333 /* We have a candidate key, perform authorisation checks */ 334 if ((fp = sshkey_fingerprint(found, 335 SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL) 336 fatal_f("fingerprint failed"); 337 338 debug("%s: matching %s found: %s %s", loc, 339 sshkey_is_cert(key) ? "CA" : "key", sshkey_type(found), fp); 340 341 if (auth_authorise_keyopts(pw, keyopts, 342 sshkey_is_cert(key), remote_ip, remote_host, loc) != 0) { 343 reason = "Refused by key options"; 344 goto fail_reason; 345 } 346 /* That's all we need for plain keys. */ 347 if (!sshkey_is_cert(key)) { 348 verbose("Accepted key %s %s found at %s", 349 sshkey_type(found), fp, loc); 350 finalopts = keyopts; 351 keyopts = NULL; 352 goto success; 353 } 354 355 /* 356 * Additional authorisation for certificates. 357 */ 358 359 /* Parse and check options present in certificate */ 360 if ((certopts = sshauthopt_from_cert(key)) == NULL) { 361 reason = "Invalid certificate options"; 362 goto cert_fail_reason; 363 } 364 if (auth_authorise_keyopts(pw, certopts, 0, 365 remote_ip, remote_host, loc) != 0) { 366 reason = "Refused by certificate options"; 367 goto cert_fail_reason; 368 } 369 if ((finalopts = sshauthopt_merge(keyopts, certopts, &reason)) == NULL) 370 goto cert_fail_reason; 371 372 /* 373 * If the user has specified a list of principals as 374 * a key option, then prefer that list to matching 375 * their username in the certificate principals list. 376 */ 377 if (keyopts->cert_principals != NULL && 378 !match_principals_option(keyopts->cert_principals, key->cert)) { 379 reason = "Certificate does not contain an authorized principal"; 380 goto cert_fail_reason; 381 } 382 if (sshkey_cert_check_authority_now(key, 0, 0, 383 keyopts->cert_principals == NULL ? pw->pw_name : NULL, 384 &reason) != 0) 385 goto cert_fail_reason; 386 387 verbose("Accepted certificate ID \"%s\" (serial %llu) " 388 "signed by CA %s %s found at %s", 389 key->cert->key_id, 390 (unsigned long long)key->cert->serial, 391 sshkey_type(found), fp, loc); 392 393 success: 394 if (finalopts == NULL) 395 fatal_f("internal error: missing options"); 396 if (authoptsp != NULL) { 397 *authoptsp = finalopts; 398 finalopts = NULL; 399 } 400 /* success */ 401 ret = 0; 402 goto out; 403 404 cert_fail_reason: 405 error("Refusing certificate ID \"%s\" serial=%llu " 406 "signed by %s CA %s via %s: %s", key->cert->key_id, 407 (unsigned long long)key->cert->serial, 408 sshkey_type(key->cert->signature_key), fp, loc, reason); 409 auth_debug_add("Refused Certificate ID \"%s\" serial=%llu: %s", 410 key->cert->key_id, (unsigned long long)key->cert->serial, reason); 411 goto out; 412 413 fail_reason: 414 error("%s at %s", reason, loc); 415 auth_debug_add("%s", reason); 416 out: 417 free(fp); 418 sshauthopt_free(keyopts); 419 sshauthopt_free(certopts); 420 sshauthopt_free(finalopts); 421 sshkey_free(found); 422 return ret; 423 } 424 425 /* 426 * Checks whether key is allowed in authorized_keys-format file, 427 * returns 1 if the key is allowed or 0 otherwise. 428 */ 429 int 430 auth_check_authkeys_file(struct passwd *pw, FILE *f, char *file, 431 struct sshkey *key, const char *remote_ip, 432 const char *remote_host, struct sshauthopt **authoptsp) 433 { 434 char *cp, *line = NULL, loc[256]; 435 size_t linesize = 0; 436 int found_key = 0; 437 u_long linenum = 0, nonblank = 0; 438 #ifdef WITH_LDAP_PUBKEY 439 struct sshauthopt *opts = NULL; 440 struct sshkey *found = NULL; 441 ldap_key_t * k; 442 unsigned int i = 0; 443 const char *reason; 444 445 found_key = 0; 446 /* allocate a new key type */ 447 found = sshkey_new(key->type); 448 449 /* first check if the options is enabled, then try.. */ 450 if (options.lpk.on) { 451 debug("[LDAP] trying LDAP first uid=%s",pw->pw_name); 452 if (ldap_ismember(&options.lpk, pw->pw_name) > 0) { 453 if ((k = ldap_getuserkey(&options.lpk, pw->pw_name)) != NULL) { 454 /* Skip leading whitespace, empty and comment lines. */ 455 for (i = 0 ; i < k->num ; i++) { 456 /* dont forget if multiple keys to reset options */ 457 char *xoptions = NULL; 458 459 for (cp = (char *)k->keys[i]->bv_val; *cp == ' ' || *cp == '\t'; cp++) 460 ; 461 if (!*cp || *cp == '\n' || *cp == '#') 462 continue; 463 464 if (sshkey_read(found, &cp) != 0) { 465 /* no key? check if there are options for this key */ 466 int quoted = 0; 467 debug2("[LDAP] user_key_allowed: check options: '%s'", cp); 468 xoptions = cp; 469 for (; *cp && (quoted || (*cp != ' ' && *cp != '\t')); cp++) { 470 if (*cp == '\\' && cp[1] == '"') 471 cp++; /* Skip both */ 472 else if (*cp == '"') 473 quoted = !quoted; 474 } 475 /* Skip remaining whitespace. */ 476 for (; *cp == ' ' || *cp == '\t'; cp++) 477 ; 478 if (sshkey_read(found, &cp) != 0) { 479 debug2("[LDAP] user_key_allowed: advance: '%s'", cp); 480 /* still no key? advance to next line*/ 481 continue; 482 } 483 } 484 if ((opts = sshauthopt_parse(xoptions, &reason)) == NULL) { 485 debug("[LDAP] %s: bad principals options: %s", xoptions, reason); 486 auth_debug_add("[LDAP] %s: bad principals options: %s", xoptions, reason); 487 continue; 488 } 489 490 491 if (sshkey_equal(found, key)) { 492 found_key = 1; 493 debug("[LDAP] matching key found"); 494 char *fp = sshkey_fingerprint(found, SSH_FP_HASH_DEFAULT, SSH_FP_HEX); 495 verbose("[LDAP] Found matching %s key: %s", sshkey_type(found), fp); 496 497 /* restoring memory */ 498 ldap_keys_free(k); 499 free(fp); 500 restore_uid(); 501 sshkey_free(found); 502 return found_key; 503 break; 504 } 505 }/* end of LDAP for() */ 506 } else { 507 logit("[LDAP] no keys found for '%s'!", pw->pw_name); 508 } 509 } else { 510 logit("[LDAP] '%s' is not in '%s'", pw->pw_name, options.lpk.sgroup); 511 } 512 } 513 #endif 514 515 if (authoptsp != NULL) 516 *authoptsp = NULL; 517 518 while (getline(&line, &linesize, f) != -1) { 519 linenum++; 520 /* Always consume entire file */ 521 if (found_key) 522 continue; 523 524 /* Skip leading whitespace, empty and comment lines. */ 525 cp = line; 526 skip_space(&cp); 527 if (!*cp || *cp == '\n' || *cp == '#') 528 continue; 529 530 nonblank++; 531 snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum); 532 if (auth_check_authkey_line(pw, key, cp, 533 remote_ip, remote_host, loc, authoptsp) == 0) 534 found_key = 1; 535 } 536 free(line); 537 debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum); 538 return found_key; 539 } 540 541 static FILE * 542 auth_openfile(const char *file, struct passwd *pw, int strict_modes, 543 int log_missing, const char *file_type) 544 { 545 char line[1024]; 546 struct stat st; 547 int fd; 548 FILE *f; 549 550 if ((fd = open(file, O_RDONLY|O_NONBLOCK)) == -1) { 551 if (errno != ENOENT) { 552 logit("Could not open user '%s' %s '%s': %s", 553 pw->pw_name, file_type, file, strerror(errno)); 554 } else if (log_missing) { 555 debug("Could not open user '%s' %s '%s': %s", 556 pw->pw_name, file_type, file, strerror(errno)); 557 } 558 return NULL; 559 } 560 561 if (fstat(fd, &st) == -1) { 562 close(fd); 563 return NULL; 564 } 565 if (!S_ISREG(st.st_mode)) { 566 logit("User '%s' %s '%s' is not a regular file", 567 pw->pw_name, file_type, file); 568 close(fd); 569 return NULL; 570 } 571 unset_nonblock(fd); 572 if ((f = fdopen(fd, "r")) == NULL) { 573 close(fd); 574 return NULL; 575 } 576 if (strict_modes && 577 safe_path_fd(fileno(f), file, pw, line, sizeof(line)) != 0) { 578 fclose(f); 579 logit("Authentication refused: %s", line); 580 auth_debug_add("Ignored %s: %s", file_type, line); 581 return NULL; 582 } 583 584 return f; 585 } 586 587 588 FILE * 589 auth_openkeyfile(const char *file, struct passwd *pw, int strict_modes) 590 { 591 return auth_openfile(file, pw, strict_modes, 1, "authorized keys"); 592 } 593 594 FILE * 595 auth_openprincipals(const char *file, struct passwd *pw, int strict_modes) 596 { 597 return auth_openfile(file, pw, strict_modes, 0, 598 "authorized principals"); 599 } 600 601