Fair enough, thanks.
On Sun, Jul 26, 2015 at 07:19:06PM +1000, David Gwynne wrote: > > > On 26 Jul 2015, at 7:07 pm, Nicholas Marriott <[email protected]> > > wrote: > > > > Hi > > > > I can't say I know a lot about bsdauth so maybe this is a stupid > > question, but could this work as a login_* authentication method instead > > of doas doing it? > > i had a look at that. > > the biggest technical problem is that the bsd_auth framework filters all > environment variables from the caller before calling the actual auth handler. > if i wrote a login_sshagent thing for bsdauth, i cant get at the > SSH_AUTH_SOCK env var to be able to talk to the agent. doas (and other > programs) would have to be modified to explicitly pass the sockets location > as an argument somehow. threading that needle doesnt look like much fun. > > it was pointed out to me that this isnt that useful anywhere except for doas > and sudo, so generalising it has limited benefit. most bsd_auth uses are to > authenticate remote users, you dont have a local agent socket to connect to > in that situation. > > cheers, > dlg > > > > > > > > > On Sun, Jul 26, 2015 at 06:43:57PM +1000, David Gwynne wrote: > >> 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); > >> +} > >> >
