]> source.dussan.org Git - rspamd.git/commitdiff
[Feature] Add preliminary version of ssl toolbox
authorVsevolod Stakhov <vsevolod@highsecure.ru>
Mon, 13 Jun 2016 11:04:29 +0000 (12:04 +0100)
committerVsevolod Stakhov <vsevolod@highsecure.ru>
Mon, 13 Jun 2016 16:31:29 +0000 (17:31 +0100)
src/libutil/CMakeLists.txt
src/libutil/ssl_util.c [new file with mode: 0644]
src/libutil/ssl_util.h [new file with mode: 0644]

index a229c7f0df3f96cbcab1f35ec532bafcd357796b..0bf4590e2dbc416a4134055d6d28acec5aabe3ff 100644 (file)
@@ -20,6 +20,7 @@ SET(LIBRSPAMDUTILSRC
                                                                ${CMAKE_CURRENT_SOURCE_DIR}/upstream.c
                                                                ${CMAKE_CURRENT_SOURCE_DIR}/util.c
                                                                ${CMAKE_CURRENT_SOURCE_DIR}/heap.c
-                                                               ${CMAKE_CURRENT_SOURCE_DIR}/multipattern.c)
+                                                               ${CMAKE_CURRENT_SOURCE_DIR}/multipattern.c
+                                                               ${CMAKE_CURRENT_SOURCE_DIR}/ssl_util.c)
 # Rspamdutil
 SET(RSPAMD_UTIL ${LIBRSPAMDUTILSRC} PARENT_SCOPE)
