/*
 * Copyright (c) 2003-2015
 * 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.
 ***************************************************************************/

/*
 * X.509 client certificate authentication module for SSL
 *
 * The client certificate (cert) is passed as an argument (SSL_CLIENT_CERT),
 * which is assumed to have been obtained from a client via SSL.  Regardless,
 * the client must have demonstrated possession of the private key
 * corresponding to the public key in this cert, which is the case when SSL
 * has been used.
 *
 * USERNAME, PASSWORD, and AUXILIARY arguments are also passed in case
 * they're needed.
 *
 * In some cases, client cert authentication can be done entirely within
 * the DACS configuration file. This module may be needed, however, if the web
 * server has not validated the client cert sufficiently, in which case the
 * module can offer some additional functionality.  It can be executed on a
 * system other than where the web server is running, for example.
 *
 * A note about the SSL_CLIENT_VERIFY environment variable created by
 * mod_ssl is in order.  This variable is not documented very well by
 * Apache/mod_ssl.  As near as I can tell, here are its possible values and
 * their semantics.  The value indicates the result of the client cert
 * verification, and is one of:
 *   NONE:     No client cert verification done was done
 *   SUCCESS:  Client cert verification was successful
 *   FAILED:   Client cert verification failed (with Apache 2.2.3, this is
 *             followed by what appears to be a null pointer dereference)
 *   GENEROUS: Client cert verification was unsuccessful because a
 *             particular verification error occurred (possibly related to a
 *             self-signed cert) but the SSLVerifyClient directive had the
 *             value optional_no_ca, meaning that the error should be ignored
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2015\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: local_cert_auth.c 2789 2015-03-04 01:24:18Z brachman $";
#endif

#include "dacs.h"

static char *log_module_name = "local_cert_auth";

/*
 * Call an external program - only the openssl command is currently supported -
 * to verify the client cert.
 *
 * Return 0 if the client cert looks ok, -1 otherwise.
 */
static int
check_cert(Kwv *kwv, char *jurisdiction, char *username, char *password,
		   char *aux, char *client_cert)
{
  int n, read_fd, write_fd;
  char buf[1024], *ca_path, *openssl_path;
  Dsvec *argv;

  if (username != NULL)
	log_msg((LOG_TRACE_LEVEL, "check_cert: username=\"%s\"", username));
  if (password != NULL)
	log_msg((LOG_TRACE_LEVEL, "check_cert: password provided"));
  if (aux != NULL)
	log_msg((LOG_TRACE_LEVEL, "check_cert: aux provided"));

  if ((openssl_path = kwv_lookup_value(kwv, "CERT_OPENSSL_PATH")) == NULL) {
#ifdef OPENSSL_PROG
	openssl_path = OPENSSL_PROG;
#else
	log_msg((LOG_ERROR_LEVEL, "CERT_OPENSSL_PATH is not defined"));
	return(-1);
#endif
  }

  if ((ca_path = kwv_lookup_value(kwv, "CERT_CA_PATH")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "CERT_CA_PATH is not defined"));
	return(-1);
  }

  log_msg((LOG_DEBUG_LEVEL, "openssl=\"%s\", ca_path=\"%s\"",
		   openssl_path, ca_path));

  argv = dsvec_init(NULL, sizeof(char *));
  dsvec_add_ptr(argv, openssl_path);
  dsvec_add_ptr(argv, "verify");
  dsvec_add_ptr(argv, "-CApath");
  dsvec_add_ptr(argv, ca_path);
  dsvec_add_ptr(argv, "-purpose");
  dsvec_add_ptr(argv, "sslclient");
  dsvec_add_ptr(argv, NULL);

  if (filterthru((char **) dsvec_base(argv), NULL, &read_fd, &write_fd,
				 NULL, NULL) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Could not filter through %s", argv[0]));
	return(-1);
  }

  if (write_buffer(write_fd, client_cert, strlen(client_cert)) == -1) {
	log_err((LOG_ERROR_LEVEL, "Error writing to %s", openssl_path));
	close(read_fd);
	close(write_fd);
	return(-1);
  }
  close(write_fd);

  if ((n = read(read_fd, buf, sizeof(buf) - 1)) <= 0) {
	if (n == -1)
	  log_err((LOG_ERROR_LEVEL, "I/O reading reply"));
	else
	  log_err((LOG_ERROR_LEVEL, "No output from %s", openssl_path));
	close(read_fd);
	return(-1);
  }
  close(read_fd);

  buf[n] = '\0';

  /*
   * Of course, this test for success is fragile.
   * If the cert is ok, OpenSSL returns the string: "stdin: OK\n".
   * But the OpenSSL documentation says nothing about the exit status
   * and in fact at least sometimes it is zero even when an error occurs.
   */
  if (!streq(buf, "stdin: OK\n") && !streq(buf, "OK\n")) {
	log_msg((LOG_ERROR_LEVEL, "Read: \"%s\"", buf));
	return(-1);
  }

  log_msg((LOG_TRACE_LEVEL, "Read: \"%s\"", buf));

  return(0);
}

