summaryrefslogtreecommitdiffstats
path: root/src/lib-ssl-iostream/iostream-openssl.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-ssl-iostream/iostream-openssl.c')
-rw-r--r--src/lib-ssl-iostream/iostream-openssl.c946
1 files changed, 946 insertions, 0 deletions
diff --git a/src/lib-ssl-iostream/iostream-openssl.c b/src/lib-ssl-iostream/iostream-openssl.c
new file mode 100644
index 0000000..6920c53
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-openssl.c
@@ -0,0 +1,946 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "ostream-private.h"
+#include "iostream-openssl.h"
+
+#include <openssl/rand.h>
+#include <openssl/err.h>
+
+static void openssl_iostream_free(struct ssl_iostream *ssl_io);
+
+void openssl_iostream_set_error(struct ssl_iostream *ssl_io, const char *str)
+{
+ char *new_str;
+
+ /* i_debug() may sometimes be overriden, making it write to this very
+ same SSL stream, in which case the provided str may be invalidated
+ before it is even used. Therefore, we duplicate it immediately. */
+ new_str = i_strdup(str);
+
+ if (ssl_io->verbose) {
+ /* This error should normally be logged by lib-ssl-iostream's
+ caller. But if verbose=TRUE, log it here as well to make
+ sure that the error is always logged. */
+ i_debug("%sSSL error: %s", ssl_io->log_prefix, new_str);
+ }
+ i_free(ssl_io->last_error);
+ ssl_io->last_error = new_str;
+}
+
+static void openssl_info_callback(const SSL *ssl, int where, int ret)
+{
+ struct ssl_iostream *ssl_io;
+
+ ssl_io = SSL_get_ex_data(ssl, dovecot_ssl_extdata_index);
+ if ((where & SSL_CB_ALERT) != 0) {
+ switch (ret & 0xff) {
+ case SSL_AD_CLOSE_NOTIFY:
+ i_debug("%sSSL alert: %s",
+ ssl_io->log_prefix,
+ SSL_alert_desc_string_long(ret));
+ break;
+ default:
+ i_debug("%sSSL alert: where=0x%x, ret=%d: %s %s",
+ ssl_io->log_prefix, where, ret,
+ SSL_alert_type_string_long(ret),
+ SSL_alert_desc_string_long(ret));
+ break;
+ }
+ } else if (ret == 0) {
+ i_debug("%sSSL failed: where=0x%x: %s",
+ ssl_io->log_prefix, where, SSL_state_string_long(ssl));
+ } else {
+ i_debug("%sSSL: where=0x%x, ret=%d: %s",
+ ssl_io->log_prefix, where, ret,
+ SSL_state_string_long(ssl));
+ }
+}
+
+static int
+openssl_iostream_use_certificate(struct ssl_iostream *ssl_io, const char *cert,
+ const char **error_r)
+{
+ BIO *in;
+ X509 *x;
+ int ret = 0;
+
+ in = BIO_new_mem_buf(t_strdup_noconst(cert), strlen(cert));
+ if (in == NULL) {
+ *error_r = t_strdup_printf("BIO_new_mem_buf() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+
+ x = PEM_read_bio_X509(in, NULL, NULL, NULL);
+ if (x != NULL) {
+ ret = SSL_use_certificate(ssl_io->ssl, x);
+ if (ERR_peek_error() != 0)
+ ret = 0;
+ X509_free(x);
+ }
+ BIO_free(in);
+
+ if (ret == 0) {
+ *error_r = t_strdup_printf("Can't load ssl_cert: %s",
+ openssl_iostream_use_certificate_error(cert, NULL));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+openssl_iostream_use_key(struct ssl_iostream *ssl_io, const char *set_name,
+ const struct ssl_iostream_cert *set,
+ const char **error_r)
+{
+ EVP_PKEY *pkey;
+ int ret = 0;
+
+ if (openssl_iostream_load_key(set, set_name, &pkey, error_r) < 0)
+ return -1;
+ if (SSL_use_PrivateKey(ssl_io->ssl, pkey) != 1) {
+ *error_r = t_strdup_printf(
+ "Can't load SSL private key (%s setting): %s",
+ set_name, openssl_iostream_key_load_error());
+ ret = -1;
+ }
+ EVP_PKEY_free(pkey);
+ return ret;
+}
+
+static int
+openssl_iostream_verify_client_cert(int preverify_ok, X509_STORE_CTX *ctx)
+{
+ int ssl_extidx = SSL_get_ex_data_X509_STORE_CTX_idx();
+ SSL *ssl;
+ struct ssl_iostream *ssl_io;
+ char certname[1024];
+ X509_NAME *subject;
+
+ ssl = X509_STORE_CTX_get_ex_data(ctx, ssl_extidx);
+ ssl_io = SSL_get_ex_data(ssl, dovecot_ssl_extdata_index);
+ ssl_io->cert_received = TRUE;
+
+ subject = X509_get_subject_name(X509_STORE_CTX_get_current_cert(ctx));
+ if (subject == NULL ||
+ X509_NAME_oneline(subject, certname, sizeof(certname)) == NULL)
+ certname[0] = '\0';
+ else
+ certname[sizeof(certname)-1] = '\0'; /* just in case.. */
+ if (preverify_ok == 0) {
+ openssl_iostream_set_error(ssl_io, t_strdup_printf(
+ "Received invalid SSL certificate: %s: %s (check %s)",
+ X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx)), certname,
+ ssl_io->ctx->client_ctx ?
+ "ssl_client_ca_* settings?" :
+ "ssl_ca setting?"));
+ if (ssl_io->verbose_invalid_cert)
+ i_info("%s", ssl_io->last_error);
+ } else if (ssl_io->verbose) {
+ i_info("Received valid SSL certificate: %s", certname);
+ }
+ if (preverify_ok == 0) {
+ ssl_io->cert_broken = TRUE;
+ if (!ssl_io->allow_invalid_cert) {
+ ssl_io->handshake_failed = TRUE;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int
+openssl_iostream_set(struct ssl_iostream *ssl_io,
+ const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ const struct ssl_iostream_settings *ctx_set = &ssl_io->ctx->set;
+ int verify_flags;
+
+ if (set->verbose)
+ SSL_set_info_callback(ssl_io->ssl, openssl_info_callback);
+
+ if (set->cipher_list != NULL &&
+ strcmp(ctx_set->cipher_list, set->cipher_list) != 0) {
+ if (SSL_set_cipher_list(ssl_io->ssl, set->cipher_list) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't set cipher list to '%s': %s",
+ set->cipher_list, openssl_iostream_error());
+ return -1;
+ }
+ }
+#ifdef HAVE_SSL_CTX_SET1_CURVES_LIST
+ if (set->curve_list != NULL && strlen(set->curve_list) > 0 &&
+ (ctx_set->curve_list == NULL || strcmp(ctx_set->curve_list, set->curve_list) != 0)) {
+ if (SSL_set1_curves_list(ssl_io->ssl, set->curve_list) == 0) {
+ *error_r = t_strdup_printf(
+ "Failed to set curve list to '%s'",
+ set->curve_list);
+ return -1;
+ }
+ }
+#endif
+#ifdef HAVE_SSL_CTX_SET_CIPHERSUITES
+ if (set->ciphersuites != NULL &&
+ strcmp(ctx_set->ciphersuites, set->ciphersuites) != 0) {
+ if (SSL_set_ciphersuites(ssl_io->ssl, set->ciphersuites) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't set ciphersuites to '%s': %s",
+ set->ciphersuites, openssl_iostream_error());
+ return -1;
+ }
+ }
+#endif
+ if (set->prefer_server_ciphers)
+ SSL_set_options(ssl_io->ssl, SSL_OP_CIPHER_SERVER_PREFERENCE);
+ if (set->min_protocol != NULL) {
+#if defined(HAVE_SSL_CLEAR_OPTIONS)
+ SSL_clear_options(ssl_io->ssl, OPENSSL_ALL_PROTOCOL_OPTIONS);
+#endif
+ long opts;
+ int min_protocol;
+ if (openssl_min_protocol_to_options(set->min_protocol, &opts,
+ &min_protocol) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown ssl_min_protocol setting '%s'",
+ set->min_protocol);
+ return -1;
+ }
+#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
+ SSL_set_min_proto_version(ssl_io->ssl, min_protocol);
+#else
+ SSL_set_options(ssl_io->ssl, opts);
+#endif
+ }
+
+ if (set->cert.cert != NULL && strcmp(ctx_set->cert.cert, set->cert.cert) != 0) {
+ if (openssl_iostream_use_certificate(ssl_io, set->cert.cert, error_r) < 0)
+ return -1;
+ }
+ if (set->cert.key != NULL && strcmp(ctx_set->cert.key, set->cert.key) != 0) {
+ if (openssl_iostream_use_key(ssl_io, "ssl_key", &set->cert, error_r) < 0)
+ return -1;
+ }
+ if (set->alt_cert.cert != NULL && strcmp(ctx_set->alt_cert.cert, set->alt_cert.cert) != 0) {
+ if (openssl_iostream_use_certificate(ssl_io, set->alt_cert.cert, error_r) < 0)
+ return -1;
+ }
+ if (set->alt_cert.key != NULL && strcmp(ctx_set->alt_cert.key, set->alt_cert.key) != 0) {
+ if (openssl_iostream_use_key(ssl_io, "ssl_alt_key", &set->alt_cert, error_r) < 0)
+ return -1;
+ }
+ if (set->verify_remote_cert) {
+ if (ssl_io->ctx->client_ctx)
+ verify_flags = SSL_VERIFY_NONE;
+ else
+ verify_flags = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;
+ SSL_set_verify(ssl_io->ssl, verify_flags,
+ openssl_iostream_verify_client_cert);
+ }
+
+ if (set->cert_username_field != NULL) {
+ ssl_io->username_nid = OBJ_txt2nid(set->cert_username_field);
+ if (ssl_io->username_nid == NID_undef) {
+ *error_r = t_strdup_printf(
+ "Invalid cert_username_field: %s",
+ set->cert_username_field);
+ return -1;
+ }
+ } else {
+ ssl_io->username_nid = ssl_io->ctx->username_nid;
+ }
+
+ ssl_io->verbose = set->verbose;
+ ssl_io->verbose_invalid_cert = set->verbose_invalid_cert || set->verbose;
+ ssl_io->allow_invalid_cert = set->allow_invalid_cert;
+ return 0;
+}
+
+static int
+openssl_iostream_create(struct ssl_iostream_context *ctx, const char *host,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r,
+ const char **error_r)
+{
+ struct ssl_iostream *ssl_io;
+ SSL *ssl;
+ BIO *bio_int, *bio_ext;
+
+ /* Don't allow an existing io_add_istream() to be use on the input.
+ It would seem to work, but it would also cause hangs. */
+ i_assert(i_stream_get_root_io(*input)->real_stream->io == NULL);
+
+ ssl = SSL_new(ctx->ssl_ctx);
+ if (ssl == NULL) {
+ *error_r = t_strdup_printf("SSL_new() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+
+ /* BIO pairs use default buffer sizes (17 kB in OpenSSL 0.9.8e).
+ Each of the BIOs have one "write buffer". BIO_write() copies data
+ to them, while BIO_read() reads from the other BIO's write buffer
+ into the given buffer. The bio_int is used by OpenSSL and bio_ext
+ is used by this library. */
+ if (BIO_new_bio_pair(&bio_int, 0, &bio_ext, 0) != 1) {
+ *error_r = t_strdup_printf("BIO_new_bio_pair() failed: %s",
+ openssl_iostream_error());
+ SSL_free(ssl);
+ return -1;
+ }
+
+ ssl_io = i_new(struct ssl_iostream, 1);
+ ssl_io->refcount = 1;
+ ssl_io->ctx = ctx;
+ ssl_iostream_context_ref(ssl_io->ctx);
+ ssl_io->ssl = ssl;
+ ssl_io->bio_ext = bio_ext;
+ ssl_io->plain_input = *input;
+ ssl_io->plain_output = *output;
+ ssl_io->connected_host = i_strdup(host);
+ ssl_io->log_prefix = host == NULL ? i_strdup("") :
+ i_strdup_printf("%s: ", host);
+ /* bio_int will be freed by SSL_free() */
+ SSL_set_bio(ssl_io->ssl, bio_int, bio_int);
+ SSL_set_ex_data(ssl_io->ssl, dovecot_ssl_extdata_index, ssl_io);
+#ifdef HAVE_SSL_GET_SERVERNAME
+ SSL_set_tlsext_host_name(ssl_io->ssl, host);
+#endif
+
+ if (openssl_iostream_set(ssl_io, set, error_r) < 0) {
+ openssl_iostream_free(ssl_io);
+ return -1;
+ }
+
+ o_stream_uncork(ssl_io->plain_output);
+
+ *input = openssl_i_stream_create_ssl(ssl_io);
+ ssl_io->ssl_input = *input;
+
+ *output = openssl_o_stream_create_ssl(ssl_io);
+ i_stream_set_name(*input, t_strconcat("SSL ",
+ i_stream_get_name(ssl_io->plain_input), NULL));
+ o_stream_set_name(*output, t_strconcat("SSL ",
+ o_stream_get_name(ssl_io->plain_output), NULL));
+
+ if (ssl_io->plain_output->real_stream->error_handling_disabled)
+ o_stream_set_no_error_handling(*output, TRUE);
+
+ ssl_io->ssl_output = *output;
+ *iostream_r = ssl_io;
+ return 0;
+}
+
+static void openssl_iostream_free(struct ssl_iostream *ssl_io)
+{
+ ssl_iostream_context_unref(&ssl_io->ctx);
+ o_stream_unref(&ssl_io->plain_output);
+ i_stream_unref(&ssl_io->plain_input);
+ BIO_free(ssl_io->bio_ext);
+ SSL_free(ssl_io->ssl);
+ i_free(ssl_io->plain_stream_errstr);
+ i_free(ssl_io->last_error);
+ i_free(ssl_io->connected_host);
+ i_free(ssl_io->sni_host);
+ i_free(ssl_io->log_prefix);
+ i_free(ssl_io);
+}
+
+static void openssl_iostream_unref(struct ssl_iostream *ssl_io)
+{
+ i_assert(ssl_io->refcount > 0);
+ if (--ssl_io->refcount > 0)
+ return;
+
+ openssl_iostream_free(ssl_io);
+}
+
+void openssl_iostream_shutdown(struct ssl_iostream *ssl_io)
+{
+ if (ssl_io->destroyed)
+ return;
+
+ i_assert(ssl_io->ssl_input != NULL);
+ i_assert(ssl_io->ssl_output != NULL);
+
+ ssl_io->destroyed = TRUE;
+ if (ssl_io->handshaked && SSL_shutdown(ssl_io->ssl) != 1) {
+ /* if bidirectional shutdown fails we need to clear
+ the error queue */
+ openssl_iostream_clear_errors();
+ }
+ if (ssl_io->handshaked) {
+ (void)openssl_iostream_bio_sync(ssl_io,
+ OPENSSL_IOSTREAM_SYNC_TYPE_WRITE);
+ }
+ (void)o_stream_flush(ssl_io->plain_output);
+ /* close the plain i/o streams, because their fd may be closed soon,
+ but we may still keep this ssl-iostream referenced until later. */
+ i_stream_close(ssl_io->plain_input);
+ o_stream_close(ssl_io->plain_output);
+}
+
+static void openssl_iostream_destroy(struct ssl_iostream *ssl_io)
+{
+ openssl_iostream_shutdown(ssl_io);
+ ssl_iostream_unref(&ssl_io);
+}
+
+static int openssl_iostream_bio_output_real(struct ssl_iostream *ssl_io)
+{
+ size_t bytes, max_bytes = 0;
+ ssize_t sent;
+ unsigned char buffer[IO_BLOCK_SIZE];
+ int result = 0;
+ int ret;
+
+ o_stream_cork(ssl_io->plain_output);
+ while ((bytes = BIO_ctrl_pending(ssl_io->bio_ext)) > 0) {
+ /* bytes contains how many SSL encrypted bytes we should be
+ sending out */
+ max_bytes = o_stream_get_buffer_avail_size(ssl_io->plain_output);
+ if (bytes > max_bytes) {
+ if (max_bytes == 0) {
+ /* wait until output buffer clears */
+ break;
+ }
+ bytes = max_bytes;
+ }
+ if (bytes > sizeof(buffer))
+ bytes = sizeof(buffer);
+
+ /* BIO_read() is guaranteed to return all the bytes that
+ BIO_ctrl_pending() returned */
+ ret = BIO_read(ssl_io->bio_ext, buffer, bytes);
+ i_assert(ret == (int)bytes);
+
+ /* we limited number of read bytes to plain_output's
+ available size. this send() is guaranteed to either
+ fully succeed or completely fail due to some error. */
+ sent = o_stream_send(ssl_io->plain_output, buffer, bytes);
+ if (sent < 0) {
+ o_stream_uncork(ssl_io->plain_output);
+ return -1;
+ }
+ i_assert(sent == (ssize_t)bytes);
+ result = 1;
+ }
+
+ ret = o_stream_uncork_flush(ssl_io->plain_output);
+ if (ret < 0)
+ return -1;
+ if (ret == 0 || (bytes > 0 && max_bytes == 0))
+ o_stream_set_flush_pending(ssl_io->plain_output, TRUE);
+
+ return result;
+}
+
+static int openssl_iostream_bio_output(struct ssl_iostream *ssl_io)
+{
+ int ret;
+
+ ret = openssl_iostream_bio_output_real(ssl_io);
+ if (ret < 0) {
+ i_assert(ssl_io->plain_output->stream_errno != 0);
+ i_free(ssl_io->plain_stream_errstr);
+ ssl_io->plain_stream_errstr =
+ i_strdup(o_stream_get_error(ssl_io->plain_output));
+ ssl_io->plain_stream_errno =
+ ssl_io->plain_output->stream_errno;
+ ssl_io->closed = TRUE;
+ }
+ return ret;
+}
+
+static ssize_t
+openssl_iostream_read_more(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type, size_t wanted,
+ const unsigned char **data_r, size_t *size_r)
+{
+ *data_r = i_stream_get_data(ssl_io->plain_input, size_r);
+ if (*size_r > 0)
+ return 0;
+
+ if (type == OPENSSL_IOSTREAM_SYNC_TYPE_CONTINUE_READ) {
+ /* only the first i_stream_read() call attempts to read more
+ input. the following reads will just process the buffered
+ data. */
+ return 0;
+ }
+
+ if (i_stream_read_limited(ssl_io->plain_input, data_r, size_r,
+ wanted) < 0)
+ return -1;
+ return 0;
+}
+
+static int
+openssl_iostream_bio_input(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type)
+{
+ const unsigned char *data;
+ size_t bytes, size;
+ int ret;
+ bool bytes_read = FALSE;
+
+ while ((bytes = BIO_ctrl_get_write_guarantee(ssl_io->bio_ext)) > 0) {
+ /* bytes contains how many bytes we can write to bio_ext */
+ ret = openssl_iostream_read_more(ssl_io, type, bytes,
+ &data, &size);
+ if (ret == -1 && size == 0 && !bytes_read) {
+ if (ssl_io->plain_input->stream_errno != 0) {
+ i_free(ssl_io->plain_stream_errstr);
+ ssl_io->plain_stream_errstr =
+ i_strdup(i_stream_get_error(ssl_io->plain_input));
+ ssl_io->plain_stream_errno =
+ ssl_io->plain_input->stream_errno;
+ }
+ ssl_io->closed = TRUE;
+ return -1;
+ }
+ if (size == 0) {
+ /* wait for more input */
+ break;
+ }
+ if (size > bytes)
+ size = bytes;
+
+ ret = BIO_write(ssl_io->bio_ext, data, size);
+ i_assert(ret == (ssize_t)size);
+
+ i_stream_skip(ssl_io->plain_input, size);
+ bytes_read = TRUE;
+ }
+ if (bytes == 0 && !bytes_read && ssl_io->want_read) {
+ /* shouldn't happen */
+ i_error("SSL BIO buffer size too small");
+ i_free(ssl_io->plain_stream_errstr);
+ ssl_io->plain_stream_errstr =
+ i_strdup("SSL BIO buffer size too small");
+ ssl_io->plain_stream_errno = EINVAL;
+ ssl_io->closed = TRUE;
+ return -1;
+ }
+ if (bytes_read) {
+ if (ssl_io->ostream_flush_waiting_input) {
+ ssl_io->ostream_flush_waiting_input = FALSE;
+ o_stream_set_flush_pending(ssl_io->plain_output, TRUE);
+ }
+ }
+ if (bytes_read || i_stream_get_data_size(ssl_io->plain_input) > 0) {
+ if (i_stream_get_data_size(ssl_io->plain_input) > 0 ||
+ (type != OPENSSL_IOSTREAM_SYNC_TYPE_FIRST_READ &&
+ type != OPENSSL_IOSTREAM_SYNC_TYPE_CONTINUE_READ))
+ i_stream_set_input_pending(ssl_io->ssl_input, TRUE);
+ ssl_io->want_read = FALSE;
+ }
+ return (bytes_read ? 1 : 0);
+}
+
+int openssl_iostream_bio_sync(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type)
+{
+ int ret;
+
+ i_assert(type != OPENSSL_IOSTREAM_SYNC_TYPE_NONE);
+
+ ret = openssl_iostream_bio_output(ssl_io);
+ if (ret >= 0 && openssl_iostream_bio_input(ssl_io, type) > 0)
+ ret = 1;
+ return ret;
+}
+
+static void openssl_iostream_closed(struct ssl_iostream *ssl_io)
+{
+ if (ssl_io->plain_stream_errno != 0) {
+ i_assert(ssl_io->plain_stream_errstr != NULL);
+ openssl_iostream_set_error(ssl_io, ssl_io->plain_stream_errstr);
+ errno = ssl_io->plain_stream_errno;
+ } else {
+ openssl_iostream_set_error(ssl_io, "Connection closed");
+ errno = EPIPE;
+ }
+}
+
+int openssl_iostream_handle_error(struct ssl_iostream *ssl_io, int ret,
+ enum openssl_iostream_sync_type type,
+ const char *func_name)
+{
+ const char *errstr = NULL;
+ int err;
+
+ err = SSL_get_error(ssl_io->ssl, ret);
+ switch (err) {
+ case SSL_ERROR_WANT_WRITE:
+ if (type != OPENSSL_IOSTREAM_SYNC_TYPE_NONE &&
+ openssl_iostream_bio_sync(ssl_io, type) == 0) {
+ if (type != OPENSSL_IOSTREAM_SYNC_TYPE_WRITE)
+ i_panic("SSL ostream buffer size not unlimited");
+ return 0;
+ }
+ if (ssl_io->closed) {
+ openssl_iostream_closed(ssl_io);
+ return -1;
+ }
+ if (type == OPENSSL_IOSTREAM_SYNC_TYPE_NONE)
+ return 0;
+ return 1;
+ case SSL_ERROR_WANT_READ:
+ ssl_io->want_read = TRUE;
+ if (type != OPENSSL_IOSTREAM_SYNC_TYPE_NONE)
+ (void)openssl_iostream_bio_sync(ssl_io, type);
+ if (ssl_io->closed) {
+ openssl_iostream_closed(ssl_io);
+ return -1;
+ }
+ if (type == OPENSSL_IOSTREAM_SYNC_TYPE_NONE)
+ return 0;
+ return ssl_io->want_read ? 0 : 1;
+ case SSL_ERROR_SYSCALL:
+ /* eat up the error queue */
+ if (ERR_peek_error() != 0) {
+ errstr = openssl_iostream_error();
+ errno = EINVAL;
+ } else if (ret == 0) {
+ /* EOF. */
+ errno = EPIPE;
+ errstr = "Disconnected";
+ break;
+ } else if (errno != 0) {
+ errstr = strerror(errno);
+ } else {
+ /* Seen this at least with v1.1.0l SSL_accept() */
+ errstr = "OpenSSL BUG: errno=0";
+ errno = EINVAL;
+ }
+ errstr = t_strdup_printf("%s syscall failed: %s",
+ func_name, errstr);
+ break;
+ case SSL_ERROR_ZERO_RETURN:
+ /* clean connection closing */
+ errno = EPIPE;
+ if (ssl_io->handshaked)
+ i_free_and_null(ssl_io->last_error);
+ else if (ssl_io->last_error == NULL) {
+ errstr = "SSL connection closed during handshake";
+ break;
+ }
+ return -1;
+ case SSL_ERROR_SSL:
+ errstr = t_strdup_printf("%s failed: %s",
+ func_name, openssl_iostream_error());
+ errno = EINVAL;
+ break;
+ default:
+ errstr = t_strdup_printf("%s failed: unknown failure %d (%s)",
+ func_name, err,
+ openssl_iostream_error());
+ errno = EINVAL;
+ break;
+ }
+
+ openssl_iostream_set_error(ssl_io, errstr);
+ return -1;
+}
+
+static bool
+openssl_iostream_cert_match_name(struct ssl_iostream *ssl_io,
+ const char *verify_name, const char **reason_r)
+{
+ if (!ssl_iostream_has_valid_client_cert(ssl_io)) {
+ *reason_r = "Invalid certificate";
+ return FALSE;
+ }
+
+ return openssl_cert_match_name(ssl_io->ssl, verify_name, reason_r);
+}
+
+static int openssl_iostream_handshake(struct ssl_iostream *ssl_io)
+{
+ const char *reason, *error = NULL;
+ int ret;
+
+ i_assert(!ssl_io->handshaked);
+
+ /* we are being destroyed, so do not do any more handshaking */
+ if (ssl_io->destroyed)
+ return 0;
+
+ if (ssl_io->ctx->client_ctx) {
+ while ((ret = SSL_connect(ssl_io->ssl)) <= 0) {
+ ret = openssl_iostream_handle_error(ssl_io, ret,
+ OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE, "SSL_connect()");
+ if (ret <= 0)
+ return ret;
+ }
+ } else {
+ while ((ret = SSL_accept(ssl_io->ssl)) <= 0) {
+ ret = openssl_iostream_handle_error(ssl_io, ret,
+ OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE, "SSL_accept()");
+ if (ret <= 0)
+ return ret;
+ }
+ }
+ /* handshake finished */
+ (void)openssl_iostream_bio_sync(ssl_io, OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE);
+
+ if (ssl_io->handshake_callback != NULL) {
+ if (ssl_io->handshake_callback(&error, ssl_io->handshake_context) < 0) {
+ i_assert(error != NULL);
+ openssl_iostream_set_error(ssl_io, error);
+ ssl_io->handshake_failed = TRUE;
+ }
+ } else if (ssl_io->connected_host != NULL && !ssl_io->handshake_failed &&
+ !ssl_io->allow_invalid_cert) {
+ if (ssl_iostream_check_cert_validity(ssl_io, ssl_io->connected_host, &reason) < 0) {
+ openssl_iostream_set_error(ssl_io, reason);
+ ssl_io->handshake_failed = TRUE;
+ }
+ }
+ if (ssl_io->handshake_failed) {
+ i_stream_close(ssl_io->plain_input);
+ o_stream_close(ssl_io->plain_output);
+ errno = EINVAL;
+ return -1;
+ }
+ i_free_and_null(ssl_io->last_error);
+ ssl_io->handshaked = TRUE;
+
+ if (ssl_io->ssl_output != NULL)
+ (void)o_stream_flush(ssl_io->ssl_output);
+ return 1;
+}
+
+static void
+openssl_iostream_set_handshake_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_handshake_callback_t *callback,
+ void *context)
+{
+ ssl_io->handshake_callback = callback;
+ ssl_io->handshake_context = context;
+}
+
+static void
+openssl_iostream_set_sni_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_sni_callback_t *callback,
+ void *context)
+{
+ ssl_io->sni_callback = callback;
+ ssl_io->sni_context = context;
+}
+
+static void
+openssl_iostream_change_context(struct ssl_iostream *ssl_io,
+ struct ssl_iostream_context *ctx)
+{
+ if (ctx != ssl_io->ctx) {
+ SSL_set_SSL_CTX(ssl_io->ssl, ctx->ssl_ctx);
+ ssl_iostream_context_ref(ctx);
+ ssl_iostream_context_unref(&ssl_io->ctx);
+ ssl_io->ctx = ctx;
+ }
+}
+
+static void openssl_iostream_set_log_prefix(struct ssl_iostream *ssl_io,
+ const char *prefix)
+{
+ i_free(ssl_io->log_prefix);
+ ssl_io->log_prefix = i_strdup(prefix);
+}
+
+static bool openssl_iostream_is_handshaked(const struct ssl_iostream *ssl_io)
+{
+ return ssl_io->handshaked;
+}
+
+static bool
+openssl_iostream_has_handshake_failed(const struct ssl_iostream *ssl_io)
+{
+ return ssl_io->handshake_failed;
+}
+
+static bool
+openssl_iostream_has_valid_client_cert(const struct ssl_iostream *ssl_io)
+{
+ return ssl_io->cert_received && !ssl_io->cert_broken;
+}
+
+static bool
+openssl_iostream_has_broken_client_cert(struct ssl_iostream *ssl_io)
+{
+ return ssl_io->cert_received && ssl_io->cert_broken;
+}
+
+static const char *
+openssl_iostream_get_peer_name(struct ssl_iostream *ssl_io)
+{
+ X509 *x509;
+ char *name;
+ int len;
+
+ if (!ssl_iostream_has_valid_client_cert(ssl_io))
+ return NULL;
+
+ x509 = SSL_get_peer_certificate(ssl_io->ssl);
+ i_assert(x509 != NULL);
+
+ len = X509_NAME_get_text_by_NID(X509_get_subject_name(x509),
+ ssl_io->username_nid, NULL, 0);
+ if (len < 0)
+ name = "";
+ else {
+ name = t_malloc0(len + 1);
+ if (X509_NAME_get_text_by_NID(X509_get_subject_name(x509),
+ ssl_io->username_nid,
+ name, len + 1) < 0)
+ name = "";
+ else if (strlen(name) != (size_t)len) {
+ /* NUL characters in name. Someone's trying to fake
+ being another user? Don't allow it. */
+ name = "";
+ }
+ }
+ X509_free(x509);
+
+ return *name == '\0' ? NULL : name;
+}
+
+static const char *openssl_iostream_get_server_name(struct ssl_iostream *ssl_io)
+{
+ return ssl_io->sni_host;
+}
+
+static const char *
+openssl_iostream_get_compression(struct ssl_iostream *ssl_io)
+{
+#if defined(HAVE_SSL_COMPRESSION) && !defined(OPENSSL_NO_COMP)
+ const COMP_METHOD *comp;
+
+ comp = SSL_get_current_compression(ssl_io->ssl);
+ return comp == NULL ? NULL : SSL_COMP_get_name(comp);
+#else
+ return NULL;
+#endif
+}
+
+static const char *
+openssl_iostream_get_security_string(struct ssl_iostream *ssl_io)
+{
+ const SSL_CIPHER *cipher;
+#if defined(HAVE_SSL_COMPRESSION) && !defined(OPENSSL_NO_COMP)
+ const COMP_METHOD *comp;
+#endif
+ const char *comp_str;
+ int bits, alg_bits;
+
+ if (!ssl_io->handshaked)
+ return "";
+
+ cipher = SSL_get_current_cipher(ssl_io->ssl);
+ bits = SSL_CIPHER_get_bits(cipher, &alg_bits);
+#if defined(HAVE_SSL_COMPRESSION) && !defined(OPENSSL_NO_COMP)
+ comp = SSL_get_current_compression(ssl_io->ssl);
+ comp_str = comp == NULL ? "" :
+ t_strconcat(" ", SSL_COMP_get_name(comp), NULL);
+#else
+ comp_str = "";
+#endif
+ return t_strdup_printf("%s with cipher %s (%d/%d bits)%s",
+ SSL_get_version(ssl_io->ssl),
+ SSL_CIPHER_get_name(cipher),
+ bits, alg_bits, comp_str);
+}
+
+static const char *
+openssl_iostream_get_last_error(struct ssl_iostream *ssl_io)
+{
+ return ssl_io->last_error;
+}
+
+static const char *
+openssl_iostream_get_cipher(struct ssl_iostream *ssl_io, unsigned int *bits_r)
+{
+ if (!ssl_io->handshaked)
+ return NULL;
+
+ const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl_io->ssl);
+ *bits_r = SSL_CIPHER_get_bits(cipher, NULL);
+ return SSL_CIPHER_get_name(cipher);
+}
+
+static const char *
+openssl_iostream_get_pfs(struct ssl_iostream *ssl_io)
+{
+ if (!ssl_io->handshaked)
+ return NULL;
+
+ const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl_io->ssl);
+#if defined(HAVE_SSL_CIPHER_get_kx_nid)
+ int nid = SSL_CIPHER_get_kx_nid(cipher);
+ return OBJ_nid2sn(nid);
+#else
+ char buf[128];
+ const char *desc, *ptr;
+ if ((desc = SSL_CIPHER_description(cipher, buf, sizeof(buf)))==NULL ||
+ (ptr = strstr(desc, "Kx=")) == NULL)
+ return "";
+ return t_strcut(ptr+3, ' ');
+#endif
+}
+
+static const char *
+openssl_iostream_get_protocol_name(struct ssl_iostream *ssl_io)
+{
+ if (!ssl_io->handshaked)
+ return NULL;
+ return SSL_get_version(ssl_io->ssl);
+}
+
+
+static const struct iostream_ssl_vfuncs ssl_vfuncs = {
+ .global_init = openssl_iostream_global_init,
+ .context_init_client = openssl_iostream_context_init_client,
+ .context_init_server = openssl_iostream_context_init_server,
+ .context_ref = openssl_iostream_context_ref,
+ .context_unref = openssl_iostream_context_unref,
+
+ .create = openssl_iostream_create,
+ .unref = openssl_iostream_unref,
+ .destroy = openssl_iostream_destroy,
+
+ .handshake = openssl_iostream_handshake,
+ .set_handshake_callback = openssl_iostream_set_handshake_callback,
+ .set_sni_callback = openssl_iostream_set_sni_callback,
+ .change_context = openssl_iostream_change_context,
+
+ .set_log_prefix = openssl_iostream_set_log_prefix,
+ .is_handshaked = openssl_iostream_is_handshaked,
+ .has_handshake_failed = openssl_iostream_has_handshake_failed,
+ .has_valid_client_cert = openssl_iostream_has_valid_client_cert,
+ .has_broken_client_cert = openssl_iostream_has_broken_client_cert,
+ .cert_match_name = openssl_iostream_cert_match_name,
+ .get_peer_name = openssl_iostream_get_peer_name,
+ .get_server_name = openssl_iostream_get_server_name,
+ .get_compression = openssl_iostream_get_compression,
+ .get_security_string = openssl_iostream_get_security_string,
+ .get_last_error = openssl_iostream_get_last_error,
+ .get_cipher = openssl_iostream_get_cipher,
+ .get_pfs = openssl_iostream_get_pfs,
+ .get_protocol_name = openssl_iostream_get_protocol_name,
+};
+
+void ssl_iostream_openssl_init(void)
+{
+ unsigned char buf;
+ if (RAND_bytes(&buf, 1) < 1)
+ i_fatal("OpenSSL RNG failed to initialize");
+ iostream_ssl_module_init(&ssl_vfuncs);
+}
+
+void ssl_iostream_openssl_deinit(void)
+{
+ openssl_iostream_global_deinit();
+}