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 bepasswd
,radius
,skey
,yubikey
, etc. There's more information about available styles inlogin.conf(5)
under theAUTHENTICATION
header.service
is the service type. Typically authentication methods will accept one of three values here:login
,challenge
, orresponse
.login
is the default if it's not specified, and is used to let the module know to interact with the user directly throughstdin
andstdout
, whilechallenge
andresponse
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 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
isNULL
, the user's default login style will be used. This ispasswd
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
- If
-
type
is the authentication type- Types are defined in
login.conf
and as a group of allowed auth styles - If
type
isNULL
, use the auth type for the user's login class. The default type isauth-default
, which allowspsaswd
andskey
auth methods.
- Types are defined in
-
password
is the password to test- If
password
isNULL
, 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 aresponse
- If
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.
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 toas
. It then sets the sessionservice
to"response"
, and adds thepassword
string to the session'sdata
.auth_setitem(as, AUTHV_SERVICE, "response"); auth_setdata(as, "", 1); auth_setdata(as, password, strlen(password) + 1);
- If
password
isNULL
, it setsas
toNULL
.
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
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
Copyright
Click here to expand copyright notices