/* tls.c - STARTTLS helper functions for imapd
 * Tim Martin
 * 9/21/99
 *
 * Based upon Lutz Jaenicke's TLS patches for postfix
 *
 * Copyright (c) 1998-2003 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any other legal
 *    details, please contact  
 *      Office of Technology Transfer
 *      Carnegie Mellon University
 *      5000 Forbes Avenue
 *      Pittsburgh, PA  15213-3890
 *      (412) 268-4387, fax: (412) 268-7395
 *      tech-transfer@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR 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.

* NAME
*	tls
* SUMMARY
*	interface to openssl routines
* SYNOPSIS
*	#include <tls.h>
*
* DESCRIPTION
*	This module is the interface between Cyrus Imapd and the OpenSSL library.
*	As of now only one filedescriptor can be handled, so only one
*	TLS channel can be open at a time.
*
*	tls_init_serverengine() is called once when the server is started
*	in order to initialize as much of the TLS stuff as possible.
*	The certificate handling is also decided during the setup phase,
*	so that a peer specific handling is not possible.
*
*	tls_start_servertls() activates the TLS feature for the
*	filedescriptor selected with tls_setfd() before. We expect
*	that all buffers are flushed and the TLS handshake can begin
*	immediately.
*
*	tls_stop_servertls() sends the "close notify" alert via
*	SSL_shutdown() to the peer and resets all connection specific
*	TLS data. As RFC2487 does not specify a seperate shutdown, it
*	is supposed that the underlying TCP connection is shut down
*	immediately afterwards, so we don't care about additional data
*	coming through the channel.
*
*	Once the TLS connection is initiated, information about the TLS
*	state is available:
*	tls_protocol holds the protocol name (SSLv2, SSLv3, TLSv1),
*	tls_cipher_name the cipher name (e.g. RC4/MD5),
*	tls_cipher_usebits the number of bits actually used (e.g. 40),
*	tls_cipher_algbits the number of bits the algorithm is based on
*	(e.g. 128).
*	The last two values may be different when talking to a crippled
*	- ahem - export controled peer (e.g. 40/128).
*
* xxx we need to offer a callback to do peer issuer certification.
*     data that should be available for inspection:
*	If the peer offered a certifcate _and_ the certificate could be
*	verified successfully, part of the certificate data are available as:
*	tls_peer_subject X509v3-oneline with the DN of the peer
*	pfixlts_peer_CN extracted CommonName of the peer
*	tls_peer_issuer  X509v3-oneline with the DN of the issuer
*	pfixlts_peer_CN extracted CommonName of the issuer
*	tls_peer_fingerprint fingerprint of the certificate
*
*/

/* $Id: tls.c,v 1.7 2005/03/05 00:37:07 dasenbro Exp $ */

#include <config.h>

#ifdef HAVE_SSL

/* System library. */

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>

/* OpenSSL library. */

#include <openssl/lhash.h>
#include <openssl/bn.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/ssl.h>

/* Apple Open Directory */
/*  Needed for default password callback */

#include "AppleOD.h"

/* Application-specific. */
#include "assert.h"
#include "xmalloc.h"
#include "tls.h"

/* Session caching/reuse stuff */
#include "global.h"
#include "cyrusdb.h"

#define DB (config_tlscache_db) /* sessions are binary -> MUST use DB3 */

static CallbackUserData *sUserData = NULL;

static struct db *sessdb = NULL;
static int sess_dbopen = 0;

/* We must keep some of the info available */
static const char hexcodes[] = "0123456789ABCDEF";

enum {
    var_imapd_tls_loglevel = 0,
    var_proxy_tls_loglevel = 0,
    CCERT_BUFSIZ = 256
};

static int verify_depth = 5;
static int verify_error = X509_V_OK;

static SSL_CTX *s_ctx = NULL, *c_ctx = NULL;

static int tls_serverengine = 0; /* server engine initialized? */
static int tls_clientengine = 0; /* client engine initialized? */
static int do_dump = 0;		/* actively dumping protocol? */


int tls_enabled(void)
{
    const char *val;

    val = config_getstring(IMAPOPT_TLS_CERT_FILE);
    if (!val || !strcasecmp(val, "disabled")) return 0;

    val = config_getstring(IMAPOPT_TLS_KEY_FILE);
    if (!val || !strcasecmp(val, "disabled")) return 0;

    return 1;
}

/* taken from OpenSSL apps/s_cb.c 
 * tim - this seems to just be giving logging messages
 */

static void apps_ssl_info_callback(SSL * s, int where, int ret)
{
    char   *str;
    int     w;

    if (var_imapd_tls_loglevel==0) return;

    w = where & ~SSL_ST_MASK;

    if (w & SSL_ST_CONNECT)
	str = "SSL_connect";
    else if (w & SSL_ST_ACCEPT)
	str = "SSL_accept";
    else
	str = "undefined";

    if (where & SSL_CB_LOOP) {
	if (tls_serverengine && (var_imapd_tls_loglevel >= 2))
	    syslog(LOG_DEBUG, "%s:%s", str, SSL_state_string_long(s));
    } else if (where & SSL_CB_ALERT) {
	str = (where & SSL_CB_READ) ? "read" : "write";
	if ((tls_serverengine && (var_imapd_tls_loglevel >= 2)) ||
	    ((ret & 0xff) != SSL3_AD_CLOSE_NOTIFY))
	    syslog(LOG_DEBUG, "SSL3 alert %s:%s:%s", str,
		   SSL_alert_type_string_long(ret),
		   SSL_alert_desc_string_long(ret));
    } else if (where & SSL_CB_EXIT) {
	if (ret == 0)
	    syslog(LOG_DEBUG, "%s:failed in %s",
		   str, SSL_state_string_long(s));
	else if (ret < 0) {
	    syslog(LOG_DEBUG, "%s:error in %s",
		   str, SSL_state_string_long(s));
	}
    }
}

