/*
 * Copyright (c) 2003-2016
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*****************************************************************************
 * COPYRIGHT AND PERMISSION NOTICE
 * 
 * Copyright (c) 2001-2003 The Queen in Right of Canada
 * 
 * All rights reserved.
 * 
 * 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, and/or sell
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, provided that the above copyright notice(s) and this
 * permission notice appear in all copies of the Software and that both the
 * above copyright notice(s) and this permission notice appear in supporting
 * documentation.
 * 
 * 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 OF THIRD PARTY RIGHTS.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE 
 * BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 
 * SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder shall not
 * be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization of the
 * copyright holder.
 ***************************************************************************/

/*
 * Support functions for DACS authentication.
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2016\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: authlib.c 2908 2016-07-05 15:01:33Z brachman $";
#endif

#include "auth.h"
#include "acs.h"
#include "group.h"
#include "frame.h"

#include <netinet/in.h>
#include <openssl/pem.h>
#include <openssl/bio.h>

static char *log_module_name = "authlib";

static int cookie_output_syntax = COOKIE_NETSCAPE;

static int debug_auth = 0;

typedef struct Auth_style_map {
  Auth_style auth_style;
  char *string;
  int min_abbrev;
  int altnum;
} Auth_style_map;

static Auth_style_map auth_style_map[] = {
  { AUTH_STYLE_PASSWORD,  "passwd",              4, 0 },	/* First alt */
  { AUTH_STYLE_PASSWORD,  "password",            0, 1 },	/* Second alt */
  { AUTH_STYLE_CERT,      "certificate",         4, 0 },
  { AUTH_STYLE_PROMPTED,  "prompted",            6, 0 },
  { AUTH_STYLE_NATIVE,    "native",              3, 0 },
  { AUTH_STYLE_EXPR,      "expr",                0, 0 },
  { AUTH_STYLE_ADMIN,     "admin",               0, 0 },
  { AUTH_STYLE_IMPORTED,  "imported",            6, 0 },
  { AUTH_STYLE_ALIEN,     "alien",               0, 0 },
  { AUTH_STYLE_GENERATED, "generated",           3, 0 },
  { AUTH_STYLE_RLINK,     "rlink",               0, 0 },
  { AUTH_STYLE_DIGEST,    "digest",              0, 0 },
  { AUTH_STYLE_CAS,       "cas",                 0, 0 },
  { AUTH_STYLE_SIMPLE,    "simple",              0, 0 },
  { AUTH_STYLE_ACS,       "acs",                 0, 0 },
  { AUTH_STYLE_INFOCARD,  "infocard",            0, 0 },
  { AUTH_STYLE_MINFOCARD, "managed_infocard",    0, 0 },
  { AUTH_STYLE_SINFOCARD, "selfissued_infocard", 0, 0 },
  { AUTH_STYLE_TGMA,      "tgma",                0, 0 },
  { AUTH_STYLE_UNKNOWN,   NULL,                  0, 0 }
};

static Auth_style_map auth_style_modifier_map[] = {
  { AUTH_STYLE_SET_ROLES,  "set_roles",          0, 0 },
  { AUTH_STYLE_ADD_ROLES,  "add_roles",          0, 0 },
  { AUTH_STYLE_UNKNOWN,   NULL,                  0, 0 }
};

static int
auth_single_cookie(void)
{

  if (conf_val_eq(CONF_AUTH_SINGLE_COOKIE, "federation"))
	return(2);

  if (conf_val_eq(CONF_AUTH_SINGLE_COOKIE, "jurisdiction"))
	return(1);

  return(0);
}

/*
 * These are default cookie name component terminators.
 * The defaults were hardwired up to and including version 1.4.30.
 *
 * Unfortunately, the original cookie name syntax used colons as name
 * component separators, which is not compliant with RFC 2965.
 * The RFC disallows the space, tab, control characters 0 through 31, and:
 *     ( ) <  > @ , ; : \ "  /  [  ]  ? = {  }
 *
 * This leaves a few characters that could potentially replace the colon
 * in this context: ! # $ % & ' * + - . ^ _ ` | ~
 *
 * A suitable character must also be disallowed within federation names and
 * jurisdiction names (eliminating the hyphen and underscore), and within
 * usernames (eliminating: ! # $ % & ' . ^ `).
 * This leaves the four characters: * + | ~
 * The '|' is not allowed with the path component of a URI (which may not be
 * significant).
 * The '|' and '~' are the best candidates.
 * It is not yet known whether these alternatives to a colon will work with
 * most clients.
 * Note that a change would not affect the syntax of jurisdiction names,
 * user names, etc., only the HTTP cookie name format.
 */
const Cookie_name_terminators default_cookie_name_terminators = {
  ":", "::", ":", ":"
};

Cookie_name_terminators current_cookie_name_terminators;

/*
 * Create and return a new, initialized cookie name terminator set.
 * If TERM_STR is NULL, create a new set equivalent to the default;
 * if TERM_STR is a single item, populate a new set having the same format as
 * the default, but using TERM_STR for each terminator;
 * if TERM_STR consists of four comma-separated items, populate a new set
 * using the four string elements; otherwise, NULL is returned.
 * All terminator strings provided are assumed to be valid.
 * A comma cannot be escaped and may therefore not be used as, or within,
 * a terminator string.  Spaces are significant.
 * E.g., init_cookie_name_terminators(":,:,:,:");
 */
Cookie_name_terminators *
init_cookie_name_terminators(char *term_str)
{
  char *p;
  Cookie_name_terminators *term;

  term = ALLOC(Cookie_name_terminators);
  if (term_str == NULL)
	*term = default_cookie_name_terminators;
  else if ((p = strchr(term_str, (int) ',')) == NULL) {
	term->app_end = strdup(term_str);
	/* XXX assumes this terminator must be doubled up. */
	term->federation_end = ds_xprintf("%s%s", term_str, term_str);
	term->jurisdiction_end = strdup(term_str);
	term->username_end = strdup(term_str);
  }
  else {
	Dsvec *dsv;

	if ((dsv =  strsplit(strdup(term_str), ",", 0)) == NULL
		|| dsvec_len(dsv) != 4)
	  return(NULL);
	term->app_end = dsvec_ptr_index(dsv, 0);
	term->federation_end = dsvec_ptr_index(dsv, 1);
	term->jurisdiction_end = dsvec_ptr_index(dsv, 2);
	term->username_end = dsvec_ptr_index(dsv, 3);
  }

  /* Disallow any null strings. */
  if (term->app_end[0] == '\0' || term->federation_end[0] == '\0'
	  || term->jurisdiction_end[0] == '\0' || term->username_end[0] == '\0')
	return(NULL);

  return(term);
}

/*
 * Set the cookie name terminators in effect to TERM, or the default set
 * if TERM is NULL.
 * TERM is assumed to be a valid set of terminators.
 */
int
set_cookie_name_terminators(Cookie_name_terminators *term)
{

  if (term == NULL)
	current_cookie_name_terminators = default_cookie_name_terminators;
  else
	current_cookie_name_terminators = *term;

  return(0);
}

/*
 * Generate an HTTP cookie name from APP, FEDERATION, JURISDICTION, USERNAME,
 * and SPECIAL, the first and last of which can be NULL to select a default.
 * Any of FEDERATION, JURISDICTION, and USERNAME may be the empty string.
 * If TERM is NULL, the default terminators are used.
 * It is assumed that the component and terminating strings are valid for
 * constructing an HTTP cookie name, but each component is checked to ensure
 * that it does not contain its terminator string (and can therefore be parsed
 * uniquely and correctly).
 *
 * Return the cookie name, or NULL if an error occurs.
 */
char *
make_cookie_name(Cookie_name_terminators *term, const char *app,
				 const char *federation, char *jurisdiction, char *username,
				 char *special)
{
  char *cookie_name;
  const char *aname, *fname, *jname, *sname, *uname;
  const Cookie_name_terminators *t;

  if (term == NULL)
	t = &current_cookie_name_terminators;
  else
	t = term;

  aname = app;
  if (aname == NULL)
	aname = DACS_COOKIE_APP_NAME;
  if (strstr(aname, t->app_end) != NULL)
	return(NULL);
  
  fname = federation;
  if (fname == NULL)
	fname = "";
  if (strstr(fname, t->federation_end) != NULL)
	return(NULL);

  jname = jurisdiction;
  if (jname == NULL)
	jname = "";
  if (strstr(jname, t->jurisdiction_end) != NULL)
	return(NULL);

  uname = username;
  if (uname == NULL)
	uname = "";

  if (special == NULL)
	cookie_name = ds_xprintf("%s%s%s%s%s%s%s",
							 aname, t->app_end,
							 fname, t->federation_end,
							 jname, t->jurisdiction_end,
							 uname);
  else {
	if (strstr(uname, t->username_end) != NULL)
	  return(NULL);
	cookie_name = ds_xprintf("%s%s%s%s%s%s%s%s%s",
							 aname, t->app_end,
							 fname, t->federation_end,
							 jname, t->jurisdiction_end,
							 uname, t->username_end,
							 special);
  }

  return(cookie_name);
}

/*
 * Return the cookie name prefix string used within the current federation.
 * This might be used when scanning through cookies submitted with an
 * HTTP request, for instance, to recognize those of interest to DACS.
 */
char *
make_cookie_name_federation_prefix(const Cookie_name_terminators *term)
{
  char *cookie_name_prefix;
  const Cookie_name_terminators *t;

  if (term == NULL)
	t = &current_cookie_name_terminators;
  else
	t = term;

  if (conf_val(CONF_COMPAT_MODE) == NULL
      || conf_val_eq(CONF_COMPAT_MODE, "off"))
    cookie_name_prefix = ds_xprintf("%s%s%s%s",
									DACS_COOKIE_APP_NAME, t->app_end,
									conf_val(CONF_FEDERATION_NAME),
									t->federation_end);
  else {
	/* This is only for backward compatibility, supposedly temporarily. */
    cookie_name_prefix = ds_xprintf("DACS:%s:", conf_val(CONF_FEDERATION_NAME));
  }

  return(cookie_name_prefix);
}

/*
 * Return the cookie name prefix string used within the application.
 */
char *
make_cookie_name_app_prefix(const Cookie_name_terminators *term)
{
  char *cookie_name_prefix;
  const Cookie_name_terminators *t;

  if (term == NULL)
	t = &current_cookie_name_terminators;
  else
	t = term;

  if (conf_val(CONF_COMPAT_MODE) == NULL
      || conf_val_eq(CONF_COMPAT_MODE, "off"))
    cookie_name_prefix = ds_xprintf("%s%s",
									DACS_COOKIE_APP_NAME, t->app_end);
  else {
	/* This is only for backward compatibility, supposedly temporarily. */
    cookie_name_prefix = ds_xprintf("DACS:");
  }

  return(cookie_name_prefix);
}

/*
 * Parse HTTP cookie name NAME into its components.
 * If TERM is NULL, the default terminators are used.
 * If a component is unspecified, it is set to the empty string, except for
 * the last component, which is set to NULL.
 * The individual name components are not checked for validity.
 *
 * Return the parsed components, or NULL if an error occurs.
 */
Cookie_name *
parse_cookie_name(Cookie_name_terminators *term, char *name)
{
  char *aname, *e, *fname, *jname, *s, *sname, *uname;
  Cookie_name *cn;
  const Cookie_name_terminators *t;

  if (term == NULL)
	t = &current_cookie_name_terminators;
  else
	t = term;

  s = name;
  if ((e = strstr(s, t->app_end)) == NULL)
	return(NULL);
  aname = strndup(s, e - s);
  s = e + strlen(t->app_end);

  if ((e = strstr(s, t->federation_end)) == NULL)
	return(NULL);
  fname = strndup(s, e - s);
  s = e + strlen(t->federation_end);

  if ((e = strstr(s, t->jurisdiction_end)) == NULL)
	return(NULL);
  jname = strndup(s, e - s);
  s = e + strlen(t->jurisdiction_end);

  if ((e = strstr(s, t->username_end)) == NULL) {
	uname = strdup(s);
	sname = NULL;
  }
  else {
	uname = strndup(s, e - s);
	s = e + strlen(t->username_end);
	sname = strdup(s);
  }

  cn = ALLOC(Cookie_name);
  cn->app_name = aname;
  cn->federation = fname;
  cn->jurisdiction = jname;
  cn->username = uname;
  cn->special = sname;

  return(cn);
}

/*
 * XXX this is semi-problematic because a colon is not allowed in
 * a cookie name, unless quoted, if RFC 2109/2965 is honoured.
 * This seems to be an issue only with some releases of Tomcat.
 */
char *
make_auth_cookie_name(Credentials *credentials)
{
  char *name;

  name = make_cookie_name(NULL, DACS_COOKIE_APP_NAME, credentials->federation,
						  (auth_single_cookie() == 2)
						  ? "" : credentials->home_jurisdiction,
						  auth_single_cookie() ? "" : credentials->username,
						  NULL);
  return(name);
}

time_t
make_auth_expiry(time_t auth_time, time_t delta_secs)
{
  time_t expires;

  expires = auth_time + delta_secs;

  return(expires);
}

/*
 * DELTA may be a negative value
 */
time_t
make_auth_expiry_delta(time_t auth_time, char *delta)
{
  time_t lifetime;

  if (delta == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Invalid delta value"));
	dacs_fatal("Bad configuration");
  }

  if (strnum(delta, STRNUM_TIME_T, &lifetime) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Invalid lifetime for credentials: %s", delta));
	dacs_fatal("Bad configuration");
	/*NOTREACHED*/
  }

  return(make_auth_expiry(auth_time, lifetime));
}

Credentials *
init_credentials(void)
{
  Credentials *c;

  c = ALLOC(Credentials);
  c->roles = NULL;
  c->federation = NULL;
  c->username = NULL;
  c->home_jurisdiction = NULL;
  c->ip_address = NULL;
  c->role_str = NULL;
  c->expires_secs = 0;
  c->auth_style = AUTH_STYLE_UNKNOWN;
  c->unique = NULL;
  c->version = NULL;
  c->valid_for = NULL;
  c->imported_by = NULL;
  c->ua_hash = NULL;
  c->cookie_name = NULL;
  c->selected = 0;
  c->next = NULL;

  return(c);
}

char *
auth_style_to_string(Auth_style auth_style)
{
  int found, i;
  Ds ds;

  ds_init(&ds);
  found = 0;

  for (i = 0; auth_style_map[i].string != NULL; i++) {
	if ((auth_style & auth_style_map[i].auth_style) != 0) {
	  ds_asprintf(&ds, "%s%s", found ? "," : "", auth_style_map[i].string);
	  found++;

	  /* Skip any synonyms for this name. */
	  while (auth_style_map[i + 1].string != NULL
			 && auth_style_map[i + 1].altnum)
		i++;
	}
  }

  if (!found)
	return(NULL);

  for (i = 0; auth_style_modifier_map[i].string != NULL; i++) {
	if ((auth_style & auth_style_modifier_map[i].auth_style) != 0)
	  ds_asprintf(&ds, ",%s", auth_style_modifier_map[i].string);
  }

  return(ds_buf(&ds));
}

Auth_style
auth_style_from_string(const char *str)
{
  int found, found_style, i;
  Auth_style auth_style;
  Auth_style_map *sm;
  Dsvec *dsv;

  if ((dsv = strsplit(str, ",", 0)) == NULL)
	return(AUTH_STYLE_UNKNOWN);

  auth_style = 0;
  found_style = 0;

  for (i = 0; i < dsvec_len(dsv); i++) {
	char *as;

	as = (char *) dsvec_ptr_index(dsv, i);
	found = 0;
	for (sm = &auth_style_map[0]; sm->string != NULL; sm++) {
	  if ((sm->min_abbrev == 0 && strcaseeq(sm->string, as))
		  || strncaseprefix(as, sm->string, sm->min_abbrev)) {
		auth_style |= sm->auth_style;
		found_style++;
		found++;
		break;
	  }
	}

	if (!found) {
	  for (sm = &auth_style_modifier_map[0]; sm->string != NULL; sm++) {
		if ((sm->min_abbrev == 0 && strcaseeq(sm->string, as))
			|| strncaseprefix(as, sm->string, sm->min_abbrev)) {
		  auth_style |= sm->auth_style;
		  found++;
		  break;
		}
	  }

	  if (!found) {
		log_msg((LOG_ERROR_LEVEL,
				 "Invalid style keyword or modifier: \"%s\"", as));
		return(AUTH_STYLE_UNKNOWN);
	  }
	}
  }

  if (!found_style) {
	log_msg((LOG_ERROR_LEVEL, "No style keyword found"));
	return(AUTH_STYLE_UNKNOWN);
  }

  if ((auth_style & AUTH_STYLE_EXPR)) {
	if ((auth_style & AUTH_STYLE_PASSWORD)
		|| (auth_style & AUTH_STYLE_SIMPLE)
		|| (auth_style & AUTH_STYLE_INFOCARD)
		|| (auth_style & AUTH_STYLE_MINFOCARD)
		|| (auth_style & AUTH_STYLE_SINFOCARD)
		|| (auth_style & AUTH_STYLE_CAS)
		|| (auth_style & AUTH_STYLE_CERT)
		|| (auth_style & AUTH_STYLE_NATIVE)
		|| (auth_style & AUTH_STYLE_TGMA)
		|| (auth_style & AUTH_STYLE_PROMPTED)) {
	  log_msg((LOG_ERROR_LEVEL, "STYLE expr cannot be combined"));
	  return(AUTH_STYLE_UNKNOWN);
	}
  }

  return(auth_style);
}

static Parse_attr_tab credentials_attr_tab[] = {
  { "federation",   NULL, ATTR_REQUIRED, NULL, 0 },
  { "username",     NULL, ATTR_REQUIRED, NULL, 0 },
  { "jurisdiction", NULL, ATTR_REQUIRED, NULL, 0 },
  { "ip",           NULL, ATTR_REQUIRED, NULL, 0 },
  { "roles",        NULL, ATTR_REQUIRED, NULL, 0 },
  { "auth_time",    NULL, ATTR_REQUIRED, NULL, 0 },
  { "expires",      NULL, ATTR_REQUIRED, NULL, 0 },
  { "auth_style",   NULL, ATTR_REQUIRED, NULL, 0 },
  { "unique",       NULL, ATTR_REQUIRED, NULL, 0 },
  { "version",      NULL, ATTR_REQUIRED, NULL, 0 },
  { "valid_for",    NULL, ATTR_REQUIRED, NULL, 0 },
  { "imported_by",  NULL, ATTR_IMPLIED,  NULL, 0 },
  { "ua_hash",      NULL, ATTR_IMPLIED,  NULL, 0 },
  { NULL,           NULL, ATTR_END,      NULL, 0 }
};

