OpenBSD mascot cutaway view with spinning gears inside

History

The way OpenBSD authenticates users is quite different from other Unix-like operating systems. Most other systems like AIX, Solaris, Linux, the other BSDs, and MacOS, use a framework called Pluggable Authentication Module (PAM). The two main implementations are Linux PAM and OpenPAM. PAM modules are created as dynamically loaded shared objects, which communicate using a combination of common and implementation specific interfaces (Linux-PAM and OpenPAM). It's configured using the pam.d directory and pam.conf file. While it can be flexible, it's highly complex and very easy to mis-configure, leaving you open to strange and hard to track down authentication bugs. On top of that, the fact that it's a shared library means that any vulnerability in a poorly vetted authentication module gives attackers direct access to the internals of your application. Author Michael W. Lucas said it best when he described PAM as unstandardized black magic.

OpenBSD on the other hand uses a mechanism called BSD Authentication. It was originally developed for a now-defunct proprietary operating system called BSD/OS by Berkeley Software Design Inc., who later donated the system. It was then adopted by OpenBSD in release 2.9. BSD Auth is comparatively much simpler than PAM. Modules or, authentication "styles", are instead stand alone applications or scripts that communicate over IPC. The module has no ability to interfere with the parent and can very easily revoke permissions using pledge(2) or unveil(2). The BSD Authentication system of configured through login.conf(5).

Documentation

All of the high level authentication functions are described in authenticate(3), with the lower level functions being described in auth_subr(3).

Click on any function prototype in this post to see its definition.

I've also created a graph at the bottom of the post to help visualize the function calls.

All code snippets from this blog post belong to the OpenBSD contributors. Please see the Copyright section for details.

BSD Auth Modules

Modules are located in /usr/libexec/auth/ with the naming convention login_<style>. They accept arguments in the following form.

login_<style> [-s service] [-v key=value] user [class]
  • <style> is the authentication method. This could be passwd, radius, skey, yubikey, etc. There's more information about available styles in login.conf(5) under the AUTHENTICATION header.
  • service is the service type. Typically authentication methods will accept one of three values here: login, challenge, or response. login is the default if it's not specified, and is used to let the module know to interact with the user directly through stdin and stdout, while challenge and response are used to pass messages back and forth through the BSD Auth API. Each style's man page will have more details on these.
  • -v key=value is an optional argument. There can be more than one arguments in this style. This is used to pass extra data to the program under certain circumstances.
  • user is the name of the user to be authenticated.
  • class is optional and specifies the login class to use for the user.

login and su pass in extra data as -v flags.

The login(1) program provides the following through the -v option:

   auth_type       The type of authentication to use.

   fqdn            The hostname provided to login by the -h option.

   hostname        The name login(1) will place in the utmp file for the
                   remote hostname.

   local_addr      The local IP address given to login(1) by the -L option.

   lastchance      Set to "yes" when a user's password has expired but the
                   user is being given one last chance to login and update
                   the password.

   login           This is a new login session (as opposed to a simple
                   identity check).

   remote_addr     The remote IP address given to login(1) by the -R option.

   style           The style of authentication used for this user (see
                   approval scripts below).

   The su(1) program provides the following through the -v option:

   wheel           Set to either "yes" or "no" to indicate if the user is in
                   group wheel when they are trying to become root.  Some
                   authentication types require the user to be in group
                   wheel when using the su(1) program to become super user.
Taken from login.conf(5)

The auth module communicates with its caller through what's called the "back channel" on file descriptor 3. This communication is covered in greater detail in the auth_call section.

Some modules require an extra file descriptor to be passed in for stateful challenge/response authentication. In these cases, an extra -v fd=4 argument will be passed. Theoretically this fd can be any number, but in practice fd=4 is hard-coded.

Most modules also have a hidden flag -d, which sets the back channel to stdout, presumably for debugging purposes.

The simplest way to authenticate a user with BSD Auth is by using auth_userokay.

For cases where challenge / response authentication is required and the user can't interacting through stdin and stdout, auth_userchallenge and auth_userresponse can be used.

Approval Scripts

Approval scripts can be much simpler than the full login modules used by the other functions. They are given the same back channel as auth modules, but should not explicitly authenticate or revoke users. They should exit with a zero status for approval, or non-zero status to signal disapproval.

Approval scrips receive arguments in the following form.

approve [-v name=value] username class service

It can also receive extra key-value -v arguments in the same format as auth modules. More information is available in the APPROVAL section of the login.conf man page.

Approval scripts are run using auth_approval.

auth_userokay

auth_userokay is the highest level function, and easiest to use. It takes four strings as arguments: name, style, type, and password. It returns either a 0 for failure, of a non-zero value for success.

int auth_userokay(char *name, char *style, char *type, char *password);

{
    auth_session_t *as;

    as = auth_usercheck(name, style, type, password);

    return (as != NULL ? auth_close(as) : 0);
}

  • name is the name of the user to be authenticated
  • style is the login method to be used

    • If style is NULL, the user's default login style will be used. This is passwd on normal accounts.
    • The style can be one of the installed authentication methods, like passwd, radius, skey, yubikey, etc.
    • Styles can also be installed through BSD Auth module packages
  • type is the authentication type

    • Types are defined in login.conf and as a group of allowed auth styles
    • If type is NULL, use the auth type for the user's login class. The default type is auth-default, which allows psaswd and skey auth methods.
  • password is the password to test

    • If password is NULL, then the user is interactively prompted. This is required for auth styles using challenge-response methods.
    • If password is specified, then it's passed to the auth module as a response

auth_userokay is just a wrapper around auth_usercheck that takes care of closing the session using auth_close, and returning the resulting value.

auth_session_t

auth_session_t is the main data structure used to represent the authentication session.

struct auth_session_t {
    char    *name;                 /* name of use being authenticated */
    char    *style;                /* style of authentication used */
    char    *class;                /* class of user */
    char    *service;              /* type of service being performed */
    char    *challenge;            /* last challenge issued */
    int     flags;                 /* see below */
    struct  passwd *pwd;           /* password entry for user */
    struct  timeval now;           /* time of authentication */

    int     state;                 /* authenticated state */

    struct  rmfiles *rmlist;       /* list of files to remove on failure */
    struct  authopts *optlist;     /* list of options to scripts */
    struct  authdata *data;        /* additional data to send to scripts */

    char    spool[MAXSPOOLSIZE];   /* data returned from login script */
    int     index;                 /* how much returned thus far */

    int     fd;                    /* connection to authenticator */

    va_list ap0;                   /* argument list to auth_call */
    va_list ap;                    /* additional arguments to auth_call */
};

Where MAXSPOOLSIZE, authdata, authopts, and rmfiles are defined as

#define	MAXSPOOLSIZE	(8*1024)	/* Spool up to 8K of back info */

struct rmfiles {
    struct rmfiles  *next;
    char            *file;
};

struct authopts {
    struct authopts *next;
    char            *opt;
};

struct authdata {
    struct  authdata *next;
    void    *ptr;
    size_t   len;
};

There are several functions which get used to operate on auth_session_t to keep it opaque.

auth_setdata

int auth_setdata(auth_session_t *as, void *ptr, size_t len)

{
    struct authdata *data, *dp;

    if (len <= 0)
        return (0);

    if ((data = malloc(sizeof(*data) + len)) == NULL)
        return (-1);

    data->next = NULL;
    data->len = len;
    data->ptr = data + 1;
    memcpy(data->ptr, ptr, len);

    if (as->data == NULL)
        as->data = data;
    else {
        for (dp = as->data; dp->next != NULL; dp = dp->next)
            ;
        dp->next = data;
    }
    return (0);
}

auth_setdata allocates and initializes a new authdata struct, storing a copy of the data from ptr and len. It then point the next field on the last authdata struct in as to its location. It returns 0 on success.

auth_setitem / auth_getitem

int auth_setitem(auth_session_t *as, auth_item_t item, char *value)

