/*
* Parse PAM options into a struct.
*
* Given a struct in which to store options and a specification for what
* options go where, parse both the PAM configuration options and any options
* from a Kerberos krb5.conf file and fill out the struct.
*
* The canonical version of this file is maintained in the rra-c-util package,
* which can be found at .
*
* Written by Russ Allbery
* Copyright 2020 Russ Allbery
* Copyright 2006-2008, 2010-2011, 2013-2014
* The Board of Trustees of the Leland Stanford Junior University
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#include
#ifdef HAVE_KRB5
# include
#endif
#include
#include
#include
#include
#include
#include
/* Used for unused parameters to silence gcc warnings. */
#define UNUSED __attribute__((__unused__))
/*
* Macros used to resolve a void * pointer to the configuration struct and an
* offset into a pointer to the appropriate type. Scary violations of the C
* type system lurk here.
*/
/* clang-format off */
#define CONF_BOOL(c, o) (bool *) (void *)((char *) (c) + (o))
#define CONF_NUMBER(c, o) (long *) (void *)((char *) (c) + (o))
#define CONF_STRING(c, o) (char **) (void *)((char *) (c) + (o))
#define CONF_LIST(c, o) (struct vector **)(void *)((char *) (c) + (o))
/* clang-format on */
/*
* We can only process times properly if we have Kerberos. If not, they fall
* back to longs and we convert them as numbers.
*/
/* clang-format off */
#ifdef HAVE_KRB5
# define CONF_TIME(c, o) (krb5_deltat *)(void *)((char *) (c) + (o))
#else
# define CONF_TIME(c, o) (long *) (void *)((char *) (c) + (o))
#endif
/* clang-format on */
/*
* Set a vector argument to its default. This needs to do a deep copy of the
* vector so that we can safely free it when freeing the configuration. Takes
* the PAM argument struct, the pointer in which to store the vector, and the
* default vector. Returns true if the default was set correctly and false on
* memory allocation failure, which is also reported with putil_crit().
*/
static bool
copy_default_list(struct pam_args *args, struct vector **setting,
const struct vector *defval)
{
struct vector *result = NULL;
*setting = NULL;
if (defval != NULL && defval->strings != NULL) {
result = vector_copy(defval);
if (result == NULL) {
putil_crit(args, "cannot allocate memory: %s", strerror(errno));
return false;
}
*setting = result;
}
return true;
}
/*
* Set a vector argument to a default based on a string. Takes the PAM
* argument struct,t he pointer into which to store the vector, and the
* default string. Returns true if the default was set correctly and false on
* memory allocation failure, which is also reported with putil_crit().
*/
static bool
default_list_string(struct pam_args *args, struct vector **setting,
const char *defval)
{
struct vector *result = NULL;
*setting = NULL;
if (defval != NULL) {
result = vector_split_multi(defval, " \t,", NULL);
if (result == NULL) {
putil_crit(args, "cannot allocate memory: %s", strerror(errno));
return false;
}
*setting = result;
}
return true;
}
/*
* Set the defaults for the PAM configuration. Takes the PAM arguments, an
* option table defined as above, and the number of entries in the table. The
* config member of the args struct must already be allocated. Returns true
* on success and false on error (generally out of memory). Errors will
* already be reported using putil_crit().
*
* This function must be called before either putil_args_krb5() or
* putil_args_parse(), since neither of those functions set defaults.
*/
bool
putil_args_defaults(struct pam_args *args, const struct option options[],
size_t optlen)
{
size_t opt;
for (opt = 0; opt < optlen; opt++) {
bool *bp;
long *lp;
#ifdef HAVE_KRB5
krb5_deltat *tp;
#else
long *tp;
#endif
char **sp;
struct vector **vp;
switch (options[opt].type) {
case TYPE_BOOLEAN:
bp = CONF_BOOL(args->config, options[opt].location);
*bp = options[opt].defaults.boolean;
break;
case TYPE_NUMBER:
lp = CONF_NUMBER(args->config, options[opt].location);
*lp = options[opt].defaults.number;
break;
case TYPE_TIME:
tp = CONF_TIME(args->config, options[opt].location);
*tp = (krb5_deltat) options[opt].defaults.number;
break;
case TYPE_STRING:
sp = CONF_STRING(args->config, options[opt].location);
if (options[opt].defaults.string == NULL)
*sp = NULL;
else {
*sp = strdup(options[opt].defaults.string);
if (*sp == NULL) {
putil_crit(args, "cannot allocate memory: %s",
strerror(errno));
return false;
}
}
break;
case TYPE_LIST:
vp = CONF_LIST(args->config, options[opt].location);
if (!copy_default_list(args, vp, options[opt].defaults.list))
return false;
break;
case TYPE_STRLIST:
vp = CONF_LIST(args->config, options[opt].location);
if (!default_list_string(args, vp, options[opt].defaults.string))
return false;
break;
}
}
return true;
}
#ifdef HAVE_KRB5
/*
* Load a boolean option from Kerberos appdefaults. Takes the PAM argument
* struct, the section name, the realm, the option, and the result location.
*
* The stupidity of rewriting the realm argument into a krb5_data is required
* by MIT Kerberos.
*/
static void
default_boolean(struct pam_args *args, const char *section, const char *realm,
const char *opt, bool *result)
{
int tmp;
# ifdef HAVE_KRB5_REALM
krb5_const_realm rdata = realm;
# else
krb5_data realm_struct;
const krb5_data *rdata;
if (realm == NULL)
rdata = NULL;
else {
rdata = &realm_struct;
realm_struct.magic = KV5M_DATA;
realm_struct.data = (void *) realm;
realm_struct.length = (unsigned int) strlen(realm);
}
# endif
/*
* The MIT version of krb5_appdefault_boolean takes an int * and the
* Heimdal version takes a krb5_boolean *, so hope that Heimdal always
* defines krb5_boolean to int or this will require more portability work.
*/
krb5_appdefault_boolean(args->ctx, section, rdata, opt, *result, &tmp);
*result = tmp;
}
/*
* Load a number option from Kerberos appdefaults. Takes the PAM argument
* struct, the section name, the realm, the option, and the result location.
* The native interface doesn't support numbers, so we actually read a string
* and then convert.
*/
static void
default_number(struct pam_args *args, const char *section, const char *realm,
const char *opt, long *result)
{
char *tmp = NULL;
char *end;
long value;
# ifdef HAVE_KRB5_REALM
krb5_const_realm rdata = realm;
# else
krb5_data realm_struct;
const krb5_data *rdata;
if (realm == NULL)
rdata = NULL;
else {
rdata = &realm_struct;
realm_struct.magic = KV5M_DATA;
realm_struct.data = (void *) realm;
realm_struct.length = (unsigned int) strlen(realm);
}
# endif
krb5_appdefault_string(args->ctx, section, rdata, opt, "", &tmp);
if (tmp != NULL && tmp[0] != '\0') {
errno = 0;
value = strtol(tmp, &end, 10);
if (errno != 0 || *end != '\0')
putil_err(args, "invalid number in krb5.conf setting for %s: %s",
opt, tmp);
else
*result = value;
}
free(tmp);
}
/*
* Load a time option from Kerberos appdefaults. Takes the PAM argument
* struct, the section name, the realm, the option, and the result location.
* The native interface doesn't support numbers, so we actually read a string
* and then convert using krb5_string_to_deltat.
*/
static void
default_time(struct pam_args *args, const char *section, const char *realm,
const char *opt, krb5_deltat *result)
{
char *tmp = NULL;
krb5_deltat value;
krb5_error_code retval;
# ifdef HAVE_KRB5_REALM
krb5_const_realm rdata = realm;
# else
krb5_data realm_struct;
const krb5_data *rdata;
if (realm == NULL)
rdata = NULL;
else {
rdata = &realm_struct;
realm_struct.magic = KV5M_DATA;
realm_struct.data = (void *) realm;
realm_struct.length = (unsigned int) strlen(realm);
}
# endif
krb5_appdefault_string(args->ctx, section, rdata, opt, "", &tmp);
if (tmp != NULL && tmp[0] != '\0') {
retval = krb5_string_to_deltat(tmp, &value);
if (retval != 0)
putil_err(args, "invalid time in krb5.conf setting for %s: %s",
opt, tmp);
else
*result = value;
}
free(tmp);
}
/*
* Load a string option from Kerberos appdefaults. Takes the PAM argument
* struct, the section name, the realm, the option, and the result location.
*
* This requires an annoying workaround because one cannot specify a default
* value of NULL with MIT Kerberos, since MIT Kerberos unconditionally calls
* strdup on the default value. There's also no way to determine if memory
* allocation failed while parsing or while setting the default value, so we
* don't return an error code.
*/
static void
default_string(struct pam_args *args, const char *section, const char *realm,
const char *opt, char **result)
{
char *value = NULL;
# ifdef HAVE_KRB5_REALM
krb5_const_realm rdata = realm;
# else
krb5_data realm_struct;
const krb5_data *rdata;
if (realm == NULL)
rdata = NULL;
else {
rdata = &realm_struct;
realm_struct.magic = KV5M_DATA;
realm_struct.data = (void *) realm;
realm_struct.length = (unsigned int) strlen(realm);
}
# endif
krb5_appdefault_string(args->ctx, section, rdata, opt, "", &value);
if (value != NULL) {
if (value[0] == '\0')
free(value);
else {
if (*result != NULL)
free(*result);
*result = value;
}
}
}
/*
* Load a list option from Kerberos appdefaults. Takes the PAM arguments, the
* context, the section name, the realm, the option, and the result location.
*
* We may fail here due to memory allocation problems, in which case we return
* false to indicate that PAM setup should abort.
*/
static bool
default_list(struct pam_args *args, const char *section, const char *realm,
const char *opt, struct vector **result)
{
char *tmp = NULL;
struct vector *value;
default_string(args, section, realm, opt, &tmp);
if (tmp != NULL) {
value = vector_split_multi(tmp, " \t,", NULL);
if (value == NULL) {
free(tmp);
putil_crit(args, "cannot allocate vector: %s", strerror(errno));
return false;
}
if (*result != NULL)
vector_free(*result);
*result = value;
free(tmp);
}
return true;
}
/*
* The public interface for getting configuration information from krb5.conf.
* Takes the PAM arguments, the krb5.conf section, the options specification,
* and the number of options in the options table. The config member of the
* args struct must already be allocated. Iterate through the option list
* and, for every option where krb5_config is true, see if it's set in the
* Kerberos configuration.
*
* This looks obviously slow, but there haven't been any reports of problems
* and there's no better interface. But if you wonder where the cycles in
* your computer are getting wasted, well, here's one place.
*/
bool
putil_args_krb5(struct pam_args *args, const char *section,
const struct option options[], size_t optlen)
{
size_t i;
char *realm;
bool free_realm = false;
/* Having no local realm may be intentional, so don't report an error. */
if (args->realm != NULL)
realm = args->realm;
else {
if (krb5_get_default_realm(args->ctx, &realm) < 0)
realm = NULL;
else
free_realm = true;
}
for (i = 0; i < optlen; i++) {
const struct option *opt = &options[i];
if (!opt->krb5_config)
continue;
switch (opt->type) {
case TYPE_BOOLEAN:
default_boolean(args, section, realm, opt->name,
CONF_BOOL(args->config, opt->location));
break;
case TYPE_NUMBER:
default_number(args, section, realm, opt->name,
CONF_NUMBER(args->config, opt->location));
break;
case TYPE_TIME:
default_time(args, section, realm, opt->name,
CONF_TIME(args->config, opt->location));
break;
case TYPE_STRING:
default_string(args, section, realm, opt->name,
CONF_STRING(args->config, opt->location));
break;
case TYPE_LIST:
case TYPE_STRLIST:
if (!default_list(args, section, realm, opt->name,
CONF_LIST(args->config, opt->location)))
return false;
break;
}
}
if (free_realm)
krb5_free_default_realm(args->ctx, realm);
return true;
}
#else /* !HAVE_KRB5 */
/*
* Stub function for getting configuration information from krb5.conf used
* when the PAM module is not built with Kerberos support so that the function
* can be called unconditionally.
*/
bool
putil_args_krb5(struct pam_args *args UNUSED, const char *section UNUSED,
const struct option options[] UNUSED, size_t optlen UNUSED)
{
return true;
}
#endif /* !HAVE_KRB5 */
/*
* bsearch comparison function for finding PAM arguments in an array of struct
* options. We only compare up to the first '=' in the key so that we don't
* have to munge the string before searching.
*/
static int
option_compare(const void *key, const void *member)
{
const char *string = key;
const struct option *option = member;
const char *p;
size_t length;
int result;
p = strchr(string, '=');
if (p == NULL)
return strcmp(string, option->name);
else {
length = (size_t)(p - string);
if (length == 0)
return -1;
result = strncmp(string, option->name, length);
if (result == 0 && strlen(option->name) > length)
return -1;
return result;
}
}
/*
* Given a PAM argument, convert the value portion of the argument to a
* boolean and store it in the provided location. If the value is missing,
* that's equivalent to a true value. If the value is invalid, report an
* error and leave the location unchanged.
*/
static void
convert_boolean(struct pam_args *args, const char *arg, bool *setting)
{
const char *value;
value = strchr(arg, '=');
if (value == NULL)
*setting = true;
else {
value++;
/* clang-format off */
if ( strcasecmp(value, "true") == 0
|| strcasecmp(value, "yes") == 0
|| strcasecmp(value, "on") == 0
|| strcmp (value, "1") == 0)
*setting = true;
else if ( strcasecmp(value, "false") == 0
|| strcasecmp(value, "no") == 0
|| strcasecmp(value, "off") == 0
|| strcmp (value, "0") == 0)
*setting = false;
else
putil_err(args, "invalid boolean in setting: %s", arg);
/* clang-format on */
}
}
/*
* Given a PAM argument, convert the value portion of the argument to a number
* and store it in the provided location. If the value is missing or isn't a
* number, report an error and leave the location unchanged.
*/
static void
convert_number(struct pam_args *args, const char *arg, long *setting)
{
const char *value;
char *end;
long result;
value = strchr(arg, '=');
if (value == NULL || value[1] == '\0') {
putil_err(args, "value missing for option %s", arg);
return;
}
errno = 0;
result = strtol(value + 1, &end, 10);
if (errno != 0 || *end != '\0') {
putil_err(args, "invalid number in setting: %s", arg);
return;
}
*setting = result;
}
/*
* Given a PAM argument, convert the value portion of the argument from a
* Kerberos time string to a krb5_deltat and store it in the provided
* location. If the value is missing or isn't a number, report an error and
* leave the location unchanged.
*/
#ifdef HAVE_KRB5
static void
convert_time(struct pam_args *args, const char *arg, krb5_deltat *setting)
{
const char *value;
krb5_deltat result;
krb5_error_code retval;
value = strchr(arg, '=');
if (value == NULL || value[1] == '\0') {
putil_err(args, "value missing for option %s", arg);
return;
}
retval = krb5_string_to_deltat((char *) value + 1, &result);
if (retval != 0)
putil_err(args, "bad time value in setting: %s", arg);
else
*setting = result;
}
#else /* HAVE_KRB5 */
static void
convert_time(struct pam_args *args, const char *arg, long *setting)
{
convert_number(args, arg, setting);
}
#endif /* !HAVE_KRB5 */
/*
* Given a PAM argument, convert the value portion of the argument to a string
* and store it in the provided location. If the value is missing, report an
* error and leave the location unchanged, returning true since that's a
* non-fatal error. If memory allocation fails, return false, since PAM setup
* should abort.
*/
static bool
convert_string(struct pam_args *args, const char *arg, char **setting)
{
const char *value;
char *result;
value = strchr(arg, '=');
if (value == NULL) {
putil_err(args, "value missing for option %s", arg);
return true;
}
result = strdup(value + 1);
if (result == NULL) {
putil_crit(args, "cannot allocate memory: %s", strerror(errno));
return false;
}
free(*setting);
*setting = result;
return true;
}
/*
* Given a PAM argument, convert the value portion of the argument to a vector
* and store it in the provided location. If the value is missing, report an
* error and leave the location unchanged, returning true since that's a
* non-fatal error. If memory allocation fails, return false, since PAM setup
* should abort.
*/
static bool
convert_list(struct pam_args *args, const char *arg, struct vector **setting)
{
const char *value;
struct vector *result;
value = strchr(arg, '=');
if (value == NULL) {
putil_err(args, "value missing for option %s", arg);
return true;
}
result = vector_split_multi(value + 1, " \t,", NULL);
if (result == NULL) {
putil_crit(args, "cannot allocate vector: %s", strerror(errno));
return false;
}
vector_free(*setting);
*setting = result;
return true;
}
/*
* Parse the PAM arguments. Takes the PAM argument struct, the argument count
* and vector, the option table, and the number of elements in the option
* table. The config member of the args struct must already be allocated.
* Returns true on success and false on error. An error return should be
* considered fatal. Report errors using putil_crit(). Unknown options will
* also be diagnosed (to syslog at LOG_ERR using putil_err()), but are not
* considered fatal errors and will still return true.
*
* If options should be retrieved from krb5.conf, call putil_args_krb5()
* first, before calling this function.
*/
bool
putil_args_parse(struct pam_args *args, int argc, const char *argv[],
const struct option options[], size_t optlen)
{
int i;
const struct option *option;
/*
* Second pass: find each option we were given and set the corresponding
* configuration parameter.
*/
for (i = 0; i < argc; i++) {
option = bsearch(argv[i], options, optlen, sizeof(struct option),
option_compare);
if (option == NULL) {
putil_err(args, "unknown option %s", argv[i]);
continue;
}
switch (option->type) {
case TYPE_BOOLEAN:
convert_boolean(args, argv[i],
CONF_BOOL(args->config, option->location));
break;
case TYPE_NUMBER:
convert_number(args, argv[i],
CONF_NUMBER(args->config, option->location));
break;
case TYPE_TIME:
convert_time(args, argv[i],
CONF_TIME(args->config, option->location));
break;
case TYPE_STRING:
if (!convert_string(args, argv[i],
CONF_STRING(args->config, option->location)))
return false;
break;
case TYPE_LIST:
case TYPE_STRLIST:
if (!convert_list(args, argv[i],
CONF_LIST(args->config, option->location)))
return false;
break;
}
}
return true;
}