- djm@cvs.openbsd.org 2012/10/30 21:29:55
     [auth-rsa.c auth.c auth.h auth2-pubkey.c servconf.c servconf.h]
     [sshd.c sshd_config sshd_config.5]
     new sshd_config option AuthorizedKeysCommand to support fetching
     authorized_keys from a command in addition to (or instead of) from
     the filesystem. The command is run as the target server user unless
     another specified via a new AuthorizedKeysCommandUser option.

     patch originally by jchadima AT redhat.com, reworked by me; feedback
     and ok markus@
diff --git a/ChangeLog b/ChangeLog
index 3cd16e4..27ec898 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,6 +3,16 @@
    - markus@cvs.openbsd.org 2012/10/05 12:34:39
      [sftp.c]
      fix signed vs unsigned warning; feedback & ok: djm@
+   - djm@cvs.openbsd.org 2012/10/30 21:29:55
+     [auth-rsa.c auth.c auth.h auth2-pubkey.c servconf.c servconf.h]
+     [sshd.c sshd_config sshd_config.5]
+     new sshd_config option AuthorizedKeysCommand to support fetching
+     authorized_keys from a command in addition to (or instead of) from
+     the filesystem. The command is run as the target server user unless
+     another specified via a new AuthorizedKeysCommandUser option.
+     
+     patch originally by jchadima AT redhat.com, reworked by me; feedback
+     and ok markus@
 
 20121019
  - (tim) [buildpkg.sh.in] Double up on some backslashes so they end up in
diff --git a/auth-rsa.c b/auth-rsa.c
index 4ab46cd..2c8a7cb 100644
--- a/auth-rsa.c
+++ b/auth-rsa.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: auth-rsa.c,v 1.80 2011/05/23 03:30:07 djm Exp $ */
+/* $OpenBSD: auth-rsa.c,v 1.81 2012/10/30 21:29:54 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -276,6 +276,8 @@
 	temporarily_use_uid(pw);
 
 	for (i = 0; !allowed && i < options.num_authkeys_files; i++) {
+		if (strcasecmp(options.authorized_keys_files[i], "none") == 0)
+			continue;
 		file = expand_authorized_keys(
 		    options.authorized_keys_files[i], pw);
 		allowed = rsa_key_allowed_in_file(pw, file, client_n, rkey);
diff --git a/auth.c b/auth.c
index a8cffd5..b5e1eef 100644
--- a/auth.c
+++ b/auth.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: auth.c,v 1.96 2012/05/13 01:42:32 dtucker Exp $ */
+/* $OpenBSD: auth.c,v 1.97 2012/10/30 21:29:54 djm Exp $ */
 /*
  * Copyright (c) 2000 Markus Friedl.  All rights reserved.
  *
@@ -409,41 +409,42 @@
 	return host_status;
 }
 
-
 /*
- * Check a given file for security. This is defined as all components
+ * Check a given path for security. This is defined as all components
  * of the path to the file must be owned by either the owner of
  * of the file or root and no directories must be group or world writable.
  *
  * XXX Should any specific check be done for sym links ?
  *
- * Takes an open file descriptor, the file name, a uid and and
+ * Takes an the file name, its stat information (preferably from fstat() to
+ * avoid races), the uid of the expected owner, their home directory and an
  * error buffer plus max size as arguments.
  *
  * Returns 0 on success and -1 on failure
  */