static int
namemap(Kwv *kwv, char *username, char **mapped_username)
{
  int rc;
  char *name, *name_attr;
  Vfs_handle *h;

  /*
   * Check the configuration for the name of the argument whose value
   * we will use as the key.
   */
  if ((h = vfs_open_item_type(ITEM_TYPE_CERTNAMEMAP)) != NULL
	  && (name_attr = kwv_lookup_value(kwv, "CERT_NAME_ATTR")) != NULL) {
	if ((name = kwv_lookup_value(kwv, name_attr)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "No %s argument was found", name_attr));
	  return(-1);
	}

	/*
	 * Lookup the key in the mapping table to get the actual username to
	 * assign to this user.
	 */
	rc = vfs_get(h, name, (void **) mapped_username, NULL);
	if (vfs_close(h) == -1)
	  log_msg((LOG_ERROR_LEVEL, "vfs_close() failed"));
	if (rc == -1) {
	  log_msg((LOG_ERROR_LEVEL, "Name lookup for \"%s\" failed", name));
	  return(-1);
	}
  }
  else {
	log_msg((LOG_TRACE_LEVEL, "Not using namemap"));
	*mapped_username = username;
  }

  if (*mapped_username == NULL || *mapped_username[0] == '\0')
    return(-1);

  return(0);
}

int
main(int argc, char **argv)
{
  int emitted_dtd, i;
  char *errmsg, *jurisdiction, *username, *password, *aux;
  char *client_cert, *dumpfile, *lifetime, *mapped_username;
  Auth_reply_ok ok;
  Kwv *kwv;

  errmsg = "Internal error";
  username = password = aux = jurisdiction = NULL;
  emitted_dtd = 0;
  client_cert = NULL;
  lifetime = NULL;
  mapped_username = NULL;

  if (dacs_init(DACS_LOCAL_SERVICE, &argc, &argv, &kwv, &errmsg) == -1) {
	/* If we fail here, we may not have a DTD with which to reply... */
  fail:
	if (password != NULL)
	  strzap(password);
	if (aux != NULL)
	  strzap(aux);

	if (emitted_dtd) {
	  printf("%s\n", make_xml_auth_reply_failed(mapped_username, NULL));
	  emit_xml_trailer(stdout);
	}
	log_msg((LOG_ALERT_LEVEL, "Failed: reason=\"%s\"", errmsg));

	exit(1);
  }

  /* This must go after initialization. */
  emitted_dtd = emit_xml_header(stdout, "auth_reply");

  if (argc > 1) {
	errmsg = "Usage: unrecognized parameter";
	goto fail;
  }

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME") && username == NULL)
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "PASSWORD") && password == NULL)
	  password = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUXILIARY") && aux == NULL)
	  aux = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION")
			 && jurisdiction == NULL)
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
    else if (streq(kwv->pairs[i]->name, "SSL_CLIENT_CERT")
             && client_cert == NULL)
      client_cert = url_decode(kwv->pairs[i]->val, NULL, NULL);
	else if (strneq(kwv->pairs[i]->name, "SSL_", 4))
	  ;
	else
	  log_msg((LOG_TRACE_LEVEL, "Parameter: '%s'", kwv->pairs[i]->name));
  }

  /* Verify that we're truly responsible for DACS_JURISDICTION */
  if (dacs_verify_jurisdiction(jurisdiction) == -1) {
	errmsg = "Missing or incorrect DACS_JURISDICTION";
	goto fail;
  }

  if (client_cert == NULL) {
	errmsg = "No client certificate found";
	goto fail;
  }
  if ((dumpfile = kwv_lookup_value(kwv, "CERT_DUMP_CLIENT")) != NULL) {
	int fd, st;
	size_t len;

	st = 0;
	if ((fd = open(dumpfile, O_WRONLY | O_TRUNC | O_CREAT, 0644)) == -1) {
	  log_err((LOG_ALERT_LEVEL, "Couldn't dump client cert to \"%s\"",
			   dumpfile));
	  st = -1;
	}

	len = strlen(client_cert);
	if (write_buffer(fd, client_cert, len) == -1) {
	  log_err((LOG_ALERT_LEVEL, "Couldn't write client cert to \"%s\"",
			   dumpfile));
	  st = -1;
	}

	close(fd);
	if (st == 0)
	  log_msg((LOG_DEBUG_LEVEL, "Wrote client cert to \"%s\" (%d bytes)",
			   dumpfile, len));
  }

  if (check_cert(kwv, jurisdiction, username, password, aux, client_cert)
	  == -1) {
	errmsg = "Username/Password/Aux/Cert incorrect";
	goto fail;
  }

  if (password != NULL)
	strzap(password);
  if (aux != NULL)
	strzap(aux);

  if (namemap(kwv, username, &mapped_username) == -1) {
	errmsg = "Name mapping failed";
	goto fail;
  }

  /* If this wasn't specified, dacs_authenticate will use the default. */
  lifetime = kwv_lookup_value(kwv, "CREDENTIALS_LIFETIME_SECS");

  log_msg((LOG_NOTICE_LEVEL,
		   "Authentication succeeded, username=\"%s\", lifetime=%s",
		   username, lifetime == NULL ? "default" : lifetime));

  ok.username = mapped_username;
  ok.lifetime = lifetime;
  ok.roles_reply = NULL;
  printf("%s\n", make_xml_auth_reply_ok(&ok));

  emit_xml_trailer(stdout);
  exit(0);
}