/* taken from OpenSSL apps/s_cb.c
   not thread safe! */
static RSA *tmp_rsa_cb(SSL * s __attribute__((unused)),
		       int export __attribute__((unused)),
		       int keylength)
{
    static RSA *rsa_tmp = NULL;

    if (rsa_tmp == NULL) {
	rsa_tmp = RSA_generate_key(keylength, RSA_F4, NULL, NULL);
    }
    return (rsa_tmp);
}

/* taken from OpenSSL apps/s_cb.c */

static int verify_callback(int ok, X509_STORE_CTX * ctx)
{
    char    buf[256];
    X509   *err_cert;
    int     err;
    int     depth;

    syslog(LOG_ERR,"Doing a peer verify");

    err_cert = X509_STORE_CTX_get_current_cert(ctx);
    err = X509_STORE_CTX_get_error(ctx);
    depth = X509_STORE_CTX_get_error_depth(ctx);

    X509_NAME_oneline(X509_get_subject_name(err_cert), buf, sizeof(buf));
    if (ok==0)
    {
      syslog(LOG_ERR, "verify error:num=%d:%s", err,
	     X509_verify_cert_error_string(err));
      
	if (verify_depth >= depth) {
	    ok = 0;
	    verify_error = X509_V_OK;
	} else {
	    ok = 0;
	    verify_error = X509_V_ERR_CERT_CHAIN_TOO_LONG;
	}
    }
    switch (ctx->error) {
    case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
	X509_NAME_oneline(X509_get_issuer_name(ctx->current_cert), buf, sizeof(buf));
	syslog(LOG_NOTICE, "issuer= %s", buf);
	break;
    case X509_V_ERR_CERT_NOT_YET_VALID:
    case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
	syslog(LOG_NOTICE, "cert not yet valid");
	break;
    case X509_V_ERR_CERT_HAS_EXPIRED:
    case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
	syslog(LOG_NOTICE, "cert has expired");
	break;
    }

    return (ok);
}


/*
 * taken from OpenSSL crypto/bio/b_dump.c, modified to save a lot of strcpy
 * and strcat by Matti Aarnio.
 */

#define TRUNCATE
#define DUMP_WIDTH	16

static int tls_dump(const char *s, int len)
{
    int     ret = 0;
    char    buf[160 + 1];
    char    *ss;
    int     i;
    int     j;
    int     rows;
    int     trunc;
    unsigned char ch;

    trunc = 0;

#ifdef TRUNCATE
    for (; (len > 0) && ((s[len - 1] == ' ') || (s[len - 1] == '\0')); len--)
	trunc++;
#endif

    rows = (len / DUMP_WIDTH);
    if ((rows * DUMP_WIDTH) < len)
	rows++;

    for (i = 0; i < rows; i++) {
	unsigned int val;
	buf[0] = '\0';				/* start with empty string */
	ss = buf;

	val = i * DUMP_WIDTH;
	assert(val <= 0xFFFF);
	sprintf(ss, "%04x ", i * DUMP_WIDTH);
	ss += strlen(ss);

	for (j = 0; j < DUMP_WIDTH; j++) {
	    if (((i * DUMP_WIDTH) + j) >= len) {
		strcpy(ss, "   ");
	    } else {
		ch = ((unsigned char) *((char *) (s) + i * DUMP_WIDTH + j))
		    & 0xFF;

		sprintf(ss, "%02x%c", ch, j == 7 ? '|' : ' ');
		ss += 3;
	    }
	}
	ss += strlen(ss);
	*ss+= ' ';
	for (j = 0; j < DUMP_WIDTH; j++) {
	    if (((i * DUMP_WIDTH) + j) >= len)
		break;
	    ch = ((unsigned char) *((char *) (s) + i * DUMP_WIDTH + j)) & 0xff;
	    *ss+= (((ch >= ' ') && (ch <= '~')) ? ch : '.');
	    if (j == 7) *ss+= ' ';
	}
	*ss = 0;
	/* 
	 * if this is the last call then update the ddt_dump thing so that
         * we will move the selection point in the debug window
         */	
	if (var_imapd_tls_loglevel>0)
	  syslog(LOG_DEBUG, "%s", buf);
	ret += strlen(buf);
    }
#ifdef TRUNCATE
    if (trunc > 0) {
	snprintf(buf, sizeof(buf), "%04x - <SPACES/NULS>\n", len+ trunc);
	if (var_imapd_tls_loglevel>0)
	  syslog(LOG_DEBUG, "%s", buf);
	ret += strlen(buf);
    }
#endif
    return (ret);
}

 /*
  * Set up the cert things on the server side. We do need both the
  * private key (in key_file) and the cert (in cert_file).
  * Both files may be identical.
  *
  * This function is taken from OpenSSL apps/s_cb.c
  */