static void
parse_xml_credentials_element_start(void *data, const char *element,
									const char **attr)
{
  char *el, *errmsg, *p;
  Credentials **credentials;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  /* XXX There should only be one element */
  credentials = (Credentials **) data;

  if (streq(el, "credentials")) {
	char *auth_time, *auth_style, *expires, *valid_for;
	Credentials *c;

	if (parse_xml_is_not_empty()) {
	  parse_xml_set_error("Stack not empty at start of parse");
	  return;
	}

	c = init_credentials();

	/* Just put something on the parse stack so it's not empty. */
	parse_xml_push(NULL);

	credentials_attr_tab[0].value  = &c->federation;
	credentials_attr_tab[1].value  = &c->username;
	credentials_attr_tab[2].value  = &c->home_jurisdiction;
	credentials_attr_tab[3].value  = &c->ip_address;
	credentials_attr_tab[4].value  = &c->role_str;
	credentials_attr_tab[5].value  = &auth_time;
	credentials_attr_tab[6].value  = &expires;
	credentials_attr_tab[7].value  = &auth_style;
	credentials_attr_tab[8].value  = &c->unique;
	credentials_attr_tab[9].value  = &c->version;
	credentials_attr_tab[10].value = &valid_for;
	credentials_attr_tab[11].value = &c->imported_by;
	credentials_attr_tab[12].value = &c->ua_hash;
	if (parse_xml_attr(credentials_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	if (!isdigit((int) auth_time[0]))
	  return;
	if (strnum(auth_time, STRNUM_TIME_T, &c->auth_time) == -1)
	  return;

	if (!isdigit((int) expires[0]))
	  return;
	if (strnum(expires, STRNUM_TIME_T, &c->expires_secs) == -1)
	  return;

	if ((c->auth_style = auth_style_from_string(auth_style))
		== AUTH_STYLE_UNKNOWN)
	  return;

	if ((p = is_valid_valid_for(valid_for)) == NULL)
	  return;
	c->valid_for = strdup(p);

	*credentials = c;
	parse_xml_pop((void **) NULL);
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown element: %s", el));
	return;
  }
}

static void
parse_xml_credentials_element_end(void *data, const char *el)
{

}

int
parse_xml_credentials(char *xml_credentials, Credentials **credentials)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
	return(-1);

  parse_xml_init("Credentials", p);

  XML_SetElementHandler(p, parse_xml_credentials_element_start,
						parse_xml_credentials_element_end);
  XML_SetUserData(p, (void *) credentials);

  st = XML_Parse(p, xml_credentials, strlen(xml_credentials), 1);

  if (parse_xml_is_error(&err) || st == 0) {
	if (err.mesg == NULL) {
	  parse_xml_set_error(NULL);
	  parse_xml_is_error(&err);
	}
    log_msg((LOG_ERROR_LEVEL, "parse_xml_credentials: line %d, pos %d",
             err.line, err.pos));
    if (err.mesg != NULL)
      log_msg((LOG_ERROR_LEVEL, "parse_xml_credentials: %s", err.mesg));
    parse_xml_end();
    return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
	return(-1);

  return(0);
}

typedef enum Parse_xml_scredentials_state_code {
  SCREDENTIALS_PARSE_SCREDENTIALS = 0,
  SCREDENTIALS_PARSE_UNAUTH       = 1,
  SCREDENTIALS_PARSE_SELECTED     = 2
} Parse_xml_scredentials_state_code;

typedef struct Parse_xml_scredentials_state {
  Parse_xml_scredentials_state_code code;
  union {
	Scredentials *scredentials;
	Scredentials_unauth *unauth;
	Scredentials_selected *selected;
  } object;
} Parse_xml_scredentials_state;

static Parse_xml_scredentials_state *
parse_xml_make_scredentials_state(Parse_xml_scredentials_state_code code,
										  void *object)
{
  Parse_xml_scredentials_state *s;

  s = ALLOC(Parse_xml_scredentials_state);
  s->code = code;

  switch (code) {
  case SCREDENTIALS_PARSE_SCREDENTIALS:
    s->object.scredentials = (Scredentials *) object;
    break;

  case SCREDENTIALS_PARSE_UNAUTH:
    s->object.unauth = (Scredentials_unauth *) object;
    break;

  case SCREDENTIALS_PARSE_SELECTED:
    s->object.selected = (Scredentials_selected *) object;
    break;

  default:
    /* XXX ??? */
    return(NULL);
  }

  return(s);
}

static Parse_attr_tab scredentials_selected_attr_tab[] = {
  { "federation",   NULL, ATTR_REQUIRED, NULL, 0 },
  { "jurisdiction", NULL, ATTR_REQUIRED, NULL, 0 },
  { "username",     NULL, ATTR_REQUIRED, NULL, 0 },
  { "unique",       NULL, ATTR_REQUIRED, NULL, 0 },
  { "version",      NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,           NULL, ATTR_END,      NULL, 0 }
};

static Parse_attr_tab scredentials_unauth_attr_tab[] = {
  { "federation",   NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,           NULL, ATTR_END,      NULL, 0 }
};

static void
parse_xml_scredentials_element_start(void *data, const char *element,
									 const char **attr)
{
  char *el, *errmsg;
  Scredentials **scp;
  Parse_xml_scredentials_state *state;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  /* XXX There should only be one element */
  scp = (Scredentials **) data;

  if (streq(el, "selected_credentials")) {
	Scredentials *sc;

	if (parse_xml_is_not_empty()) {
	  parse_xml_set_error("Unexpected \"selected_credentials\" element");
	  return;
	}

	if (parse_xml_attr(NULL, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	sc = ALLOC(Scredentials);
	sc->selected = NULL;
	sc->unauth = NULL;
	*scp = sc;

	parse_xml_push(parse_xml_make_scredentials_state(SCREDENTIALS_PARSE_SCREDENTIALS, (void *) sc));
  }
  else if (streq(el, "selected")) {
	Scredentials *sc;
	Scredentials_selected *s, **sp;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
        || state->code != SCREDENTIALS_PARSE_SCREDENTIALS) {
      parse_xml_set_error("Unexpected \"selected\" element");
      return;
    }
    sc = state->object.scredentials;

	if (sc->unauth != NULL) {
      parse_xml_set_error("Unexpected \"selected\" element -- unauth present");
      return;
    }

	s = ALLOC(Scredentials_selected);
	s->federation = NULL;
	s->jurisdiction = NULL;
	s->username = NULL;
	s->unique = NULL;
	s->version = NULL;
	s->next = NULL;

	scredentials_selected_attr_tab[0].value = &s->federation;
	scredentials_selected_attr_tab[1].value = &s->jurisdiction;
	scredentials_selected_attr_tab[2].value = &s->username;
	scredentials_selected_attr_tab[3].value = &s->unique;
	scredentials_selected_attr_tab[4].value = &s->version;
	if (parse_xml_attr(scredentials_selected_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	for (sp = &sc->selected; *sp != NULL; sp = &(*sp)->next)
	  ;
	*sp = s;
  }
  else if (streq(el, "unauth")) {
	Scredentials *sc;
	Scredentials_unauth *scu;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
        || state->code != SCREDENTIALS_PARSE_SCREDENTIALS) {
      parse_xml_set_error("Unexpected \"unauth\" element");
      return;
    }
    sc = state->object.scredentials;

	if (sc->unauth != NULL) {
      parse_xml_set_error("Duplicate \"unauth\" element");
      return;
    }
	if (sc->selected != NULL) {
      parse_xml_set_error("Unexpected \"unauth\" element - selected present");
      return;
    }

	scu = ALLOC(Scredentials_unauth);
	scu->federation = NULL;

	scredentials_unauth_attr_tab[0].value = &scu->federation;
	if (parse_xml_attr(scredentials_unauth_attr_tab, attr, &errmsg)
		== -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}
	sc->unauth = scu;
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown element: %s", el));
	return;
  }
}

static void
parse_xml_scredentials_element_end(void *data, const char *element)
{
  char *el, *err;
  Scredentials *sc;
  Parse_xml_scredentials_state *state;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  sc = *(Scredentials **) data;

  if (parse_xml_is_error(NULL))
	return;

  err = "";
  if (streq(el, "selected_credentials")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR)
	  goto err;
	if (state->code != SCREDENTIALS_PARSE_SCREDENTIALS)
	  goto err;
	sc = state->object.scredentials;
	if (sc->unauth == NULL && sc->selected == NULL)
	  goto err;
	if (sc->unauth != NULL && sc->selected != NULL)
	  goto err;
  }
  else if (streq(el, "unauth")) {
	/* Do nothing */
  }
  else if (streq(el, "selected")) {
	/* Do nothing */
  }
  else {
	err = ds_xprintf("Unknown element: %s", el);
    goto err;
  }

  return;

 err:
  parse_xml_set_error(err);
}

int
parse_xml_scredentials(char *xml_scredentials, Scredentials **scredentials)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
	return(-1);

  parse_xml_init("Scredentials", p);

  XML_SetElementHandler(p, parse_xml_scredentials_element_start,
						parse_xml_scredentials_element_end);
  XML_SetUserData(p, (void *) scredentials);

  st = XML_Parse(p, xml_scredentials, strlen(xml_scredentials), 1);

  if (parse_xml_is_error(&err) || st == 0) {
	if (err.mesg == NULL) {
	  parse_xml_set_error(NULL);
	  parse_xml_is_error(&err);
	}
    log_msg((LOG_ERROR_LEVEL, "parse_xml_scredentials: line %d, pos %d",
             err.line, err.pos));
    if (err.mesg != NULL)
      log_msg((LOG_ERROR_LEVEL, "parse_xml_scredentials: %s", err.mesg));
    parse_xml_end();
    return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
	return(-1);

  return(0);
}

int
set_cookie_output_syntax(Cookie_syntax syntax)
{

  if (syntax == COOKIE_NETSCAPE || syntax == COOKIE_EXT_NETSCAPE) {
	cookie_output_syntax = syntax;
	return(0);
  }

  log_msg((LOG_ERROR_LEVEL,
		   "Attempt to select unsupported cookie output syntax"));
  return(-1);
}

int
configure_cookie_syntax(char *cookie_syntax, char **errmsg)
{
  int st;

  if (strcaseeq(cookie_syntax, "COOKIE_NETSCAPE"))
	st = set_cookie_output_syntax(COOKIE_NETSCAPE);
  else if (strcaseeq(cookie_syntax, "COOKIE_EXT_NETSCAPE"))
	st = set_cookie_output_syntax(COOKIE_EXT_NETSCAPE);
  else if (strcaseeq(cookie_syntax, "COOKIE_RFC2109"))
	st = set_cookie_output_syntax(COOKIE_RFC2109);
  else if (strcaseeq(cookie_syntax, "COOKIE_RFC2965"))
	st = set_cookie_output_syntax(COOKIE_RFC2965);
  else if (strcaseeq(cookie_syntax, "COOKIE_RFC6265"))
	st = set_cookie_output_syntax(COOKIE_RFC6265);
  else {
	*errmsg = "Unrecognized COOKIE_SYNTAX";
	return(-1);
  }

  if (st == -1) {
	*errmsg = "Unsupported COOKIE_SYNTAX";
	return(-1);
  }

  return(0);
}

/*
 * DACS authentication-related cookie names look like:
 *   DACS:<federation-name>::<jurisdiction-name>:<username>
 * And, if SELECTED_CREDENTIALS are enabled:
 *   DACS:<federation-name>::::SELECTED
 * For the latter format, the username is NULL.
 * Return a new Cookie_name structure or NULL on error.
 */
static Cookie_name *
parse_dacs_auth_cookie_name(char *name)
{
  Cookie_name *cn;

  if ((cn = parse_cookie_name(NULL, name)) == NULL)
	return(NULL);

  if (cn->special != NULL) {
	if (streq(cn->special, "SELECTED"))
	  ;
	else if (strneq(cn->special, "TOKEN-", 6))
	  ;
	else {
	  log_msg((LOG_INFO_LEVEL,
			   "Unrecognized auth cookie name, ignored: %s", name));
	  return(NULL);
	}
  }

  return(cn);
}

static Cookie *
init_cookie(void)
{
  Cookie *cookie;

  cookie = ALLOC(Cookie);
  cookie->syntax = COOKIE_NETSCAPE;
  cookie->str = NULL;
  cookie->name = cookie->value = cookie->domain = cookie->path = NULL;
  cookie->parsed_name = NULL;
  cookie->expires = NULL;
  cookie->max_age = NULL;
  cookie->comment = cookie->version = NULL;
  cookie->discard = cookie->port = cookie->comment_url = NULL;
  cookie->secure = 0;
  cookie->httponly = 0;
  cookie->set_void = 0;
  cookie->next = NULL;

  return(cookie);
}

/*
 * Given COOKIE_STR, break up its fields and store them in a Cookie structure.
 * The caller must check the fields for validity.
 * See: developer.netscape.com/docs/manuals/js/client/jsref/cookies.htm
 * For the de facto standard Netscape syntax, We have:
 * <NAME=VALUE>[; expires=DATE][; domain=DOMAIN_NAME][; path=PATH][; secure]
 * The VALUE is allowed to be empty.
 * The optional fields are accepted in any order.
 * The spec is unclear about how to handle an '=' within the name or value;
 * we'll take the first one as the separator and require the cookie creator
 * to URL encode any other '=' characters.
 *
 * This is not an implementation of RFC 2109 or RFC 2965
 * (www.rfc-editor.org/rfc/rfc2965.txt)
 *
 * Return NULL if there is an error, otherwise the parsed cookie.
 */
Cookie *
cookie_parse(char *cookie_str, int *is_dacs_cookie)
{
  char *p, *s, *stop_chars, *str;
  Cookie *cookie;

  /* Make a copy, which we will carve up below. */
  s = str = strdup(cookie_str);
  if ((p = strchr(s, '=')) == NULL) {
	cookie = NULL;
	goto err;
  }
  *p++ = '\0';

  cookie = init_cookie();

  if ((cookie->name = url_decode(s, NULL, NULL)) == NULL)
	goto err;

  s = p;

  if ((cookie->parsed_name = parse_dacs_auth_cookie_name(cookie->name))
	  == NULL)
	*is_dacs_cookie = 0;
  else
	*is_dacs_cookie = 1;

  if ((p = strpbrk(s, "; ")) == NULL) {
	/* There are no optional fields. */
	if ((cookie->value = url_decode(s, NULL, NULL)) == NULL)
	  return(NULL);

	return(cookie);
  }
  *p++ = '\0';

  if ((cookie->value = url_decode(s, NULL, NULL)) == NULL)
	return(NULL);

  for (s = p; *s == ' ' || *s == ';'; s++)
	;

  while (*s != '\0') {
	if (strncaseeq(s, "domain=", 7)) {
	  s += 7;
	  cookie->domain = s;
	  stop_chars = "; ";
	}
	else if (strncaseeq(s, "path=", 5)) {
	  s += 5;
	  cookie->path = s;
	  stop_chars = "; ";
	}
	else if (strncaseeq(s, "secure", 6)) {
	  s += 6;
	  if (*s != ' ' && *s != ';' && *s != '\0')
		goto err;
	  cookie->secure = 1;
	  stop_chars = "; ";
	}
	else if (strncaseeq(s, "httponly", 8)) {
	  s += 8;
	  if (*s != ' ' && *s != ';' && *s != '\0')
		goto err;
	  cookie->httponly = 1;
	  stop_chars = "; ";
	}
	else if (strncaseeq(s, "expires=", 8)) {
	  s += 8;
	  cookie->expires = s;
	  stop_chars = ";";
	}
	else if (strncaseeq(s, "max-age=", 8)) {
	  s += 8;
	  cookie->max_age = s;
	  cookie->syntax = COOKIE_EXT_NETSCAPE;
	  stop_chars = ";";
	}
	else {
	  /*
	   * Ignore this field.
	   * In particular, this ought to skip RFC 2109/2965 style fields, all of
	   * which begin with a '$'.
	   */
	  stop_chars = "; ";
	}

	if ((p = strpbrk(s, stop_chars)) == NULL)
	  break;
	*p++ = '\0';
	for (s = p; *s == ' ' || *s == ';'; s++)
	  ;
  }

  return(cookie);

 err:
  if (cookie != NULL) {
	if (cookie->name != NULL)
	  free(cookie->name);
	if (cookie->value != NULL)
	  free(cookie->value);
	free(str);
  }

  return(NULL);
}

/*
 * Retain non-authentication cookies, which may be used by DACS for
 * other purposes (such as NATs) or which are not intended for DACS at all.
 */
char *non_auth_cookie_header = NULL;
Cookie *non_auth_cookies = NULL;

/*
 * Find all of the DACS-specific cookies for this application.
 * Cookies can be passed in any one of several ways.
 * If KWV is non-NULL, dacs_acs is the caller and KWV, which was obtained from
 * mod_auth_dacs, must define either SERVICE_COOKIE or HTTP_COOKIE.
 * If KWV is NULL, an Authorization header that specifies the "DACS"
 * auth-scheme (RFC 2617) is used if present, otherwise the environment
 * variable DACS_COOKIE or HTTP_COOKIE is used.
 * Whichever source is used, the string is parsed into its components as
 * a Cookie structure.
 * Only the first AUTH_MAX_CREDENTIALS are handled; the rest are dropped.
 * Return -1 upon an error, 0 otherwise and set NCOOKIES to the count.
 *
 * We only parse Netscape spec cookies, not RFC 2109 etc. cookies.
 */
int
get_cookies(Kwv *kwv, Cookie **cookies, unsigned *ncookies)
{
  int is_dacs_cookie, n, saw_http_cookie_envar;
  char *auth_header, *c, *http_cookie, *name_prefix, *p, *r, *s;
  char *app_cookie_name_prefix;
  size_t name_prefix_len;
  Cookie *cookie, *first_cookie, *last_cookie;
  Kwv_pair *v;

  *ncookies = 0;
  *cookies = NULL;

  /*
   * All authentication-related DACS cookies for this federation begin with
   * this prefix.
   * Any others may be used by DACS for other purposes or by the web service
   * being called.
   */
  name_prefix = make_cookie_name_federation_prefix(NULL);
  name_prefix_len = strlen(name_prefix);

  saw_http_cookie_envar = 0;

  s = NULL;
  if ((auth_header = kwv_lookup_value(kwv, "SERVICE_AUTHORIZATION")) != NULL) {
	char **argv;
	unsigned char *decoded;
	Mkargv conf = { 1, 0, NULL, NULL, NULL };

	log_msg((LOG_TRACE_LEVEL, "Authorization header found"));
	if ((n = mkargv(auth_header, &conf, &argv)) == 2
		&& streq(argv[0], "DACS")) {
	  if (mime_decode_base64(argv[1], &decoded) == -1)
		log_msg((LOG_ERROR_LEVEL, "Authorization header failed to decode"));
	  else
		s = (char *) decoded;
	}
	else {
	  log_msg((LOG_TRACE_LEVEL, "Authorization header is non-DACS, ignoring"));
	  auth_header = NULL;
	}
  }

  if (kwv == NULL) {
	if ((c = getenv("DACS_COOKIE")) != NULL && *c != '\0') {
	  log_msg((LOG_TRACE_LEVEL, "get_cookies: env DACS_COOKIE=%s", c));
	  if (s != NULL)
		s = ds_xprintf("%s%s%s", s, COOKIE_SEP_STR, c);
	  else
		s = strdup(c);
	  envzap("DACS_COOKIE");
	}
	else {
	  http_cookie = getenv("HTTP_COOKIE");
	  if (http_cookie == NULL || *http_cookie == '\0')
		return(0);
	  log_msg((LOG_TRACE_LEVEL,
			   "get_cookies: env HTTP_COOKIE=%s", http_cookie));
	  if (s != NULL)
		s = ds_xprintf("%s%s%s", s, COOKIE_SEP_STR, http_cookie);
	  else
		s = strdup(http_cookie);
	  envzap("HTTP_COOKIE");
	  saw_http_cookie_envar = 1;
	}
  }
  else if (auth_header == NULL) {
	if ((v = kwv_lookup(kwv, "SERVICE_COOKIE")) != NULL) {
	  if (s != NULL)
		s = ds_xprintf("%s%s%s", s, COOKIE_SEP_STR, v->val);
	  else
		s = strdup(v->val);
	  log_msg((LOG_TRACE_LEVEL, "get_cookies: SERVICE_COOKIE=%s", s));
	}
	else if ((v = kwv_lookup(kwv, "HTTP_COOKIE")) != NULL) {
	  if (s != NULL)
		s = ds_xprintf("%s%s%s", s, COOKIE_SEP_STR, v->val);
	  else
		s = strdup(v->val);
	  log_msg((LOG_TRACE_LEVEL, "get_cookies: HTTP_COOKIE=%s", s));
	}
	else
	  return(0);
  }

  log_msg((LOG_TRACE_LEVEL, "get_cookies: COOKIE=\"%s\"", non_null(s)));

  app_cookie_name_prefix = make_cookie_name_app_prefix(NULL);

  first_cookie = last_cookie = NULL;
  n = 0;
  p = s;
  while (p != NULL) {
	if (n == AUTH_MAX_CREDENTIALS) {
	  log_msg((LOG_NOTICE_LEVEL, "Too many credentials"));
	  break;
	}

	/*
	 * The Netscape spec says that a semicolon is used to separate
	 * multiple name/value pairs in a Cookie header.
	 * RFC 2109 uses a comma for this purpose; such cookies ought
	 * to be identified by an initial field of '$Version="1";' in the
	 * Cookie header, but that doesn't seem be the case for all clients.
	 * DACS doesn't use a comma in Cookie headers, so treating a comma
	 * as a semicolon ought to be ok.
	 */
	if ((r = strchr(p, ';')) != NULL) {
	  *r++ = '\0';
	  while (*r == ' ')
		r++;
	}
	else if ((r = strchr(p, ',')) != NULL) {
	  *r++ = '\0';
	  while (*r == ' ')
		r++;
	}

	s = p;
	p = r;

	/* Break the cookie up into its fields. */
	if ((cookie = cookie_parse(s, &is_dacs_cookie)) == NULL) {
	  strzap(s);
	  continue;
	}

	if (!is_dacs_cookie) {
	  if (non_auth_cookie_header == NULL) {
		non_auth_cookie_header = strdup(s);
		non_auth_cookies = cookie;
	  }
	  else {
		Cookie **cp;

		non_auth_cookie_header = ds_xprintf("%s%s%s", non_auth_cookie_header,
											COOKIE_SEP_STR, s);
		for (cp = &non_auth_cookies; *cp != NULL; cp = &(*cp)->next)
		  ;
		*cp = cookie;
	  }
	  strzap(s);
	  continue;
	}

	if (!conf_val_eq(CONF_ACCEPT_ALIEN_CREDENTIALS, "yes")) {
	  /*
	   * Ignore cookies unrelated to DACS authentication and this federation.
	   */
	  if (!strneq(cookie->name, name_prefix, name_prefix_len)) {
		log_msg((LOG_DEBUG_LEVEL, "Ignoring cookie: \"%s\"", cookie->name));
		strzap(s);
		continue;
	  }
	}
	else {
	  if (!strneq(cookie->name, app_cookie_name_prefix,
				  strlen(app_cookie_name_prefix))) {
		log_msg((LOG_DEBUG_LEVEL, "Ignoring cookie: \"%s\"", cookie->name));
		strzap(s);
		continue;
	  }
	}

	if (first_cookie == NULL)
	  first_cookie = last_cookie = cookie;
	else {
	  last_cookie->next = cookie;
	  last_cookie = cookie;
	}

	n++;
  }

  *cookies = first_cookie;
  *ncookies = n;

  if (n && saw_http_cookie_envar) {
	/*
	 * If there's a DACS cookie in the environment (via HTTP_COOKIE), we may
	 * have a problem...
	 * But only DACS cookies are a problem, not simply HTTP_COOKIE.
	 */
	if (!conf_val_eq(CONF_ALLOW_HTTP_COOKIE, "yes")) {
	  log_msg((LOG_ALERT_LEVEL, "HTTP_COOKIE present in the environment!"));
	  return(-1);
	}
  }

  if (non_auth_cookie_header != NULL)
	log_msg((LOG_DEBUG_LEVEL, "Saw non-auth cookies: \"%s\"",
			 non_auth_cookie_header));

  return(0);
}

static char *
make_ua_hash(char *ua_str)
{
  unsigned int outlen;
  char *ua_hash;
  unsigned char *outp;

  if (*ua_str != '\0') {
	outp = crypto_digest(AUTH_UA_HASH_DIGEST_NAME, ua_str, strlen(ua_str),
						 NULL, &outlen);
	mime_encode_base64(outp + 10, outlen - 10, &ua_hash);
	log_msg((LOG_TRACE_LEVEL, "UA=\"%s\", outlen=%u, uahash=\"%s\"",
			 ua_str, outlen, ua_hash));
  }
  else
	ua_hash = "";

  return(ua_hash);
}

/*
 * This is called by auth_unique() to heuristically derive an identifier that
 * will ideally be the same for a series of requests from the same user but
 * distinct among all users.
 */
char *
make_hash_from_credentials(Credentials *cr)
{
  Digest_ctx *ctx;
  unsigned int mdbuf_len;
  unsigned char *mdbuf;
  char *buf, *f, *j;

  ctx = crypto_digest_open(CRYPTO_DIGEST_ALG_NAME);
  if ((f = cr->federation) == NULL) {
	if ((f = conf_val(CONF_FEDERATION_NAME)) == NULL) {
	  /* In case there's no configuration... */
	  f = "";
	}
  }
  crypto_digest_hash(ctx, (const void *) f, strlen(f));
  crypto_digest_hash(ctx, (const void *) cr->username, strlen(cr->username));
  if ((j = cr->home_jurisdiction) == NULL)
	j = "";
  crypto_digest_hash(ctx, (const void *) j, strlen(j));
  crypto_digest_hash(ctx, (const void *) cr->ip_address,
					 strlen(cr->ip_address));
  crypto_digest_hash(ctx, (const void *) cr->version,
					 strlen(cr->version));
  if (cr->imported_by != NULL)
	crypto_digest_hash(ctx, (const void *) cr->imported_by,
					   strlen(cr->imported_by));
  if (cr->ua_hash != NULL)
	crypto_digest_hash(ctx, (const void *) cr->ua_hash,
					   strlen(cr->ua_hash));

  mdbuf = crypto_digest_close(ctx, NULL, &mdbuf_len);

  strba64(mdbuf, mdbuf_len, &buf);
  free(mdbuf);

  return(ds_xprintf("%.8s", buf));
}

/*
 * Return a heuristically-derived identifier string for the context of the
 * current request if no credentials are available, otherwise an almost
 * certainly unique identifier string.  The former is not guaranteed to be
 * unique but may be in some environments and usages.
 * The latter includes the former as a prefix.
 * This identifier is used for audit purposes, to associate a sequence of
 * requests with a particular source.
 */
char *
auth_tracker(Credentials *cr)
{
  static char *tracker = NULL;

  if (tracker == NULL) {
	char *ua_str;
	Credentials *temp;

	temp = init_credentials();
	if ((temp->username = getenv("SSL_CLIENT_S_DN")) == NULL)
	  temp->username = "";
	temp->home_jurisdiction = NULL;
	temp->federation = NULL;
	if ((temp->ip_address = getenv("REMOTE_ADDR")) == NULL)
	  temp->ip_address = DEFAULT_IP_ADDR;
	if ((ua_str = getenv("HTTP_USER_AGENT")) == NULL)
	  ua_str = "";
	temp->ua_hash = make_ua_hash(ua_str);
	temp->version = DACS_VERSION_NUMBER;
	tracker = make_hash_from_credentials(temp);
  }

  if (cr == NULL)
	return(tracker);

  return(ds_xprintf("%s,%s", tracker, cr->unique));
}

char *
auth_identity(char *federation, char *jurisdiction, char *username,
			  char *tracker)
{
  Ds ds;

  ds_init(&ds);
  if (jurisdiction == NULL || username == NULL)
	ds_asprintf(&ds, "unauth");
  else {
	if (jurisdiction == NULL)
	  jurisdiction = "???";
	if (username == NULL)
	  username = "???";
	if (federation == NULL)
	  ds_asprintf(&ds, "%s%c%s",
				  jurisdiction, JURISDICTION_NAME_SEP_CHAR, username);
	else
	  ds_asprintf(&ds, "%s%c%c%s%c%s",
				  federation,
				  JURISDICTION_NAME_SEP_CHAR, JURISDICTION_NAME_SEP_CHAR,
				  jurisdiction, JURISDICTION_NAME_SEP_CHAR,
				  username);
	if (tracker != NULL)
	  ds_asprintf(&ds, " (%s)", tracker);
  }

  return(ds_buf(&ds));
}

char *
auth_identity_mine(char *username)
{

  return(auth_identity(conf_val(CONF_FEDERATION_NAME),
					   conf_val(CONF_JURISDICTION_NAME),
					   username, NULL));
}

char *
auth_identity_from_credentials(Credentials *cr)
{

  if (cr == NULL)
	return("unauth");

  return(auth_identity(cr->federation, cr->home_jurisdiction, cr->username,
					   NULL));
}

char *
auth_identity_from_credentials_track(Credentials *cr)
{
  char *a, *tracker;

  tracker = auth_tracker(cr);
  if (cr == NULL)
	a = ds_xprintf("unauthenticated user (%s)", tracker);
  else
	a = auth_identity(cr->federation, cr->home_jurisdiction, cr->username,
					  tracker);
  return(a);
}

char *
auth_identities_from_credentials_track(Credentials *credentials)
{
  char *tracker;
  Credentials *cr;
  Ds ds;

  if (credentials == NULL)
	return(auth_identity_from_credentials_track(NULL));

  ds_init(&ds);
  for (cr = credentials; cr != NULL; cr = cr->next) {
	tracker = auth_tracker(cr);
	ds_asprintf(&ds, "%s%s", cr == credentials ? "" : ", ",
				auth_identity(cr->federation, cr->home_jurisdiction,
							  cr->username, tracker));
  }

  return(ds_buf(&ds));
}

char *
auth_identity_from_cookie(Cookie *c)
{

  if (c == NULL || c->parsed_name == NULL)
	return(NULL);
  return(auth_identity(c->parsed_name->federation,
					   c->parsed_name->jurisdiction,
					   c->parsed_name->username, NULL));
}

char *
user_info_service_uri(char *jurisdiction_name)
{
  char *jname;
  static Jurisdiction *j_info = NULL;

  if ((jname = jurisdiction_name) == NULL)
	jname = conf_val(CONF_JURISDICTION_NAME);

  if (j_info == NULL || !streq(j_info->jname, jname)) {
	if (get_jurisdiction_meta(jname, &j_info) == -1)
	  return(NULL);
  }

  return(ds_xprintf("[user_info]dacs-vfs:%s/dacs_vfs", j_info->dacs_url));
}

static Parse_attr_tab user_info_auth_attr_tab[] = {
  { "ident",        NULL, ATTR_REQUIRED,  NULL, 0 },
  { "ip",           NULL, ATTR_REQUIRED,  NULL, 0 },
  { "auth_time",    NULL, ATTR_REQUIRED,  NULL, 0 },
  { "expires",      NULL, ATTR_REQUIRED,  NULL, 0 },
  { "jurisdiction", NULL, ATTR_REQUIRED,  NULL, 0 },
  { "auth_style",   NULL, ATTR_REQUIRED,  NULL, 0 },
  { "valid_for" ,   NULL, ATTR_REQUIRED,  NULL, 0 },
  { "unique",       NULL, ATTR_REQUIRED,  NULL, 0 },
  { "imported_by",  NULL, ATTR_IMPLIED,   NULL, 0 },
  { NULL,           NULL, ATTR_END,       NULL, 0 }
};

static Parse_attr_tab user_info_signout_attr_tab[] = {
  { "ident",        NULL, ATTR_REQUIRED,  NULL, 0 },
  { "ip",           NULL, ATTR_REQUIRED,  NULL, 0 },
  { "date",         NULL, ATTR_REQUIRED,  NULL, 0 },
  { "jurisdiction", NULL, ATTR_REQUIRED,  NULL, 0 },
  { "unique",       NULL, ATTR_REQUIRED,  NULL, 0 },
  { NULL,           NULL, ATTR_END,       NULL, 0 }
};

static Parse_attr_tab user_info_access_attr_tab[] = {
  { "ident",        NULL, ATTR_REQUIRED,  NULL, 0 },
  { "ip",           NULL, ATTR_REQUIRED,  NULL, 0 },
  { "date",         NULL, ATTR_REQUIRED,  NULL, 0 },
  { "jurisdiction", NULL, ATTR_REQUIRED,  NULL, 0 },
  { "host",         NULL, ATTR_IMPLIED,   NULL, 0 },
  { "addr",         NULL, ATTR_IMPLIED,   NULL, 0 },
  { "unique",       NULL, ATTR_REQUIRED,  NULL, 0 },
  { "uri",          NULL, ATTR_REQUIRED,  NULL, 0 },
  { NULL,           NULL, ATTR_END,       NULL, 0 }
};

static void
parse_xml_user_info_element_start(void *data, const char *element,
								  const char **attr)
{
  char *el, *errmsg;
  User_info *ui, **uip;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
    parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
    return;
  }

  uip = (User_info **) data;

  ui = *uip = ALLOC(User_info);
  ui->which = USERINFO_ERROR;

  if (parse_xml_is_error(NULL))
    return;

  /* Just put something on the parse stack so it's not empty. */
  parse_xml_push(NULL);

  if (streq(el, "auth")) {
	User_auth *ua;

	ua = &ui->info.auth;
	user_info_auth_attr_tab[0].value = &ua->ident;
	user_info_auth_attr_tab[1].value = &ua->ip;
	user_info_auth_attr_tab[2].value = &ua->auth_time;
	user_info_auth_attr_tab[3].value = &ua->expires;
	user_info_auth_attr_tab[4].value = &ua->jurisdiction;
	user_info_auth_attr_tab[5].value = &ua->auth_style;
	user_info_auth_attr_tab[6].value = &ua->valid_for;
	user_info_auth_attr_tab[7].value = &ua->unique;
	user_info_auth_attr_tab[8].value = &ua->imported_by;
	ua->imported_by = NULL;
	if (parse_xml_attr(user_info_auth_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}
	ui->which = USERINFO_AUTH;
  }
  else if (streq(el, "signout")) {
	User_signout *us;

	us = &ui->info.signout;
	user_info_signout_attr_tab[0].value = &us->ident;
	user_info_signout_attr_tab[1].value = &us->ip;
	user_info_signout_attr_tab[2].value = &us->date;
	user_info_signout_attr_tab[3].value = &us->jurisdiction;
	user_info_signout_attr_tab[4].value = &us->unique;
	if (parse_xml_attr(user_info_signout_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}
	ui->which = USERINFO_SIGNOUT;
  }
  else if (streq(el, "acs")) {
	User_access *uacs;

	uacs = &ui->info.acs;
	user_info_access_attr_tab[0].value = &uacs->ident;
	user_info_access_attr_tab[1].value = &uacs->ip;
	user_info_access_attr_tab[2].value = &uacs->date;
	user_info_access_attr_tab[3].value = &uacs->jurisdiction;
	user_info_access_attr_tab[4].value = &uacs->host;
	user_info_access_attr_tab[5].value = &uacs->addr;
	user_info_access_attr_tab[6].value = &uacs->unique;
	user_info_access_attr_tab[7].value = &uacs->uri;
	uacs->host = uacs->addr = NULL;
	if (parse_xml_attr(user_info_access_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}
	ui->which = USERINFO_ACCESS;
  }
  else
	return;

  parse_xml_pop((void **) NULL);
}

static void
parse_xml_user_info_element_end(void *data, const char *el)
{

}

/*
 * Parse one user information tracking record.
 */
int
parse_xml_user_info(char *user_info_rec, User_info **uip)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
    return(-1);

  parse_xml_init("Userinfo", p);

  XML_SetElementHandler(p, parse_xml_user_info_element_start,
                        parse_xml_user_info_element_end);
  XML_SetUserData(p, (void *) uip);

  st = XML_Parse(p, user_info_rec, strlen(user_info_rec), 1);

  if (parse_xml_is_error(&err) || st == 0) {
    if (err.mesg == NULL) {
      parse_xml_set_error(NULL);
      parse_xml_is_error(&err);
    }
    log_msg((LOG_ERROR_LEVEL, "parse_xml_user_info: line %d, pos %d",
             err.line, err.pos));
    if (err.mesg != NULL)
      log_msg((LOG_ERROR_LEVEL, "parse_xml_user_info: %s", err.mesg));
    parse_xml_end();
    return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
    return(-1);

  return(0);
}

/*
 * Return a vector of user information tracking records at JURISDICTION.
 */
Dsvec *
user_info_load(char *jurisdiction)
{
  char *buf, *e, *s, *uri;
  Dsvec *dsv;
  User_info *ui;
  Vfs_handle *h;

  if ((uri = user_info_service_uri(jurisdiction)) == NULL)
	return(NULL);

  if ((h = vfs_open_item_type(ITEM_TYPE_USER_INFO)) == NULL)
	return(NULL);

  if (vfs_get(h, NULL, (void **) &buf, NULL) == -1) {
	vfs_close(h);
	return(NULL);
  }

  vfs_close(h);

  dsv = dsvec_init(NULL, sizeof(User_info *));

  s = buf;
  while (*s != '\0') {
	if ((e = strchr(s, (int) '\n')) != NULL)
	  *e = '\0';
	if (parse_xml_user_info(s, &ui) == -1)
	  return(NULL);
	dsvec_add_ptr(dsv, ui);
	s = e + 1;
  }

  return(dsv);
}

/*
 * Return a list of user information tracking records by scanning the
 * records in DSV looking for authentication instances that are apparently
 * still active (they have not expired and there is no corresponding signout
 * record) for IDENT, or all identities if IDENT is NULL.
 * Return NULL is no such records exist.
 * If IDENT and LAST_AUTH are non-NULL, set LAST_AUTH to the last
 * authentication record for IDENT.
 */
Dsvec *
user_info_active(Dsvec *dsv, char *ident)
{
  int i, j;
  time_t expires_secs, now;
  Dsvec *dsv_active;
  User_info *ui, *ui_later;

  time(&now);
  dsv_active = NULL;

  ui = NULL;
  for (i = 0; i < dsvec_len(dsv); i++) {
	ui = (User_info *) dsvec_ptr_index(dsv, i);
	if (ui->which != USERINFO_AUTH
		|| (ident != NULL && !streq(ui->info.auth.ident, ident)))
	  continue;

	if (strnum(ui->info.auth.expires, STRNUM_TIME_T, &expires_secs) == -1)
	  continue;

	if (expires_secs <= now)
	  continue;

	for (j = i + 1; j < dsvec_len(dsv); j++) {
	  ui_later = (User_info *) dsvec_ptr_index(dsv, j);
	  if (ui_later->which == USERINFO_SIGNOUT
		  && streq(ui_later->info.signout.unique, ui->info.auth.unique))
		break;
	}
	if (j != dsvec_len(dsv))
	  continue;

	if (dsv_active == NULL)
	  dsv_active = dsvec_init(NULL, sizeof(User_info *));
	dsvec_add_ptr(dsv_active, ui);
	log_msg((LOG_DEBUG_LEVEL, "Active user: %s (expires in %lu secs)\n",
			 ui->info.auth.ident,
			 (unsigned long) expires_secs - (unsigned long) now));
  }

  return(dsv_active);
}

#ifdef NOTDEF
/*
 * Return a list of user information tracking records by scanning the
 * records in DSV to remove access records and inactive sessions
 * (expired authentication records or pairs of authentication/sign-off
 * records, except for the last one).
 * This can be used to reduce the number of records without losing
 * the one for the last sign on and any active sessions.
 */
Dsvec *
user_info_trim(Dsvec *dsv)
{
  int i;
  Dsvec *dsv_new;
  User_info *ui;

  dsv_new = dsvec_init(NULL, sizeof(User_info *));
  for (i = 0; i < dsvec_len(dsv); i++) {
	ui = (User_info *) dsvec_ptr_index(dsv, i);
	if (ui->which == USERINFO_ACCESS)
	  continue;
dsvec_add_ptr(dsv_new, ui);
  }

  return(dsv_new);
}
#endif

Dsvec *
user_info_last_auth(Dsvec *dsv, char *ident)
{
  int i;
  Dsvec *dsv_auth;
  User_info *ui;

  ui = NULL;
  dsv_auth = NULL;

  for (i = dsvec_len(dsv) - 1; i >= 0; i--) {
	ui = (User_info *) dsvec_ptr_index(dsv, i);
	if (ui->which == USERINFO_AUTH && streq(ui->info.auth.ident, ident)) {
	  if (dsv_auth == NULL)
		dsv_auth = dsvec_init(NULL, sizeof(User_info *));
	  dsvec_add_ptr(dsv_auth, ui);
	}
  }

  return(dsv_auth);
}

#ifdef ENABLE_USER_INFO
void
user_info_authenticate(Credentials *cr)
{
  Ds ds;
  Vfs_conf *conf;
  Vfs_handle *h;

  if (cr->auth_style & AUTH_STYLE_ADMIN)
	return;

  conf = vfs_conf(NULL);
  conf->append_flag = 1;
  vfs_conf(conf);
  if ((h = vfs_open_item_type(ITEM_TYPE_USER_INFO)) == NULL)
	return;

  ds_init(&ds);
  ds_asprintf(&ds, "<auth");
  ds_asprintf(&ds, " ident=\"%s\"", auth_identity_from_credentials(cr));
  ds_asprintf(&ds, " ip=\"%s\"", cr->ip_address);
  ds_asprintf(&ds, " auth_time=\"%lu\"", cr->auth_time);
  ds_asprintf(&ds, " expires=\"%lu\"", cr->expires_secs);
  ds_asprintf(&ds, " jurisdiction=\"%s\"", conf_val(CONF_JURISDICTION_NAME));
  ds_asprintf(&ds, " auth_style=\"%u\"", cr->auth_style);
  ds_asprintf(&ds, " valid_for=\"%s\"", cr->valid_for);
  ds_asprintf(&ds, " unique=\"%s\"", cr->unique);
  if (cr->imported_by != NULL)
	ds_asprintf(&ds, " imported_by=\"%s\"", cr->imported_by);
  ds_asprintf(&ds, "/>\n");

  vfs_put(h, NULL, ds_buf(&ds), ds_len(&ds) - 1);

  vfs_close(h);
}

void
user_info_signout(Credentials *cr)
{
  Ds ds;
  Vfs_conf *conf;
  Vfs_handle *h;

  ds_init(&ds);
  ds_asprintf(&ds, "<signout");
  ds_asprintf(&ds, " ident=\"%s\"", auth_identity_from_credentials(cr));
  ds_asprintf(&ds, " ip=\"%s\"", cr->ip_address);
  ds_asprintf(&ds, " date=\"%lu\"", time(NULL));
  ds_asprintf(&ds, " jurisdiction=\"%s\"", conf_val(CONF_JURISDICTION_NAME));
  ds_asprintf(&ds, " unique=\"%s\"", cr->unique);
  ds_asprintf(&ds, "/>\n");

  conf = vfs_conf(NULL);
  conf->append_flag = 1;

  if (streq(cr->home_jurisdiction, conf_val(CONF_JURISDICTION_NAME))) {
	vfs_conf(conf);
	if ((h = vfs_open_item_type(ITEM_TYPE_USER_INFO)) == NULL)
	  return;
  }
  else {
	char *vfs_uri;

	/* Must write the record to cr->home_jurisdiction */
	if ((vfs_uri = user_info_service_uri(cr->home_jurisdiction)) == NULL)
	  return;
	vfs_conf(conf);
	if ((h = vfs_open_uri(vfs_uri)) == NULL)
	  return;

	if (streq(h->sd->uri->scheme, "https")) {
	  char *cookies[2];
	  Credentials *admin_cr;

	  admin_cr = make_admin_credentials();
	  credentials_to_auth_cookies(admin_cr, &cookies[0]);
	  cookies[1] = NULL;
	  vfs_control(h, VFS_SET_COOKIES, cookies);
	}
  }

  vfs_put(h, NULL, ds_buf(&ds), ds_len(&ds) - 1);

  vfs_close(h);
}

void
user_info_access(Credentials *cr, char *uri, char *remote_addr,
				 char *remote_host)
{
  char *p;
  Ds ds;
  Vfs_conf *conf;
  Vfs_handle *h;

  if (cr == NULL || (cr->auth_style & AUTH_STYLE_ADMIN))
	return;

  ds_init(&ds);
  ds_asprintf(&ds, "<acs");
  ds_asprintf(&ds, " ident=\"%s\"", auth_identity_from_credentials(cr));
  ds_asprintf(&ds, " ip=\"%s\"", cr->ip_address);
  ds_asprintf(&ds, " date=\"%lu\"", time(NULL));
  ds_asprintf(&ds, " jurisdiction=\"%s\"", conf_val(CONF_JURISDICTION_NAME));
  if (remote_host != NULL)
	ds_asprintf(&ds, " host=\"%s\"", remote_host);
  else if (remote_addr != NULL)
	ds_asprintf(&ds, " addr=\"%s\"", remote_addr);
  else if ((p = getenv("REMOTE_HOST")) != NULL)
	ds_asprintf(&ds, " host=\"%s\"", p);
  else if ((p = getenv("REMOTE_ADDR")) != NULL)
	ds_asprintf(&ds, " addr=\"%s\"", p);
  ds_asprintf(&ds, " unique=\"%s\"", cr->unique);
  ds_asprintf(&ds, " uri=\"%s\"", url_encode(uri, 0));
  ds_asprintf(&ds, "/>\n");

  conf = vfs_conf(NULL);
  conf->append_flag = 1;

  if (streq(cr->home_jurisdiction, conf_val(CONF_JURISDICTION_NAME))) {
	vfs_conf(conf);
	if ((h = vfs_open_item_type(ITEM_TYPE_USER_INFO)) == NULL)
	  return;
  }
  else {
	/* Must write the record to cr->home_jurisdiction */
	char *vfs_uri;

	/* Must write the record to cr->home_jurisdiction */
	if ((vfs_uri = user_info_service_uri(cr->home_jurisdiction)) == NULL)
	  return;
	vfs_conf(conf);
	if ((h = vfs_open_uri(vfs_uri)) == NULL)
	  return;

	if (streq(h->sd->uri->scheme, "https")) {
	  char *cookies[2];
	  Credentials *admin_cr;

	  admin_cr = make_admin_credentials();
	  credentials_to_auth_cookies(admin_cr, &cookies[0]);
	  cookies[1] = NULL;
	  vfs_control(h, VFS_SET_COOKIES, cookies);
	}
  }

  vfs_put(h, NULL, ds_buf(&ds), ds_len(&ds) - 1);

  vfs_close(h);
}
#endif

/*
 * As a structure, credentials are only used in memory.  They are converted
 * to XML before they leave DACS, so we don't need to worry about byte
 * order issues here.
 * We assume that USERNAME is already known to be valid.
 */
Credentials *
make_credentials(char *fname, char *jname, char *username, char *ip,
				 char *role_str, char *expires, Auth_style style,
				 char *validity, char *imported_by, char *ua_str)
{
  Credentials *cr;
  time_t et;

  cr = init_credentials();
  if (fname != NULL)
	cr->federation = strdup(fname);
  else
	cr->federation = strdup(conf_val(CONF_FEDERATION_NAME));
  if (jname != NULL)
	cr->home_jurisdiction = strdup(jname);
  else
	cr->home_jurisdiction = strdup(conf_val(CONF_JURISDICTION_NAME));
  cr->username = strdup(username);
  cr->ip_address = strdup(non_null(ip));
  cr->role_str = strdup(non_null(role_str));
  cr->auth_time = time(NULL);
  if (expires == NULL)
	et = make_auth_expiry_delta(cr->auth_time,
								conf_val(CONF_AUTH_CREDENTIALS_DEFAULT_LIFETIME_SECS));
  else
	et = make_auth_expiry_delta(cr->auth_time, expires);
  cr->expires_secs = et;
  cr->auth_style = style;
  cr->version = strdup(DACS_VERSION_NUMBER);
  cr->valid_for = strdup(validity);
  if (imported_by != NULL)
	cr->imported_by = strdup(imported_by);

  /*
   * As an additional measure against misappropriated credentials, optionally
   * put a hash of the user agent identification string (the USER-AGENT
   * request header) in the credentials for (optional) comparison with the
   * string supplied with subsequent requests.  This is similar in concept to
   * IP address checking.
   * A 20-byte hash seems a bit much (excuse the pun); let's only use
   * 10 bytes.
   *
   * If ua_str is NULL, no check should be made; if it is the
   * empty string, it means the client did not provide a UA identifier;
   * otherwise, ua_str is the UA identifier.
   */
  if (ua_str != NULL && !conf_val_eq(CONF_VERIFY_UA, "disable"))
	cr->ua_hash = make_ua_hash(ua_str);

  cr->unique = crypto_make_random_a64(NULL, AUTH_CREDENTIALS_UNIQUE_BYTES);

  cr->next = NULL;

#ifdef ENABLE_USER_INFO
  user_info_authenticate(cr);
#endif

  return(cr);
}

Credentials *
make_admin_credentials(void)
{
  char *expires, *role_str, *ua_str, *validity;

  if ((expires = conf_val(CONF_AUTH_CREDENTIALS_ADMIN_LIFETIME_SECS)) == NULL)
	expires = AUTH_CREDENTIALS_ADMIN_DEFAULT_LIFETIME_SECS;

  role_str = "";
  validity = AUTH_VALID_FOR_ACS;
  ua_str = NULL;

  return(make_credentials(NULL, NULL, make_dacs_admin_name("DACS-ADMIN"),
						  "", role_str, expires,
						  AUTH_STYLE_ADMIN, validity, NULL, ua_str));
}

int
credentials_externalize_with_keys(Credentials *credentials, Crypt_keys *ck,
								  char **buf)
{
  unsigned int cookie_len, len;
  unsigned char *encrypted_cookie;
  char *cr_str;

  if (credentials == NULL) {
	*buf = "";
	return(0);
  }

  cookie_len = make_xml_credentials(credentials, &cr_str);
  log_msg((LOG_TRACE_LEVEL, "XML credentials='%s'", cr_str));

  len = crypto_encrypt_string(ck, (unsigned char *) cr_str,
							  cookie_len + 1, &encrypted_cookie);
  strba64(encrypted_cookie, len, buf);

  return(strlen(*buf));
}

int
credentials_externalize(Credentials *credentials, char **buf)
{
  int length;
  Crypt_keys *ck;

  if (credentials == NULL) {
	*buf = "";
	return(0);
  }

  ck = crypt_keys_from_vfs(ITEM_TYPE_FEDERATION_KEYS);
  length = credentials_externalize_with_keys(credentials, ck, buf);
										
  return(length);
}

/*
 * Convert the list of CREDENTIALS to XML, encrypt, convert that to text,
 * and then create one or more "name=value" strings that will be the payload
 * of a Set-Cookie.
 * Return the length of the cookie (-1 implies an error occurred)
 * XXX Might consider XML Encryption
 * http://www.w3.org/TR/2002/REC-xmlenc-core-20021210/
 */
int
credentials_to_auth_cookies(Credentials *credentials, char **cookie_buf)
{
  int i, length, st;
  char *encrypted_cookie_text;
  Credentials *cr;
  Crypt_keys *ck;
  Ds ds;

  if (credentials == NULL) {
	*cookie_buf = "";
	return(0);
  }

  ds_init(&ds);
  ds.exact_flag = 1;
  ck = crypt_keys_from_vfs(ITEM_TYPE_FEDERATION_KEYS);

  st = 0;
  for (i = 0, cr = credentials; cr != NULL; cr = cr->next, i++) {
	credentials_externalize_with_keys(cr, ck, &encrypted_cookie_text);

	if (cr->cookie_name == NULL)
	  cr->cookie_name = make_auth_cookie_name(cr);

	st = ds_asprintf(&ds, "%s%s=%s", i == 0 ? "" : COOKIE_SEP_STR,
					 cr->cookie_name, encrypted_cookie_text);
	if (st == -1)
	  break;
  }

  crypt_keys_free(ck);
  if (st == -1)
	return(-1);

  *cookie_buf = ds_buf(&ds);
  length = ds_len(&ds);

  return(length);
}

/*
 * Create a Set-Cookie response header string (setting
 * SET_COOKIE_BUF to point to it) and return its length.
 * COOKIE is the 'name=value' part of the cookie.
 * A return value of zero implies an error occurred.
 *
 * XXX The values obtained from configuration aren't checked for validity.
 */
int
make_set_cookie_header(char *cookie, char *lifetime, int for_acs,
					   int cookie_only, char **set_cookie_buf)
{
  char *path, *persistent_cookie;
  Ds ds;

  if (for_acs && cookie_only) {
	/* An invalid combination. */
	return(0);
  }

  if (DACS_USE_PERSISTENT_COOKIE && lifetime != NULL) {
	if (cookie_output_syntax == COOKIE_NETSCAPE)
	  persistent_cookie = ds_xprintf("expires=Fri, 31-Dec-2012 17:17:17 GMT");
	else
	  persistent_cookie = ds_xprintf("Max-Age=%s", lifetime);
  }
  else
	persistent_cookie = NULL;

  if ((path = conf_val(CONF_COOKIE_PATH)) == NULL)
	path = "/";

  ds_init(&ds);
  if (cookie_only)
	ds_asprintf(&ds, "%s", cookie);
  else {
	/* XXX All of these things should use the new acs_emit_*header() API */
	ds_asprintf(&ds, "%sSet-Cookie%s%s",
				for_acs ? "=" : "",
				for_acs ? "=" : ": ",
				cookie);
  }

  ds_asprintf(&ds, "; path=%s", path);

  if (!conf_val_eq(CONF_COOKIE_NO_DOMAIN, "yes")) {
	/* Note the "." preceding the value of the domain attribute. */
	ds_asprintf(&ds, "; domain=.%s",
				conf_val(CONF_FEDERATION_DOMAIN));
  }

  if (persistent_cookie != NULL)
	ds_asprintf(&ds, "; %s", persistent_cookie);

  if (dacs_secure_mode() || dacs_is_https_request())
	ds_asprintf(&ds, "; secure");

  /* http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp */
  if (conf_val_eq(CONF_COOKIE_HTTPONLY, "yes"))
	ds_asprintf(&ds, "; HttpOnly");

  ds_asprintf(&ds, "\n");

  if (!cookie_only)
	ds_asprintf(&ds, "%sCache-Control%sno-cache=\"set-cookie\"\n",
				for_acs ? "=" : "",
				for_acs ? "=" : ": ");

  *set_cookie_buf = ds_buf(&ds);
  return(ds_len(&ds));
}

int
make_set_void_cookie_header(char *cookie_name, int for_acs,
							char **set_cookie_buf)
{
  char *path, *ps;
  Ds ds;

  /*
   * A time in the past.  We use the earliest possible Unix clock time
   * (time(NULL) == 0).
   * XXX The Netscape cookie spec isn't specific about what the earliest
   * possible date is.  This may be important if we want to forcibly remove
   * cookies (i.e., revoke authentication) and the user has turned back his
   * clock.
   */
  if (cookie_output_syntax == COOKIE_NETSCAPE)
	ps = "expires=Thu, 01-Jan-1970 00:00:01 GMT";
  else
	ps = ds_xprintf("Max-Age=0");

  if ((path = conf_val(CONF_COOKIE_PATH)) == NULL)
	path = "/";

  ds_init(&ds);
  if (conf_val_eq(CONF_COOKIE_NO_DOMAIN, "yes"))
	ds_asprintf(&ds, "%sSet-Cookie%s%s=; path=%s; %s\n",
				for_acs ? "=" : "",
				for_acs ? "=" : ": ",
				cookie_name, path, ps);
  else {
	/* Note the "." preceding the value of the domain attribute. */
	ds_asprintf(&ds, "%sSet-Cookie%s%s=; path=%s; domain=.%s;%s\n",
				for_acs ? "=" : "",
				for_acs ? "=" : ": ",
				cookie_name, path, conf_val(CONF_FEDERATION_DOMAIN), ps);
  }

  *set_cookie_buf = ds_buf(&ds);
  return(0);
}

/*
 * Construct a Set-Cookie response header using CREDENTIALS.
 * Return the length of the string or -1 on error.
 */
int
make_set_auth_cookie_header(Credentials *credentials, char *lifetime,
							int cookie_only, char **cookie_buf)
{
  int len;
  char *cookie;

  if (credentials_to_auth_cookies(credentials, &cookie) == -1)
	return(-1);

  len = make_set_cookie_header(cookie, lifetime, 0, cookie_only, cookie_buf);

  strzap(cookie);
  free(cookie);

  return(len);
}

/*
 * Check if the syntax of COOKIE_NAME is valid for the name of an
 * authentication cookie for this federation.
 * Return 1 if it is, 0 otherwise.
 */
int
is_auth_cookie_name(char *cookie_name)
{
  Cookie_name *cn;

  if ((cn = parse_dacs_auth_cookie_name(cookie_name)) == NULL)
	return(0);

  if (cn->app_name == NULL || cn->federation == NULL
	  || cn->jurisdiction == NULL || cn->username == NULL)
	return(0);

  if (!streq(cn->app_name, "DACS"))
	return(0);

  if (!conf_val_eq(CONF_ACCEPT_ALIEN_CREDENTIALS, "yes")) {
	if (!name_eq(cn->federation, conf_val(CONF_FEDERATION_NAME),
				 DACS_NAME_CMP_CONFIG)) {
	  log_msg((LOG_INFO_LEVEL, "Ignoring credentials from federation \"%s\"",
			   cn->federation));
	  return(0);
	}
  }
  else if (!is_valid_federation_name(cn->federation))
	return(0);

  if (auth_single_cookie() == 2) {
	/*
	 * The jurisdiction name is suppressed at this jurisdiction; we will reject
	 * a cookie name that includes it.
	 * But if it is from a different jurisdiction we don't care.
	 */
	if (name_eq(cn->jurisdiction, conf_val(CONF_JURISDICTION_NAME),
				DACS_NAME_CMP_CONFIG)) {
	  log_msg((LOG_INFO_LEVEL,
			   "Ignoring credentials because of suppressed jurisdiction"));
	  return(0);
	}
  }
  else if (auth_single_cookie() == 1) {
	/*
	 * The username is suppressed at this jurisdiction; we will reject
	 * a cookie name that includes it.
	 * But if it is from a different jurisdiction, we don't care.
	 */
	if (name_eq(cn->jurisdiction, conf_val(CONF_JURISDICTION_NAME),
				DACS_NAME_CMP_CONFIG) && *cn->username != '\0') {
	  log_msg((LOG_INFO_LEVEL,
			   "Ignoring credentials because of suppressed username"));
	  return(0);
	}
  }
  else {
	/*
	 * AUTH_SINGLE_COOKIE is disabled, so we don't care if the jurisdiction
	 * name or username are absent.  This cookie may have come from a different
	 * jurisdiction or the directive might have been changed.
	 */
  }
 
  if (*cn->jurisdiction != '\0'
	  && !is_valid_jurisdiction_name(cn->jurisdiction))
	return(0);

  if (*cn->username != '\0' && !is_valid_username(cn->username))
	return(0);

  if (*cn->jurisdiction == '\0' && *cn->username != '\0')
	return(0);

  return(1);
}

#ifdef ANONYMOUS_USERS
/*
 * Anonymous credentials are intended to represent an unauthenticated user
 * for tracking purposes and to allow such users to have roles.
 */
static char *
make_anonymous_cookie_name(void)
{
  char *name;

  name = make_cookie_name(NULL, DACS_COOKIE_APP_NAME,
						  conf_val(CONF_FEDERATION_NAME),
						  conf_val(CONF_JURISDICTION_NAME),
						  make_dacs_admin_name("ANONYMOUS"), NULL);
  return(name);
}

int
is_anonymous_cookie_name(char *cookie_name)
{
  char *cn;

  cn = make_anonymous_cookie_name();
  if (streq(cookie_name, cn))
	return(1);

  return(0);
}

Credentials *
make_anonymous_credentials(char *ip, char *role_str, char *ua_str)
{

  return(make_credentials(NULL, NULL, make_dacs_admin_name("ANONYMOUS"),
						  ip, role_str, NULL, AUTH_STYLE_ADMIN,
						  AUTH_VALID_FOR_ACS, ua_str));
}
#endif

static char *
make_selected_cookie_name(void)
{
  char *name;

  name = make_cookie_name(NULL, DACS_COOKIE_APP_NAME,
						  conf_val(CONF_FEDERATION_NAME), NULL, NULL,
						  "SELECTED");
  return(name);
}

int
make_set_void_selected_cookie_header(char **buf)
{
  char *name;

  name = make_selected_cookie_name();

  return(make_set_void_cookie_header(name, 0, buf));
}

int
is_selected_cookie_name(char *cookie_name)
{
  char *cn;

  cn = make_selected_cookie_name();
  if (streq(cookie_name, cn))
	return(1);

  return(0);
}

int
reset_scredentials(Credentials *credentials)
{
  int n;
  Credentials *cr;

  n = 0;
  for (cr = credentials; cr != NULL; cr = cr->next) {
	if (cr->selected) {
	  cr->selected = 0;
	  n++;
	}
  }

  return(n);
}

/*
 * Scan through the CREDENTIALS list, looking for credentials that
 * match any of S.
 * If a match is found, those are selected credentials, and create a list
 * at which SELECTED will point.
 * Set each credentials' 'selected' field appropriately.
 * Return -1 on error, otherwise a count of the number of selected
 * credentials;
 */
static int
find_matching_scredentials(Credentials *credentials,
						   Scredentials *s, Credentials **selected)
{
  int n;
  Credentials *cr, *new_cr, **sp;
  Scredentials_selected *sc;

  *selected = NULL;

  reset_scredentials(credentials);

  if (s->unauth != NULL)
	return(0);

  n = 0;
  sp = selected;
  for (cr = credentials; cr != NULL; cr = cr->next) {
	for (sc = s->selected; sc != NULL; sc = sc->next) {
	  if (streq(cr->unique, sc->unique)) {
		cr->selected = 1;
		new_cr = ALLOC(Credentials);
		*new_cr = *cr;
		new_cr->next = NULL;
		*sp = new_cr;
		sp = &new_cr->next;
		n++;
		break;
	  }
	}
  }

  return(n);
}

/*
 * Search the list of cookies for a SELECTED cookie.  If found, parse
 * its contents and make SCP point to it.
 * Return 0 if there is no SELECTED cookie, -1 on error, and 1 otherwise.
 */
int
get_scredentials(Cookie *cookies, Scredentials **sp)
{
  unsigned int decrypted_cookie_len, encrypted_cookie_len;
  unsigned char *decrypted_cookie, *encrypted_cookie;
  char *name;
  Cookie *c;
  Crypt_keys *ck;

  *sp = NULL;
  name = make_selected_cookie_name();
  for (c = cookies; c != NULL; c = c->next) {
	if (streq(c->name, name))
	  break;
  }

  if (c == NULL) {
	log_msg((LOG_TRACE_LEVEL, "There are no selected credentials"));
	return(0);
  }

  if (stra64b(c->value, &encrypted_cookie, &encrypted_cookie_len) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cookie unpacking failed"));
	return(-1);
  }

  ck = crypt_keys_from_vfs(ITEM_TYPE_FEDERATION_KEYS);
  if (crypto_decrypt_string(ck, encrypted_cookie, encrypted_cookie_len,
							&decrypted_cookie, &decrypted_cookie_len) == -1) {
	crypt_keys_free(ck);
	log_msg((LOG_ERROR_LEVEL, "Cookie decryption failed"));
	return(-1);
  }
  crypt_keys_free(ck);

  if (parse_xml_scredentials((char *) decrypted_cookie, sp) == -1) {
	log_msg((LOG_ERROR_LEVEL, "XML scredentials parse failed"));
	return(-1);
  }

  log_msg((LOG_TRACE_LEVEL, "Selection cookie:\n%s", decrypted_cookie));
  if ((*sp)->unauth != NULL)
	log_msg((LOG_TRACE_LEVEL, "User has selected to be unauthenticated"));
  else
	log_msg((LOG_TRACE_LEVEL, "There are selected credentials"));

  return(1);
}

/*
 * Scan through the list of cookies for one that identifies the selected set of
 * credentials.  If such a cookie is found, decrypt it and try to find
 * matching credentials in the list of valid credentials.
 * If matching credentials are found, they are the selected credentials and
 * set SELECTED to point to them.
 * Return -1 on error, otherwise a count of the number of effectively
 * selected credentials;
 *
 * XXX We assume that there can only be one selected credentials.
 */
static int
select_scredentials(Credentials *credentials, Cookie *cookies,
					Credentials **selected, Scredentials **sp)
{
  int st;
  Scredentials *s;

  if ((st = get_scredentials(cookies, &s)) == -1)
	return(-1);

  *selected = NULL;
  if (sp != NULL)
	*sp = s;

  if (st == 0) {
	int n;
	Credentials *cr;

	/* No SELECTED cookie was found so all credentials are used. */
	*selected = credentials;
	n = 0;
	for (cr = credentials; cr != NULL; cr = cr->next)
	  n++;
	return(n);
  }

  st = find_matching_scredentials(credentials, s, selected);

  return(st);
}

/*
 * Traverse a list of cookies, extract the valid credentials from them and
 * put those credentials into a list, setting SELECTED to point to it.
 * If there are selected credentials, either only those credentials or no
 * credentials will be returned.
 * Return the number of credentials, selected or not, or -1 on error.
 */ 
int
get_valid_scredentials(Cookie *cookies, char *remote_addr, int valid_for_acs,
					   Credentials **credentials,
					   Credentials **selected, Scredentials **sp)
{
  int n;
  Credentials *cr;

  *selected = NULL;
  if (sp != NULL)
	*sp = NULL;

  /* First, weed out only the valid, potentially selectable credentials. */
  n = get_valid_credentials(cookies, remote_addr, valid_for_acs, credentials);

  if (n > 0) {
	n = select_scredentials(*credentials, cookies, selected, sp);
	if (n == -1)
	  return(-1);

	for (cr = *selected; cr != NULL; cr = cr->next)
	  log_msg((LOG_TRACE_LEVEL,
			   "Credentials: identity=%s, style=%s, valid_for=%s",
			   auth_identity_from_credentials(cr),
			   auth_style_to_string(cr->auth_style),
			   cr->valid_for));
  }

  return(n);
}

/*
 * Create a cookie value to set selected credentials.
 * The cookie value will be XML that describes the selected credentials,
 * which are already in the possession of the user.  The most important part
 * of the XML is the reference to the unique id.  Encrypt the XML and convert
 * that to text, and then create a "name=value" string that will be the
 * payload of a cookie.
 * As a special case, if CREDENTIALS is NULL, it means that a special
 * cookie value should be created that will indicate that the user should
 * be treated as unauthenticated.
 * Return the length of the cookie; 0 indicates that there are no selected
 * credentials and -1 implies an error occurred.
 */
int
make_scredentials_cookie(Credentials *credentials, char **buf)
{
  int nselected;
  unsigned int len;
  unsigned char *encrypted_cookie;
  char *encrypted_cookie_text;
  Credentials *cr;
  Crypt_keys *ck;
  Ds ds;

  ds_init(&ds);
  ds.exact_flag = 1;

  if (credentials == NULL) {
	ds_asprintf(&ds, "<selected_credentials>");
	ds_asprintf(&ds, "<unauth federation=\"%s\"/>",
				conf_val(CONF_FEDERATION_NAME));
	ds_asprintf(&ds, "</selected_credentials>");
	log_msg((LOG_TRACE_LEVEL, "make_scredentials_cookie: unauth"));
  }
  else {
	nselected = 0;
	for (cr = credentials; cr != NULL; cr = cr->next) {
	  if (cr->selected) {
		if (nselected == 0)
		  ds_asprintf(&ds, "<selected_credentials>");
		ds_asprintf(&ds, "<selected");
		ds_asprintf(&ds, " federation=\"%s\"", cr->federation);
		ds_asprintf(&ds, " jurisdiction=\"%s\"", cr->home_jurisdiction);
		ds_asprintf(&ds, " username=\"%s\"", cr->username);
		ds_asprintf(&ds, " unique=\"%s\"", cr->unique);
		ds_asprintf(&ds, " version=\"%s\"", cr->version);
		ds_asprintf(&ds, "/>");
		nselected++;
		log_msg((LOG_TRACE_LEVEL, "make_scredentials_cookie: %s",
				 auth_identity_from_credentials(cr)));
	  }
	}
	
	if (nselected)
	  ds_asprintf(&ds, "</selected_credentials>");
	else {
	  *buf = NULL;
	  return(0);
	}
  }

  log_msg((LOG_TRACE_LEVEL, "make_scredentials_cookie:\n%s", ds_buf(&ds)));

  ck = crypt_keys_from_vfs(ITEM_TYPE_FEDERATION_KEYS);
  len = crypto_encrypt_string(ck, (unsigned char *) ds_buf(&ds),
							  ds_len(&ds) + 1, &encrypted_cookie);
  crypt_keys_free(ck);
  ds_free(&ds);
  strba64(encrypted_cookie, len, &encrypted_cookie_text);
  free(encrypted_cookie);

  ds_init(&ds);
  ds.exact_flag = 1;
  if (ds_asprintf(&ds, "%s=%s",
				  make_selected_cookie_name(), encrypted_cookie_text) == -1) {
	free(encrypted_cookie_text);
	return(-1);
  }

  free(encrypted_cookie_text);

  *buf = ds_buf(&ds);
  return(strlen(*buf));
}

int
make_set_scredentials_cookie_header(Credentials *credentials,
									char **cookie_buf)
{
  char *buf;

  if (make_scredentials_cookie(credentials, &buf) == -1)
	return(-1);
  if (buf == NULL)
	return(0);

  make_set_cookie_header(buf, NULL, 0, 0, cookie_buf);

  return(1);
}

void
cookies_html(FILE *fp, Cookie *cookies)
{
  int i;
  Cookie *c;

  i = 0;
  for (c = cookies; c != NULL; c = c->next) {
	fprintf(fp, "<p>");
	fprintf(fp, "Cookie %d:", i + 1);
	if (c->syntax == COOKIE_NETSCAPE)
	  fprintf(fp, "<br>Netscape format\n");
	else if (c->syntax == COOKIE_EXT_NETSCAPE)
	  fprintf(fp, "<br>Extended Netscape format\n");
	else if (c->syntax == COOKIE_RFC2109)
	  fprintf(fp, "<br>RFC 2109 format\n");
	else if (c->syntax == COOKIE_RFC2965)
	  fprintf(fp, "<br>RFC 2965 format\n");
	else if (c->syntax == COOKIE_RFC6265)
	  fprintf(fp, "<br>RFC 6265 format\n");

	if (c->name != NULL)
	  fprintf(fp, "<br>name='%s'\n", c->name);
	if (c->value != NULL)
	  fprintf(fp, "<br>value='%s'\n", c->value);
	if (c->domain != NULL)
	  fprintf(fp, "<br>domain='%s'\n", c->domain);
	if (c->path != NULL)
	  fprintf(fp, "<br>path='%s'\n", c->path);
	if (c->expires != NULL)
	  fprintf(fp, "<br>expires='%s'\n", c->expires);
	if (c->max_age != NULL)
	  fprintf(fp, "<br>max-age='%s'\n", c->max_age);
	if (c->comment != NULL)
	  fprintf(fp, "<br>comment='%s'\n", c->comment);
	if (c->version != NULL)
	  fprintf(fp, "<br>version='%s'\n", c->version);
	if (c->comment_url != NULL)
	  fprintf(fp, "<br>comment_url='%s'\n", c->comment_url);
	if (c->discard != NULL)
	  fprintf(fp, "<br>discard='%s'\n", c->discard);
	if (c->port != NULL)
	  fprintf(fp, "<br>port='%s'\n", c->port);
	fprintf(fp, "<br>secure=%d\n", c->secure);
	fprintf(fp, "<br>HttpOnly=%d\n", c->httponly);
	i++;
	fflush(fp);
  }
  fprintf(fp, "<p>");
}

/**********************************************/

typedef enum Parse_xml_auth_state_code {
  AUTH_PARSE_ROLES_REPLY            = 10,
  AUTH_PARSE_ROLES_REPLY_OK         = 11,
  AUTH_PARSE_ROLES_REPLY_FAILED     = 12,
  AUTH_PARSE_ROLES_REPLY_ERROR      = 13,
  AUTH_PARSE_AUTH_REPLY             = 20,
  AUTH_PARSE_AUTH_REPLY_OK          = 21,
  AUTH_PARSE_AUTH_REPLY_FAILED      = 22,
  AUTH_PARSE_AUTH_REPLY_ERROR       = 23,
  AUTH_PARSE_AUTH_REPLY_PROMPTS     = 24,
  AUTH_PARSE_AUTH_REPLY_PROMPT      = 25,
  AUTH_PARSE_AUTH_REPLY_ROLES_REPLY = 26
} Parse_xml_auth_state_code;

typedef struct Parse_xml_roles_reply_state {
  Parse_xml_auth_state_code code;
  union {
	Roles_reply *roles_reply;
  } object;
} Parse_xml_roles_reply_state;

static Parse_xml_roles_reply_state *
parse_xml_make_roles_reply_state(Parse_xml_auth_state_code code,
								 void *object)
{
  Parse_xml_roles_reply_state *s;

  s = ALLOC(Parse_xml_roles_reply_state);
  s->code = code;

  switch (code) {
  case AUTH_PARSE_ROLES_REPLY:
  case AUTH_PARSE_ROLES_REPLY_OK:
  case AUTH_PARSE_ROLES_REPLY_FAILED:
  case AUTH_PARSE_ROLES_REPLY_ERROR:
	s->object.roles_reply = (Roles_reply *) object;
	break;
  default:
	/* XXX ??? */
	return(NULL);
  }

  return(s);
}

static Parse_attr_tab roles_reply_attr_tab[] = {
  { XMLNS_XSI,    NULL, ATTR_IGNORE_NS, NULL, 0 },
  { XMLNS_PREFIX, NULL, ATTR_IGNORE_NS, NULL, 0 },
  { NULL,         NULL, ATTR_END,       NULL, 0 }
};

static Parse_attr_tab roles_reply_ok_attr_tab[] = {
  { "roles",      NULL, ATTR_REQUIRED,  NULL, 0 },
  { XMLNS_XSI,    NULL, ATTR_IGNORE_NS, NULL, 0 },
  { XMLNS_PREFIX, NULL, ATTR_IGNORE_NS, NULL, 0 },
  { NULL,         NULL, ATTR_END,       NULL, 0 }
};

static Parse_attr_tab roles_reply_failed_attr_tab[] = {
  { "reason",     NULL, ATTR_REQUIRED,  NULL, 0 },
  { XMLNS_XSI,    NULL, ATTR_IGNORE_NS, NULL, 0 },
  { XMLNS_PREFIX, NULL, ATTR_IGNORE_NS, NULL, 0 },
  { NULL,         NULL, ATTR_END,       NULL, 0 }
};

static void
parse_xml_roles_reply_element_start(void *data, const char *element,
									const char **attr)
{
  char *el, *errmsg;
  Roles_reply *rr;
  Parse_xml_roles_reply_state *state;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  if (parse_xml_is_error(NULL))
	return;

  rr = *(Roles_reply **) data;

  if (streq(el, "roles_reply")) {
	if (parse_xml_attr(roles_reply_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}
	parse_xml_push(parse_xml_make_roles_reply_state(AUTH_PARSE_ROLES_REPLY,
													(void *) rr));
  }
  else if (streq(el, "ok")) {
	Roles_reply_ok *ok;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_ROLES_REPLY) {
	  parse_xml_set_error("Unexpected \"failed\" element");
	  return;
	}

	ok = ALLOC(Roles_reply_ok);
	rr->ok = ok;

	roles_reply_ok_attr_tab[0].value = &rr->ok->roles;
	if (parse_xml_attr(roles_reply_ok_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	parse_xml_push(parse_xml_make_roles_reply_state(AUTH_PARSE_ROLES_REPLY_OK,
													(void *) rr));
  }
  else if (streq(el, "failed")) {
	Roles_reply_failed *failed;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_ROLES_REPLY) {
	  parse_xml_set_error("Unexpected \"failed\" element");
	  return;
	}

	failed = ALLOC(Roles_reply_failed);
	rr->failed = failed;

	roles_reply_failed_attr_tab[0].value = &rr->failed->reason;
	if (parse_xml_attr(roles_reply_failed_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	parse_xml_push(parse_xml_make_roles_reply_state(AUTH_PARSE_ROLES_REPLY_FAILED,
													(void *) rr));
  }
  else if (streq(el, "common_status")) {
	Common_status *status;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_ROLES_REPLY) {
	  parse_xml_set_error("Unexpected \"common_status\" element");
	  return;
	}

	status = ALLOC(Common_status);
	status->context = status->code = status->message = NULL;

	common_status_attr_tab[0].value = &status->context;
	common_status_attr_tab[1].value = &status->code;
	common_status_attr_tab[2].value = &status->message;
	if (parse_xml_attr(common_status_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  free(status);
	  return;
	}

	rr->status = status;
	parse_xml_push(parse_xml_make_roles_reply_state(AUTH_PARSE_ROLES_REPLY_ERROR,
												   (void *) rr));
  }
}

static void
parse_xml_roles_reply_element_end(void *data, const char *element)
{
  char *el;
  Roles_reply **rr;
  Parse_xml_roles_reply_state *state;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  rr = (Roles_reply **) data;

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "roles_reply")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_ROLES_REPLY) {
	  parse_xml_set_error("Unexpected \"roles_reply\" terminating element");
	  return;
	}
  }
  else if (streq(el, "ok")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_ROLES_REPLY_OK) {
	  parse_xml_set_error("Unexpected \"ok\" terminating element");
	  return;
	}
  }
  else if (streq(el, "failed")) {
	if (parse_xml_pop((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_ROLES_REPLY_FAILED) {
	  parse_xml_set_error("Unexpected \"failed\" terminating element");
	  return;
	}
  }
  else if (streq(el, "common_status")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_ROLES_REPLY_ERROR) {
	  parse_xml_set_error("Unexpected \"common_status\" terminating element");
	  return;
	}
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown terminating element: '%s'", el));
	return;
  }

}

int
parse_xml_roles_reply(char *reply_string, Roles_reply **roles_reply)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
	return(-1);

  parse_xml_init("Roles_reply", p);

  XML_SetElementHandler(p, parse_xml_roles_reply_element_start,
						parse_xml_roles_reply_element_end);

  *roles_reply = ALLOC(Roles_reply);
  (*roles_reply)->ok = NULL;
  (*roles_reply)->failed = NULL;
  (*roles_reply)->status = NULL;

  XML_SetUserData(p, (void *) roles_reply);

  st = XML_Parse(p, reply_string, strlen(reply_string), 1);

  if (parse_xml_is_not_empty())
	parse_xml_set_error("Unexpected \"roles_reply\" terminating element");

  if (parse_xml_is_error(&err) || st == 0) {
	if (err.mesg == NULL) {
	  parse_xml_set_error(NULL);
	  parse_xml_is_error(&err);
	}
    log_msg((LOG_ERROR_LEVEL, "parse_xml_roles_reply: line %d, pos %d",
             err.line, err.pos));
    if (err.mesg != NULL)
      log_msg((LOG_ERROR_LEVEL, "parse_xml_roles_reply: %s", err.mesg));
    parse_xml_end();
    return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
	return(-1);

  return(0);
}

void
emit_roles_reply_ok(FILE *fp, char *role_str)
{
  Roles_reply rr;
  Roles_reply_ok ok;

  rr.ok = &ok;
  rr.failed = NULL;
  rr.status = NULL;
  ok.roles = role_str;

  emit_xml_header(fp, "roles_reply");
  fprintf(fp, "%s", make_xml_roles_reply(&rr));
  emit_xml_trailer(fp);
}

void
emit_roles_reply_failed(FILE *fp, char *reason)
{
  Roles_reply rr;
  Roles_reply_failed failed;

  rr.ok = NULL;
  rr.failed = &failed;
  rr.status = NULL;
  if (reason == NULL)
	failed.reason = "unknown";
  else
	failed.reason = escape_xml_attribute(reason, '\"');

  emit_xml_header(fp, "roles_reply");
  fprintf(fp, "%s", make_xml_roles_reply(&rr));
  emit_xml_trailer(fp);
}

/**********************************************/
static Parse_attr_tab auth_reply_prompt_attr_tab[] = {
  { "type",    NULL, ATTR_REQUIRED, NULL, 0 },
  { "label",   NULL, ATTR_IMPLIED,  NULL, 0 },
  { "varname", NULL, ATTR_IMPLIED,  NULL, 0 },
  { NULL,      NULL, ATTR_END,      NULL, 0 }
};

static Auth_prompt *
parse_xml_auth_prompt(const char **attr)
{
  char *errmsg, *type;
  Auth_prompt *prompt;

  prompt = ALLOC(Auth_prompt);
  prompt->type = NULL;
  prompt->label = NULL;
  prompt->varname = NULL;
  prompt->next = NULL;

  auth_reply_prompt_attr_tab[0].value = &type;
  auth_reply_prompt_attr_tab[1].value = &prompt->label;
  auth_reply_prompt_attr_tab[2].value = &prompt->varname;
  if (parse_xml_attr(auth_reply_prompt_attr_tab, attr, &errmsg) == -1) {
	parse_xml_set_error(errmsg);
	free(prompt);
	return(NULL);
  }

  if (!streq(type, "password") && !streq(type, "text")
	  && !streq(type, "error") && !streq(type, "label")) {
	parse_xml_set_error("Invalid \"prompt\" attribute");
	free(prompt);
	return(NULL);
  }
  prompt->type = type;

  if ((streq(prompt->type, "password") || streq(prompt->type, "text"))
	  && prompt->varname == NULL) {
	parse_xml_set_error("Missing \"varname\" attribute");
	free(prompt);
	return(NULL);
  }

  if ((streq(prompt->type, "error") || streq(prompt->type, "label"))
	  && prompt->label == NULL) {
	parse_xml_set_error("Missing \"label\" attribute");
	free(prompt);
	return(NULL);
  }

  return(prompt);
}

static Parse_attr_tab auth_reply_prompts_attr_tab[] = {
  { "transid", NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,      NULL, ATTR_END,      NULL, 0 }
};

static Auth_prompts *
parse_xml_auth_prompts(const char **attr)
{
  char *errmsg;
  Auth_prompts *prompts;

  prompts = ALLOC(Auth_prompts);
  prompts->transid = NULL;
  prompts->head = NULL;

  auth_reply_prompts_attr_tab[0].value = &prompts->transid;
  if (parse_xml_attr(auth_reply_prompts_attr_tab, attr, &errmsg) == -1) {
	parse_xml_set_error(errmsg);
	free(prompts);
	return(NULL);
  }

  return(prompts);
}

typedef struct Parse_xml_auth_reply_state {
  Parse_xml_auth_state_code code;
  union {
	Auth_reply *auth_reply;
	Auth_prompts *prompts;
  } object;
} Parse_xml_auth_reply_state;

static Parse_xml_auth_reply_state *
parse_xml_make_auth_reply_state(Parse_xml_auth_state_code code, void *object)
{
  Parse_xml_auth_reply_state *s;

  s = ALLOC(Parse_xml_auth_reply_state);
  s->code = code;

  switch (code) {
  case AUTH_PARSE_AUTH_REPLY:
  case AUTH_PARSE_AUTH_REPLY_OK:
  case AUTH_PARSE_AUTH_REPLY_FAILED:
  case AUTH_PARSE_AUTH_REPLY_ERROR:
  case AUTH_PARSE_AUTH_REPLY_ROLES_REPLY:
	s->object.auth_reply = (Auth_reply *) object;
	break;
  case AUTH_PARSE_AUTH_REPLY_PROMPTS:
  case AUTH_PARSE_AUTH_REPLY_PROMPT:
	s->object.prompts = (Auth_prompts *) object;
	break;
  default:
	/* XXX ??? */
	return(NULL);
  }

  return(s);
}

static Parse_attr_tab auth_reply_ok_attr_tab[] = {
  { "username",   NULL, ATTR_REQUIRED,  NULL, 0 },
  { "lifetime",   NULL, ATTR_IMPLIED,   NULL, 0 },
  { XMLNS_XSI,    NULL, ATTR_IGNORE_NS, NULL, 0 },
  { XMLNS_PREFIX, NULL, ATTR_IGNORE_NS, NULL, 0 },
  { NULL,         NULL, ATTR_END,       NULL, 0 }
};

static Parse_attr_tab auth_reply_failed_attr_tab[] = {
  { "username",     NULL, ATTR_IMPLIED,   NULL, 0 },
  { "redirect_url", NULL, ATTR_IMPLIED,   NULL, 0 },
  { XMLNS_XSI,      NULL, ATTR_IGNORE_NS, NULL, 0 },
  { XMLNS_PREFIX,   NULL, ATTR_IGNORE_NS, NULL, 0 },
  { NULL,           NULL, ATTR_END,       NULL, 0 }
};

static void
parse_xml_auth_reply_element_start(void *data, const char *element,
								   const char **attr)
{
  char *el, *errmsg;
  Auth_reply *ar, **auth_reply;
  Parse_xml_auth_reply_state *state;

  auth_reply = (Auth_reply **) data;

  if (parse_xml_top((void **) &state) == PARSE_XML_OK
	  && (state->code == AUTH_PARSE_ROLES_REPLY
		  || state->code == AUTH_PARSE_ROLES_REPLY_OK
		  || state->code == AUTH_PARSE_ROLES_REPLY_FAILED
		  || state->code == AUTH_PARSE_ROLES_REPLY_ERROR)) {
	Roles_reply **rr;

	rr = &(*auth_reply)->ok->roles_reply;
	parse_xml_roles_reply_element_start((void *) rr, element, attr);
	return;
  }

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "auth_reply")) {
	if (parse_xml_is_not_empty()) {
	  parse_xml_set_error("Unexpected \"auth_reply\" element");
	  return;
	}
	if (parse_xml_attr(NULL, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	ar = ALLOC(Auth_reply);
	ar->ok = NULL;
	ar->failed = NULL;
	ar->status = NULL;
	ar->prompts = NULL;
	*auth_reply = ar;

	parse_xml_push(parse_xml_make_auth_reply_state(AUTH_PARSE_AUTH_REPLY,
												   (void *) ar));
  }
  else if (streq(el, "ok")) {
	Auth_reply_ok *ok;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_AUTH_REPLY) {
	  parse_xml_set_error("Unexpected \"ok\" element");
	  return;
	}
	ar = state->object.auth_reply;

	ok = ALLOC(Auth_reply_ok);
	ok->username = NULL;
	ok->lifetime = NULL;
	ok->roles_reply = NULL;

	auth_reply_ok_attr_tab[0].value = &ok->username;
	auth_reply_ok_attr_tab[1].value = &ok->lifetime;
	if (parse_xml_attr(auth_reply_ok_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  free(ok);
	  return;
	}

	ar->ok = ok;
	parse_xml_push(parse_xml_make_auth_reply_state(AUTH_PARSE_AUTH_REPLY_OK,
												   (void *) ar));
  }
  else if (streq(el, "roles_reply")) {
	Roles_reply *rr;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_AUTH_REPLY_OK) {
	  parse_xml_set_error("Unexpected \"roles_reply\" element");
	  return;
	}
	ar = state->object.auth_reply;

	rr = ALLOC(Roles_reply);
	rr->ok = NULL;
	rr->failed = NULL;
	rr->status = NULL;
	ar->ok->roles_reply = rr;
	parse_xml_roles_reply_element_start((void *) &ar->ok->roles_reply,
										element, attr);
  }
  else if (streq(el, "failed")) {
	Auth_reply_failed *failed;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_AUTH_REPLY) {
	  parse_xml_set_error("Unexpected \"failed\" element");
	  return;
	}
	ar = state->object.auth_reply;

	failed = ALLOC(Auth_reply_failed);
	failed->username = NULL;
	failed->redirect_url = NULL;

	auth_reply_failed_attr_tab[0].value = &failed->username;
	auth_reply_failed_attr_tab[1].value = &failed->redirect_url;
	if (parse_xml_attr(auth_reply_failed_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  free(failed);
	  return;
	}

	ar->failed = failed;
	parse_xml_push(parse_xml_make_auth_reply_state(AUTH_PARSE_AUTH_REPLY_FAILED,
												   (void *) ar));
  }
  else if (streq(el, "common_status")) {
	Common_status *status;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_AUTH_REPLY) {
	  parse_xml_set_error("Unexpected \"common_status\" element");
	  return;
	}
	ar = state->object.auth_reply;

	status = ALLOC(Common_status);
	status->context = status->code = status->message = NULL;

	common_status_attr_tab[0].value = &status->context;
	common_status_attr_tab[1].value = &status->code;
	common_status_attr_tab[2].value = &status->message;
	if (parse_xml_attr(common_status_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  free(status);
	  return;
	}

	ar->status = status;
	parse_xml_push(parse_xml_make_auth_reply_state(AUTH_PARSE_AUTH_REPLY_ERROR,
												   (void *) ar));
  }
  else if (streq(el, "prompts")) {
	Auth_prompts *prompts;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_AUTH_REPLY) {
	  parse_xml_set_error("Unexpected \"prompts\" element");
	  return;
	}
	ar = state->object.auth_reply;

	if ((prompts = parse_xml_auth_prompts(attr)) == NULL)
	  return;
	ar->prompts = prompts;

	parse_xml_push(parse_xml_make_auth_reply_state(AUTH_PARSE_AUTH_REPLY_PROMPTS,
												   (void *) prompts));
  }
  else if (streq(el, "prompt")) {
	Auth_prompt **ppr, *prompt;
	Auth_prompts *prs;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != AUTH_PARSE_AUTH_REPLY_PROMPTS) {
	  parse_xml_set_error("Unexpected \"prompt\" element");
	  return;
	}
	prs = state->object.prompts;
	if ((prompt = parse_xml_auth_prompt(attr)) != NULL) {
	  for (ppr = &prs->head; *ppr != NULL; ppr = &(*ppr)->next)
		;
	  *ppr = prompt;
	}
	parse_xml_push(parse_xml_make_auth_reply_state(AUTH_PARSE_AUTH_REPLY_PROMPT,
												   (void *) prs));
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown starting element: '%s'", el));
	return;
  }
}

static void
parse_xml_auth_reply_element_end(void *data, const char *element)
{
  char *el;
  Auth_reply **ar;
  Parse_xml_auth_reply_state *state;

  if (parse_xml_top((void **) &state) == PARSE_XML_OK
	  && (state->code == AUTH_PARSE_ROLES_REPLY
		  || state->code == AUTH_PARSE_ROLES_REPLY_OK
		  || state->code == AUTH_PARSE_ROLES_REPLY_FAILED
		  || state->code == AUTH_PARSE_ROLES_REPLY_ERROR)) {
	parse_xml_roles_reply_element_end(data, element);
	return;
  }

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  ar = (Auth_reply **) data;

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "auth_reply")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY || parse_xml_is_not_empty()) {
	  parse_xml_set_error("Unexpected \"auth_reply\" terminating element");
	  return;
	}
	*ar = state->object.auth_reply;
  }
  else if (streq(el, "ok")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY_OK) {
	  parse_xml_set_error("Unexpected \"ok\" terminating element");
	  return;
	}
  }
  else if (streq(el, "roles_reply")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY_ROLES_REPLY) {
	  parse_xml_set_error("Unexpected \"roles_reply\" terminating element");
	  return;
	}
	parse_xml_roles_reply_element_end(data, element);
  }
  else if (streq(el, "failed")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY_FAILED) {
	  parse_xml_set_error("Unexpected \"failed\" terminating element");
	  return;
	}
  }
  else if (streq(el, "error")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY_ERROR) {
	  parse_xml_set_error("Unexpected \"error\" terminating element");
	  return;
	}
  }
  else if (streq(el, "prompt")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY_PROMPT) {
	  parse_xml_set_error("Unexpected \"prompt\" terminating element");
	  return;
	}
  }
  else if (streq(el, "prompts")) {
	Auth_prompts *prompts;

	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR
		|| state->code != AUTH_PARSE_AUTH_REPLY_PROMPTS) {
	  parse_xml_set_error("Unexpected \"prompts\" terminating element");
	  return;
	}
	if ((prompts = state->object.prompts) == NULL
		|| prompts->head == NULL) {
	  parse_xml_set_error("No \"prompt\" elements");
	  return;
	}
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown terminating element: '%s'", el));
	return;
  }
}

