summaryrefslogtreecommitdiffstats
path: root/src/libserver/ssl_util.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libserver/ssl_util.c')
-rw-r--r--src/libserver/ssl_util.c1133
1 files changed, 1133 insertions, 0 deletions
diff --git a/src/libserver/ssl_util.c b/src/libserver/ssl_util.c
new file mode 100644
index 0000000..8ee53b0
--- /dev/null
+++ b/src/libserver/ssl_util.c
@@ -0,0 +1,1133 @@
+/*-
+ * Copyright 2016 Vsevolod Stakhov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "config.h"
+#include "libutil/util.h"
+#include "libutil/hash.h"
+#include "libserver/logger.h"
+#include "libserver/cfg_file.h"
+#include "ssl_util.h"
+#include "unix-std.h"
+#include "cryptobox.h"
+#include "contrib/libottery/ottery.h"
+
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/conf.h>
+#include <openssl/evp.h>
+#include <openssl/engine.h>
+#include <openssl/x509v3.h>
+
+enum rspamd_ssl_state {
+ ssl_conn_reset = 0,
+ ssl_conn_init,
+ ssl_conn_connected,
+ ssl_next_read,
+ ssl_next_write,
+ ssl_next_shutdown,
+};
+
+enum rspamd_ssl_shutdown {
+ ssl_shut_default = 0,
+ ssl_shut_unclean,
+};
+
+struct rspamd_ssl_ctx {
+ SSL_CTX *s;
+ rspamd_lru_hash_t *sessions;
+};
+
+struct rspamd_ssl_connection {
+ gint fd;
+ enum rspamd_ssl_state state;
+ enum rspamd_ssl_shutdown shut;
+ gboolean verify_peer;
+ SSL *ssl;
+ struct rspamd_ssl_ctx *ssl_ctx;
+ gchar *hostname;
+ struct rspamd_io_ev *ev;
+ struct rspamd_io_ev *shut_ev;
+ struct ev_loop *event_loop;
+ rspamd_ssl_handler_t handler;
+ rspamd_ssl_error_handler_t err_handler;
+ gpointer handler_data;
+ gchar log_tag[8];
+};
+
+#define msg_debug_ssl(...) rspamd_conditional_debug_fast(NULL, NULL, \
+ rspamd_ssl_log_id, "ssl", conn->log_tag, \
+ RSPAMD_LOG_FUNC, \
+ __VA_ARGS__)
+
+static void rspamd_ssl_event_handler(gint fd, short what, gpointer ud);
+
+INIT_LOG_MODULE(ssl)
+
+static GQuark
+rspamd_ssl_quark(void)
+{
+ return g_quark_from_static_string("rspamd-ssl");
+}
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) && !defined(LIBRESSL_VERSION_NUMBER)
+#ifndef X509_get_notBefore
+#define X509_get_notBefore(x) X509_get0_notBefore(x)
+#endif
+#ifndef X509_get_notAfter
+#define X509_get_notAfter(x) X509_get0_notAfter(x)
+#endif
+#ifndef ASN1_STRING_data
+#define ASN1_STRING_data(x) ASN1_STRING_get0_data(x)
+#endif
+#endif
+
+/* $OpenBSD: tls_verify.c,v 1.14 2015/09/29 10:17:04 deraadt Exp $ */
+/*
+ * Copyright (c) 2014 Jeremie Courreges-Anglas <jca@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, 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.
+ */
+
+static gboolean
+rspamd_tls_match_name(const char *cert_name, const char *name)
+{
+ const char *cert_domain, *domain, *next_dot;
+
+ if (g_ascii_strcasecmp(cert_name, name) == 0) {
+ return TRUE;
+ }
+
+ /* Wildcard match? */
+ if (cert_name[0] == '*') {
+ /*
+ * Valid wildcards:
+ * - "*.domain.tld"
+ * - "*.sub.domain.tld"
+ * - etc.
+ * Reject "*.tld".
+ * No attempt to prevent the use of eg. "*.co.uk".
+ */
+ cert_domain = &cert_name[1];
+ /* Disallow "*" */
+ if (cert_domain[0] == '\0') {
+ return FALSE;
+ }
+
+ /* Disallow "*foo" */
+ if (cert_domain[0] != '.') {
+ return FALSE;
+ }
+ /* Disallow "*.." */
+ if (cert_domain[1] == '.') {
+ return FALSE;
+ }
+ next_dot = strchr(&cert_domain[1], '.');
+ /* Disallow "*.bar" */
+ if (next_dot == NULL) {
+ return FALSE;
+ }
+ /* Disallow "*.bar.." */
+ if (next_dot[1] == '.') {
+ return FALSE;
+ }
+
+ domain = strchr(name, '.');
+
+ /* No wildcard match against a name with no host part. */
+ if (name[0] == '.') {
+ return FALSE;
+ }
+ /* No wildcard match against a name with no domain part. */
+ if (domain == NULL || strlen(domain) == 1) {
+ return FALSE;
+ }
+
+ if (g_ascii_strcasecmp(cert_domain, domain) == 0) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* See RFC 5280 section 4.2.1.6 for SubjectAltName details. */
+static gboolean
+rspamd_tls_check_subject_altname(X509 *cert, const char *name)
+{
+ STACK_OF(GENERAL_NAME) *altname_stack = NULL;
+ int addrlen, type;
+ int count, i;
+ union {
+ struct in_addr ip4;
+ struct in6_addr ip6;
+ } addrbuf;
+ gboolean ret = FALSE;
+
+ altname_stack = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
+
+ if (altname_stack == NULL) {
+ return FALSE;
+ }
+
+ if (inet_pton(AF_INET, name, &addrbuf) == 1) {
+ type = GEN_IPADD;
+ addrlen = 4;
+ }
+ else if (inet_pton(AF_INET6, name, &addrbuf) == 1) {
+ type = GEN_IPADD;
+ addrlen = 16;
+ }
+ else {
+ type = GEN_DNS;
+ addrlen = 0;
+ }
+
+ count = sk_GENERAL_NAME_num(altname_stack);
+
+ for (i = 0; i < count; i++) {
+ GENERAL_NAME *altname;
+
+ altname = sk_GENERAL_NAME_value(altname_stack, i);
+
+ if (altname->type != type) {
+ continue;
+ }
+
+ if (type == GEN_DNS) {
+ const char *data;
+ int format, len;
+
+ format = ASN1_STRING_type(altname->d.dNSName);
+
+ if (format == V_ASN1_IA5STRING) {
+ data = (const char *) ASN1_STRING_data(altname->d.dNSName);
+ len = ASN1_STRING_length(altname->d.dNSName);
+
+ if (len < 0 || len != (gint) strlen(data)) {
+ ret = FALSE;
+ break;
+ }
+
+ /*
+ * Per RFC 5280 section 4.2.1.6:
+ * " " is a legal domain name, but that
+ * dNSName must be rejected.
+ */
+ if (strcmp(data, " ") == 0) {
+ ret = FALSE;
+ break;
+ }
+
+ if (rspamd_tls_match_name(data, name)) {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+ else if (type == GEN_IPADD) {
+ const char *data;
+ int datalen;
+
+ datalen = ASN1_STRING_length(altname->d.iPAddress);
+ data = (const char *) ASN1_STRING_data(altname->d.iPAddress);
+
+ if (datalen < 0) {
+ ret = FALSE;
+ break;
+ }
+
+ /*
+ * Per RFC 5280 section 4.2.1.6:
+ * IPv4 must use 4 octets and IPv6 must use 16 octets.
+ */
+ if (datalen == addrlen && memcmp(data, &addrbuf, addrlen) == 0) {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+
+ sk_GENERAL_NAME_pop_free(altname_stack, GENERAL_NAME_free);
+ return ret;
+}
+
+static gboolean
+rspamd_tls_check_common_name(X509 *cert, const char *name)
+{
+ X509_NAME *subject_name;
+ char *common_name = NULL;
+ union {
+ struct in_addr ip4;
+ struct in6_addr ip6;
+ } addrbuf;
+ int common_name_len;
+ gboolean ret = FALSE;
+
+ subject_name = X509_get_subject_name(cert);
+ if (subject_name == NULL) {
+ goto out;
+ }
+
+ common_name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, NULL, 0);
+
+ if (common_name_len < 0) {
+ goto out;
+ }
+
+ common_name = g_malloc0(common_name_len + 1);
+ X509_NAME_get_text_by_NID(subject_name, NID_commonName, common_name,
+ common_name_len + 1);
+
+ /* NUL bytes in CN? */
+ if (common_name_len != (gint) strlen(common_name)) {
+ goto out;
+ }
+
+ if (inet_pton(AF_INET, name, &addrbuf) == 1 || inet_pton(AF_INET6, name, &addrbuf) == 1) {
+ /*
+ * We don't want to attempt wildcard matching against IP
+ * addresses, so perform a simple comparison here.
+ */
+ if (strcmp(common_name, name) == 0) {
+ ret = TRUE;
+ }
+ else {
+ ret = FALSE;
+ }
+
+ goto out;
+ }
+
+ if (rspamd_tls_match_name(common_name, name)) {
+ ret = TRUE;
+ }
+
+out:
+ g_free(common_name);
+
+ return ret;
+}
+
+static gboolean
+rspamd_tls_check_name(X509 *cert, const char *name)
+{
+ gboolean ret;
+
+ ret = rspamd_tls_check_subject_altname(cert, name);
+ if (ret) {
+ return ret;
+ }
+
+ return rspamd_tls_check_common_name(cert, name);
+}
+
+static gboolean
+rspamd_ssl_peer_verify(struct rspamd_ssl_connection *c)
+{
+ X509 *server_cert;
+ glong ver_err;
+ GError *err = NULL;
+
+ ver_err = SSL_get_verify_result(c->ssl);
+
+ if (ver_err != X509_V_OK) {
+ g_set_error(&err, rspamd_ssl_quark(), 400, "certificate validation "
+ "failed: %s",
+ X509_verify_cert_error_string(ver_err));
+ c->err_handler(c->handler_data, err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ /* Get server's certificate */
+ server_cert = SSL_get_peer_certificate(c->ssl);
+ if (server_cert == NULL) {
+ g_set_error(&err, rspamd_ssl_quark(), 401, "peer certificate is absent");
+ c->err_handler(c->handler_data, err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+
+ if (c->hostname) {
+ if (!rspamd_tls_check_name(server_cert, c->hostname)) {
+ X509_free(server_cert);
+ g_set_error(&err, rspamd_ssl_quark(), 403, "peer certificate fails "
+ "hostname verification for %s",
+ c->hostname);
+ c->err_handler(c->handler_data, err);
+ g_error_free(err);
+
+ return FALSE;
+ }
+ }
+
+ X509_free(server_cert);
+
+ return TRUE;
+}
+
+static void
+rspamd_tls_set_error(gint retcode, const gchar *stage, GError **err)
+{
+ GString *reason;
+ gchar buf[120];
+ gint err_code = 0;
+
+ reason = g_string_sized_new(sizeof(buf));
+
+ if (retcode == SSL_ERROR_SYSCALL) {
+ rspamd_printf_gstring(reason, "syscall fail: %s", strerror(errno));
+ err_code = 500;
+ }
+ else {
+ while ((err_code = ERR_get_error()) != 0) {
+ ERR_error_string(err_code, buf);
+ rspamd_printf_gstring(reason, "ssl error: %s,", buf);
+ }
+
+ err_code = 400;
+
+ if (reason->len > 0 && reason->str[reason->len - 1] == ',') {
+ reason->str[reason->len - 1] = '\0';
+ reason->len--;
+ }
+ }
+
+ g_set_error(err, rspamd_ssl_quark(), err_code,
+ "ssl %s error: %s", stage, reason->str);
+ g_string_free(reason, TRUE);
+}
+
+static void
+rspamd_ssl_connection_dtor(struct rspamd_ssl_connection *conn)
+{
+ msg_debug_ssl("closing SSL connection %p; %d sessions in the cache",
+ conn->ssl, rspamd_lru_hash_size(conn->ssl_ctx->sessions));
+ SSL_free(conn->ssl);
+
+ if (conn->hostname) {
+ g_free(conn->hostname);
+ }
+
+ /*
+ * Try to workaround for the race between timeout and ssl error
+ */
+ if (conn->shut_ev != conn->ev && ev_can_stop(&conn->ev->tm)) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ }
+
+ if (conn->shut_ev) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->shut_ev);
+ g_free(conn->shut_ev);
+ }
+
+ close(conn->fd);
+ g_free(conn);
+}
+
+static void
+rspamd_ssl_shutdown(struct rspamd_ssl_connection *conn)
+{
+ gint ret = 0, nret, retries;
+ static const gint max_retries = 5;
+
+ /*
+ * Fucking openssl...
+ * From the manual, 0 means: "The shutdown is not yet finished.
+ * Call SSL_shutdown() for a second time,
+ * if a bidirectional shutdown shall be performed.
+ * The output of SSL_get_error(3) may be misleading,
+ * as an erroneous SSL_ERROR_SYSCALL may be flagged
+ * even though no error occurred."
+ *
+ * What is `second`, what if `second` also returns 0?
+ * What a retarded behaviour!
+ */
+ for (retries = 0; retries < max_retries; retries++) {
+ ret = SSL_shutdown(conn->ssl);
+
+ if (ret != 0) {
+ break;
+ }
+ }
+
+ if (ret == 1) {
+ /* All done */
+ msg_debug_ssl("ssl shutdown: all done");
+ rspamd_ssl_connection_dtor(conn);
+ }
+ else if (ret < 0) {
+ short what;
+
+ nret = SSL_get_error(conn->ssl, ret);
+ conn->state = ssl_next_shutdown;
+
+ if (nret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl shutdown: need read");
+ what = EV_READ;
+ }
+ else if (nret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl shutdown: need write");
+ what = EV_WRITE;
+ }
+ else {
+ /* Cannot do anything else, fatal error */
+ GError *err = NULL;
+
+ rspamd_tls_set_error(nret, "final shutdown", &err);
+ msg_debug_ssl("ssl shutdown: fatal error: %e; retries=%d; ret=%d",
+ err, retries, ret);
+ g_error_free(err);
+ rspamd_ssl_connection_dtor(conn);
+
+ return;
+ }
+
+ /* As we own fd, we can try to perform shutdown one more time */
+ /* BUGON: but we DO NOT own conn->ev, and it's a big issue */
+ static const ev_tstamp shutdown_time = 5.0;
+
+ if (conn->shut_ev == NULL) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ conn->shut_ev = g_malloc0(sizeof(*conn->shut_ev));
+ rspamd_ev_watcher_init(conn->shut_ev, conn->fd, what,
+ rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, conn->shut_ev, shutdown_time);
+ /* XXX: can it be done safely ? */
+ conn->ev = conn->shut_ev;
+ }
+ else {
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->shut_ev, what);
+ }
+
+ conn->state = ssl_next_shutdown;
+ }
+ else if (ret == 0) {
+ /* What can we do here?? */
+ msg_debug_ssl("ssl shutdown: openssl failed to initiate shutdown after "
+ "%d attempts!",
+ max_retries);
+ rspamd_ssl_connection_dtor(conn);
+ }
+}
+
+static void
+rspamd_ssl_event_handler(gint fd, short what, gpointer ud)
+{
+ struct rspamd_ssl_connection *conn = ud;
+ gint ret;
+ GError *err = NULL;
+
+ if (what == EV_TIMER) {
+ if (conn->state == ssl_next_shutdown) {
+ /* No way to restore, just terminate */
+ rspamd_ssl_connection_dtor(conn);
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ g_set_error(&err, rspamd_ssl_quark(), 408,
+ "ssl connection timed out");
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ }
+
+ return;
+ }
+
+ msg_debug_ssl("ssl event; what=%d; c->state=%d", (int) what,
+ (int) conn->state);
+
+ switch (conn->state) {
+ case ssl_conn_init:
+ /* Continue connection */
+ ret = SSL_connect(conn->ssl);
+
+ if (ret == 1) {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ /* Verify certificate */
+ if ((!conn->verify_peer) || rspamd_ssl_peer_verify(conn)) {
+ msg_debug_ssl("ssl connect: connected");
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, EV_WRITE, conn->handler_data);
+ }
+ else {
+ return;
+ }
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl connect: need read");
+ what = EV_READ;
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl connect: need write");
+ what = EV_WRITE;
+ }
+ else {
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ rspamd_tls_set_error(ret, "connect", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ return;
+ }
+
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ }
+ break;
+ case ssl_next_read:
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, EV_READ);
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, EV_READ, conn->handler_data);
+ break;
+ case ssl_next_write:
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, EV_WRITE);
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, EV_WRITE, conn->handler_data);
+ break;
+ case ssl_conn_connected:
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ conn->state = ssl_conn_connected;
+ conn->handler(fd, what, conn->handler_data);
+ break;
+ case ssl_next_shutdown:
+ rspamd_ssl_shutdown(conn);
+ break;
+ default:
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ g_set_error(&err, rspamd_ssl_quark(), 500,
+ "ssl bad state error: %d", conn->state);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ break;
+ }
+}
+
+struct rspamd_ssl_connection *
+rspamd_ssl_connection_new(gpointer ssl_ctx, struct ev_loop *ev_base,
+ gboolean verify_peer, const gchar *log_tag)
+{
+ struct rspamd_ssl_connection *conn;
+ struct rspamd_ssl_ctx *ctx = (struct rspamd_ssl_ctx *) ssl_ctx;
+
+ g_assert(ssl_ctx != NULL);
+ conn = g_malloc0(sizeof(*conn));
+ conn->ssl_ctx = ctx;
+ conn->event_loop = ev_base;
+ conn->verify_peer = verify_peer;
+
+ if (log_tag) {
+ rspamd_strlcpy(conn->log_tag, log_tag, sizeof(conn->log_tag));
+ }
+ else {
+ rspamd_random_hex(conn->log_tag, sizeof(log_tag) - 1);
+ conn->log_tag[sizeof(log_tag) - 1] = '\0';
+ }
+
+ return conn;
+}
+
+
+gboolean
+rspamd_ssl_connect_fd(struct rspamd_ssl_connection *conn, gint fd,
+ const gchar *hostname, struct rspamd_io_ev *ev, ev_tstamp timeout,
+ rspamd_ssl_handler_t handler, rspamd_ssl_error_handler_t err_handler,
+ gpointer handler_data)
+{
+ gint ret;
+ SSL_SESSION *session = NULL;
+
+ g_assert(conn != NULL);
+
+ /* Ensure that we start from the empty SSL errors stack */
+ ERR_clear_error();
+ conn->ssl = SSL_new(conn->ssl_ctx->s);
+
+ if (hostname) {
+ session = rspamd_lru_hash_lookup(conn->ssl_ctx->sessions, hostname,
+ ev_now(conn->event_loop));
+ }
+
+ if (session) {
+ SSL_set_session(conn->ssl, session);
+ }
+
+ SSL_set_app_data(conn->ssl, conn);
+ msg_debug_ssl("new ssl connection %p; session reused=%s",
+ conn->ssl, SSL_session_reused(conn->ssl) ? "true" : "false");
+
+ if (conn->state != ssl_conn_reset) {
+ return FALSE;
+ }
+
+ /* We dup fd to allow graceful closing */
+ gint nfd = dup(fd);
+
+ if (nfd == -1) {
+ return FALSE;
+ }
+
+ conn->fd = nfd;
+ conn->ev = ev;
+ conn->handler = handler;
+ conn->err_handler = err_handler;
+ conn->handler_data = handler_data;
+
+ if (SSL_set_fd(conn->ssl, conn->fd) != 1) {
+ close(conn->fd);
+
+ return FALSE;
+ }
+
+ if (hostname) {
+ conn->hostname = g_strdup(hostname);
+#ifdef HAVE_SSL_TLSEXT_HOSTNAME
+ SSL_set_tlsext_host_name(conn->ssl, conn->hostname);
+#endif
+ }
+
+ conn->state = ssl_conn_init;
+
+ ret = SSL_connect(conn->ssl);
+
+ if (ret == 1) {
+ conn->state = ssl_conn_connected;
+
+ msg_debug_ssl("connected, start write event");
+ rspamd_ev_watcher_stop(conn->event_loop, ev);
+ rspamd_ev_watcher_init(ev, nfd, EV_WRITE, rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, ev, timeout);
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("not connected, want read");
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("not connected, want write");
+ }
+ else {
+ GError *err = NULL;
+
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "initial connect", &err);
+ msg_debug_ssl("not connected, fatal error %e", err);
+ g_error_free(err);
+
+
+ return FALSE;
+ }
+
+ rspamd_ev_watcher_stop(conn->event_loop, ev);
+ rspamd_ev_watcher_init(ev, nfd, EV_WRITE | EV_READ,
+ rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, ev, timeout);
+ }
+
+ return TRUE;
+}
+
+void rspamd_ssl_connection_restore_handlers(struct rspamd_ssl_connection *conn,
+ rspamd_ssl_handler_t handler,
+ rspamd_ssl_error_handler_t err_handler,
+ gpointer handler_data,
+ short ev_what)
+{
+ conn->handler = handler;
+ conn->err_handler = err_handler;
+ conn->handler_data = handler_data;
+
+ rspamd_ev_watcher_stop(conn->event_loop, conn->ev);
+ rspamd_ev_watcher_init(conn->ev, conn->fd, ev_what, rspamd_ssl_event_handler, conn);
+ rspamd_ev_watcher_start(conn->event_loop, conn->ev, conn->ev->timeout);
+}
+
+gssize
+rspamd_ssl_read(struct rspamd_ssl_connection *conn, gpointer buf,
+ gsize buflen)
+{
+ gint ret;
+ short what;
+ GError *err = NULL;
+
+ g_assert(conn != NULL);
+
+ if (conn->state != ssl_conn_connected && conn->state != ssl_next_read) {
+ errno = EINVAL;
+ g_set_error(&err, rspamd_ssl_quark(), 400,
+ "ssl state error: cannot read data");
+ conn->shut = ssl_shut_unclean;
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+
+ return -1;
+ }
+
+ ret = SSL_read(conn->ssl, buf, buflen);
+ msg_debug_ssl("ssl read: %d", ret);
+
+ if (ret > 0) {
+ conn->state = ssl_conn_connected;
+ return ret;
+ }
+ else if (ret == 0) {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_ZERO_RETURN || ret == SSL_ERROR_SYSCALL) {
+ conn->state = ssl_conn_reset;
+ return 0;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "read", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+ conn->state = ssl_next_read;
+ what = 0;
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl read: need read");
+ what |= EV_READ;
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl read: need write");
+ what |= EV_WRITE;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "read", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ errno = EAGAIN;
+ }
+
+ return -1;
+}
+
+gssize
+rspamd_ssl_write(struct rspamd_ssl_connection *conn, gconstpointer buf,
+ gsize buflen)
+{
+ gint ret;
+ short what;
+ GError *err = NULL;
+
+ g_assert(conn != NULL);
+
+ if (conn->state != ssl_conn_connected && conn->state != ssl_next_write) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ ret = SSL_write(conn->ssl, buf, buflen);
+ msg_debug_ssl("ssl write: ret=%d, buflen=%z", ret, buflen);
+
+ if (ret > 0) {
+ conn->state = ssl_conn_connected;
+ return ret;
+ }
+ else if (ret == 0) {
+ ret = SSL_get_error(conn->ssl, ret);
+
+ if (ret == SSL_ERROR_ZERO_RETURN) {
+ rspamd_tls_set_error(ret, "write", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = ECONNRESET;
+ conn->state = ssl_conn_reset;
+
+ return -1;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "write", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+ }
+ else {
+ ret = SSL_get_error(conn->ssl, ret);
+ conn->state = ssl_next_write;
+
+ if (ret == SSL_ERROR_WANT_READ) {
+ msg_debug_ssl("ssl write: need read");
+ what = EV_READ;
+ }
+ else if (ret == SSL_ERROR_WANT_WRITE) {
+ msg_debug_ssl("ssl write: need write");
+ what = EV_WRITE;
+ }
+ else {
+ conn->shut = ssl_shut_unclean;
+ rspamd_tls_set_error(ret, "write", &err);
+ conn->err_handler(conn->handler_data, err);
+ g_error_free(err);
+ errno = EINVAL;
+
+ return -1;
+ }
+
+ rspamd_ev_watcher_reschedule(conn->event_loop, conn->ev, what);
+ errno = EAGAIN;
+ }
+
+ return -1;
+}
+
+gssize
+rspamd_ssl_writev(struct rspamd_ssl_connection *conn, struct iovec *iov,
+ gsize iovlen)
+{
+ /*
+ * Static is needed to avoid issue:
+ * https://github.com/openssl/openssl/issues/6865
+ */
+ static guchar ssl_buf[16384];
+ guchar *p;
+ struct iovec *cur;
+ gsize i, remain;
+
+ remain = sizeof(ssl_buf);
+ p = ssl_buf;
+
+ for (i = 0; i < iovlen; i++) {
+ cur = &iov[i];
+
+ if (cur->iov_len > 0) {
+ if (remain >= cur->iov_len) {
+ memcpy(p, cur->iov_base, cur->iov_len);
+ p += cur->iov_len;
+ remain -= cur->iov_len;
+ }
+ else {
+ memcpy(p, cur->iov_base, remain);
+ p += remain;
+ remain = 0;
+ break;
+ }
+ }
+ }
+
+ return rspamd_ssl_write(conn, ssl_buf, p - ssl_buf);
+}
+
+/**
+ * Removes connection data
+ * @param conn
+ */
+void rspamd_ssl_connection_free(struct rspamd_ssl_connection *conn)
+{
+ if (conn) {
+ if (conn->shut == ssl_shut_unclean) {
+ /* Ignore return result and close socket */
+ msg_debug_ssl("unclean shutdown");
+ SSL_set_quiet_shutdown(conn->ssl, 1);
+ (void) SSL_shutdown(conn->ssl);
+ rspamd_ssl_connection_dtor(conn);
+ }
+ else {
+ msg_debug_ssl("normal shutdown");
+ rspamd_ssl_shutdown(conn);
+ }
+ }
+}
+
+static int
+rspamd_ssl_new_client_session(SSL *ssl, SSL_SESSION *sess)
+{
+ struct rspamd_ssl_connection *conn;
+
+ conn = SSL_get_app_data(ssl);
+
+ if (conn->hostname) {
+ rspamd_lru_hash_insert(conn->ssl_ctx->sessions,
+ g_strdup(conn->hostname), SSL_get1_session(ssl),
+ ev_now(conn->event_loop), SSL_CTX_get_timeout(conn->ssl_ctx->s));
+ msg_debug_ssl("saved new session for %s: %p", conn->hostname, conn);
+ }
+
+ return 0;
+}
+
+static struct rspamd_ssl_ctx *
+rspamd_init_ssl_ctx_common(void)
+{
+ struct rspamd_ssl_ctx *ret;
+ SSL_CTX *ssl_ctx;
+ gint ssl_options;
+ static const guint client_cache_size = 1024;
+
+ rspamd_openssl_maybe_init();
+
+ ret = g_malloc0(sizeof(*ret));
+ ssl_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
+ ssl_ctx = SSL_CTX_new(SSLv23_method());
+
+#ifdef SSL_OP_NO_COMPRESSION
+ ssl_options |= SSL_OP_NO_COMPRESSION;
+#elif OPENSSL_VERSION_NUMBER >= 0x00908000L
+ sk_SSL_COMP_zero(SSL_COMP_get_compression_methods());
+#endif
+
+ SSL_CTX_set_options(ssl_ctx, ssl_options);
+
+#ifdef TLS1_3_VERSION
+ SSL_CTX_set_min_proto_version(ssl_ctx, 0);
+ SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION);
+#endif
+
+#ifdef SSL_SESS_CACHE_CLIENT
+ SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
+#endif
+
+ ret->s = ssl_ctx;
+ ret->sessions = rspamd_lru_hash_new_full(client_cache_size,
+ g_free, (GDestroyNotify) SSL_SESSION_free, rspamd_str_hash,
+ rspamd_str_equal);
+ SSL_CTX_set_app_data(ssl_ctx, ret);
+ SSL_CTX_sess_set_new_cb(ssl_ctx, rspamd_ssl_new_client_session);
+
+ return ret;
+}
+
+gpointer
+rspamd_init_ssl_ctx(void)
+{
+ struct rspamd_ssl_ctx *ssl_ctx = rspamd_init_ssl_ctx_common();
+
+ SSL_CTX_set_verify(ssl_ctx->s, SSL_VERIFY_PEER, NULL);
+ SSL_CTX_set_verify_depth(ssl_ctx->s, 4);
+
+ return ssl_ctx;
+}
+
+gpointer rspamd_init_ssl_ctx_noverify(void)
+{
+ struct rspamd_ssl_ctx *ssl_ctx_noverify = rspamd_init_ssl_ctx_common();
+
+ SSL_CTX_set_verify(ssl_ctx_noverify->s, SSL_VERIFY_NONE, NULL);
+
+ return ssl_ctx_noverify;
+}
+
+void rspamd_openssl_maybe_init(void)
+{
+ static gboolean openssl_initialized = FALSE;
+
+ if (!openssl_initialized) {
+ ERR_load_crypto_strings();
+ SSL_load_error_strings();
+
+ OpenSSL_add_all_algorithms();
+ OpenSSL_add_all_digests();
+ OpenSSL_add_all_ciphers();
+
+#if OPENSSL_VERSION_NUMBER >= 0x1000104fL && OPENSSL_VERSION_NUMBER < 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
+ ENGINE_load_builtin_engines();
+#endif
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ SSL_library_init();
+#else
+ OPENSSL_init_ssl(0, NULL);
+#endif
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+ OPENSSL_config(NULL);
+#endif
+ if (RAND_status() == 0) {
+ guchar seed[128];
+
+ /* Try to use ottery to seed rand */
+ ottery_rand_bytes(seed, sizeof(seed));
+ RAND_seed(seed, sizeof(seed));
+ rspamd_explicit_memzero(seed, sizeof(seed));
+ }
+
+ openssl_initialized = TRUE;
+ }
+}
+
+void rspamd_ssl_ctx_config(struct rspamd_config *cfg, gpointer ssl_ctx)
+{
+ struct rspamd_ssl_ctx *ctx = (struct rspamd_ssl_ctx *) ssl_ctx;
+ static const char default_secure_ciphers[] = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
+
+ if (cfg->ssl_ca_path) {
+ if (SSL_CTX_load_verify_locations(ctx->s, cfg->ssl_ca_path,
+ NULL) != 1) {
+ msg_err_config("cannot load CA certs from %s: %s",
+ cfg->ssl_ca_path,
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+ }
+ else {
+ msg_debug_config("ssl_ca_path is not set, using default CA path");
+ SSL_CTX_set_default_verify_paths(ctx->s);
+ }
+
+ if (cfg->ssl_ciphers) {
+ if (SSL_CTX_set_cipher_list(ctx->s, cfg->ssl_ciphers) != 1) {
+ msg_err_config(
+ "cannot set ciphers set to %s: %s; fallback to %s",
+ cfg->ssl_ciphers,
+ ERR_error_string(ERR_get_error(), NULL),
+ default_secure_ciphers);
+ /* Default settings */
+ SSL_CTX_set_cipher_list(ctx->s, default_secure_ciphers);
+ }
+ }
+}
+
+void rspamd_ssl_ctx_free(gpointer ssl_ctx)
+{
+ struct rspamd_ssl_ctx *ctx = (struct rspamd_ssl_ctx *) ssl_ctx;
+
+ rspamd_lru_hash_destroy(ctx->sessions);
+ SSL_CTX_free(ctx->s);
+ g_free(ssl_ctx);
+} \ No newline at end of file