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 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 #include +#include #include #include @@ -26,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -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 --- /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 + * + * 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 +#include + +#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); +}