int
parse_xml_auth_reply(char *reply_string, Auth_reply **auth_reply)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
	return(-1);

  parse_xml_init("Auth_reply", p);

  XML_SetElementHandler(p, parse_xml_auth_reply_element_start,
						parse_xml_auth_reply_element_end);
  XML_SetUserData(p, (void *) auth_reply);

  st = XML_Parse(p, reply_string, strlen(reply_string), 1);

  if (parse_xml_is_error(&err) || st == 0) {
	if (err.mesg == NULL) {
	  parse_xml_set_error(NULL);
	  parse_xml_is_error(&err);
	}
    log_msg((LOG_ERROR_LEVEL, "parse_xml_auth_reply: line %d, pos %d",
             err.line, err.pos));
    if (err.mesg != NULL)
      log_msg((LOG_ERROR_LEVEL, "parse_xml_auth_reply: %s", err.mesg));
	log_msg((LOG_ERROR_LEVEL,
			 "parse_xml_auth_reply: Input: %s", reply_string));
    parse_xml_end();
    return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
	return(-1);

  return(0);
}

/**********************************************/

/*
 * Return 1 if the expiry time is ok (and if REMAINING is non-NULL, set it
 * to the number of seconds until the expiry date), 0 if it has passed, and -1
 * if the date is invalid or an error occurs.
 */
