view src/lib-ssl-iostream/iostream-openssl-context.c @ 12616:bd23d4e10fa1

Added lib-ssl-iostream for handling SSL connections more easily.
author Timo Sirainen <tss@iki.fi>
date Mon, 31 Jan 2011 18:40:27 +0200
parents
children 733ac4aba089
line wrap: on
line source

/* Copyright (c) 2009 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "safe-memset.h"
#include "iostream-openssl.h"

#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>

struct ssl_iostream_password_context {
	const char *password;
	const char *key_source;
};

static bool ssl_global_initialized = FALSE;
int dovecot_ssl_extdata_index;

static void ssl_iostream_init_global(void);

const char *ssl_iostream_error(void)
{
	unsigned long err;
	char *buf;
	size_t err_size = 256;

	err = ERR_get_error();
	if (err == 0) {
		if (errno != 0)
			return strerror(errno);
		return "Unknown error";
	}
	if (ERR_GET_REASON(err) == ERR_R_MALLOC_FAILURE)
		i_fatal_status(FATAL_OUTOFMEM, "OpenSSL malloc() failed");

	buf = t_malloc(err_size);
	buf[err_size-1] = '\0';
	ERR_error_string_n(err, buf, err_size-1);
	return buf;
}

const char *ssl_iostream_key_load_error(void)
{
       unsigned long err = ERR_peek_error();

       if (ERR_GET_LIB(err) == ERR_LIB_X509 &&
           ERR_GET_REASON(err) == X509_R_KEY_VALUES_MISMATCH)
               return "Key is for a different cert than ssl_cert";
       else
               return ssl_iostream_error();
}

static RSA *ssl_gen_rsa_key(SSL *ssl ATTR_UNUSED,
			    int is_export ATTR_UNUSED, int keylength)
{
	return RSA_generate_key(keylength, RSA_F4, NULL, NULL);
}

static DH *ssl_tmp_dh_callback(SSL *ssl ATTR_UNUSED,
			       int is_export, int keylength)
{
	struct ssl_iostream *ssl_io;

	ssl_io = SSL_get_ex_data(ssl, dovecot_ssl_extdata_index);
	/* Well, I'm not exactly sure why the logic in here is this.
	   It's the same as in Postfix, so it can't be too wrong. */
	if (is_export && keylength == 512 && ssl_io->ctx->dh_512 != NULL)
		return ssl_io->ctx->dh_512;
	else
		return ssl_io->ctx->dh_1024;
}

static int
pem_password_callback(char *buf, int size, int rwflag ATTR_UNUSED,
		      void *userdata)
{
	struct ssl_iostream_password_context *ctx = userdata;

	if (ctx->password == NULL) {
		i_error("%s: SSL private key file is password protected, "
			"but password isn't given", ctx->key_source);
		return 0;
	}

	if (i_strocpy(buf, userdata, size) < 0) {
		i_error("%s: SSL private key password is too long",
			ctx->key_source);
		return 0;
	}
	return strlen(buf);
}

int ssl_iostream_load_key(const struct ssl_iostream_settings *set,
			  const char *key_source, EVP_PKEY **pkey_r)
{
	struct ssl_iostream_password_context ctx;
	EVP_PKEY *pkey;
	BIO *bio;
	char *key;

	key = t_strdup_noconst(set->key);
	bio = BIO_new_mem_buf(key, strlen(key));
	if (bio == NULL) {
		i_error("BIO_new_mem_buf() failed: %s", ssl_iostream_error());
		safe_memset(key, 0, strlen(key));
		return -1;
	}

	ctx.password = set->key_password;
	ctx.key_source = key_source;

	pkey = PEM_read_bio_PrivateKey(bio, NULL, pem_password_callback, &ctx);
	if (pkey == NULL) {
		i_error("%s: Couldn't parse private SSL key: %s",
			key_source, ssl_iostream_error());
	}
	BIO_free(bio);

	safe_memset(key, 0, strlen(key));
	*pkey_r = pkey;
	return pkey == NULL ? -1 : 0;
}

static int
ssl_iostream_ctx_use_key(struct ssl_iostream_context *ctx,
			 const struct ssl_iostream_settings *set)
{
	EVP_PKEY *pkey;
	int ret = 0;

	if (ssl_iostream_load_key(set, ctx->source, &pkey) < 0)
		return -1;
	if (!SSL_CTX_use_PrivateKey(ctx->ssl_ctx, pkey)) {
		i_error("%s: Can't load SSL private key: %s",
			ctx->source, ssl_iostream_key_load_error());
		ret = -1;
	}
	EVP_PKEY_free(pkey);
	return ret;
}

static bool is_pem_key(const char *cert)
{
	return strstr(cert, "PRIVATE KEY---") != NULL;
}

const char *ssl_iostream_get_use_certificate_error(const char *cert)
{
	unsigned long err;

	err = ERR_peek_error();
	if (ERR_GET_LIB(err) != ERR_LIB_PEM ||
	    ERR_GET_REASON(err) != PEM_R_NO_START_LINE)
		return ssl_iostream_error();
	else if (is_pem_key(cert)) {
		return "The file contains a private key "
			"(you've mixed ssl_cert and ssl_key settings)";
	} else {
		return "There is no certificate.";
	}
}

