Mercurial > dovecot > core-2.2
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib-ssl-iostream/iostream-openssl-context.c Mon Jan 31 18:40:27 2011 +0200 @@ -0,0 +1,450 @@ +/* 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); +}