int
verify_expiration(time_t expires_secs, int *remaining)
{
  time_t now;

  time(&now);

  if (expires_secs == (time_t) -1)
	return(-1);

  if (remaining != NULL)
	*remaining = (int) (expires_secs - now);

  if (expires_secs > now)
	return(1);

  return(0);
}

static const char *invalid_username_chars = ",:+()~<>=|\\/\"";
static const char *reserved_username_chars = "*";

/*
 * At present, there are few restrictions on the syntax of a username.
 * It must be at least one character long and consist of ASCII
 * alphabetics, digits, and certain punctuation characters.
 * Note that "auth", "unauth", etc. are perfectly valid usernames
 * because they really mean "FOO:auth", which is different than "auth".
 *
 * NOTE: this function accepts usernames that are valid for authentication
 * purposes AND for internal purposes.  For example, the "*" is valid
 * in the general case, but invalid wrt user authentication.
 * See is_valid_auth_username().
 *
 * We want to be quite liberal, so that typical native login names will be
 * acceptable.  FreeBSD, which may or may not be a typical *nix, disallows
 * the following characters in a username, at least wrt pw(8):
 *   o any char in the set  " ,\t:+&#%$^()!@~*?<>=|\\/\""
 *   o any char less than 040
 *   o any character >= 0177
 *   o an initial '-'
 *
 * The adduser(8) command has different rules: usernames consist of characters
 * in the set "a-z0-9_-" and may not begin with a '-'.
 *
 * Linux distributions seem to use useradd(8), at least some versions of which
 * require usernames to start with a letter, and not contain colons, commas,
 * newlines, or any non-printable characters.
 *
 * MS documentation for addusers.exe says that a Windows NT user name can
 * contain any upper case or lower case character except the following:
 *     " / \ [ ] : ; | = , + * ? >. \t
 * http://support.microsoft.com/kb/q141791
 *
 * RFC 822 (Appendix D) defines quite a rich syntax that is commonly used for
 * email addresses.  Ignoring various "quoted" variations, it disallows:
 *   o any character less than 040
 *   o any character >= 0177
 *   o any character in the set " ()<>,;:\\\"[]"   
 * http://www.rfc-editor.org/rfc/rfc822.txt
 *
 * The JURISDICTION_NAME_SEP_CHAR character (a colon) may not appear.
 * Because the username appears in contexts where weird characters might
 * cause problems, such as in a cookie name, it's probably
 * wise to avoid potentially problematic characters.
 */
