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?
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);
> +}
>