/*
 * NAME:
 *	ssld - routines useful for SSL servers
 *
 * DESCRIPTION:
 *	These routines provide for client authentication using
 *	Eric Young's SSLeay library.
 *
 *	The concept and logic for the ssl.users file is based on
 *	Tim Hudson's ssl code in SSLtelnet, but has been quite
 *	severely re-worked, so don't blame Tim for any bugs.
 *
 * SEE ALSO:
 *	sslfd.h
 *
 * AUTHOR:
 *	Simon J. Gerraty <sjg@quick.com.au>
 */
/*
 *      @(#)Copyright (c) 1996 Simon J. Gerraty.
 *      
 *      This is free software.  It comes with NO WARRANTY.
 *      Permission to use, modify and distribute this source code 
 *      is granted subject to the following conditions.
 *      1/ that the above copyright notice and this notice 
 *      are preserved in all copies and that due credit be given 
 *      to the author.  
 *      2/ that any changes to this code are clearly commented 
 *      as such so that the author does not get blamed for bugs 
 *      other than his own.
 *      
 *      Please send copies of changes and bug-fixes to:
 *      sjg@quick.com.au
 */
#ifndef lint
static char RCSid[] = "$Id: ssld.c,v 1.16 1997/02/08 07:13:07 sjg Exp $";
#endif

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <syslog.h>
#include <string.h>


#include "x509.h"
#include "ssl.h"

#include "sslfd.h"

extern SSL_CTX *ssl_ctx;
extern SSL *ssl_con;
extern char x509_name[256];

#ifndef SSL_VERIFY_FLAG
# define SSL_VERIFY_FLAG SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
#endif

/*
 * This function derrived from some logic in Tim Hudson's
 * verify_callback()
 * The logic here is a little different - we only accept the
 * connection if the cert and user name match in our database.
 */
#ifndef _PATH_SSL_USERS
# define _PATH_SSL_USERS "/etc/ssl.users"
#endif

int
ssl_cert_ok(user, cert)
	const char *user;
	const char *cert;
{
	char buf[1024];
	FILE *fh;
	int line = 0;
	char *cp, *n;

	if (!_path_ssl_users)
		_path_ssl_users = _PATH_SSL_USERS;

	if (cert != (char *)0 && (fh = fopen(_path_ssl_users, "r"))) {
		while (fgets(buf, sizeof(buf), fh)) {
			line++;
			switch (buf[0]) {
			case '#':
			case '\n':
				continue;
			}
			if (cp = strchr(buf, '\n'))
				*cp = '\0';
			else {
				syslog(LOG_ERR, "%s: line %d, too long",
				       _path_ssl_users, line);
				continue;
			}
			cp = strchr(buf, ':');
			if (!cp) {
				syslog(LOG_ERR, "%s: line %d, invalid format",
				       _path_ssl_users, line);
				continue;
			}
			*cp++ = '\0';
			if (strcasecmp(cp, cert) == 0) {
				n = buf;
				while (n) {
					cp = strchr(n, ',');
					if (cp)
						*cp++ = '\0';
					/*
					 * if the cert is allowed root access
					 * the it is allowed any...
					 */
					if (user &&
					    strcmp(user, n) == 0 ||
					    strcmp("root", n) == 0) {
						fclose(fh);
						syslog(LOG_INFO,
						       "SSL: user '%s' cert '%s' OK",
						       user, cert);
						return(1);
					}
					n = cp;
				}
			}
		}
		fclose(fh);
		syslog(LOG_WARNING, "SSL: user '%s' cert '%s' REJECTED",
		       user, cert);
		return(-1);
	} 
	return(0);
}

static int
ssl_verify_cb(ok, xs, xi, depth, error)
	int ok;
	const char *xs, *xi;
	int depth, error;
{
	char *name = (char *)X509_NAME_oneline(X509_get_subject_name((X509 *)xs));


	/*
	 * If the verification fails, then don't remember the name.  However,
	 * if we don't require a certificate, then return success which will
	 * still allow us to set up an encrypted session.
	 *
	 */
	if (!ok) {
		/* If we can't verify the issuer, then don't accept the name. */
		if (depth != 0 && ssl_auth_cert) {
			free(ssl_auth_cert);
			ssl_auth_cert = 0;
		}
		ssl_verify_err = X509_cert_verify_error_string(error);
		syslog(LOG_NOTICE, "verify: ok=%d,depth=%d,err=%s,xs=%s",
		       ok, depth, ssl_verify_err, name);
		if (!ssl_verify_errcert)
			ssl_verify_errcert = name;
		else
			free(name);
		return (ssl_verify_flag <= 1);
	}
	syslog(LOG_INFO, "verify: depth=%d,xs=%s", depth, name);
	if (depth == 0)
		ssl_auth_cert = name;
	else
		free(name);
	return ok;
}

