this is rough, but enough to start a discussion.
this lets doas authenticate a user by talking to their ssh agent
by specifying 'ssh-agent' on a permit line in the config. if agent
auth fails, doas falls back to bsd auth (ie, password auth).
to minimise the amount of code needed in doas, most of the heavy
lifting is handed off to two external programs.
the first is a program that fetches a users keys. it has to be
provided by the system administrator.
doas does not look at ~/.ssh/authorized_keys because that would
allow someone on an unattended shell to modify the current users
keys file to gain whatever privs theyve been granted by doas.
instead, it followes the semantics of sshds AuthorizedKeysCommand
handler.
at work i have an AuthorizedKeysCommand thing that fetches keys
from active directory (ie, an ldap) so users can do key based auth
on all our machines instead of just the ones we nfs export their
homedirs to. doas can use the same backend to fetch the set of keys
the system trusts to auth that user. alternatively, a machine could
have a set of trusted keys for its users that is read from /etc or
such.
the second program is /usr/libexec/doas.sshagent. my implementation
reuses a lot of ssh code and links to libssh.a, so ive currently
got it in src/usr.bin/ssh/doas.sshagent.
it basically reads authorized_keys from stdin and attempts to use
them against the users ssh agent.
if one of the provided keys works, it exits with code 0. if auth
fails, it exits with code 8. every other code is considered an
error.
the code in doas basically creates a pipe to join the stdout of the
authorized_keys command to the stdin of doas.sshagent, and then
waits for the latter to exit with a useful code.
this way avoids having to add a bunch of buffering, string parsing,
and crypto to doas itself. the doas.sshagent code contains that on
its behalf.
anyway, the code is rough, i only just got it all hanging together.
thoughts?
Index: doas/Makefile
===================================================================
RCS file: /cvs/src/usr.bin/doas/Makefile,v
retrieving revision 1.1
diff -u -p -r1.1 Makefile
--- doas/Makefile 16 Jul 2015 20:44:21 -0000 1.1
+++ doas/Makefile 26 Jul 2015 04:39:17 -0000
@@ -8,7 +8,6 @@ MAN= doas.1 doas.conf.5
BINOWN= root
BINMODE=4555
-CFLAGS+= -I${.CURDIR}
COPTS+= -Wall
.include <bsd.prog.mk>
Index: doas/doas.c
===================================================================
RCS file: /cvs/src/usr.bin/doas/doas.c,v
retrieving revision 1.21
diff -u -p -r1.21 doas.c
--- doas/doas.c 24 Jul 2015 06:36:42 -0000 1.21
+++ doas/doas.c 26 Jul 2015 04:39:17 -0000
@@ -17,6 +17,7 @@
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/wait.h>
#include <limits.h>
#include <login_cap.h>
@@ -26,6 +27,8 @@
#include <stdlib.h>
#include <err.h>
#include <unistd.h>
+#include <fcntl.h>
+#include <signal.h>
#include <pwd.h>
#include <grp.h>
#include <syslog.h>
@@ -33,6 +36,8 @@
#include "doas.h"
+static int ssh_agent(const char *, uid_t, gid_t);
+
static void __dead
usage(void)
{
@@ -291,7 +296,7 @@ main(int argc, char **argv, char **envp)
struct rule *rule;
uid_t uid;
uid_t target = 0;
- gid_t groups[NGROUPS_MAX + 1];
+ gid_t groups[NGROUPS_MAX + 1], gid;
int ngroups;
int i, ch;
int sflag = 0;
@@ -331,7 +336,7 @@ main(int argc, char **argv, char **envp)
ngroups = getgroups(NGROUPS_MAX, groups);
if (ngroups == -1)
err(1, "can't get groups");
- groups[ngroups++] = getgid();
+ groups[ngroups++] = gid = getgid();
if (sflag) {
sh = getenv("SHELL");
@@ -360,13 +365,23 @@ main(int argc, char **argv, char **envp)
fail();
}
- if (!(rule->options & NOPASS)) {
+ switch (rule->options & AUTHMASK) {
+ case NOPASS:
+ break;
+ case SSHAGENT:
+ if (ssh_agent(myname, uid, gid) == 0)
+ break;
+
+ /* FALLTHROUGH */
+ case PASSAUTH:
if (!auth_userokay(myname, NULL, NULL, NULL)) {
syslog(LOG_AUTHPRIV | LOG_NOTICE,
"failed password for %s", myname);
fail();
}
+ break;
}
+
envp = copyenv((const char **)envp, rule);
pw = getpwuid(target);
@@ -385,4 +400,136 @@ main(int argc, char **argv, char **envp)
if (errno == ENOENT)
errx(1, "%s: command not found", cmd);
err(1, "%s", cmd);
+}
+
+char *keycmd = "/etc/doas/sshkeys";
+char *keyuser = "_doas";
+char *agentcmd = "/usr/libexec/doas.sshagent";
+
+static int
+ssh_agent(const char *myname, uid_t uid, gid_t gid)
+{
+ extern char *__progname;
+ struct passwd *pw;
+ int p[2], devnull;
+ const char *sock;
+ pid_t keys, agent;
+ int status;
+ char *argv [] = { agentcmd, NULL };
+ char *envv[] = { NULL, "SHELL=/bin/sh", "PATH=/bin:/usr/bin", NULL };
+
+ sock = getenv("SSH_AUTH_SOCK");
+ if (sock == NULL)
+ return (1);
+
+ pw = getpwnam(keyuser);
+ if (pw == NULL)
+ err(1, "key user \"%s\" not found", keyuser);
+
+ if (pipe(p) == -1)
+ err(1, "key pipe");
+
+ devnull = open("/dev/null", O_RDWR);
+ if (devnull == -1)
+ err(1, "open %s", "/dev/null");
+
+ keys = fork();
+ switch (keys) {
+ case -1:
+ err(1, "ssh keys fork");
+ /* NOTREACHED */
+
+ case 0: /* child */
+ close(p[0]);
+
+ if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1)
+ err(1, "keys %s setresgid", keyuser);
+
+ if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
+ err(1, "keys %s setresuid", keyuser);
+
+ if (dup2(devnull, STDIN_FILENO) == -1)
+ err(1, "dup2 stdin");
+ if (dup2(p[1], STDOUT_FILENO) == -1)
+ err(1, "dup2 stdout");
+ if (dup2(devnull, STDERR_FILENO) == -1)
+ err(1, "dup2 stderr");
+ closefrom(STDERR_FILENO + 1);
+
+ execl(keycmd, keycmd, myname, __progname, NULL);
+ /* stderr is closed, so err() isnt much use */
+ exit(127);
+ /* NOTREACHED */
+
+ default: /* parent */
+ close(p[1]);
+ break;
+ }
+
+ agent = fork();
+ switch (agent) {
+ case -1:
+ /* don't leave zombie children */
+ kill(keys, SIGTERM);
+ while (waitpid(keys, NULL, 0) == -1 && errno == EINTR)
+ ;
+
+ err(1, "ssh agent fork");
+ /* NOTREACHED */
+
+ case 0: /* child */
+ if (setresgid(gid, gid, gid) == -1)
+ err(1, "agent setresgid");
+
+ if (setresuid(uid, uid, uid) == -1)
+ err(1, "agent setresuid");
+
+ if (dup2(p[0], STDIN_FILENO) == -1)
+ err(1, "agent dup2 stdin");
+ if (dup2(devnull, STDOUT_FILENO) == -1)
+ err(1, "agent dup2 stdout");
+ if (dup2(devnull, STDERR_FILENO) == -1)
+ err(1, "agent dup2 stderr");
+ closefrom(STDERR_FILENO + 1);
+
+ /* no stderr from now on */
+ if (asprintf(&envv[0], "SSH_AUTH_SOCK=%s", sock) == -1)
+ exit(127);
+
+ execvpe(agentcmd, argv, envv);
+ exit(127);
+ break;
+
+ default: /* parent */
+ close(p[0]);
+ break;
+ }
+
+ close(devnull);
+
+ /* dont really care what happens to the keys process */
+ while (waitpid(keys, NULL, 0) == -1 && errno == EINTR)
+ ;
+
+ while (waitpid(agent, &status, 0) == -1) {
+ if (errno != EINTR)
+ err(1, "agent wait");
+ }
+
+ if (WIFSIGNALED(status))
+ errx(1, "agent exited on signal %d", WTERMSIG(status));
+
+ switch (WEXITSTATUS(status)) {
+ case 0:
+ /* auth worked */
+ break;
+ case 8:
+ /* auth failed */
+ return (1);
+ default:
+ errx(1, "agent returned status %d", WEXITSTATUS(status));
+ /* NOTREACHED */
+ }
+
+ return (0);
}
Index: doas/doas.h
===================================================================
RCS file: /cvs/src/usr.bin/doas/doas.h,v
retrieving revision 1.4
diff -u -p -r1.4 doas.h
--- doas/doas.h 24 Jul 2015 06:36:42 -0000 1.4
+++ doas/doas.h 26 Jul 2015 04:39:17 -0000
@@ -19,5 +19,9 @@ size_t arraylen(const char **);
#define PERMIT 1
#define DENY 2
+#define AUTHMASK 0x3
+#define PASSAUTH 0x0
#define NOPASS 0x1
-#define KEEPENV 0x2
+#define SSHAGENT 0x2
+
+#define KEEPENV 0x4
Index: doas/parse.y
===================================================================
RCS file: /cvs/src/usr.bin/doas/parse.y,v
retrieving revision 1.10
diff -u -p -r1.10 parse.y
--- doas/parse.y 24 Jul 2015 06:36:42 -0000 1.10
+++ doas/parse.y 26 Jul 2015 04:39:17 -0000
@@ -56,7 +56,7 @@ int yyparse(void);
%}
%token TPERMIT TDENY TAS TCMD TARGS
-%token TNOPASS TKEEPENV
+%token TNOPASS TSSHAGENT TKEEPENV
%token TSTRING
%%
@@ -92,6 +92,16 @@ rule: action ident target cmd {
} ;
action: TPERMIT options {
+ switch ($2.options & AUTHMASK) {
+ case PASSAUTH:
+ case NOPASS:
+ case SSHAGENT:
+ break;
+ default:
+ yyerror("invalid authentication options");
+ YYERROR;
+ }
+
$$.action = PERMIT;
$$.options = $2.options;
$$.envlist = $2.envlist;
@@ -113,6 +123,8 @@ options: /* none */
} ;
option: TNOPASS {
$$.options = NOPASS;
+ } | TSSHAGENT {
+ $$.options = SSHAGENT;
} | TKEEPENV {
$$.options = KEEPENV;
} | TKEEPENV '{' envlist '}' {
@@ -192,6 +204,7 @@ struct keyword {
{ "cmd", TCMD },
{ "args", TARGS },
{ "nopass", TNOPASS },
+ { "ssh-agent", TSSHAGENT },
{ "keepenv", TKEEPENV },
};
--- /dev/null Sun Jul 26 14:41:12 2015
+++ ssh/doas.sshagent/Makefile Sun Jul 26 14:28:42 2015
@@ -0,0 +1,13 @@
+# $OpenBSD$
+
+.PATH: ${.CURDIR}/..
+
+PROG= doas.sshagent
+SRCS= doas.sshagent.c
+MAN=
+LDADD+= -lcrypto
+DPADD+= ${LIBCRYPTO}
+
+BINDIR= /usr/libexec
+
+.include <bsd.prog.mk>
--- /dev/null Sun Jul 26 14:41:20 2015
+++ ssh/doas.sshagent/doas.sshagent.c Sun Jul 26 14:16:33 2015
@@ -0,0 +1,136 @@
+/* $OpenBSD$ */
+
+/*
+ * Copyright (c) 2015 David Gwynne <[email protected]>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <err.h>
+
+#include "sshkey.h"
+#include "authfd.h"
+
+#define EXIT_OK 0
+#define EXIT_ERR 1
+#define EXIT_FAIL 8
+
+struct sshkey * parse_line(char *);
+int auth_key(int, struct sshkey *);
+
+#define debug2 warnx
+
+struct sshkey *
+parse_line(char *line)
+{
+ char *cp, *key_options;
+ struct sshkey *key;
+
+ /* Skip leading whitespace, empty and comment lines. */
+ for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
+ ;
+ if (!*cp || *cp == '\n' || *cp == '#')
+ return (NULL);
+
+ key = sshkey_new(KEY_UNSPEC);
+ if (key == NULL)
+ err(EXIT_ERR, "sshkey_new");
+
+ if (sshkey_read(key, &cp) != 0) {
+ /* no key? check if there are options for this key */
+ int quoted = 0;
+ debug2("user_key_allowed: check options: '%s'", cp);
+ for (key_options = cp;
+ *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(key, &cp) != 0) {
+ debug2("user_key_allowed: advance: '%s'", cp);
+ /* still no key? advance to next line */
+ goto fail;
+ }
+ }
+
+ return (key);
+
+fail:
+ sshkey_free(key);
+ return (NULL);
+}
+
+int
+auth_key(int agent, struct sshkey *key)
+{
+ u_char data[256];
+ u_char *sig;
+ size_t siglen;
+ int rv;
+
+ arc4random_buf(data, sizeof(data));
+
+ rv = ssh_agent_sign(agent, key, &sig, &siglen, data, sizeof(data), 0);
+ if (rv != 0)
+ return (rv);
+
+ rv = sshkey_verify(key, sig, siglen, data, sizeof(data), 0);
+
+ return (rv);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *line = NULL;
+ size_t linesize = 0;
+ ssize_t linelen;
+ struct sshkey *key, **keys = NULL;
+ u_int i, nkeys = 0;
+ int agent;
+
+ while ((linelen = getline(&line, &linesize, stdin)) != -1) {
+ key = parse_line(line);
+ if (key == NULL)
+ continue;
+
+ i = nkeys++;
+ keys = reallocarray(keys, nkeys, sizeof(*key));
+ if (keys == NULL)
+ err(1, "reallocarray");
+
+ keys[i] = key;
+ }
+
+ if (ferror(stdin))
+ err(EXIT_ERR, "getline");
+
+ if (nkeys == 0)
+ return (EXIT_FAIL);
+
+ if (ssh_get_authentication_socket(&agent) != 0)
+ errx(EXIT_ERR, "ssh_get_authentication_socket");
+
+ for (i = 0; i < nkeys; i++) {
+ if (auth_key(agent, keys[i]) == 0)
+ return (EXIT_OK);
+ }
+
+ return (EXIT_FAIL);
+}