static int ssl_ctx_use_certificate_chain(SSL_CTX *ctx, const char *cert)
{
	/* mostly just copy&pasted from SSL_CTX_use_certificate_chain_file() */
	BIO *in;
	X509 *x;
	int ret = 0;

	in = BIO_new_mem_buf(t_strdup_noconst(cert), strlen(cert));
	if (in == NULL)
		i_fatal("BIO_new_mem_buf() failed");

	x = PEM_read_bio_X509(in, NULL, NULL, NULL);
	if (x == NULL)
		goto end;

	ret = SSL_CTX_use_certificate(ctx, x);
	if (ERR_peek_error() != 0)
		ret = 0;

	if (ret != 0) {
		/* If we could set up our certificate, now proceed to
		 * the CA certificates.
		 */
		X509 *ca;
		int r;
		unsigned long err;
		
		while ((ca = PEM_read_bio_X509(in,NULL,NULL,NULL)) != NULL) {
			r = SSL_CTX_add_extra_chain_cert(ctx, ca);
			if (!r) {
				X509_free(ca);
				ret = 0;
				goto end;
			}
		}
		/* When the while loop ends, it's usually just EOF. */
		err = ERR_peek_last_error();
		if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE)
			ERR_clear_error();
		else 
			ret = 0; /* some real error */
		}

end:
	if (x != NULL) X509_free(x);
	BIO_free(in);
	return ret;
}

static int load_ca(X509_STORE *store, const char *ca,
		   STACK_OF(X509_NAME) **xnames_r)
{
	/* mostly just copy&pasted from X509_load_cert_crl_file() */
	STACK_OF(X509_INFO) *inf;
	STACK_OF(X509_NAME) *xnames;
	X509_INFO *itmp;
	X509_NAME *xname;
	BIO *bio;
	int i;

	bio = BIO_new_mem_buf(t_strdup_noconst(ca), strlen(ca));
	if (bio == NULL)
		i_fatal("BIO_new_mem_buf() failed");
	inf = PEM_X509_INFO_read_bio(bio, NULL, NULL, NULL);
	BIO_free(bio);

	if (inf == NULL)
		return -1;

	xnames = sk_X509_NAME_new_null();
	if (xnames == NULL)
		i_fatal("sk_X509_NAME_new_null() failed");
	for(i = 0; i < sk_X509_INFO_num(inf); i++) {
		itmp = sk_X509_INFO_value(inf, i);
		if(itmp->x509) {
			X509_STORE_add_cert(store, itmp->x509);
			xname = X509_get_subject_name(itmp->x509);
			if (xname != NULL)
				xname = X509_NAME_dup(xname);
			if (xname != NULL)
				sk_X509_NAME_push(xnames, xname);
		}
		if(itmp->crl)
			X509_STORE_add_crl(store, itmp->crl);
	}
	sk_X509_INFO_pop_free(inf, X509_INFO_free);
	*xnames_r = xnames;
	return 0;
}

static int
ssl_iostream_ctx_verify_remote_cert(struct ssl_iostream_context *ctx,
				    STACK_OF(X509_NAME) *ca_names)
{
#if OPENSSL_VERSION_NUMBER >= 0x00907000L
	X509_STORE *store;

	store = SSL_CTX_get_cert_store(ctx->ssl_ctx);
	X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK |
			     X509_V_FLAG_CRL_CHECK_ALL);
#endif

	SSL_CTX_set_client_CA_list(ctx->ssl_ctx, ca_names);
	return 0;
}

static struct ssl_iostream_settings *
ssl_iostream_settings_dup(pool_t pool,
			  const struct ssl_iostream_settings *old_set)
{
	struct ssl_iostream_settings *new_set;

	new_set = p_new(pool, struct ssl_iostream_settings, 1);
	new_set->cipher_list = p_strdup(pool, old_set->cipher_list);
	new_set->cert = p_strdup(pool, old_set->cert);
	new_set->key = p_strdup(pool, old_set->key);
	new_set->key_password = p_strdup(pool, old_set->key_password);

	new_set->verbose = old_set->verbose;
	return new_set;
}