int
is_valid_username(const char *username)
{
  const char *p;

  /* There must be at least one character */
  if (username == NULL || username[0] == '\0')
	return(0);

  /* A single reserved character (e.g., "*") is always invalid. */
  if (username[1] == '\0' && strchr(reserved_username_chars, (int) *username))
	return(0);

  for (p = username; *p != '\0'; p++) {
	if (strchr(invalid_username_chars, (int) *p) != NULL)
	  return(0);
	if (*p <= 040 || *p >= 0177)
	  return(0);
  }

  return(1);
}

/*
 * The dacs_authenticate service must only allow usernames that are
 * acceptable to this predicate.  Some usernames are reserved for internal
 * use and we don't want them to be confused with the names of "real" users,
 * so they are constructed from characters not allowed by this function.
 */
int
is_valid_auth_username(const char *username)
{
  const char *p;

  if (!is_valid_username(username))
	return(0);

  for (p = username; *p != '\0'; p++) {
	if (strchr(reserved_username_chars, (int) *p) != NULL)
	  return(0);
  }

  return(1);
}

int
is_valid_role_str(char *role_str)
{
  unsigned int max_len;
  char *p;

  if (role_str == NULL || role_str[0] == '\0'
	  || role_str[0] == '/' || role_str[0] == ',') {
	log_msg((LOG_ERROR_LEVEL, "Empty or initially invalid role string"));
	return(0);
  }

  /* Could use strspn(), but that would be slower. */
  for (p = role_str; *p != '\0'; p++) {
	if (!isalpha((int) *p) && !isdigit((int) *p) && *p != '-' && *p != '_'
		&& *p != '/' && *p != ',') {
	  log_msg((LOG_ERROR_LEVEL, "Invalid character in role string"));
	  return(0);
	}
  }

  if (*(p - 1) == '/' || *(p - 1) == ',') {
	log_msg((LOG_ERROR_LEVEL, "Invalid final character in role string"));
	return(0);
  }

  if (conf_val_uint(CONF_ROLE_STRING_MAX_LENGTH, &max_len) != 1)
	max_len = AUTH_MAX_ROLE_STR_LENGTH;
  if (strlen(role_str) > max_len) {
	/* XXX We might instead truncate the list at the last valid element. */
	log_msg((LOG_ERROR_LEVEL,
			 "Invalid role string: exceeds maximum length of %d bytes",
			 max_len));
	return(0);
  }

  return(1);
}

/*
 * Lookup and return the role string for USERNAME.
 * If an error occurs, return NULL.
 * If no error occurs but USERNAME has no roles, return the empty string.
 */
char *
get_role_string(char *vfs_uri, char *item_type, char *username, char *fs)
{
  char *role_str;
  Vfs_handle *h;

  if (!is_valid_username(username))
	return(NULL);

  if (vfs_uri != NULL) {
	if ((h = vfs_open_uri(vfs_uri)) == NULL) {
	  log_msg((LOG_DEBUG_LEVEL, "Can't open vfs_uri \"%s\"", vfs_uri));
	  return(NULL);
	}
  }
  else if (item_type != NULL) {
	if ((h = vfs_open_item_type(item_type)) == NULL) {
	  log_msg((LOG_DEBUG_LEVEL, "Can't open item type \"%s\"", item_type));
	  return(NULL);
	}
  }
  else
	return(NULL);

  if (fs != NULL && vfs_control(h, VFS_SET_FIELD_SEP, fs) == -1)
	return(NULL);

  role_str = NULL;
  if (vfs_get(h, username, (void **) &role_str, NULL) == -1) {
	log_msg((LOG_DEBUG_LEVEL,
			 "Lookup of roles for username \"%s\" in \"%s\" failed",
			 username, item_type != NULL ? item_type : vfs_uri));
	if (h->error_num != 0)
	  role_str = NULL;
	else
	  role_str = "";
  }
  else {
	if (role_str == NULL || role_str[0] == '\0')
	  role_str = "";
  }

  vfs_close(h);

  return(role_str);
}

Dsvec *
get_roles(char *item_type, char *username, char *fs)
{
  char *role_str;
  Dsvec *dsv;

  if ((role_str = get_role_string(NULL, item_type, username, fs)) == NULL)
	return(NULL);

  dsv = strsplit(role_str, ",", 0);
  return(dsv);
}

void
set_valid_for(Credentials *credentials, char *valid_for)
{
  Credentials *cr;

  for (cr = credentials; cr != NULL; cr = cr->next)
	cr->valid_for = strdup(valid_for);
	
}

char *
is_valid_valid_for(const char *valid_for)
{

  if (strcaseeq(valid_for, AUTH_VALID_FOR_ACS))
	return(AUTH_VALID_FOR_ACS);
  if (strcaseeq(valid_for, AUTH_VALID_FOR_CHAINING))
	return(AUTH_VALID_FOR_CHAINING);
  if (strcaseeq(valid_for, AUTH_VALID_FOR_IDENT))
	return(AUTH_VALID_FOR_IDENT);
  if (strcaseeq(valid_for, AUTH_VALID_FOR_TRANSFER))
	return(AUTH_VALID_FOR_TRANSFER);

  return(NULL);
}

/*
 * Depending on the VERIFY_IP directive, check if the IP address in the
 * credentials (the apparent IP address of the host from which the user's
 * authentication request came) matches ACTUAL_IP_ADDRESS (the apparent
 * IP address of a subsequent user request).
 * Return 1 if the two addresses match, 0 if no matching was required,
 * and -1 if they do not match or an error occurs.
 */
int
validate_credentials_ip(Credentials *cr, char *actual_ip_address,
						char **errmsg)
{
  int st;
  char *msg, *verify_ip;

  st = 0;
  if (actual_ip_address != NULL && cr->auth_style != AUTH_STYLE_ADMIN) {
	verify_ip = conf_val(CONF_VERIFY_IP);

	if (strcaseeq(verify_ip, "yes")) {
	  if (!streq(cr->ip_address, actual_ip_address)) {
		if (errmsg != NULL)
		  *errmsg = "Invalid credentials: source IP address invalid";
		st = -1;
	  }
	  else {
		st = 1;
		log_msg((LOG_TRACE_LEVEL,
				 "IP address of credentials ok: exact match"));
	  }
	}
	else if (!strcaseeq(verify_ip, "no")) {
	  msg = NULL;
	  if ((st = is_from_address(verify_ip, actual_ip_address, NULL, &msg))
		  != 1) {
		if (errmsg != NULL && msg != NULL)
		  *errmsg = ds_xprintf("Invalid credentials: %s", msg);
		st = -1;
	  }
	  else {
		st = 1;
		log_msg((LOG_TRACE_LEVEL,
				 "IP address of credentials ok: matched \"%s\"", verify_ip));
	  }
	}
  }

  if (st == 0)
	log_msg((LOG_TRACE_LEVEL,
			 "IP address of credentials was not validated"));

  return(st);
}