static int set_cert_stuff(SSL_CTX * ctx,
			  const char *cert_file, const char *key_file)
{
    if (cert_file != NULL) {
	if (SSL_CTX_use_certificate_file(ctx, cert_file,
					 SSL_FILETYPE_PEM) <= 0) {
	    syslog(LOG_ERR, "unable to get certificate from '%s'", cert_file);
	    return (0);
	}
	if (key_file == NULL)
	    key_file = cert_file;

	if ( strlen( key_file ) < FILENAME_MAX )
	{
		if ( sUserData == NULL )
		{
			sUserData = xmalloc( sizeof(CallbackUserData) );
			if ( sUserData != NULL )
			{
				memset( sUserData, 0, sizeof(CallbackUserData) );
			}
		}
		char tmp[ FILENAME_MAX ];
		strlcpy( tmp, key_file, FILENAME_MAX );
		char *p = tmp;
		char *q = tmp;

		while ( *p != '\0' )
		{
			if ( *p == '/' )
			{
				q = p;
			}
			p++;
		}

		if ( (*q != '\0') && (*q == '/') && (*p+1 != '\0') )
		{
			p = ++q;
			int len = strlen( p );


			if ( sUserData != NULL )
			{
				if ( strncmp( p+(len-4), ".key", 4 ) == 0 )
				{
					len = len - 4;
					strncpy( sUserData->key, p, len );
					sUserData->key[ len ] = '\0';
					sUserData->len = len;
				}
				else
				{
					strcpy( sUserData->key, p );
					sUserData->len = strlen( p );
				}

				SSL_CTX_set_default_passwd_cb_userdata( ctx, (void *)sUserData );
				SSL_CTX_set_default_passwd_cb( ctx, &apple_password_callback );
			}
			else
			{
				syslog( LOG_NOTICE, "Could not allocate memory for custom callback: %s", key_file );
			}
		}
		else
		{
			syslog( LOG_NOTICE, "Could not set custom callback: %s", key_file );
		}
	}
	else
	{
		syslog( LOG_NOTICE, "Key file path too big for custom callback: %s", key_file );
	}

	if (SSL_CTX_use_PrivateKey_file(ctx, key_file,
					SSL_FILETYPE_PEM) <= 0) {
	    syslog(LOG_ERR, "unable to get private key from '%s'", key_file);
	    return (0);
	}
	/* Now we know that a key and cert have been set against
         * the SSL context */
	if (!SSL_CTX_check_private_key(ctx)) {
	    syslog(LOG_ERR,
		   "Private key does not match the certificate public key");
	    return (0);
	}
    }
    return (1);
}

/*
 * The new_session_cb() is called, whenever a new session has been
 * negotiated and session caching is enabled.  We save the session in
 * a database so that we can share sessions between processes. 
 */ 
static int new_session_cb(SSL *ssl __attribute__((unused)),
			  SSL_SESSION *sess)
{
    int len;
    unsigned char *data = NULL, *asn;
    time_t expire;
    int ret = -1;

    assert(sess);

    if (!sess_dbopen) return 0;

    /* find the size of the ASN1 representation of the session */
    len = i2d_SSL_SESSION(sess, NULL);

    /*
     * create the data buffer.  the data is stored as:
     * <expire time><ASN1 data>
     */
    data = (unsigned char *) xmalloc(sizeof(time_t)+len*sizeof(unsigned char));

    /* transform the session into its ASN1 representation */
    if (data) {
	asn = data + sizeof(time_t);
	len = i2d_SSL_SESSION(sess, &asn);
	if (!len) syslog(LOG_ERR, "i2d_SSL_SESSION failed");
    }

    /* set the expire time for the external cache, and prepend it to data */
    expire = SSL_SESSION_get_time(sess) + SSL_SESSION_get_timeout(sess);
    memcpy(data, &expire, sizeof(time_t));

    if (data && len) {
	/* store the session in our database */
	do {
	    ret = DB->store(sessdb, sess->session_id,
			    sess->session_id_length,
			    data, len + sizeof(time_t), NULL);
	} while (ret == CYRUSDB_AGAIN);
    }

    if (data) free(data);

    /* log this transaction */
    if (var_imapd_tls_loglevel > 0) {
	unsigned int i;
	char idstr[SSL_MAX_SSL_SESSION_ID_LENGTH*2 + 1];
	for (i = 0; i < sess->session_id_length; i++) {
	    sprintf(idstr+i*2, "%02X", sess->session_id[i]);
	}
	syslog(LOG_DEBUG, "new TLS session: id=%s, expire=%s, status=%s",
	       idstr, ctime(&expire), ret ? "failed" : "ok");
    }

    return (ret == 0);
}

/*
 * Function for removing a session from our database.
 */
static void remove_session(unsigned char *id, int idlen)
{
    int ret;

    assert(id);
    assert(idlen <= SSL_MAX_SSL_SESSION_ID_LENGTH);
    
    if (!sess_dbopen) return;

    do {
	ret = DB->delete(sessdb, id, idlen, NULL, 1);
    } while (ret == CYRUSDB_AGAIN);

    /* log this transaction */
    if (var_imapd_tls_loglevel > 0) {
	int i;
	char idstr[SSL_MAX_SSL_SESSION_ID_LENGTH*2 + 1];

	for (i = 0; i < idlen; i++) {
	    sprintf(idstr+i*2, "%02X", id[i]);
	}
	    
	syslog(LOG_DEBUG, "remove TLS session: id=%s", idstr);
    }
}