static int
ssl_iostream_context_set(struct ssl_iostream_context *ctx,
			 const struct ssl_iostream_settings *set)
{
	X509_STORE *store;
	STACK_OF(X509_NAME) *xnames = NULL;

	ctx->set = ssl_iostream_settings_dup(ctx->pool, set);
	if (set->cipher_list != NULL &&
	    !SSL_CTX_set_cipher_list(ctx->ssl_ctx, set->cipher_list)) {
		i_error("%s: Can't set cipher list to '%s': %s",
			ctx->source, set->cipher_list,
			ssl_iostream_error());
		return -1;
	}

	if (set->cert != NULL &&
	    ssl_ctx_use_certificate_chain(ctx->ssl_ctx, set->cert) < 0) {
		i_error("%s: Can't load SSL certificate: %s", ctx->source,
			ssl_iostream_get_use_certificate_error(set->cert));
	}
	if (set->key != NULL) {
		if (ssl_iostream_ctx_use_key(ctx, set) < 0)
			return -1;
	}

	/* set trusted CA certs */
	if (!set->verify_remote_cert) {
		/* no CA */
	} else if (set->ca != NULL) {
		store = SSL_CTX_get_cert_store(ctx->ssl_ctx);
		if (load_ca(store, set->ca, &xnames) < 0) {
			i_error("%s: Couldn't parse ssl_ca: %s", ctx->source,
				ssl_iostream_error());
			return -1;
		}
		if (ssl_iostream_ctx_verify_remote_cert(ctx, xnames) < 0)
			return -1;
	} else if (set->ca_dir != NULL) {
		if (!SSL_CTX_load_verify_locations(ctx->ssl_ctx, NULL,
						   set->ca_dir)) {
			i_error("%s: Can't load CA certs from directory %s: %s",
				ctx->source, set->ca_dir, ssl_iostream_error());
			return -1;
		}
	} else {
		i_error("%s: Can't verify remote certs without CA",
			ctx->source);
		return -1;
	}

	if (set->cert_username_field != NULL) {
		ctx->username_nid = OBJ_txt2nid(set->cert_username_field);
		if (ctx->username_nid == NID_undef) {
			i_error("%s: Invalid cert_username_field: %s",
				ctx->source, set->cert_username_field);
		}
	}
	return 0;
}

static int
ssl_iostream_context_init_common(struct ssl_iostream_context *ctx,
				 const char *source,
				 const struct ssl_iostream_settings *set)
{
	ctx->pool = pool_alloconly_create("ssl iostream context", 4096);
	ctx->source = p_strdup(ctx->pool, source);

	SSL_CTX_set_options(ctx->ssl_ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2);
	if (SSL_CTX_need_tmp_RSA(ctx->ssl_ctx))
		SSL_CTX_set_tmp_rsa_callback(ctx->ssl_ctx, ssl_gen_rsa_key);
	SSL_CTX_set_tmp_dh_callback(ctx->ssl_ctx, ssl_tmp_dh_callback);

	return ssl_iostream_context_set(ctx, set);
}

int ssl_iostream_context_init_client(const char *source,
				     const struct ssl_iostream_settings *set,
				     struct ssl_iostream_context **ctx_r)
{
	struct ssl_iostream_context *ctx;
	SSL_CTX *ssl_ctx;

	ssl_iostream_init_global();
	if ((ssl_ctx = SSL_CTX_new(SSLv23_client_method())) == NULL) {
		i_error("SSL_CTX_new() failed: %s", ssl_iostream_error());
		return -1;
	}

	ctx = i_new(struct ssl_iostream_context, 1);
	ctx->ssl_ctx = ssl_ctx;
	ctx->client_ctx = TRUE;
	if (ssl_iostream_context_init_common(ctx, source, set) < 0) {
		ssl_iostream_context_deinit(&ctx);
		return -1;
	}
	*ctx_r = ctx;
	return 0;
}

int ssl_iostream_context_init_server(const char *source,
				     const struct ssl_iostream_settings *set,
				     struct ssl_iostream_context **ctx_r)
{
	struct ssl_iostream_context *ctx;
	SSL_CTX *ssl_ctx;

	ssl_iostream_init_global();
	if ((ssl_ctx = SSL_CTX_new(SSLv23_server_method())) == NULL) {
		i_error("SSL_CTX_new() failed: %s", ssl_iostream_error());
		return -1;
	}

	ctx = i_new(struct ssl_iostream_context, 1);
	ctx->ssl_ctx = ssl_ctx;
	if (ssl_iostream_context_init_common(ctx, source, set) < 0) {
		ssl_iostream_context_deinit(&ctx);
		return -1;
	}
	*ctx_r = ctx;
	return 0;
}

void ssl_iostream_context_deinit(struct ssl_iostream_context **_ctx)
{
	struct ssl_iostream_context *ctx = *_ctx;

	*_ctx = NULL;
	SSL_CTX_free(ctx->ssl_ctx);
	ssl_iostream_context_free_params(ctx);
	pool_unref(&ctx->pool);
	i_free(ctx);
}

static void ssl_iostream_deinit_global(void)
{
	EVP_cleanup();
	ERR_free_strings();
}

static void ssl_iostream_init_global(void)
{
	static char dovecot[] = "dovecot";
	unsigned char buf;

	if (ssl_global_initialized)
		return;

	atexit(ssl_iostream_deinit_global);
	ssl_global_initialized = TRUE;
	SSL_library_init();
	SSL_load_error_strings();

	dovecot_ssl_extdata_index =
		SSL_get_ex_new_index(0, dovecot, NULL, NULL, NULL);

	/* PRNG initialization might want to use /dev/urandom, make sure it
	   does it before chrooting. We might not have enough entropy at
	   the first try, so this function may fail. It's still been
	   initialized though. */
	(void)RAND_bytes(&buf, 1);
}