/*
 * Validate credentials C, returning 0 if they're ok, -1 otherwise.
 * This involves optionally comparing the IP address against
 * ACTUAL_IP_ADDRESS, and checking the version and expiration date.
 */
int
validate_credentials(Credentials *cr, char *actual_ip_address, char **errmsg)
{
  int st;
  int remaining_secs;

  if (!conf_val_eq(CONF_ACCEPT_ALIEN_CREDENTIALS, "yes")) {
	if (!name_eq(cr->federation, conf_val(CONF_FEDERATION_NAME),
				 DACS_NAME_CMP_CONFIG)) {
	  log_msg((LOG_INFO_LEVEL, "Ignoring credentials from federation \"%s\"",
			   cr->federation));
	  if (errmsg != NULL)
		*errmsg = "Invalid credentials: cannot accept alien credentials";
	  return(-1);
	}
  }
  else if (!is_valid_federation_name(cr->federation)) {
	if (errmsg != NULL)
	  *errmsg = "Invalid credentials: invalid federation name syntax";
	return(-1);
  }

  if (validate_credentials_ip(cr, actual_ip_address, errmsg) == -1)
	return(-1);

  if (cr->version == NULL || !is_compatible_dacs_version(cr->version)) {
	if (errmsg != NULL)
	  *errmsg = "Invalid credentials: version incompatibility";
	return(-1);
  }

  if ((st = verify_expiration(cr->expires_secs, &remaining_secs)) == -1) {
	if (errmsg != NULL)
	  *errmsg = "Invalid credentials: invalid expiry date format";
	return(-1);
  }
  else if (st == 0) {
	if (errmsg != NULL)
	  *errmsg = ds_xprintf("Invalid credentials: credentials expired %d secs ago",
						   -remaining_secs);
	return(-1);
  }
  log_msg((LOG_TRACE_LEVEL, "Valid credentials expire in %d secs",
		   remaining_secs));

  if ((conf_val(CONF_VERIFY_UA) == NULL
	   || conf_val_eq(CONF_VERIFY_UA, "yes")) && cr->ua_hash != NULL) {
	char *ua_hash, *ua_str;

	if ((ua_str = getenv("HTTP_USER_AGENT")) != NULL) {
	  ua_hash = make_ua_hash(ua_str);
	  if (!streq(ua_hash, cr->ua_hash)) {
		if (errmsg != NULL)
		  *errmsg = "Invalid credentials: user agent mismatch";
		return(-1);
	  }
	}
	else if (cr->ua_hash[0] != '\0') {
	  if (errmsg != NULL)
		*errmsg = "Invalid credentials: user agent mismatch";
	  return(-1);
	}
	log_msg((LOG_TRACE_LEVEL, "ua_hashes match"));
  }

  return(0);
}

/*
 * Convert from the external "cookie value" form to internal credentials.
 * This involves decoding the cookie value, decrypting, XML parsing, and
 * validating the credentials.
 * If -1 is returned, the caller should not use these credentials.
 */
int
cookie_value_to_credentials(char *cookie_value, char *remote_addr,
							Credentials **credentials)
{
  char *errmsg;
  unsigned int decrypted_cookie_len, encrypted_cookie_len;
  unsigned char *decrypted_cookie, *encrypted_cookie;
  Credentials *cr;
  Crypt_keys *ck;

  if (stra64b(cookie_value, &encrypted_cookie, &encrypted_cookie_len)
	  == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cookie unpacking failed"));
	return(-1);
  }

  ck = crypt_keys_from_vfs(ITEM_TYPE_FEDERATION_KEYS);
  if (crypto_decrypt_string(ck, encrypted_cookie, encrypted_cookie_len,
							&decrypted_cookie, &decrypted_cookie_len) == -1) {
	crypt_keys_free(ck);
	log_msg((LOG_ERROR_LEVEL, "Cookie decryption failed"));
	return(-1);
  }
  crypt_keys_free(ck);

  log_msg((LOG_TRACE_LEVEL, "decrypted_cookie=\"%s\"", decrypted_cookie));

  if (parse_xml_credentials((char *) decrypted_cookie, credentials) == -1) {
	log_msg((LOG_ERROR_LEVEL, "XML credentials parse failed"));
	return(-1);
  }

  cr = *credentials;

  errmsg = NULL;
  if (validate_credentials(cr, remote_addr, &errmsg) == -1) {
	if (trace_level) {
	  int remaining;

	  if (verify_expiration(cr->expires_secs, &remaining) == 0)
		log_msg((LOG_TRACE_LEVEL, "expires_date=%lu, remaining=%d",
				 cr->expires_secs, remaining));
	}
	if (errmsg != NULL)
	  log_msg((LOG_NOTICE_LEVEL, "Validation failed: %s", errmsg));
	log_msg((LOG_NOTICE_LEVEL, "Invalid or expired credentials found"));
	if (cr->home_jurisdiction != NULL && cr->username != NULL)
	  log_msg((LOG_NOTICE_LEVEL, "Discarding credentials: %s",
			   auth_identity_from_credentials(cr)));
	return(-1);
  }

  log_msg((LOG_DEBUG_LEVEL, "Valid credentials for %s:%s",
		   cr->home_jurisdiction, cr->username));

  return(0);
}

/*
 * Convert a parsed external cookie to internal credentials and validate them.
 * If the cookie is syntactically ok, then CREDENTIALS will be made to
 * point to the parsed credentials, otherwise it will be set to NULL.
 * If they are syntactically ok, proceed to check if they are valid.
 * Return -1 if there's a problem, 0 otherwise.
 */
int
cookie_to_credentials(Cookie *cookie, char *remote_addr,
					  Credentials **credentials)
{
  Credentials *cr;

  *credentials = NULL;
  if (cookie_value_to_credentials(cookie->value, remote_addr, credentials)
	  == -1)
	return(-1);

  cr = *credentials;

  /* Check that the cookie name matches the enclosed credentials. */
  if (!name_eq(cookie->parsed_name->federation, cr->federation,
			   DACS_NAME_CMP_CONFIG)
	  || (*cookie->parsed_name->jurisdiction != '\0'
		  && !name_eq(cookie->parsed_name->jurisdiction, cr->home_jurisdiction,
					  DACS_NAME_CMP_CONFIG))
	  || (*cookie->parsed_name->username != '\0'
		  && !name_eq(cookie->parsed_name->username, cr->username,
					  DACS_NAME_CMP_CONFIG))) {
	log_msg((LOG_ALERT_LEVEL, "Cookie name doesn't match credentials!"));
	log_msg((LOG_ALERT_LEVEL,
			 "Cookie name is: %s, credentials say %s:%s:%s",
			 cookie->name, cr->federation, cr->home_jurisdiction,
			 cr->username));
	return(-1);
  }

  cr->cookie_name = strdup(cookie->name);

  log_msg((LOG_DEBUG_LEVEL, "Valid cookie for %s:%s",
		   cr->home_jurisdiction, cr->username));

  return(0);
}

/*
 * Traverse a list of cookies, extract the valid credentials from them and
 * put those credentials into a list, setting CREDENTIALS to point to it.
 * Return the number of valid credentials.
 */
int
get_valid_credentials(Cookie *cookies, char *remote_addr,
					  int valid_for_acs, Credentials **credentials)
{
  int n, valid;
  Cookie *c;
  Credentials *cr, *cred, *prev;

  n = 0;
  prev = NULL;
  log_msg((LOG_TRACE_LEVEL, "valid_for_acs=%d", valid_for_acs));

  *credentials = NULL;
  for (c = cookies; c != NULL; c = c->next) {
	/* Weed out non-auth DACS cookies. */
	if (!is_auth_cookie_name(c->name))
	  continue;

	if (cookie_to_credentials(c, remote_addr, &cr) == -1)
	  continue;

	/*
	 * Reject all credentials if this identity has more than one set
	 * of credentials.
	 * Also reject all credentials if two cookies with the same name
	 * are found.
	 * This may be harsh but better safe than sorry.  An alternative might
	 * be to only ignore an identity that is duplicated.
	 */
	for (cred = *credentials; cred != NULL; cred = cred->next) {
	  if (streq(cred->federation, cr->federation)
		  && streq(cred->home_jurisdiction, cr->home_jurisdiction)
		  && streq(cred->username, cr->username)) {
		log_msg((LOG_ALERT_LEVEL, "Duplicate identity found: %s",
				 auth_identity_from_credentials(cr)));
		*credentials = NULL;
		return(-1);
	  }

	  if (streq(cred->cookie_name, cr->cookie_name)) {
		log_msg((LOG_ALERT_LEVEL, "Duplicate cookie name found: %s",
				 cr->cookie_name));
		*credentials = NULL;
		return(-1);
	  }
	}

	if (valid_for_acs) {
	  /*
	   * The caller is only interested in credentials that are valid for
	   * access control purposes.
	   * They must be so marked, or DACS must be configured to treat as
	   * valid credentials marked as AUTH_VALID_FOR_CHAINING.
	   */
	  valid = 0;

	  if (cr->valid_for != NULL) {
		if (strcaseeq(cr->valid_for, AUTH_VALID_FOR_ACS)) {
		  valid = 1;
		  log_msg((LOG_TRACE_LEVEL, "Received credentials are valid for ACS"));
		}
		else if (strcaseeq(cr->valid_for, AUTH_VALID_FOR_CHAINING)) {
		  if (conf_val(CONF_PERMIT_CHAINING) == NULL)
			log_msg((LOG_TRACE_LEVEL, "PERMIT_CHAINING is not configured"));
		  else
			log_msg((LOG_TRACE_LEVEL, "PERMIT_CHAINING=\"%s\"",
					 conf_val(CONF_PERMIT_CHAINING)));

		  if (conf_val_eq(CONF_PERMIT_CHAINING, "yes")) {
			valid = 1;
			log_msg((LOG_NOTICE_LEVEL,
					 "Received chaining credentials are valid for ACS"));
		  }
		  else
			log_msg((LOG_TRACE_LEVEL, "Credentials not configured for ACS"));
		}
	  }
	  else
		valid = 1;

	  if (!valid) {
		log_msg((LOG_ALERT_LEVEL,
				 "Received credentials are invalid for ACS use!"));
		if (cr->valid_for != NULL)
		  log_msg((LOG_TRACE_LEVEL, "valid_for=\"%s\"", cr->valid_for));
		*credentials = NULL;
		return(-1);
	  }
	}

	if (n == AUTH_MAX_CREDENTIALS) {
	  log_msg((LOG_NOTICE_LEVEL,
			   "Maximum number of credentials exceeded for \"%s\"",
			   cr->username));
	  return(n);
	}

	cr->cookie_name = strdup(c->name);

	if (prev == NULL)
	  *credentials = cr;
	else
	  prev->next = cr;
	prev = cr;
	n++;
  }

  if (n == 0)
	*credentials = NULL;

  return(n);
}

int
count_valid_credentials(Credentials *credentials)
{
  int n;
  Credentials *cr;

  for (n = 0, cr = credentials; cr != NULL; cr = cr->next)
	n++;

  return(n);
}

/*
 * Return 1 if the given identity is a DACS administrator as defined by
 * one or more ADMIN_IDENTITY configuration directives, 0 otherwise.
 * This does not recognize AUTH_STYLE_ADMIN identities - we do not
 * rely on the name syntax, just to be on the safe side.
 */
int
is_dacs_admin_identity(char *f, char *j, char *u)
{
  char *federation, *jurisdiction, *username;
  DACS_name name;
  DACS_name_type nt;
  Kwv_pair *v;

  if (f == NULL)
	federation = conf_val(CONF_FEDERATION_NAME);
  else
	federation = f;

  if (j == NULL)
	jurisdiction = conf_val(CONF_JURISDICTION_NAME);
  else
	jurisdiction = j;

  if (u == NULL)
	return(0);
  username = u;

  if (strcaseeq(username, "unauth") || strcaseeq(username, "unauthenticated"))
	return(0);

  for (v = conf_var(CONF_ADMIN_IDENTITY); v != NULL; v = v->next) {
	nt = parse_dacs_name(v->val, &name);
	if (nt == DACS_USER_NAME) {
	  if (name.federation == NULL)
		name.federation = conf_val(CONF_FEDERATION_NAME);
	  if (name.jurisdiction == NULL)
		name.jurisdiction = conf_val(CONF_JURISDICTION_NAME);

	  if (name_eq(name.federation, federation, DACS_NAME_CMP_CONFIG)
		  && name_eq(name.jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG)
		  && name_eq(name.username, username, DACS_NAME_CMP_CONFIG))
		return(1);
	}
	else if (nt == DACS_GROUP_NAME) {
	  /*
	   * It might be reasonable to test group membership here.
	   * That is currently expensive.
	   */
	  log_msg((LOG_WARN_LEVEL, "Unrecognized name type in ADMIN_IDENTITY"));
	}
	else if (nt == DACS_FEDERATION_NAME || nt == DACS_UNKNOWN_NAME) {
	  log_msg((LOG_WARN_LEVEL, "Invalid name type in ADMIN_IDENTITY"));
	}
	else
	  log_msg((LOG_WARN_LEVEL, "Unrecognized name type in ADMIN_IDENTITY"));
  }

  return(0);
}

int
is_dacs_admin(Credentials *credentials)
{
  Credentials *cr;

  for (cr = credentials; cr != NULL; cr = cr->next) {
	if (cr->auth_style == AUTH_STYLE_ADMIN)
	  return(1);

	if (is_dacs_admin_identity(cr->federation, cr->home_jurisdiction,
							   cr->username) == 1) {
	  log_msg((LOG_TRACE_LEVEL, "Matched admin identity: %s",
			   auth_identity_from_credentials(cr)));
	  return(1);
	}
	log_msg((LOG_TRACE_LEVEL, "No admin identity match: %s",
			 auth_identity_from_credentials(cr)));
  }

  return(0);
}

/*
 * Test if USERNAME is an internal admin name.
 * N.B. code should also verify that credentials are AUTH_STYLE_ADMIN
 * before conferring any admin privileges (see is_dacs_admin()).
 */
int
is_dacs_admin_name(char *username)
{

  if (strprefix(username, AUTH_ADMIN_USERNAME_PREFIX_STR) == NULL
	  || strsuffix(username, strlen(username),
				   AUTH_ADMIN_USERNAME_SUFFIX_STR) == NULL)
	return(0);

  return(1);
}

/*
 * Create a username that cannot correspond to any name assigned through
 * authentication.
 */
char *
make_dacs_admin_name(char *basename)
{
  char *username;

  username = ds_xprintf("%s%s%s",
						AUTH_ADMIN_USERNAME_PREFIX_STR,
						basename,
						AUTH_ADMIN_USERNAME_SUFFIX_STR);
  return(username);
}

/*
 * Return 1 if these credentials were issued by this jurisdiction,
 * 0 otherwise.
 */
int
is_local_user_identity(Credentials *cr)
{
  DACS_name_cmp cmp_mode;

  cmp_mode = DACS_NAME_CMP_CONFIG;
  if (name_eq(cr->home_jurisdiction, conf_val(CONF_JURISDICTION_NAME), cmp_mode)
	  && name_eq(cr->federation, conf_val(CONF_FEDERATION_NAME), cmp_mode))
	return(1);

  return(0);
}

/*
 * The USER argument can be:
 *   o a user (e.g., "DSS:brachman")
 *     (with wildcard capability: "*:brachman", "*::*:brachman", "foo::*:bob")
 *   o an argument to the from() predicate
 *     (e.g., "10.0.0.123", "10.0.0.0/24")
 *   o a group or role to which the user belongs (e.g., "%IRMS:sysadmin")
 *   o the federation or jurisdiction that authenticated the user
 *     (e.g., "DSS:", "FEDROOT::")
 *   o an IP address (e.g., "10.0.0.123")
 *   o a wildcard that matches any locally authenticated user ("mine")
 *   o a wildcard that matches any authenticated user ("auth")
 *   o a wildcard that matches any unauthenticated user ("unauth")
 *   o a wildcard that matches any user ("any")
 *   o a wildcard that matches no user ("none")
 */
int
is_matching_user_identity(char *user, Credentials *credentials,
						  DACS_name_cmp cmp_mode, char **errmsg)
{
  int st, wildcard;
  Credentials *cr;
  DACS_name_type nt;
  DACS_name dacs_name;

  if (strcaseeq(user, "any")) {
	log_msg((LOG_TRACE_LEVEL, "Matched any user"));
	return(1);
  }

  if (strcaseeq(user, "none")) {
	log_msg((LOG_TRACE_LEVEL, "Matched user \"none\""));
	return(0);
  }

  if (strcaseeq(user, "mine")) {
	log_msg((LOG_TRACE_LEVEL, "Matched user \"mine\""));
	if (credentials != NULL && is_local_user_identity(credentials)) {
	  log_msg((LOG_TRACE_LEVEL, "Matched local user"));
	  return(1);
	}
	return(0);
  }

  if (strcaseeq(user, "unauth") || strcaseeq(user, "unauthenticated")) {
	if (credentials == NULL) {
	  log_msg((LOG_TRACE_LEVEL, "Matched unauthenticated user"));
	  return(1);
	}
	return(0);
  }

  if (strcaseeq(user, "auth") || strcaseeq(user, "authenticated")) {
	if (credentials != NULL) {
	  log_msg((LOG_TRACE_LEVEL, "Matched authenticated user"));
	  return(1);
	}
	return(0);
  }

  nt = parse_dacs_name(user, &dacs_name);
  if (dacs_name.federation == NULL)
	dacs_name.federation = conf_val(CONF_FEDERATION_NAME);
  if (dacs_name.jurisdiction == NULL)
	dacs_name.jurisdiction = conf_val(CONF_JURISDICTION_NAME);

  if (nt == DACS_UNKNOWN_NAME) {
	/*
	 * Check for the wildcard syntax for a username or a jurisdiction.
	 * o "*:bob" means any bob from any jurisdiction of this federation
	 * o "FOO::*:bob" means any bob from federation FOO
	 * o "*::*:bob" means any bob from any federation
	 */
	if (dacs_name.type != DACS_USER_NAME
		|| dacs_name.username == NULL
		|| !streq(dacs_name.jurisdiction, "*")) {
	  st = is_from_address(user,
						   (credentials == NULL) ? NULL
						   : credentials->ip_address, NULL, NULL);
	  if (st == -1) {
		if (errmsg != NULL)
		  *errmsg = ds_xprintf("Invalid argument: \"%s\" ", user);
		return(-1);
	  }
	  return(st);
	}
	log_msg((LOG_TRACE_LEVEL, "Looking for a wildcard match"));
	wildcard = 1;
  }
  else
	wildcard = 0;

  if ((cr = credentials) == NULL) {
	char *unauth_roles;

	unauth_roles = conf_val(CONF_UNAUTH_ROLES);
	if (nt == DACS_GROUP_NAME && unauth_roles != NULL) {
	  if (has_unauth_role(unauth_roles, dacs_name.jurisdiction,
						  dacs_name.username) == 1) {
		log_msg((LOG_TRACE_LEVEL, "Matched unauth role"));
		return(1);
	  }
	}
	return(0);
  }

  log_msg((LOG_TRACE_LEVEL,
		   "Matching credentials \"%s\" against \"%s\"",
		   auth_identity_from_credentials(cr), user));

  if (wildcard) {
	if (!name_eq(cr->username, dacs_name.username, cmp_mode))
	  return(0);

	if (streq(dacs_name.federation, "*")
		|| name_eq(cr->federation, dacs_name.federation, cmp_mode)) {
	  log_msg((LOG_TRACE_LEVEL, "Matched DACS username with wildcard"));
	  return(1);
	}

	return(0);
  }

  switch (nt) {
  case DACS_USER_NAME:
	if (name_eq(cr->username, dacs_name.username, cmp_mode)
		&& name_eq(cr->home_jurisdiction, dacs_name.jurisdiction, cmp_mode)
		&& name_eq(cr->federation, dacs_name.federation, cmp_mode)) {
	  log_msg((LOG_TRACE_LEVEL, "Matched DACS username"));
	  return(1);
	}
	break;

  case DACS_JURISDICTION_NAME:
	if (name_eq(cr->home_jurisdiction, dacs_name.jurisdiction, cmp_mode)
		&& name_eq(cr->federation, dacs_name.federation, cmp_mode)) {
	  log_msg((LOG_TRACE_LEVEL,
			   "Matched user's authenticating DACS jurisdiction"));
	  return(1);
	}
	break;

  case DACS_FEDERATION_NAME:
	if (name_eq(cr->federation, dacs_name.federation, cmp_mode)) {
	  log_msg((LOG_TRACE_LEVEL,
			   "Matched user's authenticating DACS federation"));
	  return(1);
	}
	break;

  case DACS_GROUP_NAME:
	if (cr->role_str != NULL)
	  cr->roles = make_group_names_from_role_str(cr->home_jurisdiction,
												 cr->role_str);

	st = is_group_member(cr->home_jurisdiction, cr->username, cr->roles,
						 dacs_name.jurisdiction, dacs_name.username);
	if (st == 1) {
	  log_msg((LOG_TRACE_LEVEL, "Matched group or role"));
	  return(1);
	}
	else if (st == -1) {
	  if (errmsg != NULL)
		*errmsg = "is_matching_user_identity: group resolution error";
	  return(-1);
	}
	break;

  case DACS_IP_NAME:
	if (streq(cr->ip_address, dacs_name.username)) {
	  log_msg((LOG_TRACE_LEVEL,
			   "Matched IP address of authenticating jurisdiction"));
	  return(1);
	}
	break;

  default:
	if (errmsg != NULL)
	  *errmsg = "is_matching_user_identity: internal error";
	return(-1);
  }

  return(0);
}

/*
 * Test if the USER filter specification matches any CREDENTIALS.
 * If so, return 1 and set MATCHED to point to the first matching credentials
 * if any.  If not, return 0.
 * If an error occurs, return -1 and set ERRMSG to an explanatory message.
 * This is called during revocation checks, the ACS "user" predicate, and
 * while processing ACL user_list elements.
 *
 * XXX It is not possible to match admin credentials, except that they
 * count as "authenticated".
 */
int
is_matching_user(char *user, Credentials *credentials, DACS_name_cmp cmp_mode,
				 Credentials **matched, char **errmsg)
{
  int st;
  Credentials *cr;

  cr = credentials;
  do {
	if ((st = is_matching_user_identity(user, cr, cmp_mode, errmsg)) == -1)
	  return(-1);
	if (st == 1) {
	  if (matched != NULL)
		*matched = cr;
	  return(1);
	}
	if (cr != NULL)
	  cr = cr->next;
  } while (cr != NULL);

  return(0);
}

char *
get_revocations(char *item_type)
{
  int st;
  char *buf;
  Vfs_handle *h;

  if ((h = vfs_open_item_type(item_type)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Could not open \"%s\"", item_type));
	return(NULL);
  }

  st = vfs_get(h, NULL, (void **) &buf, NULL);

  if (vfs_close(h) == -1) {
	log_msg((LOG_ERROR_LEVEL, "vfs_close() failed"));
	st = -1;
  }
  if (st == -1)
	return(NULL);

  return(buf);
}

/*
 * The revocation list BUF is destructively carved up into lines and typed.
 * Each line in "revocations" is either:
 *   o a comment line (first non-whitespace character, if any, is a '#')
 *   o the keyword "deny", followed by an expression
 *     If the expression is TRUE for any credentials, all access is denied
 *   o the keyword "revoke", followed by an expression
 *     If the expression is TRUE for any credentials, the credentials
 *     are considered invalid; the caller may delete them
 */