/*
 * The remove_session_cb() is called, whenever the SSL engine removes
 * a session from the internal cache. This happens if the session is
 * removed because it is expired or when a connection was not shutdown
 * cleanly.
 */
static void remove_session_cb(SSL_CTX *ctx __attribute__((unused)),
			      SSL_SESSION *sess)
{
    assert(sess);

    remove_session(sess->session_id, sess->session_id_length);
}

/*
 * The get_session_cb() is only called on SSL/TLS servers with the
 * session id proposed by the client. The get_session_cb() is always
 * called, also when session caching was disabled.  We lookup the
 * session in our database in case it was stored by another process.
 */
static SSL_SESSION *get_session_cb(SSL *ssl __attribute__((unused)),
				   unsigned char *id, int idlen, int *copy)
{
    int ret;
    const char *data = NULL;
    unsigned char *asn;
    int len = 0;
    time_t expire = 0, now = time(0);
    SSL_SESSION *sess = NULL;

    assert(id);
    assert(idlen <= SSL_MAX_SSL_SESSION_ID_LENGTH);

    if (!sess_dbopen) return NULL;

    do {
	ret = DB->fetch(sessdb, id, idlen, &data, &len, NULL);
    } while (ret == CYRUSDB_AGAIN);

    if (!ret && data) {
	assert(len >= (int) sizeof(time_t));

	/* grab the expire time */
	memcpy(&expire, data, sizeof(time_t));

	/* check if the session has expired */
	if (expire < now) {
	    remove_session(id, idlen);
	}
	else {
	    /* transform the ASN1 representation of the session
	       into an SSL_SESSION object */
	    asn = (unsigned char*) data + sizeof(time_t);
	    sess = d2i_SSL_SESSION(NULL, &asn, len - sizeof(time_t));
	    if (!sess) syslog(LOG_ERR, "d2i_SSL_SESSION failed: %m");
	}
    }

    /* log this transaction */
    if (var_imapd_tls_loglevel > 0) {
	int i;
	char idstr[SSL_MAX_SSL_SESSION_ID_LENGTH*2 + 1];
	for (i = 0; i < idlen; i++)
	    sprintf(idstr+i*2, "%02X", id[i]);

	syslog(LOG_DEBUG, "get TLS session: id=%s, expire=%s, status=%s",
	       idstr, ctime(&expire),
	       !data ? "not found" : expire < now ? "expired" : "ok");
    }

    *copy = 0;
    return sess;
}

/*
 * Seed the random number generator.
 */
static int tls_rand_init(void)
{
#ifdef EGD_SOCKET
    return (RAND_egd(EGD_SOCKET));
#else
    /* otherwise let OpenSSL do it internally */
    return 0;
#endif
}

 /*
  * This is the setup routine for the SSL server. As smtpd might be called
  * more than once, we only want to do the initialization one time.
  *
  * The skeleton of this function is taken from OpenSSL apps/s_server.c.

  * returns -1 on error
  */