{
    if (as == NULL) {
        errno = EINVAL;
        return (-1);
    }

    switch (item) {
    case AUTHV_ALL:
        if (value != NULL) {
            errno = EINVAL;
            return (-1);
        }
        auth_setitem(as, AUTHV_CHALLENGE, NULL);
        auth_setitem(as, AUTHV_CLASS, NULL);
        auth_setitem(as, AUTHV_NAME, NULL);
        auth_setitem(as, AUTHV_SERVICE, NULL);
        auth_setitem(as, AUTHV_STYLE, NULL);
        auth_setitem(as, AUTHV_INTERACTIVE, NULL);
        return (0);

    case AUTHV_CHALLENGE:
        if (value == as->challenge)
            return (0);
        if (value != NULL && (value = strdup(value)) == NULL)
            return (-1);
        free(as->challenge);
        as->challenge = value;
        return (0);

    case AUTHV_CLASS:
        if (value == as->class)
            return (0);
        if (value != NULL && (value = strdup(value)) == NULL)
            return (-1);
        free(as->class);
        as->class = value;
        return (0);

    case AUTHV_NAME:
        if (value == as->name)
            return (0);
        if (value != NULL && !_auth_validuser(value)) {
            errno = EINVAL;
            return (-1);
        }
        if (value != NULL && (value = strdup(value)) == NULL)
            return (-1);
        free(as->name);
        as->name = value;
        return (0);

    case AUTHV_SERVICE:
        if (value == as->service)
            return (0);
        if (value == NULL || strcmp(value, defservice) == 0)
            value = defservice;
        else if ((value = strdup(value)) == NULL)
            return (-1);
        if (as->service && as->service != defservice)
            free(as->service);
        as->service = value;
        return (0);

    case AUTHV_STYLE:
        if (value == as->style)
            return (0);
        if (value == NULL || strchr(value, '/') != NULL ||
            (value = strdup(value)) == NULL)
            return (-1);
        free(as->style);
        as->style = value;
        return (0);

    case AUTHV_INTERACTIVE:
        if (value == NULL)
            as->flags &= ~AF_INTERACTIVE;
        else
            as->flags |= ~AF_INTERACTIVE;
        return (0);

    default:
        errno = EINVAL;
        return (-1);
    }
}

auth_setitem is used to set one of several different fields of as to value. Depending on the value of item (auth_item_t), it can be the challenge, class, name, service, style, or interactive field. If value is NULL, it clears that field. If item is AUTHV_ALL and value is NULL, all fields are cleared. It returns 0 on success.

Note: As of writing, the man page displays the incorrect name for the constants.

AUTH_CHALLENGE
    The latest challenge, if any, set for the session.

AUTH_CLASS
    The class of the user, as defined by the /etc/login.conf file.
    This value is not directly used by BSD Authentication, rather, it
    is passed to the login scripts for their possible use.

AUTH_INTERACTIVE
    If set to any value, then the session is tagged as interactive. If
    not set, the session is not interactive. When the value is
    requested it is always either NULL or “True”. The auth subroutines
    may choose to provide additional information to standard output or
    standard error when the session is interactive. There is no
    functional change in the operation of the subroutines.

AUTH_NAME
    The name of the user being authenticated. The name should include
    the instance, if any, that is being requested.

AUTH_SERVICE
    The service requesting the authentication. Initially it is set to
    the default service which provides the traditional interactive
    service.

AUTH_STYLE
    The style of authentication being performed, as defined by the
    /etc/login.conf file. The style determines which login script
    should actually be used.
Taken from auth_subr(3)

char *auth_getitem(auth_session_t *as, auth_item_t item)

{
    if (as != NULL) {
        switch (item) {
        case AUTHV_CHALLENGE:
            return (as->challenge);
        case AUTHV_CLASS:
            return (as->class);
        case AUTHV_NAME:
            return (as->name);
        case AUTHV_SERVICE:
            return (as->service ? as->service : defservice);
        case AUTHV_STYLE:
            return (as->style);
        case AUTHV_INTERACTIVE:
            return ((as->flags & AF_INTERACTIVE) ? "True" : NULL);
        default:
            break;
        }
    }
    return (NULL);
}

auth_getitem is used to return the value of the fields listed above.

auth_item_t

auth_item_t is an enum defined in /include/bsd_auth.h.

typedef enum {
    AUTHV_ALL,
    AUTHV_CHALLENGE,
    AUTHV_CLASS,
    AUTHV_NAME,
    AUTHV_SERVICE,
    AUTHV_STYLE,
    AUTHV_INTERACTIVE
} auth_item_t;

auth_setoption

int auth_setoption(auth_session_t *as, char *n, char *v)

{
    struct authopts *opt;
    size_t len = strlen(n) + strlen(v) + 2;
    int ret;

    if ((opt = malloc(sizeof(*opt) + len)) == NULL)
        return (-1);

    opt->opt = (char *)(opt + 1);

    ret = snprintf(opt->opt, len, "%s=%s", n, v);
    if (ret < 0 || ret >= len) {
        free(opt);
        errno = ENAMETOOLONG;
        return (-1);
    }
    opt->next = as->optlist;
    as->optlist = opt;
    return(0);
}

auth_setoption initializes a new authopts struct, and sets the opt field to a string formatted as sprintf("%s=%s", n, v). It then point the next field on the last authopts struct in as to its location. It returns 0 on success.

auth_setstate / auth_getstate

void	auth_setstate(auth_session_t *as, int s)

{ as->state = s; }

auth_setstate sets the state of as to s.

int	auth_getstate(auth_session_t *as)

{ return (as->state); }

auth_getstate return the state of as.

auth_setpwd / auth_getpwd

int auth_setpwd(auth_session_t *as, struct passwd *pwd)