-static int
-secure_filename(FILE *f, const char *file, struct passwd *pw,
-    char *err, size_t errlen)
+int
+auth_secure_path(const char *name, struct stat *stp, const char *pw_dir,
+    uid_t uid, char *err, size_t errlen)
 {
-	uid_t uid = pw->pw_uid;
 	char buf[MAXPATHLEN], homedir[MAXPATHLEN];
 	char *cp;
 	int comparehome = 0;
 	struct stat st;
 
-	if (realpath(file, buf) == NULL) {
-		snprintf(err, errlen, "realpath %s failed: %s", file,
+	if (realpath(name, buf) == NULL) {
+		snprintf(err, errlen, "realpath %s failed: %s", name,
 		    strerror(errno));
 		return -1;
 	}
-	if (realpath(pw->pw_dir, homedir) != NULL)
+	if (pw_dir != NULL && realpath(pw_dir, homedir) != NULL)
 		comparehome = 1;
 
-	/* check the open file to avoid races */
-	if (fstat(fileno(f), &st) < 0 ||
-	    (st.st_uid != 0 && st.st_uid != uid) ||
-	    (st.st_mode & 022) != 0) {
+	if (!S_ISREG(stp->st_mode)) {
+		snprintf(err, errlen, "%s is not a regular file", buf);
+		return -1;
+	}
+	if ((stp->st_uid != 0 && stp->st_uid != uid) ||
+	    (stp->st_mode & 022) != 0) {
 		snprintf(err, errlen, "bad ownership or modes for file %s",
 		    buf);
 		return -1;
@@ -479,6 +480,28 @@
 	return 0;
 }
 
+/*
+ * Version of secure_path() that accepts an open file descriptor to
+ * avoid races.
+ *
+ * Returns 0 on success and -1 on failure
+ */
+static int
+secure_filename(FILE *f, const char *file, struct passwd *pw,
+    char *err, size_t errlen)
+{
+	char buf[MAXPATHLEN];
+	struct stat st;
+
+	/* check the open file to avoid races */
+	if (fstat(fileno(f), &st) < 0) {
+		snprintf(err, errlen, "cannot stat file %s: %s",
+		    buf, strerror(errno));
+		return -1;
+	}
+	return auth_secure_path(file, &st, pw->pw_dir, pw->pw_uid, err, errlen);
+}
+
 static FILE *
 auth_openfile(const char *file, struct passwd *pw, int strict_modes,
     int log_missing, char *file_type)
diff --git a/auth.h b/auth.h
index 0d786c4..0634041 100644
--- a/auth.h
+++ b/auth.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: auth.h,v 1.69 2011/05/23 03:30:07 djm Exp $ */
+/* $OpenBSD: auth.h,v 1.70 2012/10/30 21:29:54 djm Exp $ */
 
 /*
  * Copyright (c) 2000 Markus Friedl.  All rights reserved.
@@ -120,6 +120,10 @@
 int	 hostbased_key_allowed(struct passwd *, const char *, char *, Key *);
 int	 user_key_allowed(struct passwd *, Key *);
 
+struct stat;
+int	 auth_secure_path(const char *, struct stat *, const char *, uid_t,
+    char *, size_t);
+
 #ifdef KRB5
 int	auth_krb5(Authctxt *authctxt, krb5_data *auth, char **client, krb5_data *);
 int	auth_krb5_tgt(Authctxt *authctxt, krb5_data *tgt);
diff --git a/auth2-pubkey.c b/auth2-pubkey.c
index 5bccb5d..ec8f75d 100644
--- a/auth2-pubkey.c
+++ b/auth2-pubkey.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: auth2-pubkey.c,v 1.30 2011/09/25 05:44:47 djm Exp $ */
+/* $OpenBSD: auth2-pubkey.c,v 1.31 2012/10/30 21:29:54 djm Exp $ */
 /*
  * Copyright (c) 2000 Markus Friedl.  All rights reserved.
  *
@@ -27,9 +27,13 @@
 
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/wait.h>
 
+#include <errno.h>
 #include <fcntl.h>
+#include <paths.h>
 #include <pwd.h>
+#include <signal.h>
 #include <stdio.h>
 #include <stdarg.h>
 #include <string.h>
@@ -240,7 +244,7 @@
 			if (strcmp(cp, cert->principals[i]) == 0) {
 				debug3("matched principal \"%.100s\" "
 				    "from file \"%s\" on line %lu",
-			    	    cert->principals[i], file, linenum);
+				    cert->principals[i], file, linenum);
 				if (auth_parse_options(pw, line_opts,
 				    file, linenum) != 1)
 					continue;
@@ -253,31 +257,22 @@
 	fclose(f);
 	restore_uid();
 	return 0;
-}	
+}
 
-/* return 1 if user allows given key */
+/*
+ * Checks whether key is allowed in authorized_keys-format file,
+ * returns 1 if the key is allowed or 0 otherwise.
+ */
 static int
-user_key_allowed2(struct passwd *pw, Key *key, char *file)
+check_authkeys_file(FILE *f, char *file, Key* key, struct passwd *pw)
 {
 	char line[SSH_MAX_PUBKEY_BYTES];
 	const char *reason;
 	int found_key = 0;
-	FILE *f;
 	u_long linenum = 0;
 	Key *found;
 	char *fp;
 
-	/* Temporarily use the user's uid. */
-	temporarily_use_uid(pw);
-
-	debug("trying public key file %s", file);
-	f = auth_openkeyfile(file, pw, options.strict_modes);
-
-	if (!f) {
-		restore_uid();
-		return 0;
-	}
-
 	found_key = 0;
 	found = key_new(key_is_cert(key) ? KEY_UNSPEC : key->type);
 
@@ -370,8 +365,6 @@
 			break;
 		}
 	}
-	restore_uid();
-	fclose(f);
 	key_free(found);
 	if (!found_key)
 		debug2("key not found");
@@ -433,7 +426,172 @@
 	return ret;
 }
 
-/* check whether given key is in .ssh/authorized_keys* */
+/*
+ * Checks whether key is allowed in file.
+ * returns 1 if the key is allowed or 0 otherwise.
+ */
+static int
+user_key_allowed2(struct passwd *pw, Key *key, char *file)
+{
+	FILE *f;
+	int found_key = 0;
+
+	/* Temporarily use the user's uid. */
+	temporarily_use_uid(pw);
+
+	debug("trying public key file %s", file);
+	if ((f = auth_openkeyfile(file, pw, options.strict_modes)) != NULL) {
+		found_key = check_authkeys_file(f, file, key, pw);
+		fclose(f);
+	}
+
+	restore_uid();
+	return found_key;
+}
+
+/*
+ * Checks whether key is allowed in output of command.
+ * returns 1 if the key is allowed or 0 otherwise.
+ */
+static int
+user_key_command_allowed2(struct passwd *user_pw, Key *key)
+{
+	FILE *f;
+	int ok, found_key = 0;
+	struct passwd *pw;
+	struct stat st;
+	int status, devnull, p[2], i;
+	pid_t pid;
+	char errmsg[512];
+
+	if (options.authorized_keys_command == NULL ||
+	    options.authorized_keys_command[0] != '/')
+		return 0;
+
+	/* If no user specified to run commands the default to target user */
+	if (options.authorized_keys_command_user == NULL)
+		pw = user_pw;
+	else {
+		pw = getpwnam(options.authorized_keys_command_user);
+		if (pw == NULL) {
+			error("AuthorizedKeyCommandUser \"%s\" not found: %s",
+			    options.authorized_keys_command, strerror(errno));
+			return 0;
+		}
+	}
+
+	temporarily_use_uid(pw);
+
+	if (stat(options.authorized_keys_command, &st) < 0) {
+		error("Could not stat AuthorizedKeysCommand \"%s\": %s",
+		    options.authorized_keys_command, strerror(errno));
+		goto out;
+	}
+	if (auth_secure_path(options.authorized_keys_command, &st, NULL, 0,
+	    errmsg, sizeof(errmsg)) != 0) {
+		error("Unsafe AuthorizedKeysCommand: %s", errmsg);
+		goto out;
+	}
+
+	if (pipe(p) != 0) {
+		error("%s: pipe: %s", __func__, strerror(errno));
+		goto out;
+	}
+
+	debug3("Running AuthorizedKeysCommand: \"%s\" as \"%s\"",
+	    options.authorized_keys_command, pw->pw_name);
+
+	/*
+	 * Don't want to call this in the child, where it can fatal() and
+	 * run cleanup_exit() code.
+	 */
+	restore_uid();
+
+	switch ((pid = fork())) {
+	case -1: /* error */
+		error("%s: fork: %s", __func__, strerror(errno));
+		close(p[0]);
+		close(p[1]);
+		return 0;
+	case 0: /* child */
+		for (i = 0; i < NSIG; i++)
+			signal(i, SIG_DFL);
+
+		/* Don't use permanently_set_uid() here to avoid fatal() */
+		if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0) {
+			error("setresgid %u: %s", (u_int)pw->pw_gid,
+			    strerror(errno));
+			_exit(1);
+		}
+		if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) {
+			error("setresuid %u: %s", (u_int)pw->pw_uid,
+			    strerror(errno));
+			_exit(1);
+		}
+
+		close(p[0]);
+		if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) {
+			error("%s: open %s: %s", __func__, _PATH_DEVNULL,
+			    strerror(errno));
+			_exit(1);
+		}
+		if (dup2(devnull, STDIN_FILENO) == -1 ||
+		    dup2(p[1], STDOUT_FILENO) == -1 ||
+		    dup2(devnull, STDERR_FILENO) == -1) {
+			error("%s: dup2: %s", __func__, strerror(errno));
+			_exit(1);
+		}
+		closefrom(STDERR_FILENO + 1);
+
+		execl(options.authorized_keys_command,
+		    options.authorized_keys_command, pw->pw_name, NULL);
+
+		error("AuthorizedKeysCommand %s exec failed: %s",
+		    options.authorized_keys_command, strerror(errno));
+		_exit(127);
+	default: /* parent */
+		break;
+	}
+
+	temporarily_use_uid(pw);
+
+	close(p[1]);
+	if ((f = fdopen(p[0], "r")) == NULL) {
+		error("%s: fdopen: %s", __func__, strerror(errno));
+		close(p[0]);
+		/* Don't leave zombie child */
+		kill(pid, SIGTERM);
+		while (waitpid(pid, NULL, 0) == -1 && errno == EINTR)
+			;
+		goto out;
+	}
+	ok = check_authkeys_file(f, options.authorized_keys_command, key, pw);
+	fclose(f);
+
+	while (waitpid(pid, &status, 0) == -1) {
+		if (errno != EINTR) {
+			error("%s: waitpid: %s", __func__, strerror(errno));
+			goto out;
+		}
+	}
+	if (WIFSIGNALED(status)) {
+		error("AuthorizedKeysCommand %s exited on signal %d",
+		    options.authorized_keys_command, WTERMSIG(status));
+		goto out;
+	} else if (WEXITSTATUS(status) != 0) {
+		error("AuthorizedKeysCommand %s returned status %d",
+		    options.authorized_keys_command, WEXITSTATUS(status));
+		goto out;
+	}
+	found_key = ok;
+ out:
+	restore_uid();
+	return found_key;
+}
+
+/*
+ * Check whether key authenticates and authorises the user.
+ */
 int
 user_key_allowed(struct passwd *pw, Key *key)
 {
@@ -449,9 +607,17 @@
 	if (success)
 		return success;
 
+	success = user_key_command_allowed2(pw, key);
+	if (success > 0)
+		return success;
+
 	for (i = 0; !success && i < options.num_authkeys_files; i++) {
+
+		if (strcasecmp(options.authorized_keys_files[i], "none") == 0)
+			continue;
 		file = expand_authorized_keys(
 		    options.authorized_keys_files[i], pw);
+
 		success = user_key_allowed2(pw, key, file);
 		xfree(file);
 	}
diff --git a/servconf.c b/servconf.c
index f4b7dd5..8e69ea5 100644
--- a/servconf.c
+++ b/servconf.c
@@ -1,5 +1,5 @@
 
-/* $OpenBSD: servconf.c,v 1.230 2012/09/13 23:37:36 dtucker Exp $ */
+/* $OpenBSD: servconf.c,v 1.231 2012/10/30 21:29:54 djm Exp $ */
 /*
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -135,6 +135,8 @@
 	options->num_permitted_opens = -1;
 	options->adm_forced_command = NULL;
 	options->chroot_directory = NULL;
+	options->authorized_keys_command = NULL;
+	options->authorized_keys_command_user = NULL;
 	options->zero_knowledge_password_authentication = -1;
 	options->revoked_keys_file = NULL;
 	options->trusted_user_ca_keys = NULL;
@@ -329,6 +331,7 @@
 	sZeroKnowledgePasswordAuthentication, sHostCertificate,
 	sRevokedKeys, sTrustedUserCAKeys, sAuthorizedPrincipalsFile,
 	sKexAlgorithms, sIPQoS, sVersionAddendum,
+	sAuthorizedKeysCommand, sAuthorizedKeysCommandUser,
 	sDeprecated, sUnsupported
 } ServerOpCodes;
 
@@ -453,6 +456,8 @@
 	{ "authorizedprincipalsfile", sAuthorizedPrincipalsFile, SSHCFG_ALL },
 	{ "kexalgorithms", sKexAlgorithms, SSHCFG_GLOBAL },
 	{ "ipqos", sIPQoS, SSHCFG_ALL },
+	{ "authorizedkeyscommand", sAuthorizedKeysCommand, SSHCFG_ALL },
+	{ "authorizedkeyscommanduser", sAuthorizedKeysCommandUser, SSHCFG_ALL },
 	{ "versionaddendum", sVersionAddendum, SSHCFG_GLOBAL },
 	{ NULL, sBadOption, 0 }
 };
@@ -1498,6 +1503,25 @@
 		}
 		return 0;
 
+	case sAuthorizedKeysCommand:
+		len = strspn(cp, WHITESPACE);
+		if (*activep && options->authorized_keys_command == NULL) {
+			if (cp[len] != '/' && strcasecmp(cp + len, "none") != 0)
+				fatal("%.200s line %d: AuthorizedKeysCommand "
+				    "must be an absolute path",
+				    filename, linenum);
+			options->authorized_keys_command = xstrdup(cp + len);
+		}
+		return 0;
+
+	case sAuthorizedKeysCommandUser:
+		charptr = &options->authorized_keys_command_user;
+
+		arg = strdelim(&cp);
+		if (*activep && *charptr == NULL)
+			*charptr = xstrdup(arg);
+		break;
+
 	case sDeprecated:
 		logit("%s line %d: Deprecated option %s",
 		    filename, linenum, arg);
@@ -1648,6 +1672,8 @@
 	M_CP_INTOPT(hostbased_uses_name_from_packet_only);
 	M_CP_INTOPT(kbd_interactive_authentication);
 	M_CP_INTOPT(zero_knowledge_password_authentication);
+	M_CP_STROPT(authorized_keys_command);
+	M_CP_STROPT(authorized_keys_command_user);
 	M_CP_INTOPT(permit_root_login);
 	M_CP_INTOPT(permit_empty_passwd);
 
@@ -1908,6 +1934,8 @@
 	dump_cfg_string(sAuthorizedPrincipalsFile,
 	    o->authorized_principals_file);
 	dump_cfg_string(sVersionAddendum, o->version_addendum);
+	dump_cfg_string(sAuthorizedKeysCommand, o->authorized_keys_command);
+	dump_cfg_string(sAuthorizedKeysCommandUser, o->authorized_keys_command_user);
 
 	/* string arguments requiring a lookup */
 	dump_cfg_string(sLogLevel, log_level_name(o->log_level));
diff --git a/servconf.h b/servconf.h
index 096d596..0064c9b 100644
--- a/servconf.h
+++ b/servconf.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.h,v 1.103 2012/07/10 02:19:15 djm Exp $ */
+/* $OpenBSD: servconf.h,v 1.104 2012/10/30 21:29:55 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -166,6 +166,8 @@
 	char   *revoked_keys_file;
 	char   *trusted_user_ca_keys;
 	char   *authorized_principals_file;
+	char   *authorized_keys_command;
+	char   *authorized_keys_command_user;
 
 	char   *version_addendum;	/* Appended to SSH banner */
 }       ServerOptions;