/* must be called after cyrus_init */
int     tls_init_serverengine(const char *ident,
			      int verifydepth,
			      int askcert,
			      int tlsonly)
{
    int     off = 0;
    int     verify_flags = SSL_VERIFY_NONE;
    const char   *cipher_list;
    const char   *CApath;
    const char   *CAfile;
    const char   *s_cert_file;
    const char   *s_key_file;
    int    requirecert;
    int    timeout;

    if (tls_serverengine)
	return (0);				/* already running */

    if (var_imapd_tls_loglevel >= 2)
	syslog(LOG_DEBUG, "starting TLS server engine");

    SSL_library_init();
    SSL_load_error_strings();
    if (tls_rand_init() == -1) {
	syslog(LOG_ERR,"TLS server engine: cannot seed PRNG");
	return -1;
    }

#if 0
    if (tlsonly) {
	s_ctx = SSL_CTX_new(TLSv1_server_method());
    } else {
	s_ctx = SSL_CTX_new(SSLv23_server_method());
    }
#endif
    /* even if we want TLS only, we use SSLv23 server method so we can
       deal with a client sending an SSLv2 greeting message */

    s_ctx = SSL_CTX_new(SSLv23_server_method());
    if (s_ctx == NULL) {
	return (-1);
    };

    off |= SSL_OP_ALL;		/* Work around all known bugs */
    if (tlsonly) {
	off |= SSL_OP_NO_SSLv2;
	off |= SSL_OP_NO_SSLv3;
    }
    SSL_CTX_set_options(s_ctx, off);
    SSL_CTX_set_info_callback(s_ctx, (void (*)()) apps_ssl_info_callback);

    /* Don't use an internal session cache */
    SSL_CTX_sess_set_cache_size(s_ctx, 1);  /* 0 is unlimited, so use 1 */
    SSL_CTX_set_session_cache_mode(s_ctx, SSL_SESS_CACHE_SERVER |
				   SSL_SESS_CACHE_NO_AUTO_CLEAR |
				   SSL_SESS_CACHE_NO_INTERNAL_LOOKUP);

    /* Get the session timeout from the config file (in minutes) */
    timeout = config_getint(IMAPOPT_TLS_SESSION_TIMEOUT);
    if (timeout < 0) timeout = 0;
    if (timeout > 1440) timeout = 1440; /* 24 hours max */

    /* A timeout of zero disables session caching */
    if (timeout) {
	char dbdir[1024];
	int r;

	/* Set the context for session reuse -- use the service ident */
	SSL_CTX_set_session_id_context(s_ctx, (void*) ident, strlen(ident));

	/* Set the timeout for the internal/external cache (in seconds) */
	SSL_CTX_set_timeout(s_ctx, timeout*60);

	/* Set the callback functions for the external session cache */
	SSL_CTX_sess_set_new_cb(s_ctx, new_session_cb);
	SSL_CTX_sess_set_remove_cb(s_ctx, remove_session_cb);
	SSL_CTX_sess_set_get_cb(s_ctx, get_session_cb);

	/* create the name of the db file */
	strlcpy(dbdir, config_dir, sizeof(dbdir));
	strlcat(dbdir, FNAME_TLSSESSIONS, sizeof(dbdir));

	r = DB->open(dbdir, CYRUSDB_CREATE, &sessdb);
	if (r != 0) {
	    syslog(LOG_ERR, "DBERROR: opening %s: %s",
		   dbdir, cyrusdb_strerror(ret));
	}
	else
	    sess_dbopen = 1;
    }

    cipher_list = config_getstring(IMAPOPT_TLS_CIPHER_LIST);
    if (!SSL_CTX_set_cipher_list(s_ctx, cipher_list)) {
	syslog(LOG_ERR,"TLS server engine: cannot load cipher list '%s'",
	       cipher_list);
	return (-1);
    }

    CAfile = config_getstring(IMAPOPT_TLS_CA_FILE);
    CApath = config_getstring(IMAPOPT_TLS_CA_PATH);

    if ((!SSL_CTX_load_verify_locations(s_ctx, CAfile, CApath)) ||
	(!SSL_CTX_set_default_verify_paths(s_ctx))) {
	/* just a warning since this is only necessary for client auth */
	syslog(LOG_NOTICE,"TLS server engine: cannot load CA data");	
    }

    s_cert_file = config_getstring(IMAPOPT_TLS_CERT_FILE);
    s_key_file = config_getstring(IMAPOPT_TLS_KEY_FILE);

    if (!set_cert_stuff(s_ctx, s_cert_file, s_key_file)) {
	syslog(LOG_ERR,"TLS server engine: cannot load cert/key data");
	return (-1);
    }
    SSL_CTX_set_tmp_rsa_callback(s_ctx, tmp_rsa_cb);

    verify_depth = verifydepth;
    if (askcert!=0)
	verify_flags |= SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;

    requirecert = config_getswitch(IMAPOPT_TLS_REQUIRE_CERT);
    if (requirecert)
	verify_flags |= SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT
	    | SSL_VERIFY_CLIENT_ONCE;
    SSL_CTX_set_verify(s_ctx, verify_flags, verify_callback);

    if (askcert || requirecert) {
      if (CAfile == NULL) {
	  syslog(LOG_ERR, 
		 "TLS server engine: No CA file specified. "
		 "Client side certs may not work");
      } else {
	  SSL_CTX_set_client_CA_list(s_ctx, SSL_load_client_CA_file(CAfile));
      }
    }

    tls_serverengine = 1;
    return (0);
}


/* taken from OpenSSL apps/s_cb.c */

static long bio_dump_cb(BIO * bio, int cmd, const char *argp, int argi,
			long argl __attribute__((unused)), long ret)
{
    if (!do_dump)
	return (ret);

    if (cmd == (BIO_CB_READ | BIO_CB_RETURN)) {
	printf("read from %08X [%08lX] (%d bytes => %ld (0x%X))",
	       (unsigned int) bio, (long unsigned int) argp,
	       argi, ret, (unsigned int) ret);
	tls_dump(argp, (int) ret);
	return (ret);
    } else if (cmd == (BIO_CB_WRITE | BIO_CB_RETURN)) {
	printf("write to %08X [%08lX] (%d bytes => %ld (0x%X))",
	       (unsigned int) bio, (long unsigned int)argp,
	       argi, ret, (unsigned int) ret);
	tls_dump(argp, (int) ret);
    }
    return (ret);
}




 /*
  * This is the actual startup routine for the connection. We expect
  * that the buffers are flushed and the "Ready to start TLS" was
  * send to the client, so that we can immediately can start the TLS
  * handshake process.
  *
  * 'layerbits' and 'authid' are filled in on success. authid is only
  * filled in if the client authenticated. 'ret' is the SSL connection
  * on success.
  */