Dsvec *
parse_revocations(char *buf, int check_exprs)
{
  char *line;
  Ds ds;
  Dsio *dsio;
  Dsvec *dsv;
  Revocation *r;

  dsv = dsvec_init(NULL, sizeof(Revocation *));

  /* Parse the revocation file, line by line. */
  ds_init(&ds);
  ds.escnl_flag = 1;
  ds.delnl_flag = 1;
  dsio = dsio_set(&ds, NULL, buf, 0, 0);

  while ((line = dsio_gets(&ds)) != NULL) {
	char *p, *q;

	r = ALLOC(Revocation);
	line = p = ds_buf(&ds);
	while (*p == ' ' || *p == '\t')
      p++;

    if (*p == '\0') {
	  r->type = AUTH_REVOKE_COMMENT;
	  r->item = "";
	}
    else if (*p == '#') {
	  r->type = AUTH_REVOKE_COMMENT;
	  r->item = line;
	}
	else if ((q = strcaseprefix(p, "deny")) && (*q == ' ' || *q == '\t')) {
	  r->type = AUTH_REVOKE_DENY;
	  for (p = q + 1; *p == ' ' || *p == '\t'; p++)
		;
	  r->item = p;
	}
	else if ((q = strcaseprefix(p, "revoke")) && (*q == ' ' || *q == '\t')) {
	  r->type = AUTH_REVOKE_REVOKE;
	  for (p = q + 1; *p == ' ' || *p == '\t'; p++)
		;
	  r->item = p;
	}
	else if ((q = strcaseprefix(p, "disable")) && (*q == ' ' || *q == '\t')) {
	  r->type = AUTH_REVOKE_DISABLE;
	  for (p = q + 1; *p == ' ' || *p == '\t'; p++)
		;
	  r->item = p;
	}
	else if (( q= strcaseprefix(p, "block")) && (*q == ' ' || *q == '\t')) {
	  r->type = AUTH_REVOKE_BLOCK;
	  for (p = q + 1; *p == ' ' || *p == '\t'; p++)
		;
	  r->item = p;
	}
	else {
	  /* Unrecognized */
	  log_msg((LOG_ERROR_LEVEL, "Unrecognized revocation line: %s", line));
	  return(NULL);
	}

	dsvec_add_ptr(dsv, r);
	ds_reset_buf(&ds);
  }

  if (check_exprs) {
	int i;
	Acs_environment env;
	Acs_expr_result st;
	Expr_result result;

	for (i = 0; i < dsvec_len(dsv); i++) {
	  r = (Revocation *) dsvec_ptr(dsv, i, Revocation *);
	  if (r->type == AUTH_REVOKE_COMMENT)
		continue;
	  acs_new_env(&env);
	  acs_init_env(NULL, NULL, "", NULL, &env);
	  env.do_eval = 0;
	  st = acs_expr(r->item, &env, &result);
	  if (st == ACS_EXPR_SYNTAX_ERROR) {
		log_msg((LOG_ERROR_LEVEL, "Syntax error in revocation item: %s",
				 r->item));
		return(NULL);
	  }
	  if (st == ACS_EXPR_EVAL_ERROR) {
		log_msg((LOG_ERROR_LEVEL, "Evaluation error in revocation item: %s",
				 r->item));
		return(NULL);
	  }
	  if (st == ACS_EXPR_LEXICAL_ERROR) {
		log_msg((LOG_ERROR_LEVEL, "Lexical error in revocation item: %s",
				 r->item));
		return(NULL);
	  }
	}
  }

  return(dsv);
}

/*
 * Go through CREDENTIALS, checking to see if access should be denied
 * or any credentials revoked (FOR_ACS non-zero), or authentication should
 * be disallowed (FOR_ACS zero).
 *
 * Return 1 if access is denied (for any identity) or authentication disabled,
 * 0 if access is not denied (but some credentials may have been revoked),
 * and -1 if there's an error.
 * If the revocation list has been configured, it must be accessible.
 */
int
check_revocation(Credentials *credentials, Kwv *kwv, char *item_type,
				 int for_acs)
{
  int i, st;
  char *buf;
  Acs_environment env;
  Credentials *cr;
  Dsvec *revocations;
  Revocation *r;

  log_msg((LOG_TRACE_LEVEL, "Checking \"%s\" for revocations...", item_type));
  if ((buf = get_revocations(item_type)) == NULL) {
	log_msg((LOG_TRACE_LEVEL, "Could not load revocations"));
	return(-1);
  }

  if ((revocations = parse_revocations(buf, 0)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Could not parse revocations"));
	return(-1);
  }

  for (i = 0; i < dsvec_len(revocations); i++) {
	r = (Revocation *) dsvec_ptr(revocations, i, Revocation *);
	if (r->type == AUTH_REVOKE_COMMENT)
	  continue;

	if (r->type == AUTH_REVOKE_BLOCK
		|| (for_acs && (credentials == NULL || r->type == AUTH_REVOKE_DENY))) {
	  acs_new_env(&env);
	  acs_init_env(kwv, NULL, NULL, credentials, &env);
	  if ((st = acs_expr(r->item, &env, NULL)) == ACS_EXPR_TRUE) {
		log_msg((LOG_NOTICE_LEVEL,
				 "Revocation: deny%s is true: \"%s\"",
				 (r->type == AUTH_REVOKE_BLOCK) ? "/block " : "", r->item));
		return(1);
	  }
		  else if (st == ACS_EXPR_FALSE)
			log_msg((LOG_TRACE_LEVEL,
					 "Revocation: deny%s is false: \"%s\"",
					 (r->type == AUTH_REVOKE_BLOCK) ? "/block " : "",
					 r->item));
	  else {
		log_msg((LOG_ERROR_LEVEL,
				 "Revocation: deny%s processing error: \"%s\"",
				 (r->type == AUTH_REVOKE_BLOCK) ? "/block " : "", r->item));
		return(-1);
	  }
	}
	else if (for_acs && r->type == AUTH_REVOKE_REVOKE) {
	  Credentials *cr_next;

	  for (cr = credentials; cr != NULL; cr = cr->next) {
		cr_next = cr->next;
		cr->next = NULL;

		acs_new_env(&env);
		acs_init_env(kwv, NULL, NULL, cr, &env);
		st = acs_expr(r->item, &env, NULL);
		cr->next = cr_next;

		if (st == ACS_EXPR_TRUE) {
		  log_msg((LOG_NOTICE_LEVEL,
				   "Revocation: revoke is true: \"%s\"", r->item));
		  cr->valid_for = AUTH_VALID_FOR_NOTHING;
		}
		else if (st == ACS_EXPR_FALSE)
		  log_msg((LOG_TRACE_LEVEL,
				   "Revocation: revoke is false: \"%s\"", r->item));
		else {
		  log_msg((LOG_ERROR_LEVEL,
				   "Revocation: revoke processing error: \"%s\"", r->item));
		  return(-1);
		}
	  }
	}
	else if (!for_acs) {
	  if (r->type == AUTH_REVOKE_DISABLE) {
		acs_new_env(&env);
		acs_init_env(kwv, NULL, NULL, credentials, &env);
		if ((st = acs_expr(r->item, &env, NULL)) == ACS_EXPR_TRUE) {
		  log_msg((LOG_NOTICE_LEVEL,
				   "Revocation: disable is true: \"%s\"", r->item));
		  return(1);
		}
		else if (st == ACS_EXPR_FALSE)
		  log_msg((LOG_TRACE_LEVEL,
				   "Revocation: disable is false: \"%s\"", r->item));
		else {
		  log_msg((LOG_ERROR_LEVEL,
				   "Revocation: disable processing error: \"%s\"", r->item));
		  return(-1);
		}
	  }
	}
  }

  return(0);
}

/*
 * Parse an HTTP digest WWW-Authenticate response header
 * RFC 2617, S2 and S3.2.1
 */
Http_auth_www_authenticate *
http_auth_www_authenticate_parse(char *challenge, char **errmsg)
{
  char *p, *q, *start;
  Http_auth_www_authenticate *wwwa;
  Kwv *kwv;
  static Kwv_conf conf = {
	"=", NULL, NULL, KWV_CONF_DEFAULT, ",", 10, NULL, NULL
  };

  wwwa = ALLOC(Http_auth_www_authenticate);
  wwwa->www_authenticate = strdup(challenge);

  p = challenge;
  while (*p == ' ')
	p++;
  if ((q = strcaseprefix(p, "Basic ")) != NULL) {
	wwwa->scheme = HTTP_AUTH_BASIC;
	wwwa->scheme_name = "Basic";
  }
  else if ((q = strcaseprefix(p, "Digest ")) != NULL) {
	wwwa->scheme = HTTP_AUTH_DIGEST;
	wwwa->scheme_name = "Digest";
  }
  else {
	/* XXX Extend for arbitrary schemes */
	return(NULL);
  }

  while (*q == ' ')
	q++;
  start = q;

  wwwa->domain = wwwa->nonce = wwwa->opaque = wwwa->stale = NULL;
  wwwa->algorithm = wwwa->qop_options = wwwa->auth_param = NULL;

  kwv = kwv_init(10);
  kwv->icase = 1;
  kwv->dup_mode = KWV_NO_DUPS;
  if ((kwv = kwv_make_sep(kwv, start, &conf)) == NULL) {
	*errmsg = "Error parsing WWW-Authenticate header";
	return(NULL);
  }

  if ((wwwa->realm = kwv_lookup_value(kwv, "realm")) == NULL) {
	*errmsg = "No realm in WWW-Authenticate?";
	return(NULL);
  }

  if (wwwa->scheme == HTTP_AUTH_BASIC) {
	if (kwv_count(kwv, NULL) != 1) {
	  *errmsg = "Too many directives in Basic WWW-Authenticate";
	  return(NULL);
	}
	return(wwwa);
  }

  if ((wwwa->domain = kwv_lookup_value(kwv, "domain")) == NULL) {
	*errmsg = "No domain in Digest WWW-Authenticate?";
	return(NULL);
  }
  if ((wwwa->nonce = kwv_lookup_value(kwv, "nonce")) == NULL) {
	*errmsg = "No nonce in Digest WWW-Authenticate?";
	return(NULL);
  }
  wwwa->opaque = kwv_lookup_value(kwv, "opaque");
  wwwa->stale = kwv_lookup_value(kwv, "stale");
  wwwa->algorithm = kwv_lookup_value(kwv, "algorithm");
  wwwa->qop_options = kwv_lookup_value(kwv, "qop");

  /* XXX Ignore any unrecognized directives */
  wwwa->auth_param = NULL;

  return(wwwa);
}

Http_auth_authorization *
http_auth_authorization_init(char *username, char *scheme_name, char *realm)
{
  Http_auth_authorization *aa;

  aa = ALLOC(Http_auth_authorization);
  if (scheme_name != NULL) {
	if (strcaseeq(scheme_name, "Basic"))
	  aa->scheme = HTTP_AUTH_BASIC;
	else if (strcaseeq(scheme_name, "Digest"))
	  aa->scheme = HTTP_AUTH_DIGEST;
	else
	  aa->scheme = HTTP_AUTH_UNKNOWN;
	aa->scheme_name = strdup(scheme_name);
  }
  else {
	aa->scheme = HTTP_AUTH_UNKNOWN;
	aa->scheme_name = NULL;
  }

  if (username != NULL)
	aa->username = strdup(username);
  else
	aa->username = NULL;

  if (realm != NULL)
	aa->realm = strdup(realm);
  else
	aa->realm = NULL;

  aa->authorization = NULL;
  aa->password = aa->nonce = aa->digest_uri = NULL;
  aa->response = aa->algorithm = aa->cnonce = aa->opaque = NULL;
  aa->message_qop = aa->nonce_count = aa->auth_param = NULL;
  aa->http_method = NULL;
  aa->www_auth = NULL;

  return(aa);
}

/*
 * Parse an HTTP Authorization request header
 * RFC 2617, S3.2.2
 */
Http_auth_authorization *
http_auth_authorization_parse(char *response, char **errmsg)
{
  char *p, *q, *start;
  Http_auth_authorization *aa;
  Kwv *kwv;
  static Kwv_conf conf = {
	"=", NULL, NULL, KWV_CONF_DEFAULT, ",", 10, NULL, NULL
  };

  aa = http_auth_authorization_init(NULL, NULL, NULL);
  aa->authorization = strdup(response);

  p = response;
  while (*p == ' ')
	p++;

  if ((q = strcaseprefix(p, "Basic ")) != NULL) {
	char *basic_credentials;

	aa->scheme = HTTP_AUTH_BASIC;
	aa->scheme_name = "Basic";

	while (*q == ' ')
	  q++;
	if (mime_decode_base64(q, (unsigned char **) &basic_credentials) == -1) {
	  *errmsg = "Base64 decoding error in Basic Authorization header";
	  return(NULL);
	}

	log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG,
			 "Basic auth info: \"%s\"", basic_credentials));

	if ((p = strchr(basic_credentials, (int) ':')) == NULL) {
	  *errmsg = "Invalid Basic Authorization header format, no colon";
	  return(NULL);
	}

	aa->username = basic_credentials;
	*p++ = '\0';
	aa->password = p;

	return(aa);
  }
  else if ((q = strcaseprefix(p, "Digest ")) != NULL) {
	aa->scheme = HTTP_AUTH_DIGEST;
	aa->scheme_name = "Digest";
  }
  else {
	/* XXX Extend for arbitrary schemes */
	log_msg((LOG_ERROR_LEVEL, "Unrecognized scheme: \"%s\"", response));
	return(NULL);
  }

  while (*q == ' ')
	q++;
  start = q;

  kwv = kwv_init(10);
  kwv->icase = 1;
  kwv->dup_mode = KWV_NO_DUPS;
  if ((kwv = kwv_make_sep(kwv, start, &conf)) == NULL) {
	*errmsg = "Error parsing Digest Authorization header";
	return(NULL);
  }

  if ((aa->username = kwv_lookup_value(kwv, "username")) == NULL) {
	*errmsg = "No username in Digest Authorization?";
	return(NULL);
  }
  if ((aa->realm = kwv_lookup_value(kwv, "realm")) == NULL) {
	*errmsg = "No realm in Digest Authorization?";
	return(NULL);
  }
  if ((aa->nonce = kwv_lookup_value(kwv, "nonce")) == NULL) {
	*errmsg = "No nonce in Digest Authorization?";
	return(NULL);
  }
  if ((aa->digest_uri = kwv_lookup_value(kwv, "uri")) == NULL) {
	*errmsg = "No uri in Digest Authorization?";
	return(NULL);
  }
  if ((aa->response = kwv_lookup_value(kwv, "response")) == NULL) {
	*errmsg = "No response in Digest Authorization?";
	return(NULL);
  }
  aa->algorithm = kwv_lookup_value(kwv, "algorithm");
  aa->cnonce = kwv_lookup_value(kwv, "cnonce");
  aa->opaque = kwv_lookup_value(kwv, "opaque");
  aa->message_qop = kwv_lookup_value(kwv, "qop");
  aa->nonce_count = kwv_lookup_value(kwv, "nc");
  /* XXX Ignore any unrecognized directives */
  aa->auth_param = NULL;

  return(aa);
}

/*
 * Create a nonce; return it or NULL if an error occurs.
 * If TIME_STAMP is given, use it (presumably for validation), otherwise
 * create a new one using the current time.
 * A nonce consists of a time stamp, followed by a colon, followed by
 * a hash value (we'll use an HMAC) of the time stamp plus (optionally)
 * other select data.
 */
static char *
make_digest_nonce(char *time_stamp)
{
  unsigned int hmac_len;
  char *hashval, *nonce, *nonce_hash_str, *ts;
  unsigned char *outp;
  Crypt_keys *ck;
  Hmac_handle *hmac;

  if ((ck = crypt_keys_from_vfs(ITEM_TYPE_JURISDICTION_KEYS)) == NULL)
	return(NULL);

  if (time_stamp == NULL) {
	time_t now;

	time(&now);
	ts = ds_xprintf("%lu", (unsigned long) now);
  }
  else
	ts = time_stamp;

  /* We'll just hash the time stamp for now. */
  nonce_hash_str = ts;

  hmac = crypto_hmac_open(AUTH_DIGEST_NONCE_DIGEST_NAME, ck->hmac_key,
						  CRYPTO_HMAC_KEY_LENGTH);
  crypto_hmac_hash(hmac, (unsigned char *) nonce_hash_str,
				   strlen(nonce_hash_str));
  outp = crypto_hmac_close(hmac, NULL, &hmac_len);
  crypt_keys_free(ck);

  strba64(outp, hmac_len, &hashval);
  nonce = ds_xprintf("%s:%s", ts, hashval);

  return(nonce);
}

enum {
  /*
   * Time, in seconds, from when WWW-Authenticate is issued to when an
   * Authorization request header becomes invalid.
   * The minimal value should allow for some think time while a user
   * enters his/her username and password.
   * Note that this is unrelated to the lifetime of DACS credentials.
   */
  NONCE_TIMEOUT_SECS = 20
};

/*
 * Given NONCE, as received from an Authorization request header, check if
 * it is valid.
 * Return 0 if so, -1 otherwise.
 */
static int
validate_nonce(char *nonce)
{
  char *check_nonce, *n, *nonce_timeout_secs, *p, *time_stamp;
  time_t creation_time, diff, now, timeout;
  Kwv *akwv;

  log_msg((LOG_TRACE_LEVEL, "nonce=\"%s\"", nonce));

  n = strdup(nonce);
  if ((p = strchr(n, (int) ':')) == NULL)
	return(-1);
  time_stamp = n;
  *p++ = '\0';
  
  if (strnum(time_stamp, STRNUM_TIME_T, &creation_time) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Invalid nonce creation time"));
	return(-1);
  }

  if (dacs_conf != NULL
	  && (akwv = var_ns_lookup_kwv(dacs_conf->conf_var_ns, "Conf")) != NULL
	  && (nonce_timeout_secs
		  = kwv_lookup_value(akwv, "http_auth_timeout_secs")) != NULL) {
	if (strnum(nonce_timeout_secs, STRNUM_TIME_T, &timeout) == -1) {
	  log_msg((LOG_ERROR_LEVEL, "Invalid http_auth_timeout_secs: \"%s\"",
			   nonce_timeout_secs));
	  return(-1);
	}
  }
  else
	timeout = NONCE_TIMEOUT_SECS;
  log_msg((LOG_TRACE_LEVEL, "Using nonce timeout: %lu secs", timeout));

  time(&now);
  if (((diff = now - creation_time)) > timeout) {
	log_msg((LOG_DEBUG_LEVEL, "Nonce \"%s\" has expired", nonce));
	return(-1);
  }
  log_msg((LOG_TRACE_LEVEL, "Nonce \"%s\" remaining secs: %lu", nonce, diff));

  check_nonce = make_digest_nonce(time_stamp);
  if (!streq(check_nonce, nonce)) {
	log_msg((LOG_ERROR_LEVEL, "Recomputed nonce != Authorization nonce"));
	return(-1);
  }
  log_msg((LOG_TRACE_LEVEL, "Nonce recomputes ok"));

  return(0);
}

/*
 * Create a digest-challenge
 * RFC 2617, S3.2.1
 */
char *
http_auth_digest_auth(Http_auth *auth, char *domain)
{
  unsigned int len;
  char *nonce, *opaque, *qop, *www_authenticate;
  unsigned char *encrypted_opaque;
  Crypt_keys *ck;
  Ds ds;

  nonce = make_digest_nonce(NULL);
  qop = "auth";

  ds_init(&ds);
  ds_asprintf(&ds, "%s realm=\"%s\"", auth->scheme_name, auth->realm);
  ds_asprintf(&ds, ", nonce=\"%s\"", nonce);
  ds_asprintf(&ds, ", algorithm=\"MD5\"");
  ds_asprintf(&ds, ", domain=\"%s\"", domain);
  ds_asprintf(&ds, ", qop=\"%s\"", qop);

  ck = crypt_keys_from_vfs(ITEM_TYPE_FEDERATION_KEYS);
  len = crypto_encrypt_string(ck, (unsigned char *) ds_buf(&ds),
							  ds_len(&ds), &encrypted_opaque);
  crypt_keys_free(ck);
  strba64(encrypted_opaque, len, &opaque);
  ds_asprintf(&ds, ", opaque=\"%s\"", opaque);

  www_authenticate = ds_buf(&ds);

  log_msg((LOG_TRACE_LEVEL, "Generating new RFC2617 Digest challenge: \"%s\"",
		   www_authenticate));

  return(www_authenticate);
}

/*
 * RFC 2617, S2
 */
char *
http_auth_basic_auth(Http_auth *auth)
{
  char *www_authenticate;

  www_authenticate = ds_xprintf("%s realm=\"%s\"%s%s",
								auth->scheme_name, auth->realm,
								auth->param != NULL ? ", " : "",
								auth->param != NULL ? auth->param : "");
  log_msg((LOG_TRACE_LEVEL, "Generating new RFC2617 Basic challenge: \"%s\"",
		   www_authenticate));

  return(www_authenticate);
}

/*
 * RFC 2617, S3.2.2.2
 */
static char *
digest_a1_md5(Http_auth_authorization *aa)
{
  char *a1;

  a1 = ds_xprintf("%s:%s:%s", aa->username, aa->realm, aa->password);

  return(a1);
}

/*
 * RFC 2617, S3.2.2.3
 */
static char *
digest_a2_auth(Http_auth_authorization *aa)
{
  char *a2;

  a2 = ds_xprintf("%s:%s", aa->http_method, aa->digest_uri);

  return(a2);
}

/*
 * RFC 2617, S3.2.1
 */
static char *
digest_h(char *str)
{
  char *hashval;
  unsigned char final[CRYPTO_DIGEST_MD5_BYTE_LENGTH];
  MD5_CTX ctx;

  MD5_Init(&ctx);
  MD5_Update(&ctx, str, strlen(str));
  MD5_Final(final, &ctx);

  hashval = strbtohex(final, CRYPTO_DIGEST_MD5_BYTE_LENGTH, 0);

  return(hashval);
}

/*
 * RFC 2617, S3.2.1
 */
static char *
digest_kd(char *secret, char *data)
{
  char *d, *hashval;

  d = ds_xprintf("%s:%s", secret, data);
  hashval = digest_h(d);

  return(hashval);
}

/*
 * RFC 2617, S3.2.2.1
 */
static char *
digest_request_auth(Http_auth_authorization *aa)
{
  char *data, *h_a1, *h_a2, *hashval;

  /*
  h_a1 = digest_h(digest_a1_md5(aa));
  */
  h_a1 = aa->password;
  h_a2 = digest_h(digest_a2_auth(aa));
  data = ds_xprintf("%s:%s:%s:%s:%s",
					aa->nonce, aa->nonce_count,
					aa->cnonce, aa->message_qop, h_a2);
  hashval = digest_kd(h_a1, data);

  return(hashval);
}
					
/*
 * Check that the Authorization header looks reasonable and hasn't been
 * forged or replayed.
 * DACS requires and assumes that all communication takes place over
 * secure and private channels whenever necessary but we must still beware
 * of accidental and intentional attempts to breach security.
 * Return 0 if so, -1 otherwise.
 */
static int
validate_auth_request(Http_auth_authorization *aa)
{

  if (validate_nonce(aa->nonce) == -1)
	return(-1);

  if (aa->opaque != NULL && aa->www_auth->opaque != NULL) {
	if (!streq(aa->opaque, aa->www_auth->opaque)) {
	  log_msg((LOG_ERROR_LEVEL, "WWW opaque=\"%s\", AUTH opaque=\"%s\"",
			   aa->opaque, aa->www_auth->opaque));
	  return(-1);
	}
  }

  return(0);
}

/*
 * Recompute the password value that appears in the htdigest file and
 * compare it with the value that is actually there.
 */
int
http_digest_check(Http_auth_authorization *aa, char *stored_password)
{
  char *h_a1;

  h_a1 = digest_h(digest_a1_md5(aa));

  if (streq(h_a1, stored_password))
	return(0);

  return(-1);
}

/*
 * Recompute the Authorization request-digest (RFC 2617, S3.2.2.1) and compare
 * it to the one given in the Authorization request header by the client.
 * Return 0 if authentication succeeds, -1 otherwise.
 */
int
http_digest_auth(char *username, Http_auth_authorization *aa)
{
  char *request_digest;

  if (username == NULL || *username == '\0' || aa == NULL)
	return(-1);

  if (validate_auth_request(aa) == -1)
	return(-1);

  request_digest = digest_request_auth(aa);
  log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG,
		   "Computed request-digest: %s", request_digest));

  if (streq(request_digest, aa->response)) {
	log_msg((LOG_INFO_LEVEL, "Digest Auth succeeded for username=\"%s\"",
			 username));
	return(0);
  }

  return(-1);
}