\ No newline at end of file
diff --git a/src/libutil/ssl_util.c b/src/libutil/ssl_util.c
new file mode 100644 (file)
index 0000000..d0f73a9
--- /dev/null
@@ -0,0 +1,487 @@
+/*-
+ * 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 "ssl_util.h"
+
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/conf.h>
+#include <openssl/x509v3.h>
+
+struct rspamd_ssl_connection {
+       gint fd;
+       enum {
+               ssl_conn_reset = 0,
+               ssl_conn_init,
+               ssl_conn_connected
+       } state;
+       SSL *ssl;
+       gchar *hostname;
+       struct event *ev;
+       struct event_base *ev_base;
+       struct timeval *tv;
+       rspamd_ssl_handler_t handler;
+       rspamd_ssl_error_handler_t err_handler;
+       gpointer handler_data;
+};
+
+static GQuark
+rspamd_ssl_quark (void)
+{
+       return g_quark_from_static_string ("rspamd-ssl");
+}
+
+/* $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) {
+                       unsigned char *data;
+                       int format, len;
+
+                       format = ASN1_STRING_type (altname->d.dNSName);
+
+                       if (format == V_ASN1_IA5STRING) {
+                               data = 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) {
+                       unsigned char *data;
+                       int datalen;
+
+                       datalen = ASN1_STRING_length (altname->d.iPAddress);
+                       data = 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 (), ver_err, "certificate validation "
+                               "failed: %s", X509_verify_cert_error_string (ver_err));
+               c->err_handler (c->handler_data, err);
+               g_error_free (err);
+
+               return FALSE;
+       }
+       else {
+               ver_err = ERR_get_error ();
+               g_set_error (&err, rspamd_ssl_quark (), ver_err,
+                               "ssl connect error: %s", ERR_error_string (ver_err, NULL));
+               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 (), ver_err, "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)) {
+                       g_set_error (&err, rspamd_ssl_quark (), ver_err, "peer certificate fails "
+                                       "hostname verification for %s", c->hostname);
+                       c->err_handler (c->handler_data, err);
+                       g_error_free (err);
+
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+static void
+rspamd_ssl_event_handler (gint fd, short what, gpointer ud)
+{
+       struct rspamd_ssl_connection *c = ud;
+       gint ret;
+       GError *err = NULL;
+
+       if (c->state == ssl_conn_init) {
+               /* Continue connection */
+               ret = SSL_connect (c->ssl);
+
+               if (ret == 1) {
+                       /* Verify certificate */
+                       if (rspamd_ssl_peer_verify (c)) {
+                               c->state = ssl_conn_connected;
+                               c->handler (fd, what, c->handler_data);
+                       }
+                       else {
+                               /* Error handler has been called from peer verify */
+                               return;
+                       }
+
+                       if (ret == SSL_ERROR_WANT_READ) {
+                               what = EV_READ;
+                       }
+                       else if (ret == SSL_ERROR_WANT_WRITE) {
+                               what = EV_WRITE;
+                       }
+                       else {
+                               g_set_error (&err, rspamd_ssl_quark (), ret,
+                                               "ssl connect error: %s", ERR_error_string (ret, NULL));
+                               c->err_handler (c->handler_data, err);
+                               g_error_free (err);
+                               return;
+                       }
+
+                       event_set (c->ev, fd, EV_WRITE, rspamd_ssl_event_handler, c);
+                       event_base_set (c->ev_base, c->ev);
+                       event_add (c->ev, c->tv);
+               }
+       }
+}
+
+struct rspamd_ssl_connection *
+rspamd_ssl_connection_new (gpointer ssl_ctx)
+{
+       struct rspamd_ssl_connection *c;
+
+       g_assert (ssl_ctx != NULL);
+       c = g_slice_alloc0 (sizeof (*c));
+       c->ssl = SSL_new (ssl_ctx);
+
+       return c;
+}
+
+
+gboolean
+rspamd_ssl_connect_fd (struct rspamd_ssl_connection *conn, gint fd,
+               const gchar *hostname, struct event *ev, struct timeval *tv,
+               rspamd_ssl_handler_t handler, rspamd_ssl_error_handler_t err_handler,
+               gpointer handler_data)
+{
+       gint ret;
+       short what;
+
+       g_assert (conn != NULL);
+
+       if (conn->state != ssl_conn_reset) {
+               return FALSE;
+       }
+
+       conn->fd = fd;
+       conn->ev = ev;
+       conn->handler = handler;
+       conn->err_handler = err_handler;
+       conn->handler_data = handler_data;
+       conn->ev_base = event_get_base (ev);
+
+       if (SSL_set_fd (conn->ssl, fd) != 1) {
+               return FALSE;
+       }
+
+       if (hostname) {
+               conn->hostname = g_strdup (hostname);
+#ifdef HAVE_SSL_TLSEXT_HOSTNAME
+               SSL_set_tlsext_host_name (conn->ssl, hostname);
+#endif
+       }
+
+       conn->state = ssl_conn_init;
+
+       ret = SSL_connect (conn->ssl);
+
+       if (ret == 1) {
+               conn->state = ssl_conn_connected;
+               event_set (ev, fd, EV_WRITE, rspamd_ssl_event_handler, conn);
+               event_base_set (conn->ev_base, ev);
+               event_add (ev, tv);
+       }
+       else {
+               ret = SSL_get_error (conn->ssl, ret);
+
+               if (ret == SSL_ERROR_WANT_READ) {
+                       what = EV_READ;
+               }
+               else if (ret == SSL_ERROR_WANT_WRITE) {
+                       what = EV_WRITE;
+               }
+               else {
+                       return FALSE;
+               }
+
+               event_set (ev, fd, EV_WRITE, rspamd_ssl_event_handler, conn);
+               event_base_set (conn->ev_base, ev);
+               event_add (ev, tv);
+       }
+
+       return TRUE;
+}
+
+/**
+ * Removes connection data
+ * @param conn
+ */
+void
+rspamd_ssl_connection_free (struct rspamd_ssl_connection *conn)
+{
+       if (conn) {
+               SSL_free (conn->ssl);
+
+               if (conn->hostname) {
+                       g_free (conn->hostname);
+               }
+
+               g_slice_free1 (sizeof (*conn), conn);
+       }
+}
diff --git a/src/libutil/ssl_util.h b/src/libutil/ssl_util.h
new file mode 100644 (file)
index 0000000..1fcd65a
--- /dev/null
@@ -0,0 +1,56 @@
+/*-
+ * 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.
+ */
+#ifndef SRC_LIBUTIL_SSL_UTIL_H_
+#define SRC_LIBUTIL_SSL_UTIL_H_
+
+#include "config.h"
+#include "libutil/addr.h"
+
+struct rspamd_ssl_connection;
+
+typedef void (*rspamd_ssl_handler_t)(gint fd, short what, gpointer d);
+typedef void (*rspamd_ssl_error_handler_t)(gpointer d, GError *err);
+
+/**
+ * Creates a new ssl connection data structure
+ * @param ssl_ctx initialized SSL_CTX structure
+ * @return opaque connection data
+ */
+struct rspamd_ssl_connection * rspamd_ssl_connection_new (gpointer ssl_ctx);
+
+/**
+ * Connects SSL session using the specified (connected) FD
+ * @param conn connection
+ * @param fd fd to use
+ * @param hostname hostname for SNI
+ * @param ev event to use
+ * @param tv timeout for connection
+ * @param handler connected session handler
+ * @param handler_data opaque data
+ * @return TRUE if a session has been connected
+ */
+gboolean rspamd_ssl_connect_fd (struct rspamd_ssl_connection *conn, gint fd,
+               const gchar *hostname, struct event *ev, struct timeval *tv,
+               rspamd_ssl_handler_t handler, rspamd_ssl_error_handler_t err_handler,
+               gpointer handler_data);
+
+/**
+ * Removes connection data
+ * @param conn
+ */
+void rspamd_ssl_connection_free (struct rspamd_ssl_connection *conn);
+
+#endif /* SRC_LIBUTIL_SSL_UTIL_H_ */