int tls_start_servertls(int readfd, int writefd,
			int *layerbits, char **authid, SSL **ret)
{
    int     sts;
    int     j;
    unsigned int n;
    SSL_CIPHER *cipher;
    X509   *peer;
    const char *tls_protocol = NULL;
    const char *tls_cipher_name = NULL;
    int tls_cipher_usebits = 0;
    int tls_cipher_algbits = 0;
    SSL *tls_conn;
    int r = 0;

    assert(tls_serverengine);
    assert(ret);
    if (var_imapd_tls_loglevel >= 1)
	syslog(LOG_DEBUG, "setting up TLS connection");

    if (authid) *authid = NULL;

    tls_conn = (SSL *) SSL_new(s_ctx);
    if (tls_conn == NULL) {
	*ret = NULL;
	r = -1;
	goto done;
    }
    SSL_clear(tls_conn);
    
    /* set the file descriptors for SSL to use */
    if ((SSL_set_rfd(tls_conn, readfd) == 0) || 
	(SSL_set_wfd(tls_conn, writefd) == 0)) {
	r = -1;
	goto done;
    }

    /*
     * This is the actual handshake routine. It will do all the negotiations
     * and will check the client cert etc.
     */
    SSL_set_accept_state(tls_conn);

    /*
     * We do have an SSL_set_fd() and now suddenly a BIO_ routine is called?
     * Well there is a BIO below the SSL routines that is automatically
     * created for us, so we can use it for debugging purposes.
     */
    if (var_imapd_tls_loglevel >= 3)
	BIO_set_callback(SSL_get_rbio(tls_conn), bio_dump_cb);

    /* Dump the negotiation for loglevels 3 and 4*/
    if (var_imapd_tls_loglevel >= 3)
	do_dump = 1;

    if ((sts = SSL_accept(tls_conn)) <= 0) {
	SSL_SESSION *session = SSL_get_session(tls_conn);
	if (session) {
	    SSL_CTX_remove_session(s_ctx, session);
	}
	r = -1;
	goto done;
    }
    /* Only loglevel==4 dumps everything */
    if (var_imapd_tls_loglevel < 4)
	do_dump = 0;

    /*
     * Lets see, whether a peer certificate is available and what is
     * the actual information. We want to save it for later use.
     */
    peer = SSL_get_peer_certificate(tls_conn);
    if (peer != NULL) {
	char fingerprint[EVP_MAX_MD_SIZE * 3];
	char issuer_CN[CCERT_BUFSIZ];
	char peer_issuer[CCERT_BUFSIZ];
	char peer_CN[CCERT_BUFSIZ];
	char peer_subject[CCERT_BUFSIZ];
	unsigned char md[EVP_MAX_MD_SIZE];

	syslog(LOG_DEBUG, "received client certificate");

	X509_NAME_oneline(X509_get_subject_name(peer),
			  peer_subject, CCERT_BUFSIZ);
	syslog(LOG_DEBUG, "subject=%s", peer_subject);
	X509_NAME_oneline(X509_get_issuer_name(peer),
			  peer_issuer, CCERT_BUFSIZ);
	if (var_imapd_tls_loglevel >= 2)
	    syslog(LOG_DEBUG, "issuer=%s", peer_issuer);

	if (X509_digest(peer, EVP_md5(), md, &n)) {
	    for (j = 0; j < (int) n; j++)
	    {
		fingerprint[j * 3] = hexcodes[(md[j] & 0xf0) >> 4];
		fingerprint[(j * 3) + 1] = hexcodes[(md[j] & 0x0f)];
		if (j + 1 != (int) n) {
		    fingerprint[(j * 3) + 2] = '_';
		} else {
		    fingerprint[(j * 3) + 2] = '\0';
		}
	    }
	    if (var_imapd_tls_loglevel >= 2)
		syslog(LOG_DEBUG, "fingerprint=%s", fingerprint);
	}
	X509_NAME_get_text_by_NID(X509_get_subject_name(peer),
			  NID_commonName, peer_CN, CCERT_BUFSIZ);
	X509_NAME_get_text_by_NID(X509_get_issuer_name(peer),
			  NID_commonName, issuer_CN, CCERT_BUFSIZ);
	if (var_imapd_tls_loglevel >= 3)
	    syslog(LOG_DEBUG, "subject_CN=%s, issuer_CN=%s", 
		   peer_CN, issuer_CN);

	/* xxx verify that we like the peer_issuer/issuer_CN */

	if (authid != NULL) {
	    /* save the peer id for our caller */
	    *authid = peer_CN ? xstrdup(peer_CN) : NULL;
	}
	X509_free(peer);
    }
    tls_protocol = SSL_get_version(tls_conn);
    cipher = SSL_get_current_cipher(tls_conn);
    tls_cipher_name = SSL_CIPHER_get_name(cipher);
    tls_cipher_usebits = SSL_CIPHER_get_bits(cipher, &tls_cipher_algbits);

    if (layerbits != NULL) {
	*layerbits = tls_cipher_usebits;
    }

    if (authid && *authid) {
	syslog(LOG_NOTICE, "starttls: %s with cipher %s (%d/%d bits %s)"
	                   " authenticated as %s", 
	       tls_protocol, tls_cipher_name,
	       tls_cipher_usebits, tls_cipher_algbits, 
	       SSL_session_reused(tls_conn) ? "reused" : "new",
	       *authid);
    } else {
	syslog(LOG_NOTICE, "starttls: %s with cipher %s (%d/%d bits %s)"
	                   " no authentication", 
	       tls_protocol, tls_cipher_name,
	       tls_cipher_usebits, tls_cipher_algbits,
	       SSL_session_reused(tls_conn) ? "reused" : "new");
    }

 done:
    if (r && tls_conn) {
	/* error; clean up */
	SSL_free(tls_conn);
	tls_conn = NULL;
    }
    *ret = tls_conn;
    return r;
}

