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

Reply via email to