diff --git a/sshd.c b/sshd.c
index 9aff5e8..eff0290 100644
--- a/sshd.c
+++ b/sshd.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshd.c,v 1.393 2012/07/10 02:19:15 djm Exp $ */
+/* $OpenBSD: sshd.c,v 1.394 2012/10/30 21:29:55 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -359,6 +359,15 @@
 	if (use_privsep && pmonitor != NULL && pmonitor->m_pid > 0)
 		kill(pmonitor->m_pid, SIGALRM);
 
+	/*
+	 * Try to kill any processes that we have spawned, E.g. authorized
+	 * keys command helpers.
+	 */
+	if (getpgid(0) == getpid()) {
+		signal(SIGTERM, SIG_IGN);
+		killpg(0, SIGTERM);
+	}
+
 	/* Log error and exit. */
 	sigdie("Timeout before authentication for %s", get_remote_ipaddr());
 }
diff --git a/sshd_config b/sshd_config
index 9424ee2..3d35bef 100644
--- a/sshd_config
+++ b/sshd_config
@@ -1,4 +1,4 @@
-#	$OpenBSD: sshd_config,v 1.87 2012/07/10 02:19:15 djm Exp $
+#	$OpenBSD: sshd_config,v 1.88 2012/10/30 21:29:55 djm Exp $
 
 # This is the sshd server system-wide configuration file.  See
 # sshd_config(5) for more information.