int
ssld_init(cert_file, key_file, cipher_list, verify_flag, flags)
	const char *cert_file, *key_file, *cipher_list;
	int verify_flag;
	int flags;
{

	return ssl_init(cert_file, key_file, cipher_list, verify_flag,
			ssl_verify_cb, LOG_NOTICE, flags);
}

static void
_rply(msg)
	char *msg;
{
	write_net(ssl_fd, msg, strlen(msg));
}

void (*ssld_reply)() = _rply;

void
set_ssldreply(f)
	void (*f)();
{
	if (!f)
		f = _rply;
	ssld_reply = f;
}

int
ssld_auth(hostname, remuser, locuser)
	const char *hostname, *remuser, *locuser;
{
	char msg[512];
	int sts = -1;
	char *cert_name;

	msg[0] = 0;
	if (cert_name = get_x509_name((char *)0, 0)) {
		/*
		 * we are using the convention that the CN
		 * field contains either a fully qualified
		 * hostname for a server cert or a user-name.
		 * If x509_name contains a '.', check if its a
		 * hostname, and if so, it better match the
		 * host using it.
		 */
		if (strchr(cert_name, '.') && /* contains a . */
		    gethostbyname(cert_name) && /* valid hostname */
		    strcasecmp(hostname, cert_name)) { /* does not match */
			syslog(LOG_NOTICE, "/CN=%s not %s, denied",
			       cert_name, hostname);
			ssl_verify_err = "wrong hostcert";
		} else {
			/*
			 * Ok, all we really care about is
			 * whether the given cert is allowed
			 * access as locuser.
			 */
			sts = ssl_cert_ok(locuser, ssl_auth_cert);
			ssl_verify_err = "cert denied permission";
		}
		ssl_verify_errcert = ssl_auth_cert;
	} else {
		cert_name = "";
		(void)strcpy(msg, "no cert supplied");
	}
	if (sts < 0 && ssl_verify_err != 0) {
		(void) sprintf(msg,
			       "\r\nssld_auth: %s: %.*s\r\n",
			       ssl_verify_err,
			       sizeof(msg)/2, ssl_verify_errcert);
		ssld_reply(msg);
		sprintf(msg, "DENIED (%.*s)", sizeof(msg), ssl_verify_err);
	} else if (sts == 0)
		sprintf(msg, "no %s file", _path_ssl_users);
	
	syslog(LOG_INFO|LOG_AUTH, "%s@%s (/CN=%s) as %s: %s",
	       remuser, hostname, cert_name, locuser,
	       (sts > 0) ? "OK" : msg);
	return (sts);
}

#ifdef MAIN

#define OPTARG_VAL(str, cp, key) ((cp = strchr(str, '=')) && strncmp(str, key, cp - str) == 0 && ++cp)

int
main(argc, argv)
	int argc;
	char **argv;
{
	extern int Debug, optind;
	extern const char *optarg;
	extern char *sock_id();
	struct sockaddr_in me;
	int 	s;
	int 	c;
	char	*hostname, *cp;
	char	*cipher_list = SSL_CIPHER_LIST;
	char	*cert_file = SSL_CERT_FILE;
	char	*key_file = SSL_KEY_FILE;
	int	local_port = -1, verify_flag = -1;
	const char	*logfac = "local1";
	const char	*remuser, *locuser;

	remuser = locuser = 0;
	Debug = 1;				/* stop inetd_init() from forking */
	while ((c = getopt(argc, argv, "r:l:z:p:L:")) != EOF) {
		switch (c) {
		case 'r':
			remuser = optarg;
			break;
		case 'l':
			locuser = optarg;
			break;
		case 'L':
			logfac = optarg;
			break;
		case 'p':
			local_port = atoi(optarg);
			break;
		case 'z':
			if (OPTARG_VAL(optarg, cp, "cert"))
				cert_file = cp;
			else if (OPTARG_VAL(optarg, cp, "cipher"))
				cipher_list = cp;
			else if (OPTARG_VAL(optarg, cp, "key"))
				key_file = cp;
			else if (OPTARG_VAL(optarg, cp, "verify"))
				verify_flag = atoi(cp);
			break;
		}
	}
	argc -= optind;
	argv += optind;

	openlog("ssld", LOG_PID, logfacs(logfac));

	if ((s = inetd_init(local_port, SOCK_STREAM, 6, &me)) < 0) {
		syslog(LOG_ERR, "inetd_init(%d) failed: %m");
		exit(1);
	} else if (s > 0) {
		for (c = 0; c < 3; c++) {
			if (c != s) {
				close(c);
				dup(s);
			}
		}
		if (s >= 3)
			close(s);
		s = 0;
	}
	
	ssld_init(s, cert_file, key_file, cipher_list, verify_flag);
	hostname = sock_id(s, 3);
	if (cp = strrchr(hostname, '.'))
	    *cp = '\0';

	if (!remuser)
		remuser = getenv("USER");
	if (!locuser)
		locuser = remuser;

	c = (ssld_auth(hostname, remuser, locuser) <= 0);
	exit(c);
}
#endif
