mirror of
				https://github.com/PowerShell/openssh-portable.git
				synced 2025-10-31 19:44:16 +01:00 
			
		
		
		
	We've previously removed a lot of the really old compatibility code, and with it went the need to include compat.h in most of the files that have it. OpenBSD-Commit-ID: 5af8baa194be00a3092d17598e88a5b29f7ea2b4
		
			
				
	
	
		
			501 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			501 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /* $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"
 | |
| 
 | |
| #include <sys/types.h>
 | |
| #include <sys/stat.h>
 | |
| 
 | |
| #include <stdlib.h>
 | |
| #include <errno.h>
 | |
| #include <fcntl.h>
 | |
| #include <pwd.h>
 | |
| #include <stdio.h>
 | |
| #include <stdarg.h>
 | |
| #include <string.h>
 | |
| #include <time.h>
 | |
| #include <unistd.h>
 | |
| 
 | |
| #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"
 | |
| 
 | |
| 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;
 | |
| 
 | |
| 	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, 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");
 | |
| }
 | |
| 
 |