@@ -51,6 +51,9 @@
 
 #AuthorizedPrincipalsFile none
 
+#AuthorizedKeysCommand none
+#AuthorizedKeysCommandUser nobody
+
 # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
 #RhostsRSAAuthentication no
 # similar for protocol version 2
diff --git a/sshd_config.5 b/sshd_config.5
index 987558a..de8f0f8 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -33,8 +33,8 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.\" $OpenBSD: sshd_config.5,v 1.145 2012/10/04 13:21:50 markus Exp $
-.Dd $Mdocdate: October 4 2012 $
+.\" $OpenBSD: sshd_config.5,v 1.146 2012/10/30 21:29:55 djm Exp $
+.Dd $Mdocdate: October 30 2012 $
 .Dt SSHD_CONFIG 5
 .Os
 .Sh NAME
@@ -151,6 +151,22 @@
 in
 .Xr ssh_config 5
 for more information on patterns.
+.It Cm AuthorizedKeysCommand
+Specifies a program to be used for lookup of the user's public keys.
+The program will be invoked with a single argument of the username
+being authenticated, and should produce on standard output zero or
+more lines of authorized_keys output (see AUTHORIZED_KEYS in
+.Xr sshd 8 )
+If a key supplied by AuthorizedKeysCommand does not successfully authenticate
+and authorize the user then public key authentication continues using the usual
+.Cm AuthorizedKeysFile
+files.
+By default, no AuthorizedKeysCommand is run.
+.It Cm AuthorizedKeysCommandUser
+Specifies the user under whose account the AuthorizedKeysCommand is run.
+The default is the user being authenticated.
+It is recommended to use a dedicated user that has no other role on the host
+than running authorized keys commands.
 .It Cm AuthorizedKeysFile
 Specifies the file that contains the public keys that can be used
 for user authentication.
@@ -712,6 +728,8 @@
 .Cm AllowTcpForwarding ,
 .Cm AllowUsers ,
 .Cm AuthorizedKeysFile ,
+.Cm AuthorizedKeysCommand ,
+.Cm AuthorizedKeysCommandUser ,
 .Cm AuthorizedPrincipalsFile ,
 .Cm Banner ,
 .Cm ChrootDirectory ,