int tls_reset_servertls(SSL **conn)
{
    int r = 0;

    if (*conn) {
	if (TLS_FAST_SHUTDOWN) {
	    /*
	     * Don't bother spending time closing the TLS session,
	     * but make sure its available for reuse.
	     */
	    SSL_set_shutdown(*conn,SSL_SENT_SHUTDOWN|SSL_RECEIVED_SHUTDOWN);
	}
	else {
	    /* Follow the TLS protocol and do a shutdown handshake */
	    r = SSL_shutdown(*conn);
	    if (r == 0) {
		/* Just sent, now wait for peer to ack */
		r = SSL_shutdown(*conn);
	    }
	    if (r == 0) r = -1;
	}
	SSL_free(*conn);
    }
    
    return r;
}

int tls_shutdown_serverengine(void)
{
    int r;

    if (tls_serverengine && sess_dbopen) {
	r = DB->close(sessdb);
	if (r) {
	    syslog(LOG_ERR, "DBERROR: error closing tlsdb: %s",
		   cyrusdb_strerror(r));
	}
	sessdb = NULL;
	sess_dbopen = 0;
    }

    return 0;

}

/*
 * Delete expired sessions.
 */
struct prunerock {
    int count;
    int deletions;
};

static int prune_p(void *rock, const char *id, int idlen,
		   const char *data, int datalen)
{
    struct prunerock *prock = (struct prunerock *) rock;
    time_t expire;

    prock->count++;

    assert(datalen >= (int) sizeof(time_t));

    /* grab the expire time */
    memcpy(&expire, data, sizeof(time_t));

    /* log this transaction */
    if (var_imapd_tls_loglevel > 0) {
	int i;
	char idstr[SSL_MAX_SSL_SESSION_ID_LENGTH*2 + 1];

	assert(idlen <= SSL_MAX_SSL_SESSION_ID_LENGTH);

	for (i = 0; i < idlen; i++) {
	    sprintf(idstr+i*2, "%02X", (unsigned char) id[i]);
	}
	
	syslog(LOG_DEBUG, "found TLS session: id=%s, expire=%s",
	       idstr, ctime(&expire));
    }

    /* check if the session has expired */
    return (expire < time(0));
}

static int prune_cb(void *rock, const char *id, int idlen,
		    const char *data __attribute__((unused)), 
                    int datalen __attribute__((unused)))
{
    struct prunerock *prock = (struct prunerock *) rock;

    prock->deletions++;

    remove_session((unsigned char*) id, idlen);

    return 0;
}

/* must be called after cyrus_init */
int tls_prune_sessions(void)
{
    char dbdir[1024];
    int ret;
    struct prunerock prock;

   /* create the name of the db file */
    strlcpy(dbdir, config_dir, sizeof(dbdir));
    strlcat(dbdir, FNAME_TLSSESSIONS, sizeof(dbdir));

    ret = DB->open(dbdir, CYRUSDB_CREATE, &sessdb);
    if (ret != CYRUSDB_OK) {
	syslog(LOG_ERR, "DBERROR: opening %s: %s",
	       dbdir, cyrusdb_strerror(ret));
	return 1;
    }
    else {
	/* check each session in our database */
	sess_dbopen = 1;
	prock.count = prock.deletions = 0;
	DB->foreach(sessdb, "", 0, &prune_p, &prune_cb, &prock, NULL);
	DB->close(sessdb);
	sessdb = NULL;
	sess_dbopen = 0;

	syslog(LOG_NOTICE, "tls_prune: purged %d out of %d entries",
	       prock.deletions, prock.count);
    }

    return 0;
}

/* fill string buffer with info about tls connection */
int tls_get_info(SSL *conn, char *buf, size_t len)
{
    int usebits = 0;
    int algbits = 0;

    usebits = SSL_get_cipher_bits(conn, &algbits);
    snprintf(buf, len, "version=%s cipher=%s bits=%d/%d verify=%s",
	     SSL_get_cipher_version(conn), SSL_get_cipher_name(conn),
	     usebits, algbits,
	     SSL_get_verify_result(conn) == X509_V_OK ? "YES" : "NO");

    return (strlen(buf));
}

int tls_init_clientengine(int verifydepth,
			  char *var_tls_cert_file,
			  char *var_tls_key_file)
{
    int     off = 0;
    int     verify_flags = SSL_VERIFY_NONE;
    const char   *CApath;
    const char   *CAfile;
    char   *c_cert_file;
    char   *c_key_file;
    
    if (tls_clientengine)
	return (0);				/* already running */

    if (var_proxy_tls_loglevel >= 2)
	syslog(LOG_DEBUG, "starting TLS client engine");

    SSL_library_init();
    SSL_load_error_strings();
    if (tls_rand_init() == -1) {
	printf("TLS client engine: cannot seed PRNG\n");
	return -1;
    }
    
    c_ctx = SSL_CTX_new(TLSv1_client_method());
    if (c_ctx == NULL) {
	return (-1);
    };
    
    off |= SSL_OP_ALL;		/* Work around all known bugs */
    SSL_CTX_set_options(c_ctx, off);
    SSL_CTX_set_info_callback(c_ctx, (void (*)()) apps_ssl_info_callback);
    
    CAfile = config_getstring(IMAPOPT_TLS_CA_FILE);
    CApath = config_getstring(IMAPOPT_TLS_CA_PATH);

    if ((!SSL_CTX_load_verify_locations(c_ctx, CAfile, CApath)) ||
	(!SSL_CTX_set_default_verify_paths(c_ctx))) {
	/* just a warning since this is only necessary for client auth */
	syslog(LOG_NOTICE,"TLS client engine: cannot load CA data");	
    }

    if (strlen(var_tls_cert_file) == 0)
	c_cert_file = NULL;
    else
	c_cert_file = var_tls_cert_file;
    if (strlen(var_tls_key_file) == 0)
	c_key_file = NULL;
    else
	c_key_file = var_tls_key_file;
    
    if (c_cert_file || c_key_file) {
	if (!set_cert_stuff(c_ctx, c_cert_file, c_key_file)) {
	    syslog(LOG_ERR,"TLS client engine: cannot load cert/key data");
	    return (-1);
	}
    }
    SSL_CTX_set_tmp_rsa_callback(c_ctx, tmp_rsa_cb);
    
    verify_depth = verifydepth;
    SSL_CTX_set_verify(c_ctx, verify_flags, verify_callback);
    
    tls_clientengine = 1;
    return (0);
}

