/* $NetBSD: auth2-pubkeyfile.c,v 1.3.2.2 2023/12/25 12:31:03 martin Exp $ */ /* $OpenBSD: auth2-pubkeyfile.c,v 1.4 2023/03/05 05:34:09 dtucker Exp $ */ /* * Copyright (c) 2000 Markus Friedl. All rights reserved. * Copyright (c) 2010 Damien Miller. 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: auth2-pubkeyfile.c,v 1.3.2.2 2023/12/25 12:31:03 martin Exp $"); #include #include #include #include #include #include #include #include #include #include #include #include "ssh.h" #include "log.h" #include "misc.h" #include "sshkey.h" #include "digest.h" #include "hostfile.h" #include "auth.h" #include "auth-options.h" #include "authfile.h" #include "match.h" #include "ssherr.h" #ifdef WITH_LDAP_PUBKEY #include "servconf.h" #include "uidswap.h" #include "ldapauth.h" extern ServerOptions options; #endif int auth_authorise_keyopts(struct passwd *pw, struct sshauthopt *opts, int allow_cert_authority, const char *remote_ip, const char *remote_host, const char *loc) { time_t now = time(NULL); char buf[64]; /* * Check keys/principals file expiry time. * NB. validity interval in certificate is handled elsewhere. */ if (opts->valid_before && now > 0 && opts->valid_before < (uint64_t)now) { format_absolute_time(opts->valid_before, buf, sizeof(buf)); debug("%s: entry expired at %s", loc, buf); auth_debug_add("%s: entry expired at %s", loc, buf); return -1; } /* Consistency checks */ if (opts->cert_principals != NULL && !opts->cert_authority) { debug("%s: principals on non-CA key", loc); auth_debug_add("%s: principals on non-CA key", loc); /* deny access */ return -1; } /* cert-authority flag isn't valid in authorized_principals files */ if (!allow_cert_authority && opts->cert_authority) { debug("%s: cert-authority flag invalid here", loc); auth_debug_add("%s: cert-authority flag invalid here", loc); /* deny access */ return -1; } /* Perform from= checks */ if (opts->required_from_host_keys != NULL) { switch (match_host_and_ip(remote_host, remote_ip, opts->required_from_host_keys )) { case 1: /* Host name matches. */ break; case -1: default: debug("%s: invalid from criteria", loc); auth_debug_add("%s: invalid from criteria", loc); /* FALLTHROUGH */ case 0: logit("%s: Authentication tried for %.100s with " "correct key but not from a permitted " "host (host=%.200s, ip=%.200s, required=%.200s).", loc, pw->pw_name, remote_host, remote_ip, opts->required_from_host_keys); auth_debug_add("%s: Your host '%.200s' is not " "permitted to use this key for login.", loc, remote_host); /* deny access */ return -1; } } /* Check source-address restriction from certificate */ if (opts->required_from_host_cert != NULL) { switch (addr_match_cidr_list(remote_ip, opts->required_from_host_cert)) { case 1: /* accepted */ break; case -1: default: /* invalid */ error("%s: Certificate source-address invalid", loc); /* FALLTHROUGH */ case 0: logit("%s: Authentication tried for %.100s with valid " "certificate but not from a permitted source " "address (%.200s).", loc, pw->pw_name, remote_ip); auth_debug_add("%s: Your address '%.200s' is not " "permitted to use this certificate for login.", loc, remote_ip); return -1; } } /* * * XXX this is spammy. We should report remotely only for keys * that are successful in actual auth attempts, and not PK_OK * tests. */ auth_log_authopts(loc, opts, 1); return 0; } static int match_principals_option(const char *principal_list, struct sshkey_cert *cert) { char *result; u_int i; /* XXX percent_expand() sequences for authorized_principals? */ for (i = 0; i < cert->nprincipals; i++) { if ((result = match_list(cert->principals[i], principal_list, NULL)) != NULL) { debug3("matched principal from key options \"%.100s\"", result); free(result); return 1; } } return 0; } /* * Process a single authorized_principals format line. Returns 0 and sets * authoptsp is principal is authorised, -1 otherwise. "loc" is used as a * log preamble for file/line information. */ int auth_check_principals_line(char *cp, const struct sshkey_cert *cert, const char *loc, struct sshauthopt **authoptsp) { u_int i, found = 0; char *ep, *line_opts; const char *reason = NULL; struct sshauthopt *opts = NULL; if (authoptsp != NULL) *authoptsp = NULL; /* Trim trailing whitespace. */ ep = cp + strlen(cp) - 1; while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t')) *ep-- = '\0'; /* * If the line has internal whitespace then assume it has * key options. */ line_opts = NULL; if ((ep = strrchr(cp, ' ')) != NULL || (ep = strrchr(cp, '\t')) != NULL) { for (; *ep == ' ' || *ep == '\t'; ep++) ; line_opts = cp; cp = ep; } if ((opts = sshauthopt_parse(line_opts, &reason)) == NULL) { debug("%s: bad principals options: %s", loc, reason); auth_debug_add("%s: bad principals options: %s", loc, reason); return -1; } /* Check principals in cert against those on line */ for (i = 0; i < cert->nprincipals; i++) { if (strcmp(cp, cert->principals[i]) != 0) continue; debug3("%s: matched principal \"%.100s\"", loc, cert->principals[i]); found = 1; } if (found && authoptsp != NULL) { *authoptsp = opts; opts = NULL; } sshauthopt_free(opts); return found ? 0 : -1; } int auth_process_principals(FILE *f, const char *file, const struct sshkey_cert *cert, struct sshauthopt **authoptsp) { char loc[256], *line = NULL, *cp, *ep; size_t linesize = 0; u_long linenum = 0, nonblank = 0; u_int found_principal = 0; if (authoptsp != NULL) *authoptsp = NULL; while (getline(&line, &linesize, f) != -1) { linenum++; /* Always consume entire input */ if (found_principal) continue; /* Skip leading whitespace. */ for (cp = line; *cp == ' ' || *cp == '\t'; cp++) ; /* Skip blank and comment lines. */ if ((ep = strchr(cp, '#')) != NULL) *ep = '\0'; if (!*cp || *cp == '\n') continue; nonblank++; snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum); if (auth_check_principals_line(cp, cert, loc, authoptsp) == 0) found_principal = 1; } debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum); free(line); return found_principal; } /* * Check a single line of an authorized_keys-format file. Returns 0 if key * matches, -1 otherwise. Will return key/cert options via *authoptsp * on success. "loc" is used as file/line location in log messages. */ int auth_check_authkey_line(struct passwd *pw, struct sshkey *key, char *cp, const char *remote_ip, const char *remote_host, const char *loc, struct sshauthopt **authoptsp) { int want_keytype = sshkey_is_cert(key) ? KEY_UNSPEC : key->type; struct sshkey *found = NULL; struct sshauthopt *keyopts = NULL, *certopts = NULL, *finalopts = NULL; char *key_options = NULL, *fp = NULL; const char *reason = NULL; int ret = -1; if (authoptsp != NULL) *authoptsp = NULL; if ((found = sshkey_new(want_keytype)) == NULL) { debug3_f("keytype %d failed", want_keytype); goto out; } /* XXX djm: peek at key type in line and skip if unwanted */ if (sshkey_read(found, &cp) != 0) { /* no key? check for options */ debug2("%s: check options: '%s'", loc, cp); key_options = cp; if (sshkey_advance_past_options(&cp) != 0) { reason = "invalid key option string"; goto fail_reason; } skip_space(&cp); if (sshkey_read(found, &cp) != 0) { /* still no key? advance to next line*/ debug2("%s: advance: '%s'", loc, cp); goto out; } } /* Parse key options now; we need to know if this is a CA key */ if ((keyopts = sshauthopt_parse(key_options, &reason)) == NULL) { debug("%s: bad key options: %s", loc, reason); auth_debug_add("%s: bad key options: %s", loc, reason); goto out; } /* Ignore keys that don't match or incorrectly marked as CAs */ if (sshkey_is_cert(key)) { /* Certificate; check signature key against CA */ if (!sshkey_equal(found, key->cert->signature_key) || !keyopts->cert_authority) goto out; } else { /* Plain key: check it against key found in file */ if (!sshkey_equal(found, key) || keyopts->cert_authority) goto out; } /* We have a candidate key, perform authorisation checks */ if ((fp = sshkey_fingerprint(found, SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL) fatal_f("fingerprint failed"); debug("%s: matching %s found: %s %s", loc, sshkey_is_cert(key) ? "CA" : "key", sshkey_type(found), fp); if (auth_authorise_keyopts(pw, keyopts, sshkey_is_cert(key), remote_ip, remote_host, loc) != 0) { reason = "Refused by key options"; goto fail_reason; } /* That's all we need for plain keys. */ if (!sshkey_is_cert(key)) { verbose("Accepted key %s %s found at %s", sshkey_type(found), fp, loc); finalopts = keyopts; keyopts = NULL; goto success; } /* * Additional authorisation for certificates. */ /* Parse and check options present in certificate */ if ((certopts = sshauthopt_from_cert(key)) == NULL) { reason = "Invalid certificate options"; goto fail_reason; } if (auth_authorise_keyopts(pw, certopts, 0, remote_ip, remote_host, loc) != 0) { reason = "Refused by certificate options"; goto fail_reason; } if ((finalopts = sshauthopt_merge(keyopts, certopts, &reason)) == NULL) goto fail_reason; /* * If the user has specified a list of principals as * a key option, then prefer that list to matching * their username in the certificate principals list. */ if (keyopts->cert_principals != NULL && !match_principals_option(keyopts->cert_principals, key->cert)) { reason = "Certificate does not contain an authorized principal"; goto fail_reason; } if (sshkey_cert_check_authority_now(key, 0, 0, 0, keyopts->cert_principals == NULL ? pw->pw_name : NULL, &reason) != 0) goto fail_reason; verbose("Accepted certificate ID \"%s\" (serial %llu) " "signed by CA %s %s found at %s", key->cert->key_id, (unsigned long long)key->cert->serial, sshkey_type(found), fp, loc); success: if (finalopts == NULL) fatal_f("internal error: missing options"); if (authoptsp != NULL) { *authoptsp = finalopts; finalopts = NULL; } /* success */ ret = 0; goto out; fail_reason: error("%s", reason); auth_debug_add("%s", reason); out: free(fp); sshauthopt_free(keyopts); sshauthopt_free(certopts); sshauthopt_free(finalopts); sshkey_free(found); return ret; } /* * Checks whether key is allowed in authorized_keys-format file, * returns 1 if the key is allowed or 0 otherwise. */ int auth_check_authkeys_file(struct passwd *pw, FILE *f, char *file, struct sshkey *key, const char *remote_ip, const char *remote_host, struct sshauthopt **authoptsp) { char *cp, *line = NULL, loc[256]; size_t linesize = 0; int found_key = 0; u_long linenum = 0, nonblank = 0; #ifdef WITH_LDAP_PUBKEY struct sshauthopt *opts = NULL; struct sshkey *found = NULL; ldap_key_t * k; unsigned int i = 0; const char *reason; found_key = 0; /* allocate a new key type */ found = sshkey_new(key->type); /* first check if the options is enabled, then try.. */ if (options.lpk.on) { debug("[LDAP] trying LDAP first uid=%s",pw->pw_name); if (ldap_ismember(&options.lpk, pw->pw_name) > 0) { if ((k = ldap_getuserkey(&options.lpk, pw->pw_name)) != NULL) { /* Skip leading whitespace, empty and comment lines. */ for (i = 0 ; i < k->num ; i++) { /* dont forget if multiple keys to reset options */ char *xoptions = NULL; for (cp = (char *)k->keys[i]->bv_val; *cp == ' ' || *cp == '\t'; cp++) ; if (!*cp || *cp == '\n' || *cp == '#') continue; if (sshkey_read(found, &cp) != 0) { /* no key? check if there are options for this key */ int quoted = 0; debug2("[LDAP] user_key_allowed: check options: '%s'", cp); xoptions = cp; for (; *cp && (quoted || (*cp != ' ' && *cp != '\t')); cp++) { if (*cp == '\\' && cp[1] == '"') cp++; /* Skip both */ else if (*cp == '"') quoted = !quoted; } /* Skip remaining whitespace. */ for (; *cp == ' ' || *cp == '\t'; cp++) ; if (sshkey_read(found, &cp) != 0) { debug2("[LDAP] user_key_allowed: advance: '%s'", cp); /* still no key? advance to next line*/ continue; } } if ((opts = sshauthopt_parse(xoptions, &reason)) == NULL) { debug("[LDAP] %s: bad principals options: %s", xoptions, reason); auth_debug_add("[LDAP] %s: bad principals options: %s", xoptions, reason); continue; } if (sshkey_equal(found, key)) { found_key = 1; debug("[LDAP] matching key found"); char *fp = sshkey_fingerprint(found, SSH_FP_HASH_DEFAULT, SSH_FP_HEX); verbose("[LDAP] Found matching %s key: %s", sshkey_type(found), fp); /* restoring memory */ ldap_keys_free(k); free(fp); restore_uid(); sshkey_free(found); return found_key; break; } }/* end of LDAP for() */ } else { logit("[LDAP] no keys found for '%s'!", pw->pw_name); } } else { logit("[LDAP] '%s' is not in '%s'", pw->pw_name, options.lpk.sgroup); } } #endif if (authoptsp != NULL) *authoptsp = NULL; while (getline(&line, &linesize, f) != -1) { linenum++; /* Always consume entire file */ if (found_key) continue; /* Skip leading whitespace, empty and comment lines. */ cp = line; skip_space(&cp); if (!*cp || *cp == '\n' || *cp == '#') continue; nonblank++; snprintf(loc, sizeof(loc), "%.200s:%lu", file, linenum); if (auth_check_authkey_line(pw, key, cp, remote_ip, remote_host, loc, authoptsp) == 0) found_key = 1; } free(line); debug2_f("%s: processed %lu/%lu lines", file, nonblank, linenum); return found_key; } static FILE * auth_openfile(const char *file, struct passwd *pw, int strict_modes, int log_missing, const char *file_type) { char line[1024]; struct stat st; int fd; FILE *f; if ((fd = open(file, O_RDONLY|O_NONBLOCK)) == -1) { if (errno != ENOENT) { logit("Could not open user '%s' %s '%s': %s", pw->pw_name, file_type, file, strerror(errno)); } else if (log_missing) { debug("Could not open user '%s' %s '%s': %s", pw->pw_name, file_type, file, strerror(errno)); } return NULL; } if (fstat(fd, &st) == -1) { close(fd); return NULL; } if (!S_ISREG(st.st_mode)) { logit("User '%s' %s '%s' is not a regular file", pw->pw_name, file_type, file); close(fd); return NULL; } unset_nonblock(fd); if ((f = fdopen(fd, "r")) == NULL) { close(fd); return NULL; } if (strict_modes && safe_path_fd(fileno(f), file, pw, line, sizeof(line)) != 0) { fclose(f); logit("Authentication refused: %s", line); auth_debug_add("Ignored %s: %s", file_type, line); return NULL; } return f; } FILE * auth_openkeyfile(const char *file, struct passwd *pw, int strict_modes) { return auth_openfile(file, pw, strict_modes, 1, "authorized keys"); } FILE * auth_openprincipals(const char *file, struct passwd *pw, int strict_modes) { return auth_openfile(file, pw, strict_modes, 0, "authorized principals"); }