{
    struct passwd pwstore;
    char *instance, pwbuf[_PW_BUF_LEN];

    if (pwd == NULL && as->pwd == NULL && as->name == NULL)
        return (-1);		/* true failure */

    if (pwd == NULL) {
        /*
         * If we were not passed in a pwd structure we need to
         * go find one for ourself.  Always look up the username
         * (if it is defined) in the passwd database to see if there
         * is an entry for the user.  If not, either use the current
         * entry or simply return a 1 which implies there is
         * no user by that name here.  This is not a failure, just
         * a point of information.
         */
        if (as->name == NULL)
            return (0);
        getpwnam_r(as->name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
        if (pwd == NULL) {
            instance = strchr(as->name, '/');
            if (instance == NULL)
                return (as->pwd ? 0 : 1);
            if (strcmp(instance, "/root") == 0) {
                getpwnam_r(instance + 1, &pwstore, pwbuf,
                    sizeof(pwbuf), &pwd);
            }
            if (pwd == NULL)
                return (as->pwd ? 0 : 1);
        }
    }
    if ((pwd = pw_dup(pwd)) == NULL)
        return (-1);		/* true failure */
    if (as->pwd) {
        explicit_bzero(as->pwd->pw_passwd, strlen(as->pwd->pw_passwd));
        free(as->pwd);
    }
    as->pwd = pwd;
    return (0);
}

auth_setpwd is used to retrieve and set the password database entry in as if one isn't already set.

If a passwd entry is passed in through pwd, it uses that to set as->pwd. If pwd is NULL, it tries to find the passwd entry associated with as->name. If it finds one, it sets as->pwd and returns 0. If there is no entry with that username, it returns 1.

struct passwd *auth_getpwd(auth_session_t *as)

{ return (as->pwd); }

auth_getpwd returns as->pwd.

auth_set_va_list

void auth_set_va_list(auth_session_t *as, va_list ap)

{ va_copy(as->ap, ap); }

auth_set_va_list copies ap to as->ap.

auth_clrenv

void auth_clrenv(auth_session_t *as)

{
    char *line;

    for (line = as->spool; line < as->spool + as->index;) {
        if (!strncasecmp(line, BI_SETENV, sizeof(BI_SETENV)-1)) {
            if (isblank((unsigned char)line[sizeof(BI_SETENV) - 1])) {
                line[0] = 'i'; line[1] = 'g'; line[2] = 'n';
            }
        } else
        if (!strncasecmp(line, BI_UNSETENV, sizeof(BI_UNSETENV)-1)) {
            if (isblank((unsigned char)line[sizeof(BI_UNSETENV) - 1])) {
                line[2] = 'i'; line[3] = 'g'; line[4] = 'n';
            }
        }
        while (*line++)
            ;
    }
}

auth_clrenv removes all lines containing BI_SETENV and BI_UNSETENV from as->spool. This is explained under the auth_call section.

auth_clroption

void auth_clroption(auth_session_t *as, char *option)

{
    struct authopts *opt, *oopt;
    size_t len;

    len = strlen(option);

    if ((opt = as->optlist) == NULL)
        return;

    if (strncmp(opt->opt, option, len) == 0 &&
        (opt->opt[len] == '=' || opt->opt[len] == '\0')) {
        as->optlist = opt->next;
        free(opt);
        return;
    }

    while ((oopt = opt->next) != NULL) {
        if (strncmp(oopt->opt, option, len) == 0 &&
            (oopt->opt[len] == '=' || oopt->opt[len] == '\0')) {
            opt->next = oopt->next;
            free(oopt);
            return;
        }
        opt = oopt;
    }
}

auth_clroption removes the option named option from as.

auth_clroptions

void auth_clroptions(auth_session_t *as)

{
    struct authopts *opt;

    while ((opt = as->optlist) != NULL) {
        as->optlist = opt->next;
        free(opt);
    }
}

auth_clroptions clears all options from as.

auth_setenv

void auth_setenv(auth_session_t *as)

{
    char *line, *name;

    /*
     ,* Set any environment variables we were asked for
     ,*/
        for (line = as->spool; line < as->spool + as->index;) {
        if (!strncasecmp(line, BI_SETENV, sizeof(BI_SETENV)-1)) {
            if (isblank((unsigned char)line[sizeof(BI_SETENV) - 1])) {
                /* only do it once! */
                line[0] = 'd'; line[1] = 'i'; line[2] = 'd';
                line += sizeof(BI_SETENV) - 1;
                for (name = line;
                    isblank((unsigned char)*name); ++name)
                    ;
                for (line = name;
                    ,*line && !isblank((unsigned char)*line);
                    ++line)
                    ;
                if (*line)
                    ,*line++ = '\0';
                for (; isblank((unsigned char)*line); ++line)
                    ;
                if (*line != '\0' && setenv(name, line, 1))
                    warn("setenv(%s, %s)", name, line);
            }
        } else
        if (!strncasecmp(line, BI_UNSETENV, sizeof(BI_UNSETENV)-1)) {
            if (isblank((unsigned char)line[sizeof(BI_UNSETENV) - 1])) {
                /* only do it once! */
                line[2] = 'd'; line[3] = 'i'; line[4] = 'd';
                line += sizeof(BI_UNSETENV) - 1;
                for (name = line;
                    isblank((unsigned char)*name); ++name)
                    ;
                for (line = name;
                    ,*line && !isblank((unsigned char)*line);
                    ++line)
                    ;
                if (*line)
                    ,*line++ = '\0';
                unsetenv(name);
            }
        }
        while (*line++)
            ;
    }
}

auth_setenv scans through as->spool, modifying the environment according to BI_SETENV and BI_UNSETENV instructions.

auth_getvalue

char *auth_getvalue(auth_session_t *as, char *what)

{
    char *line, *v, *value;
    int n, len;

    len = strlen(what);

        for (line = as->spool; line < as->spool + as->index;) {
        if (strncasecmp(line, BI_VALUE, sizeof(BI_VALUE)-1) != 0)
            goto next;
        line += sizeof(BI_VALUE) - 1;

        if (!isblank((unsigned char)*line))
            goto next;

        while (isblank((unsigned char)*++line))
            ;

        if (strncmp(line, what, len) != 0 ||
            !isblank((unsigned char)line[len]))
            goto next;
        line += len;
        while (isblank((unsigned char)*++line))
            ;
        value = strdup(line);
        if (value == NULL)
            return (NULL);

        /*
         ,* XXX - There should be a more standardized
         ,* routine for doing this sort of thing.
         ,*/
        for (line = v = value; *line; ++line) {
            if (*line == '\\') {
                switch (*++line) {
                case 'r':
                    ,*v++ = '\r';
                    break;
                case 'n':
                    ,*v++ = '\n';
                    break;
                case 't':
                    ,*v++ = '\t';
                    break;
                case '0': case '1': case '2':
                case '3': case '4': case '5':
                case '6': case '7':
                    n = *line - '0';
                    if (isdigit((unsigned char)line[1])) {
                        ++line;
                        n <<= 3;
                        n |= *line-'0';
                    }
                    if (isdigit((unsigned char)line[1])) {
                        ++line;
                        n <<= 3;
                        n |= *line-'0';
                    }
                    break;
                default:
                    ,*v++ = *line;
                    break;
                }
            } else
                ,*v++ = *line;
        }
        ,*v = '\0';
        return (value);
next:
        while (*line++)
            ;
    }
    return (NULL);
}

auth_getvalue scans as->spool looking for lines beginning with BI_VALUE. It then checks if the next word is equal to what.

When it finds the desired line, it duplicates the string, converts escape sequences in the value, and returns the newly created string.

For convenience, the function auth_mkvalue can be used inside of the authentication module to create and return appropriately escaped value strings.

auth_getchallenge

The auth_subr(3) man page claims this function exists, but I can't find it anywhere in the source code. I suspect this is an error.

auth_open

auth_session_t *auth_open(void)

{
    auth_session_t *as;

    if ((as = calloc(1, sizeof(auth_session_t))) != NULL) {
        as->service = defservice;
        as->fd = -1;
    }

    return (as);
}

auth_open is used by several functions to create a new auth session. It allocates an auth_session_t struct on the heap, sets its default service to that defined by LOGIN_DEFSERVICE in /include/login_cap.h, which is currently "login".

#define	LOGIN_DEFSERVICE	"login"

It then sets the fd field to -1, and returns the pointer.

auth_usercheck

auth_session_t *auth_usercheck(char *name, char *style, char *type, char *password)

{
    char namebuf[LOGIN_NAME_MAX + 1 + NAME_MAX + 1];
    char pwbuf[_PW_BUF_LEN];
    auth_session_t *as;
    login_cap_t *lc;
    struct passwd pwstore, *pwd = NULL;
    char *slash;

    if (!_auth_validuser(name))
        return (NULL);
    if (strlcpy(namebuf, name, sizeof(namebuf)) >= sizeof(namebuf))
        return (NULL);
    name = namebuf;

    /*
     ,* Split up user:style names if we were not given a style
     ,*/
    if (style == NULL && (style = strchr(name, ':')) != NULL)
        ,*style++ = '\0';

    /*
     ,* Cope with user/instance.  We are only using this to get
     ,* the class so it is okay if we strip a /root instance
     ,* The actual login script will pay attention to the instance.
     ,*/
    getpwnam_r(name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
    if (pwd == NULL) {
        if ((slash = strchr(name, '/')) != NULL) {
            ,*slash = '\0';
            getpwnam_r(name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
            ,*slash = '/';
        }
    }
    if ((lc = login_getclass(pwd ? pwd->pw_class : NULL)) == NULL)
        return (NULL);

    if ((style = login_getstyle(lc, style, type)) == NULL) {
        login_close(lc);
        return (NULL);
    }

    if (password) {
        if ((as = auth_open()) == NULL) {
            login_close(lc);
            return (NULL);
        }
        auth_setitem(as, AUTHV_SERVICE, "response");
        auth_setdata(as, "", 1);
        auth_setdata(as, password, strlen(password) + 1);
        explicit_bzero(password, strlen(password));
    } else
        as = NULL;
    as = auth_verify(as, style, name, lc->lc_class, (char *)NULL);
    login_close(lc);
    return (as);
}

auth_usercheck is very similar to auth_userokay. It takes the same arguments, except it returns the auth_session_t struct instead of just the status.

It first checks that name is valid according to _auth_validuser.

If style is NULL, it checks if name is in the user:style format, and splits it accordingly.

It then gets the user's password database entry through getpwman_r(3), which operates on the passwd(5) database. After it uses that to retrieve the user's login class using login_getclass(3), which returns a login_cap_t. Login classes are stored in the login.conf(5) database.

That struct is then passed into login_getstyle(3), which also received the style and type. If type is NULL, it returns the first available login style for that class. If style is specified, it is returned if available, otherwise NULL is returned, which causes auth_usercheck to return NULL as well.

It then creates a pointer as of type auth_session_t, and handles it differently based on whether password is NULL.

  • If the password is a string, it creates a new session using auth_open and assigns it to as. It then sets the session service to "response", and adds the password string to the session's data.

    auth_setitem(as, AUTHV_SERVICE, "response");
    auth_setdata(as, "", 1);
    auth_setdata(as, password, strlen(password) + 1);
  • If password is NULL, it sets as to NULL.

It then passes the auth_session_t pointer (as), name, style, login class (lc->lc_class), and a NULL char pointer to auth_verify. Finally it returns the auth session pointer.

as = auth_verify(as, style, name, lc->lc_class, (char *)NULL);
// [...] some cleanup
return (as);

auth_verify

auth_session_t *auth_verify(auth_session_t *as, char *style, char *name, ...)

{
    va_list ap;
    char path[PATH_MAX];

    if ((name == NULL || style == NULL) && as == NULL)
        return (NULL);

    if (as == NULL && (as = auth_open()) == NULL)
        return (NULL);
    auth_setstate(as, 0);

    if (style != NULL && auth_setitem(as, AUTHV_STYLE, style) < 0)
        return (as);

    if (name != NULL && auth_setitem(as, AUTHV_NAME, name) < 0)
        return (as);

    style = auth_getitem(as, AUTHV_STYLE);
    name = auth_getitem(as, AUTHV_NAME);
    if (!_auth_validuser(name))
        return (as);

    snprintf(path, sizeof(path), _PATH_AUTHPROG "%s", style);
    va_start(ap, name);
    auth_set_va_list(as, ap);
    auth_call(as, path, auth_getitem(as, AUTHV_STYLE), "-s",
        auth_getitem(as, AUTHV_SERVICE), "--", name, (char *)NULL);
    va_end(ap);
    return (as);
}

auth_verify is used as a frontend for auth_call.

It creates an auth session using auth_open if as is NULL.

The state of the session is set to 0.

It sets the name and style of the session, if the style and/or name are non-NULL.

After that it constructs the path of the authentication module, placing it in the variable path. It is constructed by combining _PATH_AUTHPROG, which is defined in login_cap.h as /usr/libexec/auth/login_, and the authentication style. For the case of auth style passwd, it would result in the path /usr/libexec/auth/login_passwd.

snprintf(path, sizeof(path), _PATH_AUTHPROG "%s", style);

It then copies its variable arguments to the auth session using auth_set_va_list.

Then auth_call is called with the session struct, the path to the auth module, the auth style, the -s flag followed by the service (login, challenge, or response), a double dash, the user name, and a NULL character pointer. The return value of auth_call is ignored and a pointer to the auth session is returned immediately afterwards.

va_start(ap, name);
auth_set_va_list(as, ap);
auth_call(as, path, auth_getitem(as, AUTHV_STYLE), "-s",
          auth_getitem(as, AUTHV_SERVICE), "--", name, (char *)NULL);
va_end(ap);
return (as);

auth_call

int auth_call(auth_session_t *as, char *path, ...)

{
    char *line;
    struct authdata *data;
    struct authopts *opt;
    pid_t pid;
    int status;
    int okay;
    int pfd[2];
    int argc;
    char *argv[64];		/* 64 args should be more than enough */
#define	Nargc	(sizeof(argv)/sizeof(argv[0]))

    va_start(as->ap0, path);

    argc = 0;
    if ((argv[argc] = _auth_next_arg(as)) != NULL)
        ++argc;

    if (as->fd != -1) {
        argv[argc++] = "-v";
        argv[argc++] = "fd=4";		/* AUTH_FD, see below */
    }
    /* XXX - fail if out of space in argv */
    for (opt = as->optlist; opt != NULL; opt = opt->next) {
        if (argc < Nargc - 2) {
            argv[argc++] = "-v";
            argv[argc++] = opt->opt;
        } else {
            syslog(LOG_ERR, "too many authentication options");
            goto fail;
        }
    }
    while (argc < Nargc - 1 && (argv[argc] = _auth_next_arg(as)))
        ++argc;

    if (argc >= Nargc - 1 && _auth_next_arg(as)) {
        if (memcmp(&nilap, &(as->ap0), sizeof(nilap)) != 0) {
            va_end(as->ap0);
            explicit_bzero(&(as->ap0), sizeof(as->ap0));
        }
        if (memcmp(&nilap, &(as->ap), sizeof(nilap)) != 0) {
            va_end(as->ap);
            explicit_bzero(&(as->ap), sizeof(as->ap));
        }
        syslog(LOG_ERR, "too many arguments");
        goto fail;
    }

    argv[argc] = NULL;

    if (socketpair(PF_LOCAL, SOCK_STREAM, 0, pfd) == -1) {
        syslog(LOG_ERR, "unable to create backchannel %m");
        warnx("internal resource failure");
        goto fail;
    }

    switch (pid = fork()) {
    case -1:
        syslog(LOG_ERR, "%s: %m", path);
        warnx("internal resource failure");
        close(pfd[0]);
        close(pfd[1]);
        goto fail;
    case 0:
#define	COMM_FD	3
#define	AUTH_FD	4
        if (dup2(pfd[1], COMM_FD) == -1)
            err(1, "dup of backchannel");
        if (as->fd != -1) {
            if (dup2(as->fd, AUTH_FD) == -1)
                err(1, "dup of auth fd");
            closefrom(AUTH_FD + 1);
        } else
            closefrom(COMM_FD + 1);
        execve(path, argv, auth_environ);
        syslog(LOG_ERR, "%s: %m", path);
        err(1, "%s", path);
    default:
        close(pfd[1]);
        if (as->fd != -1) {
            close(as->fd);		/* so child has only ref */
            as->fd = -1;
        }
        while ((data = as->data) != NULL) {
            as->data = data->next;
            if (data->len > 0) {
                write(pfd[0], data->ptr, data->len);
                explicit_bzero(data->ptr, data->len);
            }
            free(data);
        }
        as->index = 0;
        _auth_spool(as, pfd[0]);
        close(pfd[0]);
        do {
            if (waitpid(pid, &status, 0) != -1) {
                if (!WIFEXITED(status))
                    goto fail;
                break;
            }
            /*
             ,* could get ECHILD if it was waited for by
             ,* another thread or from a signal handler
             ,*/
        } while (errno == EINTR);
    }

    /*
     ,* Now scan the spooled data
     ,* It is easier to wait for all the data before starting
     ,* to scan it.
     ,*/
        for (line = as->spool; line < as->spool + as->index;) {
        if (!strncasecmp(line, BI_REJECT, sizeof(BI_REJECT)-1)) {
            line += sizeof(BI_REJECT) - 1;
            if (!*line || *line == ' ' || *line == '\t') {
                while (*line == ' ' || *line == '\t')
                    ++line;
                if (!strcasecmp(line, "silent")) {
                    as->state = AUTH_SILENT;
                    break;
                }
                if (!strcasecmp(line, "challenge")) {
                    as->state  = AUTH_CHALLENGE;
                    break;
                }
                if (!strcasecmp(line, "expired")) {
                    as->state  = AUTH_EXPIRED;
                    break;
                }
                if (!strcasecmp(line, "pwexpired")) {
                    as->state  = AUTH_PWEXPIRED;
                    break;
                }
            }
            break;
        } else if (!strncasecmp(line, BI_AUTH, sizeof(BI_AUTH)-1)) {
            line += sizeof(BI_AUTH) - 1;
            if (!*line || *line == ' ' || *line == '\t') {
                while (*line == ' ' || *line == '\t')
                    ++line;
                if (*line == '\0')
                    as->state |= AUTH_OKAY;
                else if (!strcasecmp(line, "root"))
                    as->state |= AUTH_ROOTOKAY;
                else if (!strcasecmp(line, "secure"))
                    as->state |= AUTH_SECURE;
            }
        } else if (!strncasecmp(line, BI_REMOVE, sizeof(BI_REMOVE)-1)) {
            line += sizeof(BI_REMOVE) - 1;
            while (*line == ' ' || *line == '\t')
                ++line;
            if (*line)
                _add_rmlist(as, line);
        }
        while (*line++)
            ;
    }

    if (WEXITSTATUS(status))
        as->state &= ~AUTH_ALLOW;

    okay = as->state & AUTH_ALLOW;

    if (!okay)
        auth_clrenv(as);

    if (0) {
fail:
        auth_clrenv(as);
        as->state = 0;
        okay = -1;
    }

    while ((data = as->data) != NULL) {
        as->data = data->next;
        free(data);
    }

    if (memcmp(&nilap, &(as->ap0), sizeof(nilap)) != 0) {
        va_end(as->ap0);
        explicit_bzero(&(as->ap0), sizeof(as->ap0));
    }

    if (memcmp(&nilap, &(as->ap), sizeof(nilap)) != 0) {
        va_end(as->ap);
        explicit_bzero(&(as->ap), sizeof(as->ap));
    }
    return (okay);
}

auth_call is responsible for setting up the environment, calling the modules, and communicating with them.

An array of char pointers called argv is allocated to hold the arguments for the auth module.

char *argv[64];		/* 64 args should be more than enough */

First, the variable arguments are placed in as->ap0.

_auth_next_arg is called once, with the result being set as the first element in argv. If as->fd is set, adds -v and fd=4 to argv.

Then it loops through the optlist and appends -v followed the option for each of them.

After that the rest of the arguments are retrieved from _auth_next_arg and added to the end of argv. Finally a NULL is added to the end of argv.

Next a socket pair of type PF_LOCAL, SOCK_STREAM is created. This is called the "back channel", and is used to communicate with the authentication module.

The process then calls fork(2).

Here two constants are set for the back channel and optional authentication file descriptors.

#define	COMM_FD	3
#define	AUTH_FD	4

In the child process, the back channel is set to file descriptor 3, or COMM_FD using dup2(3). If as->fd, is not -1, it is set to file descriptor 4, or AUTH_FD, also using dup2(3). The remainder of the file descriptors are closed using closefrom(2) by calling either closefrom(COMM_FD + 1) or closefrom(AUTH_FD + 1), depending on whether or not AUTH_FD is used.

The child process then executes the module.

execve(path, argv, auth_environ);

auth_environ is defined at the top of the file as a very minimal environment.

static char *auth_environ[] = {
    "PATH=" _PATH_DEFPATH,
    "SHELL=" _PATH_BSHELL,
    NULL,
};

Where both constants are defined in /include/paths.h.

#define	_PATH_DEFPATH	"/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin"
#define	_PATH_BSHELL	"/bin/sh"

In the parent process, the child's end of the back channel is closed, and so is the parent's copy of as->fd if it exists.

The data from as->data is then written to the back channel sequentially, zeroed, and freed.

Next as->index is set to 0.

The response from the authentication module is then read from the back channel and put into as->spool with an optional received file descriptor placed in as->fd, using _auth_spool.

_auth_spool(as, pfd[0]);

Once the back channel data has finished spooling, it is scanned for key words defined in login_cap.h.

#define BI_AUTH         "authorize"         /* Accepted authentication */
#define BI_REJECT       "reject"            /* Rejected authentication */
#define BI_CHALLENGE    "reject challenge"  /* Reject with a challenge */
#define BI_SILENT       "reject silent"     /* Reject silently */
#define BI_REMOVE       "remove"            /* remove file on error */
#define BI_ROOTOKAY     "authorize root"    /* root authenticated */
#define BI_SECURE       "authorize secure"  /* okay on non-secure line */
#define BI_SETENV       "setenv"            /* set environment variable */
#define BI_UNSETENV     "unsetenv"          /* unset environment variable */
#define BI_VALUE        "value"             /* set local variable */
#define BI_EXPIRED      "reject expired"    /* account expired */
#define BI_PWEXPIRED    "reject pwexpired"  /* password expired */
#define BI_FDPASS       "fd"                /* child is passing an fd */

The login.conf(5) man page once again goes into greater detail on these values.

authorize  The user has been authorized.

authorize secure
           The user has been authorized and root should be allowed to
           login even if this is not a secure terminal.  This should only
           be sent by authentication styles that are secure over insecure
           lines.

reject     Authorization is rejected.  This overrides any indication that
           the user was authorized (though one would question the wisdom
           in sending both a reject and an authorize command).

reject challenge
           Authorization was rejected and a challenge has been made
           available via the value challenge.

reject silent
           Authorization is rejected, but no error messages should be
           generated.

remove file
           If the login session fails for any reason, remove file before
           termination.

setenv name value
           If the login session succeeds, the environment variable name
           should be set to the specified value.

unsetenv name
           If the login session succeeds, the environment variable name
           should be removed.

value name value
           Set the internal variable name to the specified value.  The
           value should only contain printable characters.  Several \
           sequences may be used to introduce non printing characters.
           These are:

           \n      A newline.

           \r      A carriage return.

           \t      A tab.

           \xxx    The character represented by the octal value xxx.  The
                   value may be one, two, or three octal digits.

           \c      The string is replaced by the value of c.  This allows
                   quoting an initial space or the \ character itself.


           The following values are currently defined:

           challenge
                   See section on challenges below.

           errormsg
                   If set, the value is the reason authentication failed.
                   The calling program may choose to display this when
                   rejecting the user, but display is not required.

The scanner is looking for lines that begin with BI_AUTH, BI_REJECT, or BI_REMOVE.

Here as->state is set according to the values defined on login_cap.h.

/*
 * bits which can be returned by authenticate()/auth_scan()
 */
#define  AUTH_OKAY       0x01            /* user authenticated */
#define  AUTH_ROOTOKAY   0x02            /* authenticated as root */
#define  AUTH_SECURE     0x04            /* secure login */
#define  AUTH_SILENT     0x08            /* silent rejection */
#define  AUTH_CHALLENGE  0x10            /* a challenge was given */
#define  AUTH_EXPIRED    0x20            /* account expired */
#define  AUTH_PWEXPIRED  0x40            /* password expired */

If a rejection is received (any line starting with BI_REJECT), as->state is set according to the rejection, and the scanning is stopped. Rejections are final and take precedence over any authorizations.

If an authorization is received (any line starting with BI_AUTH), the appropriate state is bitwise or-ed onto as->state. This allows multiple authorizations, such as a case where both BI_ROOTOKAY and BI_SECURE are sent. This would result in a state of AUTH_OKAY || AUTH_ROOTOKAY || AUTH_SECURE.

For any lines beginning with BI_REMOVE, the file names after the key word are sent to _add_rmlist.

_add_rmlist(as, line);

After scanning is complete, the exit status of the process is checked. A non-zero exit status means the request will get denied.

An okay value is then defined by masking the state with the value AUTH_ALLOW.

okay = as->state & AUTH_ALLOW;

AUTH_ALLOW is defined in login_cap.h.

#define	AUTH_ALLOW	(AUTH_OKAY | AUTH_ROOTOKAY | AUTH_SECURE)

If the status results in a rejection, auth_clrenv is called with as. This removes any requests the login script has made to set environment variables from as->spool.

okay is then returned to the caller.

_auth_next_arg

static char *_auth_next_arg(auth_session_t *as)

{
    char *arg;

    if (memcmp(&nilap, &(as->ap0), sizeof(nilap)) != 0) {
        if ((arg = va_arg(as->ap0, char *)) != NULL)
            return (arg);
        va_end(as->ap0);
        explicit_bzero(&(as->ap0), sizeof(as->ap0));
    }
    if (memcmp(&nilap, &(as->ap), sizeof(nilap)) != 0) {
        if ((arg = va_arg(as->ap, char *)) != NULL)
            return (arg);
        va_end(as->ap);
        explicit_bzero(&(as->ap), sizeof(as->ap));
    }
    return (NULL);
}

Loops through as->ap0 then as->ap, returning one argument per call. Calls va_end on each list once it finishes with them, then explicit_bzero(3)'s them.

Finally when it's gone through both lists, returns NULL

_auth_spool

static void _auth_spool(auth_session_t *as, int fd)

{
    ssize_t r;
    char *b, *s;

    for (s = as->spool + as->index; as->index < sizeof(as->spool) - 1; ) {
        r = read(fd, as->spool + as->index,
            sizeof(as->spool) - as->index);
        if (r <= 0) {
            as->spool[as->index] = '\0';
            return;
        }
        b = as->spool + as->index;
        as->index += r;
        /*
         ,* Convert newlines into NULs to allow easy scanning of the
         ,* file and receive an fd if there is a BI_FDPASS message.
         ,* XXX - checking for BI_FDPASS here is annoying but
         ,*       we need to avoid the read() slurping in control data.
         ,*/
        while (r-- > 0) {
            if (*b++ == '\n') {
                b[-1] = '\0';
                if (strcasecmp(s, BI_FDPASS) == 0)
                    _recv_fd(as, fd);
                s = b;
            }
        }
    }

    syslog(LOG_ERR, "Overflowed backchannel spool buffer");
    errx(1, "System error in authentication program");
}

_auth_spool's job is to read data from fd and place it in as->spool, and to update as->index with the length of the data on the spool. While spooling it converts newlines to NUL's in order to parse the output more easily. It also handles any file descriptors passed through the back channel by sending them to _recv_fd.

// [...]
if (strcasecmp(s, BI_FDPASS) == 0)
    _recv_fd(as, fd);

_recv_fd

static void _recv_fd(auth_session_t *as, int fd)

{
    struct msghdr msg;
    struct cmsghdr *cmp;
    union {
        struct cmsghdr hdr;
        char buf[CMSG_SPACE(sizeof(int))];
    } cmsgbuf;

    memset(&msg, 0, sizeof(msg));
    msg.msg_control = &cmsgbuf.buf;
    msg.msg_controllen = sizeof(cmsgbuf.buf);
    if (recvmsg(fd, &msg, 0) == -1)
        syslog(LOG_ERR, "recvmsg: %m");
    else if (msg.msg_flags & MSG_TRUNC)
        syslog(LOG_ERR, "message truncated");
    else if (msg.msg_flags & MSG_CTRUNC)
        syslog(LOG_ERR, "control message truncated");
    else if ((cmp = CMSG_FIRSTHDR(&msg)) == NULL)
        syslog(LOG_ERR, "missing control message");
    else {
        if (cmp->cmsg_level != SOL_SOCKET)
            syslog(LOG_ERR, "unexpected cmsg_level %d",
                cmp->cmsg_level);
        else if (cmp->cmsg_type != SCM_RIGHTS)
            syslog(LOG_ERR, "unexpected cmsg_type %d",
                cmp->cmsg_type);
        else if (cmp->cmsg_len != CMSG_LEN(sizeof(int)))
            syslog(LOG_ERR, "bad cmsg_len %d",
                cmp->cmsg_len);
        else {
            if (as->fd != -1)
                close(as->fd);
            as->fd = *(int *)CMSG_DATA(cmp);
        }
    }
}

_recv_fd reads control messages, also called ancillary data, from fd and tries to receive a file descriptor. It does this using the control message API.

If it receives one and as->fd is equal to -1, it sets it to the received file descriptor. Otherwise it closes the received file descriptor.

_add_rmlist

static void _add_rmlist(auth_session_t *as, char *file)

{
    struct rmfiles *rm;
    size_t i = strlen(file) + 1;

    // XXX should rangecheck i since we are about to add?

    if ((rm = malloc(sizeof(struct rmfiles) + i)) == NULL) {
        syslog(LOG_ERR, "Failed to allocate rmfiles: %m");
        return;
    }
    rm->file = (char *)(rm + 1);
    rm->next = as->rmlist;
    strlcpy(rm->file, file, i);
    as->rmlist = rm;
}

_add_rmlist is used to add to the list of files to be removed after authentication is complete

A rmfiles struct is allocated and appended to the end of the as->rmlist linked list.

auth_close

int auth_close(auth_session_t *as)

{
    struct rmfiles *rm;
    struct authopts *opt;
    struct authdata *data;
    int s;

    /*
     ,* Save our return value
     ,*/
    s = as->state & AUTH_ALLOW;

    if (s == 0)
        as->index = 0;

    auth_setenv(as);


    /*
     ,* Clean out the rmlist and remove specified files if the
     ,* authentication failed
     ,*/
    while ((rm = as->rmlist) != NULL) {
        as->rmlist = rm->next;
        if (s == 0)
            unlink(rm->file);
        free(rm);
    }

    /*
     ,* Clean out the opt list
     ,*/
    while ((opt = as->optlist) != NULL) {
        as->optlist = opt->next;
        free(opt);
    }

    /*
     ,* Clean out data
     ,*/
    while ((data = as->data) != NULL) {
        if (as->data->len)
            explicit_bzero(as->data->ptr, as->data->len);
        as->data = data->next;
        free(data);
    }

    if (as->pwd != NULL) {
        explicit_bzero(as->pwd->pw_passwd, strlen(as->pwd->pw_passwd));
        free(as->pwd);
        as->pwd = NULL;
    }

    /*
     ,* Clean up random variables
     ,*/
    if (as->service && as->service != defservice)
        free(as->service);
    free(as->challenge);
    free(as->class);
    free(as->style);
    free(as->name);

    free(as);
    return (s);
}

auth_close is responsible for setting the environment variables, removing any files requested by the authentication module, and freeing as.

First it saves the allow state of as->state in a variable s.

s = as->state & AUTH_ALLOW;

If s is equal to 0 (failure), as->index is set to 0, truncating as->spool so that no further functions will be able to read from it.

It then modifies the environment using auth_setenv

auth_setenv(as);

All as->rmlist structs are checked. If s is equal to 0, the files are deleted. All rmlist structs are then freed.

All as->optlist structs are freed.

All as->data structs are explicit_bzero(3)'d and then freed.

as->pwd is explicit_bzero'd and freed.

All remaining structs referenced by as are freed.

as is freed.

s is returned.

auth_userchallenge

auth_session_t *auth_userchallenge(char *name, char *style, char *type, char **challengep)

{
    char namebuf[LOGIN_NAME_MAX + 1 + NAME_MAX + 1];
    auth_session_t *as;
    login_cap_t *lc;
    struct passwd pwstore, *pwd = NULL;
    char *slash, pwbuf[_PW_BUF_LEN];

    if (!_auth_validuser(name))
        return (NULL);
    if (strlen(name) >= sizeof(namebuf))
        return (NULL);
    strlcpy(namebuf, name, sizeof namebuf);
    name = namebuf;

    /*
     ,* Split up user:style names if we were not given a style
     ,*/
    if (style == NULL && (style = strchr(name, ':')) != NULL)
        ,*style++ = '\0';

    /*
     ,* Cope with user/instance.  We are only using this to get
     ,* the class so it is okay if we strip a /root instance
     ,* The actual login script will pay attention to the instance.
     ,*/
    getpwnam_r(name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
    if (pwd == NULL) {
        if ((slash = strchr(name, '/')) != NULL) {
            ,*slash = '\0';
            getpwnam_r(name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
            ,*slash = '/';
        }
    }
    if ((lc = login_getclass(pwd ? pwd->pw_class : NULL)) == NULL)
        return (NULL);

    if ((style = login_getstyle(lc, style, type)) == NULL ||
        (as = auth_open()) == NULL) {
        login_close(lc);
        return (NULL);
    }
    if (auth_setitem(as, AUTHV_STYLE, style) < 0 ||
        auth_setitem(as, AUTHV_NAME, name) < 0 ||
        auth_setitem(as, AUTHV_CLASS, lc->lc_class) < 0) {
        auth_close(as);
        login_close(lc);
        return (NULL);
    }
    login_close(lc);
    ,*challengep = auth_challenge(as);
    return (as);
}

auth_userchallenge is used when the authentication style requires that the user be presented with a challenge, but the user cannot be directly interacted with over the terminal. As an example, this might be used in cases where the user is using S/KEY authentication over SSH.

auth_userresponse is then used to check the validity of the user's response.

A fair portion of this function is very similar to auth_usercheck. Instead of having a password argument however, it has a pointer to string, which is used to return the challenge to the calling function.

It first checks that name is a valid username using _auth_validuser.

If style is NULL, it checks if name is in the user:style format, and splits it accordingly.

It then gets the user's password database entry through getpwman_r(3), which operates on the passwd(5) database. It then uses that to retrieve the user's login class using login_getclass(3), which returns a login_cap_t. Login classes are stored in the login.conf(5) database.

That struct is then passed into login_getstyle(3), which also received the style and type. If type is NULL, it returns the first available login style for that class. If style is specified, it is returned if available, otherwise NULL is returned, which causes auth_userchallenge to return NULL as well.

This is where auth_userchallenge and auth_usercheck begin to diverge.

It creates a new auth session using auth_open as variable as.

The style, name and class properties of the session are then set using auth_setitem.

It then calls auth_challenge with as as the argument. The return value from that call is used to set challengep, and as is returned.

*challengep = auth_challenge(as);
return (as);

auth_challenge

char *auth_challenge(auth_session_t *as)

{
    char path[PATH_MAX];
    int len;

    if (as == NULL || as->style == NULL || as->name == NULL ||
        !_auth_validuser(as->name))
        return (NULL);

    len = snprintf(path, sizeof(path), _PATH_AUTHPROG "%s", as->style);
    if (len < 0 || len >= sizeof(path))
        return (NULL);

    as->state = 0;

    free(as->challenge);
    as->challenge = NULL;

    auth_call(as, path, as->style, "-s", "challenge", "--", as->name,
        as->class, (char *)NULL);
    if (as->state & AUTH_CHALLENGE)
        as->challenge = auth_getvalue(as, "challenge");
    as->state = 0;
    as->index = 0;	/* toss our data */
    return (as->challenge);
}

auth_challenge, much like auth_verify is a function that acts as a front-end for auth_call, except used specifically for challenges.

First the session as is checked. If it's NULL, or as->style is NULL, as->name is NULL, or if the username begins with a hyphen, or has a length of zero, the function returns NULL.

Then the path to the auth module is created.

snprintf(path, sizeof(path), _PATH_AUTHPROG "%s", as->style);

as->state and as->challenge are then reset, in case they were already set.

Then auth_call is called, with the challenge style set.

auth_call(as, path, as->style, "-s", "challenge", "--", as->name, as->class, (char *)NULL);

as->state is checked for the AUTH_CHALLENGE bit, indicating the auth module has returned a challenge. If it's present, the challenge is extracted from the back channel output, and used to set as->challenge.

if (as->state & AUTH_CHALLENGE)
as->challenge = auth_getvalue(as, "challenge");

as->state and as->index are then set to zero, discarding the data.

as->challenge is then returned.

auth_userresponse

int auth_userresponse(auth_session_t *as, char *response, int more)

{
    char path[PATH_MAX];
    char *style, *name, *challenge, *class;
    int len;

    if (as == NULL)
        return (0);

    auth_setstate(as, 0);

    if ((style = auth_getitem(as, AUTHV_STYLE)) == NULL ||
        (name = auth_getitem(as, AUTHV_NAME)) == NULL ||
        !_auth_validuser(name)) {
        if (more == 0)
            return (auth_close(as));
        return(0);
    }

    len = snprintf(path, sizeof(path), _PATH_AUTHPROG "%s", style);
    if (len < 0 || len >= sizeof(path)) {
        if (more == 0)
            return (auth_close(as));
        return (0);
    }

    challenge = auth_getitem(as, AUTHV_CHALLENGE);
    class = auth_getitem(as, AUTHV_CLASS);

    if (challenge)
        auth_setdata(as, challenge, strlen(challenge) + 1);
    else
        auth_setdata(as, "", 1);
    if (response) {
        auth_setdata(as, response, strlen(response) + 1);
        explicit_bzero(response, strlen(response));
    } else
        auth_setdata(as, "", 1);

    auth_call(as, path, style, "-s", "response", "--", name,
              class, (char *)NULL);

    /*
     * If they authenticated then make sure they did not expire
     */
    if (auth_getstate(as) & AUTH_ALLOW)
        auth_check_expire(as);
    if (more == 0)
        return (auth_close(as));
    return (auth_getstate(as) & AUTH_ALLOW);
}

auth_userresponse is used to pass the user's session and response from auth_userchallenge back to the authentication module. Similar to auth_userchallenge, it is also a front-end for auth_call.

If as is NULL, 0 is returned.

The state of as is then set to 0.

auth_setstate(as, 0);

as is then checked to ensure all the required items are set. It checks if as->style or as->name are NULL, or if the username is invalid using _auth_validuser. If any of those checks fail, and more is equal to 0, then the session is closed using auth_close, and the return value of that returned. Otherwise 0 is returned.

Then the path to the auth module is created similarly to how it is created in auth_verify.

The challenge and class of the session are extracted and stored in variables challenge and class respectively.

If challenge contains data, its contents are added to the as->data spool, otherwise an empty string is added to the spool.

If response contains data, it is added to the data spool as well, and then respose is explicit_bzero'd. Otherwise an empty string is added to the data spool.

Next auth_call is used to call the auth module with service type response.

auth_call(as, path, style, "-s", "response", "--", name,
        class, (char *)NULL);

If the request is allowed, it's checked to make sure it's not expired using auth_check_expire.

If more is equal to 0, the session is closed using auth_close and the return value from it is returned.

The allow state of the session is then returned.

return (auth_getstate(as) & AUTH_ALLOW);

auth_approval

int auth_approval(auth_session_t *as, login_cap_t *lc, char *name, char *type)

{
    int close_on_exit, close_lc_on_exit, len;
    struct passwd pwstore, *pwd;
    char *approve, *s, path[PATH_MAX], pwbuf[_PW_BUF_LEN];

    pwd = NULL;
    close_on_exit = as == NULL;
    close_lc_on_exit = lc == NULL;

    if (as != NULL && name == NULL)
        name = auth_getitem(as, AUTHV_NAME);

    if (as != NULL)
        pwd = auth_getpwd(as);

    if (pwd == NULL) {
        if (name != NULL) {
            if (!_auth_validuser(name)) {
                warnx("cannot approve who we don't recognize");
                return (0);
            }
            getpwnam_r(name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
        } else {
            getpwuid_r(getuid(), &pwstore, pwbuf, sizeof(pwbuf),
                &pwd);
            if (pwd == NULL) {
                syslog(LOG_ERR, "no such user id %u", getuid());
                warnx("cannot approve who we don't recognize");
                return (0);
            }
            name = pwd->pw_name;
        }
    }

    if (name == NULL)
        name = pwd->pw_name;

    if (lc == NULL) {
        if (strlen(name) >= PATH_MAX) {
            syslog(LOG_ERR, "username to login %.*s...",
                PATH_MAX, name);
            warnx("username too long");
            return (0);
        }
        if (pwd == NULL && (approve = strchr(name, '.')) != NULL) {
            strlcpy(path, name, sizeof path);
            path[approve - name] = '\0';
            getpwnam_r(name, &pwstore, pwbuf, sizeof(pwbuf), &pwd);
        }
        lc = login_getclass(pwd ? pwd->pw_class : NULL);
        if (lc == NULL) {
            warnx("unable to classify user");
            return (0);
        }
    }

    if (!type)
        type = LOGIN_DEFSERVICE;
    else {
        if (strncmp(type, "approve-", 8) == 0)
            type += 8;

        len = snprintf(path, sizeof(path), "approve-%s", type);
        if (len < 0 || len >= sizeof(path)) {
            if (close_lc_on_exit)
                login_close(lc);
            syslog(LOG_ERR, "approval path too long %.*s...",
                PATH_MAX, type);
            warnx("approval script path too long");
            return (0);
        }
    }

    if ((approve = login_getcapstr(lc, s = path, NULL, NULL)) == NULL)
        approve = login_getcapstr(lc, s = "approve", NULL, NULL);

    if (approve && approve[0] != '/') {
        if (close_lc_on_exit)
            login_close(lc);
        syslog(LOG_ERR, "Invalid %s script: %s", s, approve);
        warnx("invalid path to approval script");
        free(approve);
        return (0);
    }

    if (as == NULL && (as = auth_open()) == NULL) {
        if (close_lc_on_exit)
            login_close(lc);
        syslog(LOG_ERR, "%m");
        warn(NULL);
        free(approve);
        return (0);
    }

    auth_setstate(as, AUTH_OKAY);
    if (auth_setitem(as, AUTHV_NAME, name) < 0) {
        syslog(LOG_ERR, "%m");
        warn(NULL);
        goto out;
    }
    if (auth_check_expire(as) < 0)	/* is this account expired */
        goto out;
    if (_auth_checknologin(lc,
        auth_getitem(as, AUTHV_INTERACTIVE) != NULL)) {
        auth_setstate(as, (auth_getstate(as) & ~AUTH_ALLOW));
        goto out;
    }
    if (login_getcapbool(lc, "requirehome", 0) && pwd && pwd->pw_dir &&
        pwd->pw_dir[0]) {
        struct stat sb;

        if (stat(pwd->pw_dir, &sb) == -1 || !S_ISDIR(sb.st_mode) ||
            (pwd->pw_uid && sb.st_uid == pwd->pw_uid &&
            (sb.st_mode & S_IXUSR) == 0)) {
            auth_setstate(as, (auth_getstate(as) & ~AUTH_ALLOW));
            goto out;
        }
    }
    if (approve)
        auth_call(as, approve, strrchr(approve, '/') + 1, "--", name,
            lc->lc_class, type, (char *)NULL);

out:
    free(approve);
    if (close_lc_on_exit)
        login_close(lc);

    if (close_on_exit)
        return (auth_close(as));
    return (auth_getstate(as) & AUTH_ALLOW);
}

auth_approval is used to check a user against the approval script for service type. It is a front end for auth_call. Approval script types all begin with approval-.

Before running the scripts, first the validity of the account is checked. This is done first using auth_check_expired, then _auth_checknologin, and finally login_getcapbool to ensure the user has a home directory if one is required by their login class.

If type doesn't begin with approval- it will be prepended internally.

if as is NULL, an auth session will be created and destroyed inside the function.

If lc is NULL, it will be retrieved internally by looking up name.

If type is NULL, the default of LOGIN_DEFSERVICE is used. This is defined in login_cap.h as login. This should call the default approval script, according to the CAPABILITIES section of the login.conf man page.

It returns either 0 for disapproval, or non-zero for approval.

auth_check_expire

quad_t auth_check_expire(auth_session_t *as)

{
    if (as->pwd == NULL && auth_setpwd(as, NULL) < 0) {
        as->state &= ~AUTH_ALLOW;
        as->state |= AUTH_EXPIRED;	/* XXX */
        return (-1);
    }

    if (as->pwd == NULL)
        return (0);

    if (as->pwd && (quad_t)as->pwd->pw_expire != 0) {
        if (as->now.tv_sec == 0)
            WRAP(gettimeofday)(&as->now, NULL);
        if ((quad_t)as->now.tv_sec >= (quad_t)as->pwd->pw_expire) {
            as->state &= ~AUTH_ALLOW;
            as->state |= AUTH_EXPIRED;
        }
        if ((quad_t)as->now.tv_sec == (quad_t)as->pwd->pw_expire)
            return (-1);
        return ((quad_t)as->pwd->pw_expire - (quad_t)as->now.tv_sec);
    }
    return (0);
}

auth_check_expire is used to check if the account used for a session is expired. If an account is valid, it returns 0. Otherwise it returns a negative number representing the number of seconds elapsed since the account expired. If there's no account associated with the session, it will return -1.

It first checks if as->pwd is set, and if it isn't it tries to set it using auth_setpwd. If both of those fail, then it returns -1 and removes the AUTH_ALLOW bitmask from as->state, and adds the bitmask for AUTH_EXPIRED.

auth_check_change

quad_t auth_check_change(auth_session_t *as)

{
    if (as->pwd == NULL && auth_setpwd(as, NULL) < 0) {
        as->state &= ~AUTH_ALLOW;
        as->state |= AUTH_PWEXPIRED;	/* XXX */
        return (-1);
    }

    if (as->pwd == NULL)
        return (0);

    if (as->pwd && (quad_t)as->pwd->pw_change) {
        if (as->now.tv_sec == 0)
            WRAP(gettimeofday)(&as->now, NULL);
        if (as->now.tv_sec >= (quad_t)as->pwd->pw_change) {
            as->state &= ~AUTH_ALLOW;
            as->state |= AUTH_PWEXPIRED;
        }
        if ((quad_t)as->now.tv_sec == (quad_t)as->pwd->pw_change)
            return (-1);
        return ((quad_t)as->pwd->pw_change - (quad_t)as->now.tv_sec);
    }
    return (0);
}

auth_check_change is used to check if the password associated with an account is expired. If the password isn't expired, it returns 0. Otherwise it returns a negative number representing the number of seconds elapsed since the password expired. If there's no account associated with the session, it will return -1.

It operates very similarly to auth_check_expire.

auth_checknologin

void auth_checknologin(login_cap_t *lc)

{
    if (_auth_checknologin(lc, 1))
        exit(1);
}

auth_checknologin is a simple wrapper around the internal _auth_checknologin. If the user is now allowed to login, it prints the reason and calls exit(1).

auth_cat

int auth_cat(char *file)

{
    int fd, nchars;
    char tbuf[8192];

    if ((fd = open(file, O_RDONLY, 0)) == -1)
        return (0);
    while ((nchars = read(fd, tbuf, sizeof(tbuf))) > 0)
        (void)write(fileno(stdout), tbuf, nchars);
    (void)close(fd);
    return (1);
}

auth_cat is a helper function that will write the contents of a file to stdout. It returns 0 on failure or 1 on success.

auth_mkvalue

char *auth_mkvalue(char *value)

{
	char *big, *p;

	big = malloc(strlen(value) * 4 + 1);
	if (big == NULL)
		return (NULL);
	/*
	 ,* XXX - There should be a more standardized
	 ,* routine for doing this sort of thing.
	 ,*/
	for (p = big; *value; ++value) {
		switch (*value) {
		case '\r':
			,*p++ = '\\';
			,*p++ = 'r';
			break;
		case '\n':
			,*p++ = '\\';
			,*p++ = 'n';
			break;
		case '\\':
			,*p++ = '\\';
			,*p++ = *value;
			break;
		case '\t':
		case ' ':
			if (p == big)
				,*p++ = '\\';
			,*p++ = *value;
			break;
		default:
			if (!isprint((unsigned char)*value)) {
				,*p++ = '\\';
				,*p++ = ((*value >> 6) & 0x3) + '0';
				,*p++ = ((*value >> 3) & 0x7) + '0';
				,*p++ = ((*value     ) & 0x7) + '0';
			} else
				,*p++ = *value;
			break;
		}
	}
	,*p = '\0';
	return (big);
}

auth_mkvalue creates an escaped string which can be decoded by auth_getvalue.

_auth_validuser

int _auth_validuser(const char *name)

{
    /* User name must be specified and may not start with a '-'. */
    if (*name == '\0' || *name == '-') {
        syslog(LOG_ERR, "invalid user name %s", name);
        return 0;
    }
    return 1;
}

_auth_validuser is a small helper function used to check if a username passes some very basic validity criteria. Those being that it must not be an empty sting, and that it doesn't start with a hyphen.

If a username is invalid, it is logged in the syslog.

It returns 1 if the username is valid, otherwise it returns 0.

_auth_checknologin

static int _auth_checknologin(login_cap_t *lc, int print)

{
    struct stat sb;
    char *nologin;
    int mustfree;

    if (login_getcapbool(lc, "ignorenologin", 0))
        return (0);

    /*
     ,* If we fail to get the nologin file due to a database error,
     ,* assume there should have been one...
     ,*/
    nologin = login_getcapstr(lc, "nologin", "", NULL);
    mustfree = nologin && *nologin != '\0';
    if (nologin == NULL)
        goto print_nologin;

    /* First try the nologin file specified in login.conf. */
    if (*nologin != '\0' && stat(nologin, &sb) == 0)
        goto print_nologin;
    if (mustfree) {
        free(nologin);
        mustfree = 0;
    }

    /* If that doesn't exist try _PATH_NOLOGIN. */
    if (stat(_PATH_NOLOGIN, &sb) == 0) {
        nologin = _PATH_NOLOGIN;
        goto print_nologin;
    }

    /* Couldn't stat any nologin files, must be OK to login. */
    return (0);

print_nologin:
    if (print) {
        if (!nologin || *nologin == '\0' || auth_cat(nologin) == 0) {
            puts("Logins are not allowed at this time.");
            fflush(stdout);
        }
    }
    if (mustfree)
        free(nologin);
    return (-1);
}

_auth_checknologin is a helper function in authenticate.c. It is used to check the nologin status of the account. If print is non-zero, it will print the reason for the failure, and print the contents of the nologin file using auth_cat.

It returns 0 if the user is allowed to login, and -1 otherwise.

Call Graph

graph.svg

Click here to see the code that generates the call graph.

#!/usr/bin/env ruby
# frozen_string_literal: true

# Copyright (C) 2021 Dante Catalfamo
# SPDX-License-Identifier: MIT

require 'digest'
require 'set'

SOURCE_DIR = File.join Dir.home, 'src', 'github.com', 'openbsd', 'src', 'lib', 'libc', 'gen'
FILENAMES = %w[authenticate.c auth_subr.c login_cap.c].freeze

FUNCTION_REGEX = /^\w.*?$\n(?!DEF)(\w*)\(.*?\)\n\{(.*?)^\}/m.freeze
ONELINE_FUNCTION_REFGEX = /^\w.*?(\w*)\(.*?\).*?\{(.*?)\}/.freeze
CALL_REGEX = /[^\n](\w+)\(.*?\)/.freeze

class FunctionDigraph
  attr_accessor :pairs, :subgraphs

  class Subgraph
    attr_accessor :name, :label, :functions

    def initialize(name, label)
      @name = name
      @label = label
      @functions = []
    end

    def emit
      puts "subgraph cluster_#{name} {"
      puts "label = \"#{label}\""
      functions.each { |f| puts f unless f == 'DEF_WEAK' }
      puts '}'
    end
  end

  class Pair
    attr_accessor :to, :from

    def initialize(from, to)
      @from = from
      @to = to
    end

    def emit
      puts "#{from} -> #{to} [color = \"##{color}\"]"
    end

    def color
      Digest::MD5.hexdigest(from)[..5]
    end
  end

  def initialize
    @pairs = []
    @subgraphs = []
  end

  def emit
    puts 'digraph G {'
    puts 'rankdir=LR'
    puts 'splines=ortho'
    puts 'graph [pad="0.5", nodesep="0.5", ranksep="1.5"]'
    all_functions = Set.new
    @subgraphs.each { |s| all_functions.merge(s.functions) }
    @subgraphs.each(&:emit)
    @pairs.uniq { |p| [p.to, p.from] }.each do |p|
      p.emit if all_functions.include?(p.to)
    end
    puts '}'
  end

  def parse_files(filenames)
    filenames.each do |filename|
      contents = File.read(filename)
      basename = File.basename filename
      subgraph = Subgraph.new(basename.gsub(/\..*/, ''), basename)
      functions = contents.scan(FUNCTION_REGEX)
      oneliners = contents.scan(ONELINE_FUNCTION_REFGEX)
      functions.concat(oneliners) unless oneliners.empty?
      functions.each do |function|
        function_name = function[0]
        function_body = function[1]
        subgraph.functions << function_name
        function_body.scan(CALL_REGEX) do |call|
          @pairs << Pair.new(function_name, call[0])
        end
      end
      @subgraphs << subgraph
    end
  end
end

fg = FunctionDigraph.new

files = FILENAMES.map { |f| File.join(SOURCE_DIR, f) }
fg.parse_files files

fg.emit