/*
 * Process one command line flag specific to authentication module
 * configuration.  This syntax is used by dacsauth and the HTTP_AUTH
 * directive and is documented in dacsauth(1).
 * The caller sets IND to the current index within ARGV and M_CURRENT to the
 * auth module currently being configured, or NULL.
 * If no flag is recognized, return 0.
 * If an error occurs, return -1.
 * Otherwise return 0, and if a new module is specified, set M_CURRENT to it
 * and update IND to the index of the next argument to be processed.
 */
static int
auth_module_parse_flag(char **argv, int *ind, Auth_module **am_current)
{
  int i;
  Auth_module *am;

  i = *ind;
  if (am_current != NULL)
	am = *am_current;
  else
	am = NULL;

  if (streq(argv[i], "-m")) {
	i++;
	if ((am = auth_module_config(argv, &i, NULL)) == NULL)
	  return(-1);
	*am_current = am;
  }
  else
	return(0);

  *ind = i;
  return(1);
}

/*
 * If a roles module spec follows, parse its flags.
 * See roles_module_config() for the syntax.
 */
static int
roles_module_parse_flag(char **argv, int *ind, Roles_module **rm_current)
{
  int i;
  Roles_module *rm, **rmp;

  i = *ind;
  if (rm_current != NULL)
	rm = *rm_current;
  else
	rm = NULL;

  if (streq(argv[i], "-r")) {
	/*
	 * A roles module spec follows.
	 */
	i++;
	if ((rm = roles_module_config(argv, &i)) == NULL)
	  return(-1);
	*rm_current = rm;
  }
  else
	return(0);

  *ind = i;
  return(1);
}

/*
 * Parse an HTTP_AUTH directive
 * Syntax:
 *  <auth_scheme> <auth_realm> <url_pattern> \
 *      [-pre] {-m auth-module-spec [auth-flag]}+ [[-param] <params>] \
 *      [-r roles-module-spec]*
 * or
 *  except <url_pattern>
 * where:
 *  <auth_scheme> must be "Basic" or "Digest" (case insensitive)
 *  <auth_realm> is the realm-value string (RFC 2617)
 *  <url_pattern> is URI path component matched against the request for
 *    which access was denied to an unauthenticated user; it is like the
 *    url_pattern used in access control rules in that it can be an exact
 *    match or can end in slash-star.
 *  <params> are additional, optional WWW-Authenticate authentication
 *  parameters
 * The second form of the syntax (using the "except" keyword) is used to
 * indicate that a request that matches <url_pattern> is NOT subject to
 * RFC 2617 authentication.
 */
static Http_auth *
parse_http_auth_directive(char *value)
{
  int i, mcount, n, st;
  char **argv;
  Auth_module *am, **amp, *am_next;
  Roles_module *rm, **rmp, *rm_next;
  Http_auth *http_auth;
  Mkargv conf = { 0, 0, " ", NULL, NULL };

  if ((n = mkargv(value, &conf, &argv)) == -1 || n < 2) {
	log_msg((LOG_ERROR_LEVEL, "Invalid HTTP_AUTH directive: \"%s\"", value));
	return(NULL);
  }

  http_auth = ALLOC(Http_auth);
  http_auth->scheme_name = NULL;
  http_auth->realm = NULL;
  http_auth->url_patterns = dsvec_init(NULL, sizeof(char *));
  http_auth->pre_flag = 0;
  http_auth->param = NULL;
  http_auth->auth_modules = NULL;
  http_auth->roles_modules = NULL;

  if (strcaseeq(argv[0], "except") && n == 2) {
	http_auth->scheme = HTTP_AUTH_EXCEPT;
	http_auth->scheme_name = "Except";
	http_auth->realm = NULL;
	for (i = 1; argv[i] != NULL; i++)
	  dsvec_add_ptr(http_auth->url_patterns, argv[i]);

	return(http_auth);
  }

  if (strcaseeq(argv[0], "Basic")) {
	http_auth->scheme = HTTP_AUTH_BASIC;
	http_auth->scheme_name = "Basic";
  }
  else if (strcaseeq(argv[0], "Digest")) {
	http_auth->scheme = HTTP_AUTH_DIGEST;
	http_auth->scheme_name = "Digest";
  }
  else {
	http_auth->scheme = HTTP_AUTH_UNKNOWN;
	http_auth->scheme_name = argv[0];
  }

  http_auth->realm = argv[1];
  for (i = 2; argv[i] != NULL && argv[i][0] == '/'; i++)
	dsvec_add_ptr(http_auth->url_patterns, argv[i]);

  mcount = 0;
  am = NULL;
  rm = NULL;

  while (argv[i] != NULL) {
	am_next = am;
	rm_next = rm;
	if ((st = auth_module_parse_flag(argv, &i, &am_next)) == 1) {
	  if (am_next != am) {
		am = am_next;
		am->id = ds_xprintf("module%d", mcount++);
		for (amp = &http_auth->auth_modules; *amp != NULL; amp = &(*amp)->next)
		  ;
		*amp = am;
	  }
	  continue;
	}
	if (st == -1)
	  return(NULL);

	if ((st = roles_module_parse_flag(argv, &i, &rm_next)) == 1) {
	  if (rm_next != rm) {
		rm = rm_next;
		rm->id = ds_xprintf("module%d", mcount++);
		for (rmp = &http_auth->roles_modules; *rmp != NULL; rmp = &(*rmp)->next)
		  ;
		*rmp = rm;
	  }
	  continue;
	}
	if (st == -1)
	  return(NULL);

	if (strprefix(argv[i], "-D") != NULL) {
	  if (am == NULL || am->argv_spec == NULL)
		return(NULL);
	  dsvec_add_ptr(am->argv_spec, argv[i++]);
	}
	else if (streq(argv[i], "-fj") || streq(argv[i], "-fn")) {
	  if (am == NULL || am->argv_spec == NULL || argv[i + 1] == NULL)
		return(NULL);
	  dsvec_add_ptr(am->argv_spec, argv[i++]);
	  dsvec_add_ptr(am->argv_spec, argv[i++]);
	}
	else if (streq(argv[i], "-pre")) {
	  http_auth->pre_flag++;
	  i++;
	}
	else {
	  if (streq(argv[i], "-param"))
		i++;

	  if (http_auth->param != NULL) {
		/* Only one param spec is allowed. */
		return(NULL);
	  }
	  http_auth->param = argv[i++];
	}
  }

  return(http_auth);
}

typedef struct Http_auth_match {
  int exact;
  int best_segs;
  char *best_pattern;
} Http_auth_match;

/*
 * Match each of the DIRECTIVE_PATTERNS against DSV_PATH, noting
 * whether any of them is an exact match or which of them is the best
 * match so far.
 * Return 1 if a better match is found, otherwise 0.
 */
static int
http_auth_match_patterns(Dsvec *directive_patterns, Dsvec *dsv_path,
						 Http_auth_match *match)
{
  int better, i, n;
  char *url_pattern;

  better = 0;
  for (i = 0; i < dsvec_len(directive_patterns); i++) {
	int exact;

	url_pattern = (char *) dsvec_ptr_index(directive_patterns, i);

	exact = 0;
	n = acs_match_url_segs(url_pattern, dsv_path, &exact);

	if (exact) {
	  match->exact = 1;
	  match->best_segs = n;
	  match->best_pattern = url_pattern;

	  return(1);
	}

	if (n > match->best_segs) {
	  match->exact = 0;
	  match->best_segs = n;
	  match->best_pattern = url_pattern;
	  better = 1;
	}
  }

  return(better);
}

/*
 * Examine HTTP_AUTH directives, matching their url_pattern against URL_PATH.
 * Return the first exact match, the closest match that is found first,
 * or NULL.
 */
Http_auth *
http_auth_match_directive(char *url_path)
{
  char *best_val;
  Dsvec *dsv_path;
  Http_auth *auth, *best_auth;
  Http_auth_match match;
  Kwv_pair *v;

  if (url_path == NULL)
	return(NULL);

  if ((dsv_path = uri_path_parse(url_path)) == NULL)
	return(NULL);

  if ((v = conf_var(CONF_HTTP_AUTH)) == NULL) {
	log_msg((LOG_TRACE_LEVEL, "No HTTP_AUTH is configured"));
	return(NULL);
  }

  match.exact = 0;
  match.best_segs = 0;
  match.best_pattern = NULL;

  best_auth = NULL;
  while (v != NULL) {
	int st;

	if ((auth = parse_http_auth_directive(v->val)) == NULL) {
	  log_msg((LOG_WARN_LEVEL, "Invalid HTTP_AUTH directive: \"%s\"", v->val));
	  return(NULL);
	}

	match.exact = 0;
	
	st = http_auth_match_patterns(auth->url_patterns, dsv_path, &match);

	if (st == 1) {
	  if (match.exact) {
		log_msg((LOG_TRACE_LEVEL, "HTTP_AUTH exact match: \"%s\"",
				 match.best_pattern));
		return(auth);
	  }
	  best_auth = auth;
	}

	v = v->next;
  }

  if (best_auth != NULL)
	log_msg((LOG_TRACE_LEVEL, "HTTP_AUTH best match: \"%s\"",
			 match.best_pattern));
  else
	log_msg((LOG_TRACE_LEVEL, "No HTTP_AUTH match"));

  return(best_auth);
}

/*
 * Convenience function for reading a password from a variety of sources.
 * If TYPE is GET_PASSWD_FILE, the argument is the name of a file containing
 * the password on a single line.
 * If TYPE is GET_PASSWD_STDIN, the argument is unused and the password is
 * read from stdin with echoing disabled.
 * If TYPE is GET_PASSWD_STREAM, the argument is a stream from which the
 * password is read with echoing disabled.
 * If TYPE is GET_PASSWD_PROMPT, the argument is a prompt to display before
 * the password is read from stdin with echoing disabled.
 * Return the password or NULL.
 *
 * XXX This should use use something like ds_fopen_secure() so material
 * is not left in buffers...
 */
char *
get_passwd(Get_passwd_type type, void *arg)
{
  char *passwd, *pfile;
  Ds *ds;
  FILE *fp;

  ds = ds_init(NULL);
  ds->clear_flag = 1;
  ds->delnl_flag = 1;

  pfile = NULL;
  fp = NULL;

  if (type == GET_PASSWD_FILE) {
	pfile = (char *) arg;
	if ((fp = fopen(pfile, "r")) == NULL)
	  goto fail;
  }
  else if (type == GET_PASSWD_STDIN)
	fp = stdin;
  else if (type == GET_PASSWD_STREAM)
	fp = (FILE *) arg;
  else if (type == GET_PASSWD_PROMPT) {
	char *prompt;

	prompt = (char *) arg;
	passwd = ds_prompt(ds, prompt, DS_PROMPT_NOECHO);

	return(passwd);
  }
  else
	return(NULL);

  if (type == GET_PASSWD_FILE || type == GET_PASSWD_STREAM) {
	if (ds_agetf(ds, fp) == NULL)
	  goto fail;
	passwd = ds_buf(ds);
  }
  else {
	if ((passwd = ds_agets(ds, fp)) == NULL)
	  goto fail;
  }

  if (pfile != NULL)
	fclose(fp);

  return(passwd);

 fail:
  if (pfile != NULL && fp != NULL)
	fclose(fp);

  return(NULL);
}

/*
 * If DIGEST_NAME is given, it is the name of a digest algorithm to use
 * instead of the one specified by the PASSWORD_DIGEST directive or the
 * default compile-time algorithm.
 * Verify that the name is acceptable and map it into its internal descriptor.
 */
Digest_desc *
passwd_get_digest_algorithm(const char *digest_name)
{
  const char *dn;
  Digest_desc *dd;

  if (digest_name != NULL)
	dn = digest_name;
  else if ((dn = conf_val(CONF_PASSWORD_DIGEST)) == NULL)
	dn = PASSWD_ALG_DEFAULT_NAME;

  if ((dd = crypto_lookup_passwd_digest_desc_by_name(dn)) == NULL) {
	log_msg((LOG_ERROR_LEVEL,
			 "An unrecognized digest method is configured: \"%s\"", dn));
	return(NULL);
  }

  return(dd);
}

/*
 * Return a password-style digest of PASSWD, using DIGEST_NAME (and SALT,
 * if non-NULL), as a DACS base-64 encoded string.
 * Return NULL if an error occurs.
 *
 * See authlib.c for the recognized digest function names.
 * You should use pbkdf2 or scrypt if you are concerned about an attacker
 * getting a copy of your password file and brute-forcing it to get passwords.
 * There is no substitute for a good password, however.
 */
char *
passwd_hash(Digest_desc *dd, char *passwd, char *salt)
{
  char *outstr;
  unsigned char *outp;
  unsigned int outlen;

  if (dd == NULL)
	return(NULL);

  switch (dd->dt->alg) {
	unsigned int count, dklen;

  case DIGEST_PBKDF2_SHA1:
	count = dd->u.pbkdf2.count;
	dklen = dd->u.pbkdf2.dklen;
	outp = crypto_pbkdf2((unsigned char *) passwd, -1, (unsigned char *) salt,
						 -1, "SHA1", count, dklen);
	outlen = dklen;
	break;

  case DIGEST_PBKDF2_SHA256:
	count = dd->u.pbkdf2.count;
	dklen = dd->u.pbkdf2.dklen;
	outp = crypto_pbkdf2((unsigned char *) passwd, -1, (unsigned char *) salt,
						 -1, "SHA256", count, dklen);
	outlen = dklen;
	break;

  case DIGEST_PBKDF2_SHA512:
	count = dd->u.pbkdf2.count;
	dklen = dd->u.pbkdf2.dklen;
	outp = crypto_pbkdf2((unsigned char *) passwd, -1, (unsigned char *) salt,
						 -1, "SHA512", count, dklen);
	outlen = dklen;
	break;

  case DIGEST_SCRYPT:
	{
	  uint64_t N;
	  unsigned int r, p, dklen;

	  N = dd->u.scrypt.N;
	  r = dd->u.scrypt.r;
	  p = dd->u.scrypt.p;
	  dklen = dd->u.scrypt.dklen;
	  outp = crypto_scrypt((unsigned char *) passwd, -1, (unsigned char *) salt,
						   -1, N, r, p, dklen);
	  outlen = dklen;
	  break;
	}

  case DIGEST_ARGON2:
	{
	  int rc;
	  char encoded[256], hval[32];
	  size_t enclen;
	  Argon2_desc *desc;

	  desc = &dd->u.argon2;

	  outlen = desc->outlen;
	  outp = malloc(outlen);

	  rc = argon2_hash(desc->t_cost, desc->m_cost, desc->lanes,
					   passwd, strlen(passwd),
					   salt, strlen(salt),
					   NULL, 0, desc->ad, desc->adlen,
					   outp, outlen, encoded, sizeof(encoded),
					   desc->type, desc->version);
	  if (rc != 0) {
		log_msg((LOG_ERROR_LEVEL,
				 "Invalid argon2 parameters: \"%s\"", dd->desc));
		return(NULL);
	  }

	  break;
	}

  default:
	{
	  Digest_ctx *ctx;

	  ctx = crypto_digest_open_dd(dd);

	  if (ctx == NULL) {
		log_msg((LOG_ERROR_LEVEL,
				 "Unrecognized hash function or invalid parameters: \"%s\"",
				 dd->desc));
		return(NULL);
	  }

	  if (salt != NULL)
		crypto_digest_hash(ctx, salt, strlen(salt));

	  crypto_digest_hash(ctx, passwd, strlen(passwd));
	  outp = crypto_digest_close(ctx, NULL, &outlen);
	}
  }

  if (outp == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Digest hash failed"));
	return(NULL);
  }
  strba64(outp, outlen, &outstr);

  return(outstr);
}

/*
 * For dacspasswd/local_passwd_auth.
 * Create a message digest by applying algorithm ALG to PASSWD.
 * The password is salted before being hashed.
 * The return value contains the digest algorithm number, salt string,
 * and the computed digest, separated by the PASSWD_SEP_CHAR character.
 *
 * XXX Note that the system's crypt(3) function (DIGEST_CRYPT)
 * isn't necessarily the same everywhere, potentially making the passwords
 * non-portable.  Some versions (e.g., FreeBSD) examine the format of the salt
 * argument to select among different hashing algorithms.
 * Our strba64(), used as a binary to ASCII converter, uses the alphabet
 * [0-9][a-z][A-Z] with '.' and '/', which ought not result in a special salt
 * format.
 */
char *
passwd_make_digest(Digest_desc *dd, char *passwd, Pw_entry *opw)
{
  char *digest, *salt_prefix, *salt, *salt_str, *val;
  unsigned char *buf;
  Pw_entry *ppw, pw;

  buf = crypto_make_random_buffer(PASSWD_SALT_BYTES);
  strba64((const unsigned char *) buf, PASSWD_SALT_BYTES, &salt);
  salt[PASSWD_SALT_BYTES] = '\0';
  if ((salt_prefix = conf_val(CONF_PASSWORD_SALT_PREFIX)) != NULL)
	salt_str = ds_xprintf("%s%s", salt_prefix, salt);
  else
	salt_str = salt;

  if (dd->dt->alg == DIGEST_CRYPT)
	digest = crypt(passwd, salt_str);
  else {
	if ((digest = passwd_hash(dd, passwd, salt_str)) == NULL) {
	  log_msg((LOG_ALERT_LEVEL, "Password hash failed"));
	  dacs_fatal("Internal error");
	  /*NOTREACHED*/
	}
  }

  if (opw == NULL)
	ppw = pw_init_entry(&pw, NULL);
  else
	ppw = opw;

  ppw->dd = dd;
  ppw->salt = salt_str;
  ppw->digest = digest;
  val = pw_make_entry(ppw);

  if (opw == NULL) {
	if (salt != NULL)
	  strzap(salt);
	if (salt_str != NULL)
	  strzap(salt_str);
	strzap(passwd);
	memzap(buf, PASSWD_SALT_BYTES);
  }

  return(val);
}

/*
 * Return 1 if the result of applying password hashing algorithm ALG to
 * GIVEN_PASSWORD matches PASSWD_DIGEST; return 0 if it does not and -1
 * if an error occurs.
 * That is, return 1 iff GIVEN_PASSWORD is (most likely) the same password
 * previously given when PASSWD_DIGEST was created using ALG.
 * Caller is responsible for zapping the cleartext password.
 */
int
passwd_check_digest(Digest_desc *dd, char *given_passwd, Pw_entry *pw)
{
  int st;
  char *digest;

  /* A special initial character indicates a disabled entry. */
  if (pw->state == PW_STATE_DISABLED)
	return(0);

  /* This does not necessarily indicate an error. */
  if (dd->dt->alg != pw->dd->dt->alg)
	log_msg((LOG_INFO_LEVEL,
			 "Stored algorithm doesn't match configured algorithm"));

  st = -1;
  if (pw->dd->dt->alg == DIGEST_CRYPT) {
	if ((digest = crypt(given_passwd, pw->salt)) != NULL)
	  st = streq(digest, pw->digest);
  }
  else {
	if ((digest = passwd_hash(pw->dd, given_passwd, pw->salt)) == NULL) {
	  log_msg((LOG_ALERT_LEVEL, "Password hash failed"));
	  return(-1);
	}

	{
	  unsigned char *d, *sd;
	  unsigned int dlen, sdlen;

	  stra64b(digest, &d, &dlen);
	  stra64b(pw->digest, &sd, &sdlen);
	  st = (dlen == sdlen && memcmp(d, sd, dlen) == 0);
	  memzap(d, dlen);
	  memzap(sd, sdlen);
	}
  }

  if (digest != NULL)
	strzap(digest);

  return(st);
}

static Dsvec *auth_success_list = NULL;

int
auth_add_success_expr(char *expr)
{

  if (auth_success_list == NULL)
	auth_success_list = dsvec_init(NULL, sizeof(char *));

  dsvec_add_ptr(auth_success_list, strdup(expr));

  return(0);
}

int
auth_get_success_exprs(char ***list)
{

  *list = (char **) dsvec_base(auth_success_list);

  return(dsvec_len(auth_success_list));
}

int
auth_success(Kwv *kwv, Kwv *kwv_auth)
{
  int i, st;
  char *p;
  Kwv_pair *v;

  for (v = conf_var(CONF_AUTH_SUCCESS); v != NULL; v = v->next) {
	p = v->val;
	log_msg((LOG_TRACE_LEVEL, "Evaluating AUTH_SUCCESS expr: \"%s\"", p));
	st = auth_eval(p, kwv, kwv_auth, NULL, NULL);
	if (acs_expr_error_occurred(st))
	  log_msg((LOG_ERROR_LEVEL, "Invalid AUTH_SUCCESS directive: \"%s\"", p));
  }

  for (i = 0; i < dsvec_len(auth_success_list); i++) {
	p = (char *) dsvec_ptr_index(auth_success_list, i);
	log_msg((LOG_TRACE_LEVEL, "Evaluating auth_success_list expr: \"%s\"", p));
	st = auth_eval(p, kwv, kwv_auth, NULL, NULL);
	if (acs_expr_error_occurred(st))
	  log_msg((LOG_ERROR_LEVEL, "Invalid AUTH_SUCCESS directive: \"%s\"", p));
  }

  return(0);
}

#ifdef NOTDEF
/*
 * Lookup IDENT:
 *  o if it is not found or if it is found but has expired, create a new
 *    record for (IDENT, NEW_EXPIRES_SECS), and return 1
 *  o if it is found but has not expired, return 0
 *  o if an error occurs, return -1
 */
int
auth_status_update(char *ident, time_t new_expires_secs)
{
  char *buf, *errmsg, *p;
  Vfs_handle *h;

  errmsg = NULL;

  if ((h = vfs_open_item_type(ITEM_TYPE_AUTH_STATUS)) == NULL) {
    errmsg = "Can't open item type auth_status";
    goto fail;
  }

  if (vfs_get(h, ident, (void **) &buf, NULL) != -1) {
	time_t expires, now;

	time(&now);
	if (strnum(buf, STRNUM_TIME_T, &expires) == -1) {
	  errmsg = "Invalid expiration time found";
	  goto fail;
	}
	if (expires > now) {
	  vfs_close(h);
	  return(0);
	}
  }

  p = ds_xprintf("%u", new_expires_secs);
  if (vfs_put(h, ident, p, strlen(p)) == -1) {
	errmsg = "Update failed";
	goto fail;
  }

  vfs_close(h);

  return(1);

 fail:
  if (h != NULL)
	vfs_close(h);

  return(-1);
}

/*
 * Lookup IDENT:
 *  o if it is not found, return 0
 *  o if is found return 1 and its expiration time
 *  o if an error occurs, return -1
 */
int
auth_status_lookup(char *ident, time_t *expires_secs)
{
  int st;
  char *buf, *errmsg, *p;
  Vfs_handle *h;

  errmsg = NULL;

  if ((h = vfs_open_item_type(ITEM_TYPE_AUTH_STATUS)) == NULL) {
    errmsg = "Can't open item type auth_status";
    goto fail;
  }

  if (vfs_get(h, ident, (void **) &buf, NULL) != -1) {
	time_t expires;

	if (strnum(buf, STRNUM_TIME_T, expires_secs) == -1) {
	  errmsg = "Invalid expiration time found";
	  goto fail;
	}
	st = 1;
  }
  else
	st = 0;

  vfs_close(h);

  return(st);

 fail:
  if (h != NULL)
	vfs_close(h);

  return(-1);
}

/*
 * Lookup IDENT:
 *  o if it is not found, return 0
 *  o if is found, remove it and return 1 
 *  o if an error occurs, return -1
 */
int
auth_status_remove(char *ident)
{
  int st;
  char *buf, *errmsg, *p;
  Vfs_handle *h;

  errmsg = NULL;

  if ((h = vfs_open_item_type(ITEM_TYPE_AUTH_STATUS)) == NULL) {
    errmsg = "Can't open item type auth_status";
    goto fail;
  }

  if ((st = vfs_exists(h, ident)) == -1 || st == 0) {
	vfs_close(h);
	return(st);
  }

  if ((st = vfs_delete(h, ident)) == -1) {
	st = -1;
  }

  vfs_close(h);

  return(st);

 fail:
  if (h != NULL)
	vfs_close(h);

  return(-1);
}

int
auth_status_reset(void)
{

  return(-1);
}
#endif