int tls_start_clienttls(int readfd, int writefd,
			int *layerbits, char **authid, SSL **ret,
			SSL_SESSION **sess)
{
    int     sts;
    SSL_CIPHER *cipher;
    X509   *peer;
    const char *tls_protocol = NULL;
    const char *tls_cipher_name = NULL;
    int tls_cipher_usebits = 0;
    int tls_cipher_algbits = 0;
    SSL *tls_conn;
    int r = 0;
    
    assert(tls_clientengine);
    assert(ret);
    if (var_proxy_tls_loglevel >= 1)
	syslog(LOG_DEBUG, "setting up TLS connection");

    if (authid) *authid = NULL;

    tls_conn = (SSL *) SSL_new(c_ctx);
    if (tls_conn == NULL) {
	*ret = NULL;
	r = -1;
	goto done;
    }
    SSL_clear(tls_conn);
    
    /* set the file descriptors for SSL to use */
    if ((SSL_set_rfd(tls_conn, readfd) == 0) || 
	(SSL_set_wfd(tls_conn, writefd) == 0)) {
	r = -1;
	goto done;
    }

    /*
     * This is the actual handshake routine. It will do all the negotiations
     * and will check the client cert etc.
     */
    SSL_set_connect_state(tls_conn);
    
    /*
     * We do have an SSL_set_fd() and now suddenly a BIO_ routine is called?
     * Well there is a BIO below the SSL routines that is automatically
     * created for us, so we can use it for debugging purposes.
     */
    if (var_proxy_tls_loglevel >= 3)
	BIO_set_callback(SSL_get_rbio(tls_conn), bio_dump_cb);

    /* Dump the negotiation for loglevels 3 and 4*/
    if (var_proxy_tls_loglevel >= 3)
	do_dump = 1;

    if (sess && *sess)  /* Reuse a session if we have one */
	SSL_set_session(tls_conn, *sess);

    if ((sts = SSL_connect(tls_conn)) <= 0) {
	SSL_SESSION *session = SSL_get_session(tls_conn);
	if (session) {
	    SSL_CTX_remove_session(c_ctx, session);
	}
	if (sess) *sess = NULL;
	r = -1;
	goto done;
    }
    if (sess) *sess = SSL_get_session(tls_conn);

    /* Only loglevel==4 dumps everything */
    if (var_proxy_tls_loglevel < 4)
	do_dump = 0;
    
    /*
     * Lets see, whether a peer certificate is available and what is
     * the actual information. We want to save it for later use.
     */
    peer = SSL_get_peer_certificate(tls_conn);
    if (peer != NULL) {
	char issuer_CN[CCERT_BUFSIZ];
	char peer_CN[CCERT_BUFSIZ];

	syslog(LOG_DEBUG, "received server certificate");

	X509_NAME_get_text_by_NID(X509_get_subject_name(peer),
				  NID_commonName, peer_CN, CCERT_BUFSIZ);
	X509_NAME_get_text_by_NID(X509_get_issuer_name(peer),
				  NID_commonName, issuer_CN, CCERT_BUFSIZ);
	if (var_proxy_tls_loglevel >= 3)
	    syslog(LOG_DEBUG, "subject_CN=%s, issuer_CN=%s", 
		   peer_CN, issuer_CN);

	/* xxx verify that we like the peer_issuer/issuer_CN */

	if (authid != NULL) {
	    /* save the peer id for our caller */
	    *authid = peer_CN ? xstrdup(peer_CN) : NULL;
	}
	X509_free(peer);
    }
    tls_protocol = SSL_get_version(tls_conn);
    cipher = SSL_get_current_cipher(tls_conn);
    tls_cipher_name = SSL_CIPHER_get_name(cipher);
    tls_cipher_usebits = SSL_CIPHER_get_bits(cipher, &tls_cipher_algbits);
    
    if (layerbits != NULL)
	*layerbits = tls_cipher_usebits;
    
    syslog(LOG_NOTICE, "starttls: %s with cipher %s (%d/%d bits %s)"
	   " no authentication", 
	   tls_protocol, tls_cipher_name,
	   tls_cipher_usebits, tls_cipher_algbits,
	   SSL_session_reused(tls_conn) ? "reused" : "new");

  done:
    if (r && tls_conn) {
	/* error; clean up */
	SSL_free(tls_conn);
	tls_conn = NULL;
    }
    *ret = tls_conn;
    return r;

}

#else

int tls_enabled(void)
{
    return 0;
}

#endif /* HAVE_SSL */
