summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/OS/os.h-Darwin4
-rw-r--r--src/README.UPDATING24
-rw-r--r--src/exim_monitor/em_globals.c2
-rw-r--r--src/src/buildconfig.c10
-rw-r--r--src/src/exim.h4
-rw-r--r--src/src/expand.c24
-rw-r--r--src/src/functions.h11
-rw-r--r--src/src/globals.c2
-rw-r--r--src/src/globals.h2
-rw-r--r--src/src/host.c2
-rw-r--r--src/src/log.c4
-rw-r--r--src/src/malware.c2
-rw-r--r--src/src/readconf.c1
-rw-r--r--src/src/receive.c2
-rw-r--r--src/src/smtp_in.c8
-rw-r--r--src/src/spool_in.c4
-rw-r--r--src/src/spool_out.c2
-rw-r--r--src/src/tls-gnu.c1809
-rw-r--r--src/src/tls-openssl.c34
-rw-r--r--src/src/tls.c6
-rw-r--r--src/src/transports/smtp.c6
21 files changed, 1170 insertions, 793 deletions
diff --git a/src/OS/os.h-Darwin b/src/OS/os.h-Darwin
index 888b9c759..af06bdcbc 100644
--- a/src/OS/os.h-Darwin
+++ b/src/OS/os.h-Darwin
@@ -35,4 +35,8 @@ updating Exim to use the newer interface. */
/* It's not .so for dynamic libraries on Darwin. */
#define DYNLIB_FN_EXT "dylib"
+/* We currently need some assistance getting OFF_T_FMT correct on MacOS */
+#define OFF_T_FMT "%llu"
+#define LONGLONG_T long int
+
/* End */
diff --git a/src/README.UPDATING b/src/README.UPDATING
index 0d729a384..81e767efe 100644
--- a/src/README.UPDATING
+++ b/src/README.UPDATING
@@ -77,6 +77,30 @@ Exim version 4.78
new option, you can safely force it off before upgrading, to decouple
configuration changes from the binary upgrade while remaining RFC compliant.
+ * The GnuTLS support has been mostly rewritten, to use 2.12.x APIs. As part
+ of this, these three options are no longer supported:
+
+ gnutls_require_kx
+ gnutls_require_mac
+ gnutls_require_protocols
+
+ Their functionality is entirely subsumed into tls_require_ciphers, which is
+ no longer parsed apart by Exim but is instead given to
+ gnutls_priority_init(3), which is no longer an Exim list. See:
+
+ http://www.gnu.org/software/gnutls/manual/html_node/Priority-Strings.html
+
+ for fuller documentation of the strings parsed. The three gnutls_require_*
+ options are still parsed by Exim and, for this release, silently ignored.
+ A future release will add warnings, before a later still release removes
+ parsing entirely and the presence of the options will be a configuration
+ error.
+
+ This rewrite means that Exim will continue to build against GnuTLS in the
+ future, brings Exim closer to other GnuTLS applications and lets us add
+ support for SNI and other features more readily. We regret that it wasn't
+ feasible to retain the three dropped options.
+
Exim version 4.77
-----------------
diff --git a/src/exim_monitor/em_globals.c b/src/exim_monitor/em_globals.c
index 720549382..e74118c45 100644
--- a/src/exim_monitor/em_globals.c
+++ b/src/exim_monitor/em_globals.c
@@ -214,7 +214,7 @@ BOOL timestamps_utc = FALSE;
BOOL tls_certificate_verified = FALSE;
uschar *tls_cipher = NULL;
uschar *tls_peerdn = NULL;
-#ifndef USE_GNUTLS
+#ifdef SUPPORT_TLS
uschar *tls_sni = NULL;
#endif
diff --git a/src/src/buildconfig.c b/src/src/buildconfig.c
index 64902a1ee..1eaf2f505 100644
--- a/src/src/buildconfig.c
+++ b/src/src/buildconfig.c
@@ -103,6 +103,7 @@ off_t test_off_t = 0;
time_t test_time_t = 0;
#if ! (__STDC_VERSION__ >= 199901L)
size_t test_size_t = 0;
+ssize_t test_ssize_t = 0;
unsigned long test_ulong_t = 0L;
#endif
long test_long_t = 0;
@@ -179,15 +180,22 @@ else
fprintf(new, "#endif\n\n");
/* And for sizeof() results, size_t, which should with C99 be just %zu, deal
-with C99 not being ubiquitous yet. Unfortunately. */
+with C99 not being ubiquitous yet. Unfortunately. Assume ssize_t is same
+size as size_t on C99; if someone comes up with a version where it's not, fix
+it then. */
#if __STDC_VERSION__ >= 199901L
fprintf(new, "#define SIZE_T_FMT \"%%zu\"\n");
+fprintf(new, "#define SSIZE_T_FMT \"%%zd\"\n");
#else
if (sizeof(test_size_t) > sizeof (test_ulong_t))
fprintf(new, "#define SIZE_T_FMT \"%%llu\"\n");
else
fprintf(new, "#define SIZE_T_FMT \"%%lu\"\n");
+if (sizeof(test_ssize_t) > sizeof(test_long_t))
+ fprintf(new, "#define SSIZE_T_FMT \"%%lld\"\n");
+else
+ fprintf(new, "#define SSIZE_T_FMT \"%%ld\"\n");
#endif
/* Now search the makefile for certain settings */
diff --git a/src/src/exim.h b/src/src/exim.h
index e6e72facc..49f52474a 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -77,6 +77,10 @@ making unique names. */
#include <limits.h>
#endif
+/* C99 integer types, figure out how to undo this if needed for older systems */
+
+#include <inttypes.h>
+
/* Just in case some aged system doesn't define them... */
#ifndef INT_MAX
diff --git a/src/src/expand.c b/src/src/expand.c
index dcc813801..2fc953aec 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -615,7 +615,7 @@ static var_entry var_table[] = {
{ "tls_certificate_verified", vtype_int, &tls_certificate_verified },
{ "tls_cipher", vtype_stringptr, &tls_cipher },
{ "tls_peerdn", vtype_stringptr, &tls_peerdn },
-#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS)
+#ifdef SUPPORT_TLS
{ "tls_sni", vtype_stringptr, &tls_sni },
#endif
{ "tod_bsdinbox", vtype_todbsdin, NULL },
@@ -776,6 +776,7 @@ return rc;
+
/*************************************************
* Pseudo-random number generation *
*************************************************/
@@ -788,19 +789,23 @@ weirdness they'll twist this into. The result should ideally handle fork().
However, if we're stuck unable to provide this, then we'll fall back to
appallingly bad randomness.
-If SUPPORT_TLS is defined and OpenSSL is used, then this will not be used.
-The GNUTLS randomness functions found do not seem amenable to extracting
-random numbers outside of a TLS context. Any volunteers?
+If SUPPORT_TLS is defined then this will not be used except as an emergency
+fallback.
Arguments:
max range maximum
Returns a random number in range [0, max-1]
*/
-#if !defined(SUPPORT_TLS) || defined(USE_GNUTLS)
+#ifdef SUPPORT_TLS
+# define vaguely_random_number vaguely_random_number_fallback
+#endif
int
-pseudo_random_number(int max)
+vaguely_random_number(int max)
{
+#ifdef SUPPORT_TLS
+# undef vaguely_random_number
+#endif
static pid_t pid = 0;
pid_t p2;
#if defined(HAVE_SRANDOM) && !defined(HAVE_SRANDOMDEV)
@@ -843,7 +848,8 @@ pseudo_random_number(int max)
#endif
}
-#endif
+
+
/*************************************************
* Pick out a name from a string *
@@ -5914,7 +5920,7 @@ while (*s != 0)
continue;
}
- /* pseudo-random number less than N */
+ /* vaguely random number less than N */
case EOP_RANDINT:
{
@@ -5924,7 +5930,7 @@ while (*s != 0)
max = expand_string_integer(sub, TRUE);
if (expand_string_message != NULL)
goto EXPAND_FAILED;
- s = string_sprintf("%d", pseudo_random_number((int)max));
+ s = string_sprintf("%d", vaguely_random_number((int)max));
yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
continue;
}
diff --git a/src/src/functions.h b/src/src/functions.h
index 8986f3a49..557b7e72a 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -23,13 +23,13 @@ extern uschar *init_perl(uschar *);
#ifdef SUPPORT_TLS
extern int tls_client_start(int, host_item *, address_item *, uschar *,
uschar *, uschar *, uschar *, uschar *, uschar *, uschar *,
- uschar *, uschar *, uschar *, int);
+ int);
extern void tls_close(BOOL);
extern int tls_feof(void);
extern int tls_ferror(void);
extern int tls_getc(void);
extern int tls_read(uschar *, size_t);
-extern int tls_server_start(uschar *, uschar *, uschar *, uschar *);
+extern int tls_server_start(const uschar *);
extern BOOL tls_smtp_buffered(void);
extern int tls_ungetc(int);
extern int tls_write(const uschar *, size_t);
@@ -199,7 +199,10 @@ extern uschar *parse_fix_phrase(uschar *, int, uschar *, int);
extern uschar *parse_message_id(uschar *, uschar **, uschar **);
extern uschar *parse_quote_2047(uschar *, int, uschar *, uschar *, int, BOOL);
extern uschar *parse_date_time(uschar *str, time_t *t);
-extern int pseudo_random_number(int);
+extern int vaguely_random_number(int);
+#ifdef SUPPORT_TLS
+extern int vaguely_random_number_fallback(int);
+#endif
extern BOOL queue_action(uschar *, int, uschar **, int, int);
extern void queue_check_only(void);
@@ -379,4 +382,6 @@ extern BOOL verify_sender(int *, uschar **);
extern BOOL verify_sender_preliminary(int *, uschar **);
extern void version_init(void);
+extern ssize_t write_to_fd_buf(int, const uschar *, size_t);
+
/* End of functions.h */
diff --git a/src/src/globals.c b/src/src/globals.c
index b68544e54..d341ceb4a 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -116,9 +116,7 @@ BOOL tls_offered = FALSE;
uschar *tls_privatekey = NULL;
BOOL tls_remember_esmtp = FALSE;
uschar *tls_require_ciphers = NULL;
-#ifndef USE_GNUTLS
uschar *tls_sni = NULL;
-#endif
uschar *tls_try_verify_hosts = NULL;
uschar *tls_verify_certificates= NULL;
uschar *tls_verify_hosts = NULL;
diff --git a/src/src/globals.h b/src/src/globals.h
index d267a52b7..37b0f6cf0 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -98,9 +98,7 @@ extern BOOL tls_offered; /* Server offered TLS */
extern uschar *tls_privatekey; /* Private key file */
extern BOOL tls_remember_esmtp; /* For YAEB */
extern uschar *tls_require_ciphers; /* So some can be avoided */
-#ifndef USE_GNUTLS
extern uschar *tls_sni; /* Server Name Indication */
-#endif
extern uschar *tls_try_verify_hosts; /* Optional client verification */
extern uschar *tls_verify_certificates;/* Path for certificates to check */
extern uschar *tls_verify_hosts; /* Mandatory client verification */
diff --git a/src/src/host.c b/src/src/host.c
index 0571772cb..c45c2d77b 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -68,7 +68,7 @@ sprintf(addr, "%d.%d.%d.%d",
very good for the uses to which it is put. When running the regression tests,
start with a fixed seed.
-If you need better, see pseudo_random_number() which is potentially stronger,
+If you need better, see vaguely_random_number() which is potentially stronger,
if a crypto library is available, but might end up just calling this instead.
Arguments:
diff --git a/src/src/log.c b/src/src/log.c
index 75285c136..0ba4e638b 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -528,11 +528,11 @@ Returns:
length actually written, persisting an errno from write()
*/
ssize_t
-write_to_fd_buf(int fd, uschar *buf, size_t length)
+write_to_fd_buf(int fd, const uschar *buf, size_t length)
{
ssize_t wrote;
size_t total_written = 0;
-uschar *p = buf;
+const uschar *p = buf;
size_t left = length;
while (1)
diff --git a/src/src/malware.c b/src/src/malware.c
index 864564ffc..79e2e3827 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -115,7 +115,7 @@ malware_in_file(uschar *eml_filename) {
/* spool_mbox() assumes various parameters exist, when creating
the relevant directory and the email within */
(void) string_format(message_id_buf, sizeof(message_id_buf),
- "dummy-%d", pseudo_random_number(INT_MAX));
+ "dummy-%d", vaguely_random_number(INT_MAX));
message_id = message_id_buf;
sender_address = US"malware-sender@example.net";
return_path = US"";
diff --git a/src/src/readconf.c b/src/src/readconf.c
index b35811e48..cdd32be58 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -235,6 +235,7 @@ static optionlist optionlist_config[] = {
{ "gecos_pattern", opt_stringptr, &gecos_pattern },
#ifdef SUPPORT_TLS
{ "gnutls_compat_mode", opt_bool, &gnutls_compat_mode },
+ /* These three gnutls_require_* options stopped working in Exim 4.78 */
{ "gnutls_require_kx", opt_stringptr, &gnutls_require_kx },
{ "gnutls_require_mac", opt_stringptr, &gnutls_require_mac },
{ "gnutls_require_protocols", opt_stringptr, &gnutls_require_proto },
diff --git a/src/src/receive.c b/src/src/receive.c
index aaaf64ce6..b3b06e867 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -3488,12 +3488,10 @@ if ((log_extra_selector & LX_tls_certificate_verified) != 0 &&
if ((log_extra_selector & LX_tls_peerdn) != 0 && tls_peerdn != NULL)
s = string_append(s, &size, &sptr, 3, US" DN=\"",
string_printing(tls_peerdn), US"\"");
-#ifndef USE_GNUTLS
if ((log_extra_selector & LX_tls_sni) != 0 && tls_sni != NULL)
s = string_append(s, &size, &sptr, 3, US" SNI=\"",
string_printing(tls_sni), US"\"");
#endif
-#endif
if (sender_host_authenticated != NULL)
{
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index d1c10f00f..5d241d960 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -841,12 +841,10 @@ if ((log_extra_selector & LX_tls_certificate_verified) != 0 &&
if ((log_extra_selector & LX_tls_peerdn) != 0 && tls_peerdn != NULL)
s = string_append(s, &size, &ptr, 3, US" DN=\"",
string_printing(tls_peerdn), US"\"");
-#ifndef USE_GNUTLS
if ((log_extra_selector & LX_tls_sni) != 0 && tls_sni != NULL)
s = string_append(s, &size, &ptr, 3, US" SNI=\"",
string_printing(tls_sni), US"\"");
#endif
-#endif
sep = (smtp_connection_had[SMTP_HBUFF_SIZE-1] != SCH_NONE)?
US" C=..." : US" C=";
@@ -1676,8 +1674,7 @@ if (!sender_host_unknown)
#ifdef SUPPORT_TLS
if (tls_on_connect &&
- tls_server_start(tls_require_ciphers,
- gnutls_require_mac, gnutls_require_kx, gnutls_require_proto) != OK)
+ tls_server_start(tls_require_ciphers) != OK)
return FALSE;
#endif
@@ -3893,8 +3890,7 @@ while (done <= 0)
We must allow for an extra EHLO command and an extra AUTH command after
STARTTLS that don't add to the nonmail command count. */
- if ((rc = tls_server_start(tls_require_ciphers, gnutls_require_mac,
- gnutls_require_kx, gnutls_require_proto)) == OK)
+ if ((rc = tls_server_start(tls_require_ciphers)) == OK)
{
if (!tls_remember_esmtp)
helo_seen = esmtp = auth_advertised = pipelining_advertised = FALSE;
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index bdc3903c0..f6aa18c34 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -286,10 +286,8 @@ dkim_collect_input = FALSE;
tls_certificate_verified = FALSE;
tls_cipher = NULL;
tls_peerdn = NULL;
-#ifndef USE_GNUTLS
tls_sni = NULL;
#endif
-#endif
#ifdef WITH_CONTENT_SCAN
spam_score_int = NULL;
@@ -552,10 +550,8 @@ for (;;)
tls_cipher = string_copy(big_buffer + 12);
else if (Ustrncmp(p, "ls_peerdn", 9) == 0)
tls_peerdn = string_unprinting(string_copy(big_buffer + 12));
- #ifndef USE_GNUTLS
else if (Ustrncmp(p, "ls_sni", 6) == 0)
tls_sni = string_unprinting(string_copy(big_buffer + 9));
- #endif
break;
#endif
diff --git a/src/src/spool_out.c b/src/src/spool_out.c
index fa4f1b6e2..0d32fd6ca 100644
--- a/src/src/spool_out.c
+++ b/src/src/spool_out.c
@@ -229,10 +229,8 @@ if (bmi_verdicts != NULL) fprintf(f, "-bmi_verdicts %s\n", bmi_verdicts);
if (tls_certificate_verified) fprintf(f, "-tls_certificate_verified\n");
if (tls_cipher != NULL) fprintf(f, "-tls_cipher %s\n", tls_cipher);
if (tls_peerdn != NULL) fprintf(f, "-tls_peerdn %s\n", string_printing(tls_peerdn));
-#ifndef USE_GNUTLS
if (tls_sni != NULL) fprintf(f, "-tls_sni %s\n", string_printing(tls_sni));
#endif
-#endif
/* To complete the envelope, write out the tree of non-recipients, followed by
the list of recipients. These won't be disjoint the first time, when no
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 7e87dded0..f0e391f97 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -2,178 +2,178 @@
* Exim - an Internet mail transport agent *
*************************************************/
-/* Copyright (c) University of Cambridge 1995 - 2009 */
+/* Copyright (c) University of Cambridge 1995 - 2012 */
/* See the file NOTICE for conditions of use and distribution. */
-/* This module provides TLS (aka SSL) support for Exim using the GnuTLS
-library. It is #included into tls.c when that library is used. The code herein
-is based on a patch that was contributed by Nikos Mavroyanopoulos.
+/* Copyright (c) Phil Pennock 2012 */
-No cryptographic code is included in Exim. All this module does is to call
-functions from the GnuTLS library. */
+/* This file provides TLS/SSL support for Exim using the GnuTLS library,
+one of the available supported implementations. This file is #included into
+tls.c when USE_GNUTLS has been set.
-/* Note: This appears to be using an old API from compat.h; it is likely that
-someone familiary with GnuTLS programming could rework a lot of this to a
-modern API and perhaps remove the explicit knowledge of crypto algorithms from
-Exim. Such a re-work would be most welcome and we'd sacrifice support for
-older GnuTLS releases without too many qualms -- maturity and experience
-in crypto libraries tends to improve their robustness against attack.
-Frankly, if you maintain it, you decide what's supported and what isn't. */
+The code herein is a revamp of GnuTLS integration using the current APIs; the
+original tls-gnu.c was based on a patch which was contributed by Nikos
+Mavroyanopoulos. The revamp is partially a rewrite, partially cut&paste as
+appropriate.
-/* Heading stuff for GnuTLS */
+APIs current as of GnuTLS 2.12.18; note that the GnuTLS manual is for GnuTLS 3,
+which is not widely deployed by OS vendors. Will note issues below, which may
+assist in updating the code in the future. Another sources of hints is
+mod_gnutls for Apache (SNI callback registration and handling).
-#include <gnutls/gnutls.h>
-#include <gnutls/x509.h>
+Keeping client and server variables more split than before and is currently
+the norm, in anticipation of TLS in ACL callouts.
+I wanted to switch to gnutls_certificate_set_verify_function() so that
+certificate rejection could happen during handshake where it belongs, rather
+than being dropped afterwards, but that was introduced in 2.10.0 and Debian
+(6.0.5) is still on 2.8.6. So for now we have to stick with sub-par behaviour.
-#define UNKNOWN_NAME "unknown"
-#define DH_BITS 1024
-#define PARAM_SIZE 2*1024
+(I wasn't looking for libraries quite that old, when updating to get rid of
+compiler warnings of deprecated APIs. If it turns out that a lot of the rest
+require current GnuTLS, then we'll drop support for the ancient libraries).
+*/
+#include <gnutls/gnutls.h>
+/* needed for cert checks in verification and DN extraction: */
+#include <gnutls/x509.h>
+/* man-page is incorrect, gnutls_rnd() is not in gnutls.h: */
+#include <gnutls/crypto.h>
-/* Values for verify_requirment */
+/* GnuTLS 2 vs 3
-enum { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
+GnuTLS 3 only:
+ gnutls_global_set_audit_log_function()
-/* Local static variables for GNUTLS */
+Changes:
+ gnutls_certificate_verify_peers2(): is new, drop the 2 for old version
+*/
-static host_item *client_host;
+/* Local static variables for GnuTLS */
-static gnutls_dh_params dh_params = NULL;
+/* Values for verify_requirement */
-static gnutls_certificate_server_credentials x509_cred = NULL;
-static gnutls_session tls_session = NULL;
+enum peer_verify_requirement { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
-static char ssl_errstring[256];
+/* This holds most state for server or client; with this, we can set up an
+outbound TLS-enabled connection in an ACL callout, while not stomping all
+over the TLS variables available for expansion.
-static int ssl_session_timeout = 200;
-static int verify_requirement;
+Some of these correspond to variables in globals.c; those variables will
+be set to point to content in one of these instances, as appropriate for
+the stage of the process lifetime.
-/* Priorities for TLS algorithms to use. In each case there's a default table,
-and space into which it can be copied and altered. */
+Not handled here: globals tls_active, tls_bits, tls_cipher, tls_peerdn,
+tls_certificate_verified, tls_channelbinding_b64, tls_sni.
+*/
-static const int default_proto_priority[16] = {
- /* These are gnutls_protocol_t enum values */
-#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 7
- GNUTLS_TLS1_2,
-#endif
-#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 2
- GNUTLS_TLS1_1,
-#endif
- GNUTLS_TLS1,
- GNUTLS_SSL3,
- 0 };
+typedef struct exim_gnutls_state {
+ gnutls_session_t session;
+ gnutls_certificate_credentials_t x509_cred;
+ gnutls_priority_t priority_cache;
+ enum peer_verify_requirement verify_requirement;
+ int fd_in;
+ int fd_out;
+ BOOL peer_cert_verified;
+ BOOL trigger_sni_changes;
+ const struct host_item *host;
+ uschar *peerdn;
+ uschar *received_sni;
+
+ const uschar *tls_certificate;
+ const uschar *tls_privatekey;
+ const uschar *tls_sni; /* client send only, not received */
+ const uschar *tls_verify_certificates;
+ const uschar *tls_crl;
+ const uschar *tls_require_ciphers;
+ uschar *exp_tls_certificate;
+ uschar *exp_tls_privatekey;
+ uschar *exp_tls_sni;
+ uschar *exp_tls_verify_certificates;
+ uschar *exp_tls_crl;
+ uschar *exp_tls_require_ciphers;
+
+ uschar *xfer_buffer;
+ int xfer_buffer_lwm;
+ int xfer_buffer_hwm;
+ int xfer_eof;
+ int xfer_error;
+
+ uschar cipherbuf[256];
+} exim_gnutls_state_st;
+
+static const exim_gnutls_state_st exim_gnutls_state_init = {
+ NULL, NULL, NULL, VERIFY_NONE, -1, -1, FALSE, FALSE,
+ NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, 0, 0, 0, 0,
+ ""
+};
-static int proto_priority[16];
+/* Not only do we have our own APIs which don't pass around state, assuming
+it's held in globals, GnuTLS doesn't appear to let us register callback data
+for callbacks, or as part of the session, so we have to keep a "this is the
+context we're currently dealing with" pointer and rely upon being
+single-threaded to keep from processing data on an inbound TLS connection while
+talking to another TLS connection for an outbound check. This does mean that
+there's no way for heart-beats to be responded to, for the duration of the
+second connection. */
-static const int default_kx_priority[16] = {
- GNUTLS_KX_RSA,
- GNUTLS_KX_DHE_DSS,
- GNUTLS_KX_DHE_RSA,
- 0 };
+static exim_gnutls_state_st state_server, state_client;
+static exim_gnutls_state_st *current_global_tls_state;
-static int kx_priority[16];
+/* dh_params are initialised once within the lifetime of a process using TLS;
+if we used TLS in a long-lived daemon, we'd have to reconsider this. But we
+don't want to repeat this. */
-static int default_cipher_priority[16] = {
- GNUTLS_CIPHER_AES_256_CBC,
- GNUTLS_CIPHER_AES_128_CBC,
- GNUTLS_CIPHER_3DES_CBC,
- GNUTLS_CIPHER_ARCFOUR_128,
- 0 };
+static gnutls_dh_params_t dh_server_params = NULL;
-static int cipher_priority[16];
+/* No idea how this value was chosen; preserving it. Default is 3600. */
-static const int default_mac_priority[16] = {
- GNUTLS_MAC_SHA,
- GNUTLS_MAC_MD5,
- 0 };
+static const int ssl_session_timeout = 200;
-static int mac_priority[16];
+static const char * const exim_default_gnutls_priority = "NORMAL";
-/* These two are currently not changeable. */
+/* Guard library core initialisation */
-static const int comp_priority[16] = { GNUTLS_COMP_NULL, 0 };
-static const int cert_type_priority[16] = { GNUTLS_CRT_X509, 0 };
+static BOOL exim_gnutls_base_init_done = FALSE;
-/* Tables of priority names and equivalent numbers */
-typedef struct pri_item {
- uschar *name;
- int *values;
-} pri_item;
+/* ------------------------------------------------------------------------ */
+/* Callback declarations */
+static void exim_gnutls_logger_cb(int level, const char *message);
+static int exim_sni_handling_cb(gnutls_session_t session);
-#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 7
-static int tls1_2_codes[] = { GNUTLS_TLS1_2, 0 };
-#endif
-#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 2
-static int tls1_1_codes[] = { GNUTLS_TLS1_1, 0 };
-#endif
-/* more recent libraries define this as an equivalent value to the
-canonical GNUTLS_TLS1_0; since they're the same, we stick to the
-older name. */
-static int tls1_0_codes[] = { GNUTLS_TLS1, 0 };
-static int ssl3_codes[] = { GNUTLS_SSL3, 0 };
-
-static pri_item proto_index[] = {
-#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 7
- { US"TLS1.2", tls1_2_codes },
-#endif
-#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 2
- { US"TLS1.1", tls1_1_codes },
-#endif
- { US"TLS1.0", tls1_0_codes },
- { US"TLS1", tls1_0_codes },
- { US"SSL3", ssl3_codes }
-};
+/* ------------------------------------------------------------------------ */
+/* macros */
+#define MAX_HOST_LEN 255
-static int kx_rsa_codes[] = { GNUTLS_KX_RSA,
- GNUTLS_KX_DHE_RSA, 0 };
-static int kx_dhe_codes[] = { GNUTLS_KX_DHE_DSS,
- GNUTLS_KX_DHE_RSA, 0 };
-static int kx_dhe_dss_codes[] = { GNUTLS_KX_DHE_DSS, 0 };
-static int kx_dhe_rsa_codes[] = { GNUTLS_KX_DHE_RSA, 0 };
-
-static pri_item kx_index[] = {
- { US"DHE_DSS", kx_dhe_dss_codes },
- { US"DHE_RSA", kx_dhe_rsa_codes },
- { US"RSA", kx_rsa_codes },
- { US"DHE", kx_dhe_codes }
-};
+/* Set this to control gnutls_global_set_log_level(); values 0 to 9 will setup
+the library logging; a value less than 0 disables the calls to set up logging
+callbacks. */
+#define EXIM_GNUTLS_LIBRARY_LOG_LEVEL -1
+#define EXIM_CLIENT_DH_MIN_BITS 1024
-static int arcfour_128_codes[] = { GNUTLS_CIPHER_ARCFOUR_128, 0 };
-static int arcfour_40_codes[] = { GNUTLS_CIPHER_ARCFOUR_40, 0 };
-static int arcfour_codes[] = { GNUTLS_CIPHER_ARCFOUR_128,
- GNUTLS_CIPHER_ARCFOUR_40, 0 };
-static int aes_256_codes[] = { GNUTLS_CIPHER_AES_256_CBC, 0 };
-static int aes_128_codes[] = { GNUTLS_CIPHER_AES_128_CBC, 0 };
-static int aes_codes[] = { GNUTLS_CIPHER_AES_256_CBC,
- GNUTLS_CIPHER_AES_128_CBC, 0 };
-static int des3_codes[] = { GNUTLS_CIPHER_3DES_CBC, 0 };
-
-static pri_item cipher_index[] = {
- { US"ARCFOUR_128", arcfour_128_codes },
- { US"ARCFOUR_40", arcfour_40_codes },
- { US"ARCFOUR", arcfour_codes },
- { US"AES_256", aes_256_codes },
- { US"AES_128", aes_128_codes },
- { US"AES", aes_codes },
- { US"3DES", des3_codes }
-};
+#define exim_gnutls_err_check(Label) do { \
+ if (rc != GNUTLS_E_SUCCESS) { return tls_error((Label), gnutls_strerror(rc), host); } } while (0)
+#define exim_gnutls_err_debugreturn0(Label) do { \
+ if (rc != GNUTLS_E_SUCCESS) { \
+ DEBUG(D_tls) debug_printf("TLS failure: %s: %s", (Label), gnutls_strerror(rc)); \
+ return 0; } } while (0)
-static int mac_sha_codes[] = { GNUTLS_MAC_SHA, 0 };
-static int mac_md5_codes[] = { GNUTLS_MAC_MD5, 0 };
-
-static pri_item mac_index[] = {
- { US"SHA", mac_sha_codes },
- { US"SHA1", mac_sha_codes },
- { US"MD5", mac_md5_codes }
-};
+#define expand_check_tlsvar(Varname) expand_check(state->Varname, US #Varname, &state->exp_##Varname)
+#if GNUTLS_VERSION_NUMBER >= 0x020c00
+#define HAVE_GNUTLS_SESSION_CHANNEL_BINDING
+#endif
+/* ------------------------------------------------------------------------ */
+/* Static functions */
/*************************************************
* Handle TLS error *
@@ -188,120 +188,169 @@ some shared functions.
Argument:
prefix text to include in the logged error
- host NULL if setting up a server;
- the connected host if setting up a client
msg additional error string (may be NULL)
usually obtained from gnutls_strerror()
+ host NULL if setting up a server;
+ the connected host if setting up a client
Returns: OK/DEFER/FAIL
*/
static int
-tls_error(uschar *prefix, host_item *host, const char *msg)
+tls_error(const uschar *prefix, const char *msg, const host_item *host)
{
-if (host == NULL)
+if (host)
+ {
+ log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s%s",
+ host->name, host->address, prefix, msg ? ": " : "", msg ? msg : "");
+ return FAIL;
+ }
+else
{
uschar *conn_info = smtp_get_connection_info();
- if (strncmp(conn_info, "SMTP ", 5) == 0)
+ if (Ustrncmp(conn_info, US"SMTP ", 5) == 0)
conn_info += 5;
log_write(0, LOG_MAIN, "TLS error on %s (%s)%s%s",
- conn_info, prefix, msg ? ": " : "", msg ? msg : "");
+ conn_info, prefix, msg ? ": " : "", msg ? msg : "");
return DEFER;
}
-else
- {
- log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s%s",
- host->name, host->address, prefix, msg ? ": " : "", msg ? msg : "");
- return FAIL;
- }
}
+
/*************************************************
-* Verify certificate *
+* Deal with logging errors during I/O *
*************************************************/
-/* Called after a successful handshake, when certificate verification is
-required or optional, for both server and client.
+/* We have to get the identity of the peer from saved data.
-Arguments:
- session GNUTLS session
- error where to put text giving a reason for failure
+Argument:
+ state the current GnuTLS exim state container
+ rc the GnuTLS error code, or 0 if it's a local error
+ when text identifying read or write
+ text local error text when ec is 0
-Returns: TRUE/FALSE
+Returns: nothing
*/
-static BOOL
-verify_certificate(gnutls_session session, const char **error)
+static void
+record_io_error(exim_gnutls_state_st *state, int rc, uschar *when, uschar *text)
{
-int rc = -1;
-uschar *dn_string = US"";
-const gnutls_datum *cert;
-unsigned int verify, cert_size = 0;
+const char *msg;
-*error = NULL;
+if (rc == GNUTLS_E_FATAL_ALERT_RECEIVED)
+ msg = CS string_sprintf("%s: %s", US gnutls_strerror(rc),
+ US gnutls_alert_get_name(gnutls_alert_get(state->session)));
+else
+ msg = gnutls_strerror(rc);
-/* Get the peer's certificate. If it sent one, extract it's DN, and then
-attempt to verify the certificate. If no certificate is supplied, verification
-is forced to fail. */
+tls_error(when, msg, state->host);
+}
-cert = gnutls_certificate_get_peers(session, &cert_size);
-if (cert != NULL)
- {
- uschar buff[1024];
- gnutls_x509_crt gcert;
- gnutls_x509_crt_init(&gcert);
- dn_string = US"unknown";
- if (gnutls_x509_crt_import(gcert, cert, GNUTLS_X509_FMT_DER) == 0)
- {
- size_t bufsize = sizeof(buff);
- if (gnutls_x509_crt_get_dn(gcert, CS buff, &bufsize) >= 0)
- dn_string = string_copy_malloc(buff);
- }
- rc = gnutls_certificate_verify_peers2(session, &verify);
- }
-else
- {
- DEBUG(D_tls) debug_printf("no peer certificate supplied\n");
- verify = GNUTLS_CERT_INVALID;
- *error = "not supplied";
- }
+/*************************************************
+* Set various Exim expansion vars *
+*************************************************/
-/* Handle the result of verification. INVALID seems to be set as well
-as REVOKED, but leave the test for both. */
+/* We set various Exim global variables from the state, once a session has
+been established. With TLS callouts, may need to change this to stack
+variables, or just re-call it with the server state after client callout
+has finished.
-if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
- {
- tls_certificate_verified = FALSE;
- if (*error == NULL) *error = ((verify & GNUTLS_CERT_REVOKED) != 0)?
- "revoked" : "invalid";
- if (verify_requirement == VERIFY_REQUIRED)
- {
- DEBUG(D_tls) debug_printf("TLS certificate verification failed (%s): "
- "peerdn=%s\n", *error, dn_string);
- gnutls_alert_send(session, GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
- return FALSE; /* reject */
- }
- DEBUG(D_tls) debug_printf("TLS certificate verify failure (%s) overridden "
- "(host in tls_try_verify_hosts): peerdn=%s\n", *error, dn_string);
- }
-else
+Make sure anything set here is inset in tls_getc().
+
+Sets:
+ tls_active fd
+ tls_bits strength indicator
+ tls_certificate_verified bool indicator
+ tls_channelbinding_b64 for some SASL mechanisms
+ tls_cipher a string
+ tls_peerdn a string
+ tls_sni a (UTF-8) string
+Also:
+ current_global_tls_state for API limitations
+
+Argument:
+ state the relevant exim_gnutls_state_st *
+*/
+
+static void
+extract_exim_vars_from_tls_state(exim_gnutls_state_st *state)
+{
+gnutls_protocol_t protocol;
+gnutls_cipher_algorithm_t cipher;
+gnutls_kx_algorithm_t kx;
+gnutls_mac_algorithm_t mac;
+uschar *p;
+#ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
+int old_pool;
+int rc;
+gnutls_datum_t channel;
+#endif
+
+current_global_tls_state = state;
+
+tls_active = state->fd_out;
+
+cipher = gnutls_cipher_get(state->session);
+/* returns size in "bytes" */
+tls_bits = gnutls_cipher_get_key_size(cipher) * 8;
+
+if (!*state->cipherbuf)
{
- tls_certificate_verified = TRUE;
- DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n",
- dn_string);
+ protocol = gnutls_protocol_get_version(state->session);
+ mac = gnutls_mac_get(state->session);
+ kx = gnutls_kx_get(state->session);
+
+ string_format(state->cipherbuf, sizeof(state->cipherbuf),
+ "%s:%s:%u",
+ gnutls_protocol_get_name(protocol),
+ gnutls_cipher_suite_get_name(kx, cipher, mac),
+ tls_bits);
+
+ /* I don't see a way that spaces could occur, in the current GnuTLS
+ code base, but it was a concern in the old code and perhaps older GnuTLS
+ releases did return "TLS 1.0"; play it safe, just in case. */
+ for (p = state->cipherbuf; *p != '\0'; ++p)
+ if (isspace(*p))
+ *p = '-';
}
+tls_cipher = state->cipherbuf;
+
+DEBUG(D_tls) debug_printf("cipher: %s\n", tls_cipher);
+
+tls_certificate_verified = state->peer_cert_verified;
-tls_peerdn = dn_string;
-return TRUE; /* accept */
+/* note that tls_channelbinding_b64 is not saved to the spool file, since it's
+only available for use for authenticators while this TLS session is running. */
+
+tls_channelbinding_b64 = NULL;
+#ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
+channel.data = NULL;
+channel.size = 0;
+rc = gnutls_session_channel_binding(state->session, GNUTLS_CB_TLS_UNIQUE, &channel);
+if (rc) {
+ DEBUG(D_tls) debug_printf("Channel binding error: %s\n", gnutls_strerror(rc));
+} else {
+ old_pool = store_pool;
+ store_pool = POOL_PERM;
+ tls_channelbinding_b64 = auth_b64encode(channel.data, (int)channel.size);
+ store_pool = old_pool;
+ DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage.\n");
+}
+#endif
+
+tls_peerdn = state->peerdn;
+
+tls_sni = state->received_sni;
}
+
/*************************************************
* Setup up DH parameters *
*************************************************/
@@ -323,23 +372,30 @@ Returns: OK/DEFER/FAIL
*/
static int
-init_dh(host_item *host)
+init_server_dh(void)
{
-int fd;
-int ret;
+int fd, rc;
+unsigned int dh_bits;
gnutls_datum m;
-uschar filename[200];
+uschar filename[PATH_MAX];
+size_t sz;
+host_item *host = NULL; /* dummy for macros */
+const char * const dh_param_fn_ext = "normal"; /* change as dh_bits changes */
-/* Initialize the data structures for holding the parameters */
+DEBUG(D_tls) debug_printf("Initialising GnuTLS server params.\n");
-ret = gnutls_dh_params_init(&dh_params);
-if (ret < 0) return tls_error(US"init dh_params", host, gnutls_strerror(ret));
+rc = gnutls_dh_params_init(&dh_server_params);
+exim_gnutls_err_check(US"gnutls_dh_params_init");
-/* Set up the name of the cache file */
+/* If you change this, also change dh_param_fn_ext so that we can use a
+different filename and ensure we have sufficient bits. */
+dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_NORMAL);
+if (!dh_bits)
+ return tls_error(US"gnutls_sec_param_to_pk_bits() failed", NULL, NULL);
-if (!string_format(filename, sizeof(filename), "%s/gnutls-params",
- spool_directory))
- return tls_error(US"overlong filename", host, NULL);
+if (!string_format(filename, sizeof(filename),
+ "%s/gnutls-params-%s", spool_directory, dh_param_fn_ext))
+ return tls_error(US"overlong filename", NULL, NULL);
/* Open the cache file for reading and if successful, read it and set up the
parameters. */
@@ -348,27 +404,50 @@ fd = Uopen(filename, O_RDONLY, 0);
if (fd >= 0)
{
struct stat statbuf;
- if (fstat(fd, &statbuf) < 0)
+ FILE *fp;
+ int saved_errno;
+
+ if (fstat(fd, &statbuf) < 0) /* EIO */
+ {
+ saved_errno = errno;
+ (void)close(fd);
+ return tls_error(US"TLS cache stat failed", strerror(saved_errno), NULL);
+ }
+ if (!S_ISREG(statbuf.st_mode))
{
(void)close(fd);
- return tls_error(US"TLS cache stat failed", host, strerror(errno));
+ return tls_error(US"TLS cache not a file", NULL, NULL);
+ }
+ fp = fdopen(fd, "rb");
+ if (!fp)
+ {
+ saved_errno = errno;
+ (void)close(fd);
+ return tls_error(US"fdopen(TLS cache stat fd) failed",
+ strerror(saved_errno), NULL);
}
m.size = statbuf.st_size;
m.data = malloc(m.size);
if (m.data == NULL)
- return tls_error(US"memory allocation failed", host, strerror(errno));
- errno = 0;
- if (read(fd, m.data, m.size) != m.size)
- return tls_error(US"TLS cache read failed", host, strerror(errno));
- (void)close(fd);
-
- ret = gnutls_dh_params_import_pkcs3(dh_params, &m, GNUTLS_X509_FMT_PEM);
- if (ret < 0)
- return tls_error(US"DH params import", host, gnutls_strerror(ret));
- DEBUG(D_tls) debug_printf("read D-H parameters from file\n");
+ {
+ fclose(fp);
+ return tls_error(US"malloc failed", strerror(errno), NULL);
+ }
+ sz = fread(m.data, m.size, 1, fp);
+ if (!sz)
+ {
+ saved_errno = errno;
+ fclose(fp);
+ free(m.data);
+ return tls_error(US"fread failed", strerror(saved_errno), NULL);
+ }
+ fclose(fp);
+ rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM);
free(m.data);
+ exim_gnutls_err_check(US"gnutls_dh_params_import_pkcs3");
+ DEBUG(D_tls) debug_printf("read D-H parameters from file \"%s\"\n", filename);
}
/* If the file does not exist, fall through to compute new data and cache it.
@@ -376,13 +455,13 @@ If there was any other opening error, it is serious. */
else if (errno == ENOENT)
{
- ret = -1;
+ rc = -1;
DEBUG(D_tls)
- debug_printf("parameter cache file %s does not exist\n", filename);
+ debug_printf("D-H parameter cache file \"%s\" does not exist\n", filename);
}
else
- return tls_error(string_open_failed(errno, "%s for reading", filename),
- host, NULL);
+ return tls_error(string_open_failed(errno, "\"%s\" for reading", filename),
+ NULL, NULL);
/* If ret < 0, either the cache file does not exist, or the data it contains
is not useful. One particular case of this is when upgrading from an older
@@ -390,59 +469,72 @@ release of Exim in which the data was stored in a different format. We don't
try to be clever and support both formats; we just regenerate new data in this
case. */
-if (ret < 0)
+if (rc < 0)
{
- uschar tempfilename[sizeof(filename) + 10];
+ uschar *temp_fn;
- DEBUG(D_tls) debug_printf("generating %d bit Diffie-Hellman key...\n",
- DH_BITS);
- ret = gnutls_dh_params_generate2(dh_params, DH_BITS);
- if (ret < 0) return tls_error(US"D-H key generation", host, gnutls_strerror(ret));
+ if ((PATH_MAX - Ustrlen(filename)) < 10)
+ return tls_error(US"Filename too long to generate replacement",
+ CS filename, NULL);
- /* Write the parameters to a file in the spool directory so that we
- can use them from other Exim processes. */
-
- sprintf(CS tempfilename, "%s-%d", filename, (int)getpid());
- fd = Uopen(tempfilename, O_WRONLY|O_CREAT, 0400);
+ temp_fn = string_copy(US "%s.XXXXXXX");
+ fd = mkstemp(CS temp_fn); /* modifies temp_fn */
if (fd < 0)
- return tls_error(string_open_failed(errno, "%s for writing", filename),
- host, NULL);
+ return tls_error(US"Unable to open temp file", strerror(errno), NULL);
(void)fchown(fd, exim_uid, exim_gid); /* Probably not necessary */
- /* export the parameters in a format that can be generated using GNUTLS'
- * certtool or other programs.
- *
- * The commands for certtool are:
- * $ certtool --generate-dh-params --bits 1024 > params
- */
-
- m.size = PARAM_SIZE;
+ DEBUG(D_tls) debug_printf("generating %d bits Diffie-Hellman key ...\n", dh_bits);
+ rc = gnutls_dh_params_generate2(dh_server_params, dh_bits);
+ exim_gnutls_err_check(US"gnutls_dh_params_generate2");
+
+ /* gnutls_dh_params_export_pkcs3() will tell us the exact size, every time,
+ and I confirmed that a NULL call to get the size first is how the GnuTLS
+ sample apps handle this. */
+
+ sz = 0;
+ m.data = NULL;
+ rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
+ m.data, &sz);
+ if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
+ exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3(NULL) sizing");
+ m.size = sz;
m.data = malloc(m.size);
if (m.data == NULL)
- return tls_error(US"memory allocation failed", host, strerror(errno));
-
- m.size = PARAM_SIZE;
- ret = gnutls_dh_params_export_pkcs3(dh_params, GNUTLS_X509_FMT_PEM, m.data,
- &m.size);
- if (ret < 0)
- return tls_error(US"DH params export", host, gnutls_strerror(ret));
-
- m.size = Ustrlen(m.data);
- errno = 0;
- if (write(fd, m.data, m.size) != m.size || write(fd, "\n", 1) != 1)
- return tls_error(US"TLS cache write failed", host, strerror(errno));
+ return tls_error(US"memory allocation failed", strerror(errno), NULL);
+ rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
+ m.data, &sz);
+ if (rc != GNUTLS_E_SUCCESS)
+ {
+ free(m.data);
+ exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3() real");
+ }
+ sz = write_to_fd_buf(fd, m.data, (size_t) m.size);
+ if (sz != m.size)
+ {
+ free(m.data);
+ return tls_error(US"TLS cache write D-H params failed",
+ strerror(errno), NULL);
+ }
free(m.data);
- (void)close(fd);
+ sz = write_to_fd_buf(fd, US"\n", 1);
+ if (sz != 1)
+ return tls_error(US"TLS cache write D-H params final newline failed",
+ strerror(errno), NULL);
+
+ rc = close(fd);
+ if (rc)
+ return tls_error(US"TLS cache write close() failed",
+ strerror(errno), NULL);
- if (rename(CS tempfilename, CS filename) < 0)
- return tls_error(string_sprintf("failed to rename %s as %s",
- tempfilename, filename), host, strerror(errno));
+ if (Urename(temp_fn, filename) < 0)
+ return tls_error(string_sprintf("failed to rename \"%s\" as \"%s\"",
+ temp_fn, filename), strerror(errno), NULL);
- DEBUG(D_tls) debug_printf("wrote D-H parameters to file %s\n", filename);
+ DEBUG(D_tls) debug_printf("wrote D-H parameters to file \"%s\"\n", filename);
}
-DEBUG(D_tls) debug_printf("initialized D-H parameters\n");
+DEBUG(D_tls) debug_printf("initialized server D-H parameters\n");
return OK;
}
@@ -450,90 +542,116 @@ return OK;
/*************************************************
-* Initialize for GnuTLS *
+* Variables re-expanded post-SNI *
*************************************************/
-/* Called from both server and client code. In the case of a server, errors
-before actual TLS negotiation return DEFER.
+/* Called from both server and client code, via tls_init(), and also from
+the SNI callback after receiving an SNI, if tls_certificate includes "tls_sni".
+
+We can tell the two apart by state->received_sni being non-NULL in callback.
+
+The callback should not call us unless state->trigger_sni_changes is true,
+which we are responsible for setting on the first pass through.
Arguments:
- host connected host, if client; NULL if server
- certificate certificate file
- privatekey private key file
- cas CA certs file
- crl CRL file
+ state exim_gnutls_state_st *
Returns: OK/DEFER/FAIL
*/
static int
-tls_init(host_item *host, uschar *certificate, uschar *privatekey, uschar *cas,
- uschar *crl)
+tls_expand_session_files(exim_gnutls_state_st *state)
{
int rc;
-uschar *cert_expanded, *key_expanded, *cas_expanded, *crl_expanded;
-
-client_host = host;
+const host_item *host = state->host; /* macro should be reconsidered? */
+uschar *saved_tls_certificate = NULL;
+uschar *saved_tls_privatekey = NULL;
+uschar *saved_tls_verify_certificates = NULL;
+uschar *saved_tls_crl = NULL;
+int cert_count;
+
+/* We check for tls_sni *before* expansion. */
+if (!state->host)
+ {
+ if (!state->received_sni)
+ {
+ if (Ustrstr(state->tls_certificate, US"tls_sni"))
+ {
+ DEBUG(D_tls) debug_printf("We will re-expand TLS session files if we receive SNI.\n");
+ state->trigger_sni_changes = TRUE;
+ }
+ }
+ else
+ {
+ saved_tls_certificate = state->exp_tls_certificate;
+ saved_tls_privatekey = state->exp_tls_privatekey;
+ saved_tls_verify_certificates = state->exp_tls_verify_certificates;
+ saved_tls_crl = state->exp_tls_crl;
+ }
+ }
-rc = gnutls_global_init();
-if (rc < 0) return tls_error(US"tls-init", host, gnutls_strerror(rc));
+/* remember: expand_check_tlsvar() is expand_check() but fiddling with
+state members, assuming consistent naming; and expand_check() returns
+false if expansion failed, unless expansion was forced to fail. */
-/* Create D-H parameters, or read them from the cache file. This function does
-its own SMTP error messaging. */
+/* check if we at least have a certificate, before doing expensive
+D-H generation. */
-rc = init_dh(host);
-if (rc != OK) return rc;
+if (!expand_check_tlsvar(tls_certificate))
+ return DEFER;
-/* Create the credentials structure */
+/* certificate is mandatory in server, optional in client */
-rc = gnutls_certificate_allocate_credentials(&x509_cred);
-if (rc < 0)
- return tls_error(US"certificate_allocate_credentials",
- host, gnutls_strerror(rc));
-
-/* This stuff must be done for each session, because different certificates
-may be required for different sessions. */
+if ((state->exp_tls_certificate == NULL) ||
+ (*state->exp_tls_certificate == '\0'))
+ {
+ if (state->host == NULL)
+ return tls_error(US"no TLS server certificate is specified", NULL, NULL);
+ else
+ DEBUG(D_tls) debug_printf("TLS: no client certificate specified; okay\n");
+ }
-if (!expand_check(certificate, US"tls_certificate", &cert_expanded))
+if (state->tls_privatekey && !expand_check_tlsvar(tls_privatekey))
return DEFER;
-key_expanded = NULL;
-if (privatekey != NULL)
+/* tls_privatekey is optional, defaulting to same file as certificate */
+
+if (state->tls_privatekey == NULL || *state->tls_privatekey == '\0')
{
- if (!expand_check(privatekey, US"tls_privatekey", &key_expanded))
- return DEFER;
+ state->tls_privatekey = state->tls_certificate;
+ state->exp_tls_privatekey = state->exp_tls_certificate;
}
-/* If expansion was forced to fail, key_expanded will be NULL. If the result of
-the expansion is an empty string, ignore it also, and assume that the private
-key is in the same file as the certificate. */
-
-if (key_expanded == NULL || *key_expanded == 0)
- key_expanded = cert_expanded;
-/* Set the certificate and private keys */
-
-if (cert_expanded != NULL)
+if (state->exp_tls_certificate && *state->exp_tls_certificate)
{
+ BOOL setit = TRUE;
DEBUG(D_tls) debug_printf("certificate file = %s\nkey file = %s\n",
- cert_expanded, key_expanded);
- rc = gnutls_certificate_set_x509_key_file(x509_cred, CS cert_expanded,
- CS key_expanded, GNUTLS_X509_FMT_PEM);
- if (rc < 0)
+ state->exp_tls_certificate, state->exp_tls_privatekey);
+
+ if (state->received_sni)
{
- uschar *msg = string_sprintf("cert/key setup: cert=%s key=%s",
- cert_expanded, key_expanded);
- return tls_error(msg, host, gnutls_strerror(rc));
+ if ((Ustrcmp(state->exp_tls_certificate, saved_tls_certificate) == 0) &&
+ (Ustrcmp(state->exp_tls_privatekey, saved_tls_privatekey) == 0))
+ {
+ DEBUG(D_tls) debug_printf("cert and key unchanged with SNI.\n");
+ setit = FALSE;
+ }
+ else
+ {
+ DEBUG(D_tls) debug_printf("SNI changed cert/key pair.\n");
+ }
}
- }
-/* A certificate is mandatory in a server, but not in a client */
-
-else
- {
- if (host == NULL)
- return tls_error(US"no TLS server certificate is specified", NULL, NULL);
- DEBUG(D_tls) debug_printf("no TLS client certificate is specified\n");
+ if (setit)
+ {
+ rc = gnutls_certificate_set_x509_key_file(state->x509_cred,
+ CS state->exp_tls_certificate, CS state->exp_tls_privatekey,
+ GNUTLS_X509_FMT_PEM);
+ exim_gnutls_err_check(
+ string_sprintf("cert/key setup: cert=%s key=%s",
+ state->exp_tls_certificate, state->exp_tls_privatekey));
+ }
}
/* Set the trusted CAs file if one is provided, and then add the CRL if one is
@@ -542,359 +660,516 @@ error message is provided. However, if we just refrain from setting anything up
in that case, certificate verification fails, which seems to be the correct
behaviour. */
-if (cas != NULL)
+if (state->tls_verify_certificates && *state->tls_verify_certificates)
{
struct stat statbuf;
+ BOOL setit_vc = TRUE, setit_crl = TRUE;
- if (!expand_check(cas, US"tls_verify_certificates", &cas_expanded))
+ if (!expand_check_tlsvar(tls_verify_certificates))
return DEFER;
+ if (state->tls_crl && *state->tls_crl)
+ if (!expand_check_tlsvar(tls_crl))
+ return DEFER;
- if (stat(CS cas_expanded, &statbuf) < 0)
+ if (state->received_sni)
+ {
+ if (Ustrcmp(state->exp_tls_verify_certificates, saved_tls_verify_certificates) == 0)
+ setit_vc = FALSE;
+ if (Ustrcmp(state->exp_tls_crl, saved_tls_crl) == 0)
+ setit_crl = FALSE;
+ }
+
+ /* nb: early exit; change if add more expansions to this function */
+ if (!(setit_vc || setit_crl))
+ return OK;
+
+ if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0)
{
log_write(0, LOG_MAIN|LOG_PANIC, "could not stat %s "
- "(tls_verify_certificates): %s", cas_expanded, strerror(errno));
+ "(tls_verify_certificates): %s", state->exp_tls_verify_certificates,
+ strerror(errno));
+ return DEFER;
+ }
+
+ if (!S_ISREG(statbuf.st_mode))
+ {
+ DEBUG(D_tls)
+ debug_printf("verify certificates path is not a file: \"%s\"\n%s\n",
+ state->exp_tls_verify_certificates,
+ S_ISDIR(statbuf.st_mode)
+ ? " it's a directory, that's OpenSSL, this is GnuTLS"
+ : " (not a directory either)");
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "tls_verify_certificates \"%s\" is not a file",
+ state->exp_tls_verify_certificates);
return DEFER;
}
DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n",
- cas_expanded, statbuf.st_size);
+ state->exp_tls_verify_certificates, statbuf.st_size);
- /* If the cert file is empty, there's no point in loading the CRL file. */
+ /* If the CA cert file is empty, there's no point in loading the CRL file,
+ as we aren't verifying, so checking for revocation is pointless. */
if (statbuf.st_size > 0)
{
- rc = gnutls_certificate_set_x509_trust_file(x509_cred, CS cas_expanded,
- GNUTLS_X509_FMT_PEM);
- if (rc < 0) return tls_error(US"setup_certs", host, gnutls_strerror(rc));
-
- if (crl != NULL && *crl != 0)
+ if (setit_vc)
{
- if (!expand_check(crl, US"tls_crl", &crl_expanded))
- return DEFER;
- DEBUG(D_tls) debug_printf("loading CRL file = %s\n", crl_expanded);
- rc = gnutls_certificate_set_x509_crl_file(x509_cred, CS crl_expanded,
- GNUTLS_X509_FMT_PEM);
- if (rc < 0) return tls_error(US"CRL setup", host, gnutls_strerror(rc));
+ cert_count = gnutls_certificate_set_x509_trust_file(state->x509_cred,
+ CS state->exp_tls_verify_certificates, GNUTLS_X509_FMT_PEM);
+ if (cert_count < 0)
+ {
+ rc = cert_count;
+ exim_gnutls_err_check(US"gnutls_certificate_set_x509_trust_file");
+ }
+ DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count);
}
- }
- }
-/* Associate the parameters with the x509 credentials structure. */
-
-gnutls_certificate_set_dh_params(x509_cred, dh_params);
+ if (setit_crl && state->tls_crl && *state->tls_crl)
+ {
+ if (state->exp_tls_crl && *state->exp_tls_crl)
+ {
+ DEBUG(D_tls) debug_printf("loading CRL file = %s\n", state->exp_tls_crl);
+ rc = gnutls_certificate_set_x509_crl_file(state->x509_cred,
+ CS state->exp_tls_crl, GNUTLS_X509_FMT_PEM);
+ exim_gnutls_err_check(US"gnutls_certificate_set_x509_crl_file");
+ }
+ }
+ } /* statbuf.st_size */
+ } /* tls_verify_certificates */
-DEBUG(D_tls) debug_printf("initialized certificate stuff\n");
return OK;
+/* also above, during verify_certificates/crl, during SNI, if unchanged */
}
/*************************************************
-* Remove from a priority list *
+* Initialize for GnuTLS *
*************************************************/
-/* Cautiously written so that it will remove duplicates if present.
+/* Called from both server and client code. In the case of a server, errors
+before actual TLS negotiation return DEFER.
Arguments:
- list a zero-terminated list
- remove_list a zero-terminated list to be removed
+ host connected host, if client; NULL if server
+ certificate certificate file
+ privatekey private key file
+ sni TLS SNI to send, sometimes when client; else NULL
+ cas CA certs file
+ crl CRL file
+ require_ciphers tls_require_ciphers setting
-Returns: nothing
+Returns: OK/DEFER/FAIL
*/
-static void
-remove_priority(int *list, int *remove_list)
+static int
+tls_init(
+ const host_item *host,
+ const uschar *certificate,
+ const uschar *privatekey,
+ const uschar *sni,
+ const uschar *cas,
+ const uschar *crl,
+ const uschar *require_ciphers,
+ exim_gnutls_state_st **caller_state)
{
-for (; *remove_list != 0; remove_list++)
+exim_gnutls_state_st *state;
+int rc;
+size_t sz;
+const char *errpos;
+uschar *p;
+BOOL want_default_priorities;
+
+if (!exim_gnutls_base_init_done)
{
- int *p = list;
- while (*p != 0)
+ DEBUG(D_tls) debug_printf("GnuTLS global init required.\n");
+
+ rc = gnutls_global_init();
+ exim_gnutls_err_check(US"gnutls_global_init");
+
+#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0
+ DEBUG(D_tls)
{
- if (*p == *remove_list)
- {
- int *pp = p;
- do { pp[0] = pp[1]; pp++; } while (*pp != 0);
- }
- else p++;
+ gnutls_global_set_log_function(exim_gnutls_logger_cb);
+ /* arbitrarily chosen level; bump upto 9 for more */
+ gnutls_global_set_log_level(EXIM_GNUTLS_LIBRARY_LOG_LEVEL);
}
+#endif
+
+ exim_gnutls_base_init_done = TRUE;
}
-}
+if (host)
+ {
+ state = &state_client;
+ memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
+ DEBUG(D_tls) debug_printf("initialising GnuTLS client session\n");
+ rc = gnutls_init(&state->session, GNUTLS_CLIENT);
+ }
+else
+ {
+ state = &state_server;
+ memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
+ DEBUG(D_tls) debug_printf("initialising GnuTLS server session\n");
+ rc = gnutls_init(&state->session, GNUTLS_SERVER);
+ }
+exim_gnutls_err_check(US"gnutls_init");
+state->host = host;
-/*************************************************
-* Add to a priority list *
-*************************************************/
+state->tls_certificate = certificate;
+state->tls_privatekey = privatekey;
+state->tls_sni = sni;
+state->tls_verify_certificates = cas;
+state->tls_crl = crl;
-/* Cautiously written to check the list size
+rc = gnutls_certificate_allocate_credentials(&state->x509_cred);
+exim_gnutls_err_check(US"gnutls_certificate_allocate_credentials");
-Arguments:
- list a zero-terminated list
- list_max maximum offset in the list
- add_list a zero-terminated list to be added
+/* This handles the variables that might get re-expanded after TLS SNI;
+that's tls_certificate, tls_privatekey, tls_verify_certificates, tls_crl */
-Returns: TRUE if OK; FALSE if list overflows
-*/
+DEBUG(D_tls)
+ debug_printf("Expanding various TLS configuration options for session credentials.\n");
+rc = tls_expand_session_files(state);
+if (rc != OK) return rc;
-static BOOL
-add_priority(int *list, int list_max, int *add_list)
-{
-int next = 0;
-while (list[next] != 0) next++;
-while (*add_list != 0)
+/* Create D-H parameters, or read them from the cache file. This function does
+its own SMTP error messaging. This only happens for the server, TLS D-H ignores
+client-side params. */
+
+if (!host)
{
- if (next >= list_max) return FALSE;
- list[next++] = *add_list++;
+ rc = init_server_dh();
+ if (rc != OK) return rc;
+ gnutls_certificate_set_dh_params(state->x509_cred, dh_server_params);
}
-list[next] = 0;
-return TRUE;
-}
-
-
-
-/*************************************************
-* Adjust a priority list *
-*************************************************/
-
-/* This function is called to adjust the lists of cipher algorithms, MAC
-algorithms, key-exchange methods, and protocols.
-Arguments:
- plist the appropriate priority list
- psize the length of the list
- s the configuation string
- index the index of recognized strings
- isize the length of the index
+/* Link the credentials to the session. */
+rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred);
+exim_gnutls_err_check(US"gnutls_credentials_set");
- which text for an error message
+/* set SNI in client, only */
+if (host)
+ {
+ if (!expand_check_tlsvar(tls_sni))
+ return DEFER;
+ if (state->exp_tls_sni && *state->exp_tls_sni)
+ {
+ DEBUG(D_tls)
+ debug_printf("Setting TLS client SNI to \"%s\"\n", state->exp_tls_sni);
+ sz = Ustrlen(state->exp_tls_sni);
+ rc = gnutls_server_name_set(state->session,
+ GNUTLS_NAME_DNS, state->exp_tls_sni, sz);
+ exim_gnutls_err_check(US"gnutls_server_name_set");
+ }
+ }
+else if (state->tls_sni)
+ DEBUG(D_tls) debug_printf("*** PROBABLY A BUG *** " \
+ "have an SNI set for a client [%s]\n", state->tls_sni);
-Returns: FALSE if the table overflows, else TRUE
-*/
+/* This is the priority string support,
+http://www.gnu.org/software/gnutls/manual/html_node/Priority-Strings.html
+and replaces gnutls_require_kx, gnutls_require_mac & gnutls_require_protocols.
+This was backwards incompatible, but means Exim no longer needs to track
+all algorithms and provide string forms for them. */
-static BOOL
-set_priority(int *plist, int psize, uschar *s, pri_item *index, int isize,
- uschar *which)
-{
-int sep = 0;
-BOOL first = TRUE;
-uschar *t;
+want_default_priorities = TRUE;
-while ((t = string_nextinlist(&s, &sep, big_buffer, big_buffer_size)) != NULL)
+if (state->tls_require_ciphers && *state->tls_require_ciphers)
{
- int i;
- BOOL exclude = t[0] == '!';
- if (first && !exclude) plist[0] = 0;
- first = FALSE;
- for (i = 0; i < isize; i++)
+ if (!expand_check_tlsvar(tls_require_ciphers))
+ return DEFER;
+ if (state->exp_tls_require_ciphers && *state->exp_tls_require_ciphers)
{
- uschar *ss = strstric(t, index[i].name, FALSE);
- if (ss != NULL)
- {
- uschar *endss = ss + Ustrlen(index[i].name);
- if ((ss == t || !isalnum(ss[-1])) && !isalnum(*endss))
- {
- if (exclude)
- remove_priority(plist, index[i].values);
- else
- {
- if (!add_priority(plist, psize, index[i].values))
- {
- log_write(0, LOG_MAIN|LOG_PANIC, "GnuTLS init failed: %s "
- "priority table overflow", which);
- return FALSE;
- }
- }
- }
- }
+ DEBUG(D_tls) debug_printf("GnuTLS session cipher/priority \"%s\"\n",
+ state->exp_tls_require_ciphers);
+
+ rc = gnutls_priority_init(&state->priority_cache,
+ CS state->exp_tls_require_ciphers, &errpos);
+ want_default_priorities = FALSE;
+ p = state->exp_tls_require_ciphers;
}
}
+if (want_default_priorities)
+ {
+ rc = gnutls_priority_init(&state->priority_cache,
+ exim_default_gnutls_priority, &errpos);
+ p = US exim_default_gnutls_priority;
+ }
-DEBUG(D_tls)
+exim_gnutls_err_check(string_sprintf(
+ "gnutls_priority_init(%s) failed at offset %ld, \"%.6s..\"",
+ p, errpos - CS p, errpos));
+
+rc = gnutls_priority_set(state->session, state->priority_cache);
+exim_gnutls_err_check(US"gnutls_priority_set");
+
+gnutls_db_set_cache_expiration(state->session, ssl_session_timeout);
+
+/* Reduce security in favour of increased compatibility, if the admin
+decides to make that trade-off. */
+if (gnutls_compat_mode)
{
- int *ptr = plist;
- debug_printf("adjusted %s priorities:", which);
- while (*ptr != 0) debug_printf(" %d", *ptr++);
- debug_printf("\n");
+#if LIBGNUTLS_VERSION_NUMBER >= 0x020104
+ DEBUG(D_tls) debug_printf("lowering GnuTLS security, compatibility mode\n");
+ gnutls_session_enable_compatibility_mode(state->session);
+#else
+ DEBUG(D_tls) debug_printf("Unable to set gnutls_compat_mode - GnuTLS version too old\n");
+#endif
}
-return TRUE;
+*caller_state = state;
+/* needs to happen before callbacks during handshake */
+current_global_tls_state = state;
+return OK;
}
/*************************************************
-* Initialize a single GNUTLS session *
+* Extract peer information *
*************************************************/
-/* Set the algorithm, the db backend, whether to request certificates etc.
-
-TLS in Exim was first implemented using OpenSSL. This has a function to which
-you pass a list of cipher suites that are permitted/not permitted. GnuTLS works
-differently. It operates using priority lists for the different components of
-cipher suites.
-
-For compatibility of configuration, we scan a list of cipher suites and set
-priorities therefrom. However, at the moment, we pay attention only to the bulk
-cipher.
+/* Called from both server and client code.
+Only this is allowed to set state->peerdn and we use that to detect double-calls.
Arguments:
- side one of GNUTLS_SERVER, GNUTLS_CLIENT
- expciphers expanded ciphers list or NULL
- expmac expanded MAC list or NULL
- expkx expanded key-exchange list or NULL
- expproto expanded protocol list or NULL
+ state exim_gnutls_state_st *
-Returns: a gnutls_session, or NULL if there is a problem
+Returns: OK/DEFER/FAIL
*/
-static gnutls_session
-tls_session_init(int side, uschar *expciphers, uschar *expmac, uschar *expkx,
- uschar *expproto)
+static int
+peer_status(exim_gnutls_state_st *state)
{
-gnutls_session session;
+const gnutls_datum *cert_list;
+int rc;
+unsigned int cert_list_size = 0;
+gnutls_certificate_type_t ct;
+gnutls_x509_crt_t crt;
+uschar *dn_buf;
+size_t sz;
-gnutls_init(&session, side);
+if (state->peerdn)
+ return OK;
-/* Initialize the lists of permitted protocols, key-exchange methods, ciphers,
-and MACs. */
+state->peerdn = US"unknown";
-memcpy(cipher_priority, default_cipher_priority, sizeof(cipher_priority));
-memcpy(mac_priority, default_mac_priority, sizeof(mac_priority));
-memcpy(kx_priority, default_kx_priority, sizeof(kx_priority));
-memcpy(proto_priority, default_proto_priority, sizeof(proto_priority));
+cert_list = gnutls_certificate_get_peers(state->session, &cert_list_size);
-/* The names OpenSSL uses in tls_require_ciphers are of the form DES-CBC3-SHA,
-using hyphen separators. GnuTLS uses underscore separators. So that I can use
-either form for tls_require_ciphers in my tests, and also for general
-convenience, we turn hyphens into underscores before scanning the list. */
+if (cert_list == NULL || cert_list_size == 0)
+ {
+ state->peerdn = US"unknown (no certificate)";
+ DEBUG(D_tls) debug_printf("TLS: no certificate from peer (%p & %d)\n",
+ cert_list, cert_list_size);
+ if (state->verify_requirement == VERIFY_REQUIRED)
+ return tls_error(US"certificate verification failed",
+ "no certificate received from peer", state->host);
+ return OK;
+ }
-if (expciphers != NULL)
+ct = gnutls_certificate_type_get(state->session);
+if (ct != GNUTLS_CRT_X509)
{
- uschar *s = expciphers;
- while (*s != 0) { if (*s == '-') *s = '_'; s++; }
+ const char *ctn = gnutls_certificate_type_get_name(ct);
+ state->peerdn = string_sprintf("unknown (type %s)", ctn);
+ DEBUG(D_tls)
+ debug_printf("TLS: peer cert not X.509 but instead \"%s\"\n", ctn);
+ if (state->verify_requirement == VERIFY_REQUIRED)
+ return tls_error(US"certificate verification not possible, unhandled type",
+ ctn, state->host);
+ return OK;
}
-if ((expciphers != NULL &&
- !set_priority(cipher_priority, sizeof(cipher_priority)/sizeof(int),
- expciphers, cipher_index, sizeof(cipher_index)/sizeof(pri_item),
- US"cipher")) ||
- (expmac != NULL &&
- !set_priority(mac_priority, sizeof(mac_priority)/sizeof(int),
- expmac, mac_index, sizeof(mac_index)/sizeof(pri_item),
- US"MAC")) ||
- (expkx != NULL &&
- !set_priority(kx_priority, sizeof(kx_priority)/sizeof(int),
- expkx, kx_index, sizeof(kx_index)/sizeof(pri_item),
- US"key-exchange")) ||
- (expproto != NULL &&
- !set_priority(proto_priority, sizeof(proto_priority)/sizeof(int),
- expproto, proto_index, sizeof(proto_index)/sizeof(pri_item),
- US"protocol")))
+#define exim_gnutls_peer_err(Label) do { \
+ if (rc != GNUTLS_E_SUCCESS) { \
+ DEBUG(D_tls) debug_printf("TLS: peer cert problem: %s: %s\n", (Label), gnutls_strerror(rc)); \
+ if (state->verify_requirement == VERIFY_REQUIRED) { return tls_error((Label), gnutls_strerror(rc), state->host); } \
+ return OK; } } while (0)
+
+rc = gnutls_x509_crt_init(&crt);
+exim_gnutls_peer_err(US"gnutls_x509_crt_init (crt)");
+
+rc = gnutls_x509_crt_import(crt, &cert_list[0], GNUTLS_X509_FMT_DER);
+exim_gnutls_peer_err(US"failed to import certificate [gnutls_x509_crt_import(cert 0)]");
+sz = 0;
+rc = gnutls_x509_crt_get_dn(crt, NULL, &sz);
+if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
{
- gnutls_deinit(session);
- return NULL;
+ exim_gnutls_peer_err(US"getting size for cert DN failed");
+ return FAIL; /* should not happen */
}
+dn_buf = store_get_perm(sz);
+rc = gnutls_x509_crt_get_dn(crt, CS dn_buf, &sz);
+exim_gnutls_peer_err(US"failed to extract certificate DN [gnutls_x509_crt_get_dn(cert 0)]");
+state->peerdn = dn_buf;
+
+return OK;
+#undef exim_gnutls_peer_err
+}
-/* Define the various priorities */
-gnutls_cipher_set_priority(session, cipher_priority);
-gnutls_compression_set_priority(session, comp_priority);
-gnutls_kx_set_priority(session, kx_priority);
-gnutls_protocol_set_priority(session, proto_priority);
-gnutls_mac_set_priority(session, mac_priority);
-gnutls_cred_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred);
-gnutls_dh_set_prime_bits(session, DH_BITS);
+/*************************************************
+* Verify peer certificate *
+*************************************************/
-/* Request or demand a certificate of the peer, as configured. This will
-happen only in a server. */
+/* Called from both server and client code.
+*Should* be using a callback registered with
+gnutls_certificate_set_verify_function() to fail the handshake if we dislike
+the peer information, but that's too new for some OSes.
-if (verify_requirement != VERIFY_NONE)
- gnutls_certificate_server_set_request(session,
- (verify_requirement == VERIFY_OPTIONAL)?
- GNUTLS_CERT_REQUEST : GNUTLS_CERT_REQUIRE);
+Arguments:
+ state exim_gnutls_state_st *
+ error where to put an error message
-gnutls_db_set_cache_expiration(session, ssl_session_timeout);
+Returns:
+ FALSE if the session should be rejected
+ TRUE if the cert is okay or we just don't care
+*/
-/* Reduce security in favour of increased compatibility, if the admin
-decides to make that trade-off. */
-if (gnutls_compat_mode)
+static BOOL
+verify_certificate(exim_gnutls_state_st *state, const char **error)
+{
+int rc;
+unsigned int verify;
+
+*error = NULL;
+
+rc = peer_status(state);
+if (rc != OK)
{
-#if LIBGNUTLS_VERSION_NUMBER >= 0x020104
- DEBUG(D_tls) debug_printf("lowering GnuTLS security, compatibility mode\n");
- gnutls_session_enable_compatibility_mode(session);
-#else
- DEBUG(D_tls) debug_printf("Unable to set gnutls_compat_mode - GnuTLS version too old\n");
-#endif
+ verify = GNUTLS_CERT_INVALID;
+ *error = "not supplied";
+ }
+else
+ {
+ rc = gnutls_certificate_verify_peers2(state->session, &verify);
}
-DEBUG(D_tls) debug_printf("initialized GnuTLS session\n");
-return session;
-}
+/* Handle the result of verification. INVALID seems to be set as well
+as REVOKED, but leave the test for both. */
+if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
+ {
+ state->peer_cert_verified = FALSE;
+ if (*error == NULL)
+ *error = ((verify & GNUTLS_CERT_REVOKED) != 0) ? "revoked" : "invalid";
+ DEBUG(D_tls)
+ debug_printf("TLS certificate verification failed (%s): peerdn=%s\n",
+ *error, state->peerdn);
-/*************************************************
-* Get name of cipher in use *
-*************************************************/
+ if (state->verify_requirement == VERIFY_REQUIRED)
+ {
+ gnutls_alert_send(state->session, GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
+ return FALSE;
+ }
+ DEBUG(D_tls)
+ debug_printf("TLS verify failure overriden (host in tls_try_verify_hosts)\n");
+ }
+else
+ {
+ state->peer_cert_verified = TRUE;
+ DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n", state->peerdn);
+ }
-/* The answer is left in a static buffer, and tls_cipher is set to point
-to it.
+tls_peerdn = state->peerdn;
-Argument: pointer to a GnuTLS session
-Returns: nothing
-*/
+return TRUE;
+}
+
+
+
+/* ------------------------------------------------------------------------ */
+/* Callbacks */
+
+/* Logging function which can be registered with
+ * gnutls_global_set_log_function()
+ * gnutls_global_set_log_level() 0..9
+ */
static void
-construct_cipher_name(gnutls_session session)
+exim_gnutls_logger_cb(int level, const char *message)
{
-static uschar cipherbuf[256];
-uschar *ver;
-int c, kx, mac;
-#ifdef GNUTLS_CB_TLS_UNIQUE
-int rc;
-gnutls_datum_t channel;
-#endif
+ DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s\n", level, message);
+}
-ver = string_copy(
- US gnutls_protocol_get_name(gnutls_protocol_get_version(session)));
-if (Ustrncmp(ver, "TLS ", 4) == 0) ver[3] = '-'; /* Don't want space */
-c = gnutls_cipher_get(session);
-/* returns size in "bytes" */
-tls_bits = gnutls_cipher_get_key_size(c) * 8;
+/* Called after client hello, should handle SNI work.
+This will always set tls_sni (state->received_sni) if available,
+and may trigger presenting different certificates,
+if state->trigger_sni_changes is TRUE.
-mac = gnutls_mac_get(session);
-kx = gnutls_kx_get(session);
+Should be registered with
+ gnutls_handshake_set_post_client_hello_function()
-string_format(cipherbuf, sizeof(cipherbuf), "%s:%s:%u", ver,
- gnutls_cipher_suite_get_name(kx, c, mac), tls_bits);
-tls_cipher = cipherbuf;
+"This callback must return 0 on success or a gnutls error code to terminate the
+handshake.".
-DEBUG(D_tls) debug_printf("cipher: %s\n", cipherbuf);
+For inability to get SNI information, we return 0.
+We only return non-zero if re-setup failed.
+*/
-if (tls_channelbinding_b64)
- free(tls_channelbinding_b64);
-tls_channelbinding_b64 = NULL;
+static int
+exim_sni_handling_cb(gnutls_session_t session)
+{
+char sni_name[MAX_HOST_LEN];
+size_t data_len = MAX_HOST_LEN;
+exim_gnutls_state_st *state = current_global_tls_state;
+unsigned int sni_type;
+int rc, old_pool;
+
+rc = gnutls_server_name_get(session, sni_name, &data_len, &sni_type, 0);
+exim_gnutls_err_debugreturn0("gnutls_server_name_get()");
+if (sni_type != GNUTLS_NAME_DNS)
+ {
+ DEBUG(D_tls) debug_printf("TLS: ignoring SNI of unhandled type %u\n", sni_type);
+ return 0;
+ }
-#ifdef GNUTLS_CB_TLS_UNIQUE
-channel = { NULL, 0 };
-rc = gnutls_session_channel_binding(session, GNUTLS_CB_TLS_UNIQUE, &channel);
-if (rc) {
- DEBUG(D_tls) debug_printf("Channel binding error: %s\n", gnutls_strerror(rc));
-} else {
- tls_channelbinding_b64 = auth_b64encode(channel.data, (int)channel.size);
- DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage.\n");
-}
-#endif
+/* We now have a UTF-8 string in sni_name */
+old_pool = store_pool;
+store_pool = POOL_PERM;
+state->received_sni = string_copyn(US sni_name, data_len);
+store_pool = old_pool;
+
+/* We set this one now so that variable expansions below will work */
+tls_sni = state->received_sni;
+
+DEBUG(D_tls) debug_printf("Received TLS SNI \"%s\"%s\n", sni_name,
+ state->trigger_sni_changes ? "" : " (unused for certificate selection)");
+
+if (!state->trigger_sni_changes)
+ return 0;
+
+rc = tls_expand_session_files(state);
+if (rc != OK)
+ {
+ /* If the setup of certs/etc failed before handshake, TLS would not have
+ been offered. The best we can do now is abort. */
+ return GNUTLS_E_APPLICATION_ERROR_MIN;
+ }
+
+rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred);
+return (rc == GNUTLS_E_SUCCESS) ? 0 : rc;
}
+
+/* ------------------------------------------------------------------------ */
+/* Exported functions */
+
+
+
+
/*************************************************
* Start a TLS session in a server *
*************************************************/
@@ -905,9 +1180,6 @@ a TLS session.
Arguments:
require_ciphers list of allowed ciphers or NULL
- require_mac list of allowed MACs or NULL
- require_kx list of allowed key_exchange methods or NULL
- require_proto list of allowed protocols or NULL
Returns: OK on success
DEFER for errors before the start of the negotiation
@@ -916,21 +1188,18 @@ Returns: OK on success
*/
int
-tls_server_start(uschar *require_ciphers, uschar *require_mac,
- uschar *require_kx, uschar *require_proto)
+tls_server_start(const uschar *require_ciphers)
{
int rc;
const char *error;
-uschar *expciphers = NULL;
-uschar *expmac = NULL;
-uschar *expkx = NULL;
-uschar *expproto = NULL;
+exim_gnutls_state_st *state = NULL;
/* Check for previous activation */
+/* nb: this will not be TLS callout safe, needs reworking as part of that. */
if (tls_active >= 0)
{
- tls_error("STARTTLS received after TLS started", NULL, "");
+ tls_error(US"STARTTLS received after TLS started", "", NULL);
smtp_printf("554 Already in TLS\r\n");
return FAIL;
}
@@ -938,36 +1207,40 @@ if (tls_active >= 0)
/* Initialize the library. If it fails, it will already have logged the error
and sent an SMTP response. */
-DEBUG(D_tls) debug_printf("initializing GnuTLS as a server\n");
+DEBUG(D_tls) debug_printf("initialising GnuTLS as a server\n");
-rc = tls_init(NULL, tls_certificate, tls_privatekey, tls_verify_certificates,
- tls_crl);
+rc = tls_init(NULL, tls_certificate, tls_privatekey,
+ NULL, tls_verify_certificates, tls_crl,
+ require_ciphers, &state);
if (rc != OK) return rc;
-if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers) ||
- !expand_check(require_mac, US"gnutls_require_mac", &expmac) ||
- !expand_check(require_kx, US"gnutls_require_kx", &expkx) ||
- !expand_check(require_proto, US"gnutls_require_proto", &expproto))
- return FAIL;
-
/* If this is a host for which certificate verification is mandatory or
optional, set up appropriately. */
-tls_certificate_verified = FALSE;
-verify_requirement = VERIFY_NONE;
-
if (verify_check_host(&tls_verify_hosts) == OK)
- verify_requirement = VERIFY_REQUIRED;
+ {
+ DEBUG(D_tls) debug_printf("TLS: a client certificate will be required.\n");
+ state->verify_requirement = VERIFY_REQUIRED;
+ gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUIRE);
+ }
else if (verify_check_host(&tls_try_verify_hosts) == OK)
- verify_requirement = VERIFY_OPTIONAL;
+ {
+ DEBUG(D_tls) debug_printf("TLS: a client certificate will be requested but not required.\n");
+ state->verify_requirement = VERIFY_OPTIONAL;
+ gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUEST);
+ }
+else
+ {
+ DEBUG(D_tls) debug_printf("TLS: a client certificate will not be requested.\n");
+ state->verify_requirement = VERIFY_NONE;
+ gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_IGNORE);
+ }
-/* Prepare for new connection */
+/* Register SNI handling; always, even if not in tls_certificate, so that the
+expansion variable $tls_sni is always available. */
-tls_session = tls_session_init(GNUTLS_SERVER, expciphers, expmac, expkx,
- expproto);
-if (tls_session == NULL)
- return tls_error(US"tls_session_init", NULL,
- gnutls_strerror(GNUTLS_E_MEMORY_ERROR));
+gnutls_handshake_set_post_client_hello_function(state->session,
+ exim_sni_handling_cb);
/* Set context and tell client to go ahead, except in the case of TLS startup
on connection, where outputting anything now upsets the clients and tends to
@@ -984,19 +1257,24 @@ if (!tls_on_connect)
/* Now negotiate the TLS session. We put our own timer on it, since it seems
that the GnuTLS library doesn't. */
-gnutls_transport_set_ptr2(tls_session, (gnutls_transport_ptr)fileno(smtp_in),
- (gnutls_transport_ptr)fileno(smtp_out));
+gnutls_transport_set_ptr2(state->session,
+ (gnutls_transport_ptr)fileno(smtp_in),
+ (gnutls_transport_ptr)fileno(smtp_out));
+state->fd_in = fileno(smtp_in);
+state->fd_out = fileno(smtp_out);
sigalrm_seen = FALSE;
if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout);
-rc = gnutls_handshake(tls_session);
+do
+ {
+ rc = gnutls_handshake(state->session);
+ } while ((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED));
alarm(0);
-if (rc < 0)
+if (rc != GNUTLS_E_SUCCESS)
{
- tls_error(US"gnutls_handshake", NULL,
- sigalrm_seen ? "timed out" : gnutls_strerror(rc));
-
+ tls_error(US"gnutls_handshake",
+ sigalrm_seen ? "timed out" : gnutls_strerror(rc), NULL);
/* It seems that, except in the case of a timeout, we have to close the
connection right here; otherwise if the other end is running OpenSSL it hangs
until the server times out. */
@@ -1012,21 +1290,39 @@ if (rc < 0)
DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n");
-if (verify_requirement != VERIFY_NONE &&
- !verify_certificate(tls_session, &error))
+/* Verify after the fact */
+
+if (state->verify_requirement != VERIFY_NONE)
{
- tls_error(US"certificate verification failed", NULL, error);
- return FAIL;
+ if (!verify_certificate(state, &error))
+ {
+ if (state->verify_requirement == VERIFY_OPTIONAL)
+ {
+ DEBUG(D_tls)
+ debug_printf("TLS: continuing on only because verification was optional, after: %s\n",
+ error);
+ }
+ else
+ {
+ tls_error(US"certificate verification failed", error, NULL);
+ return FAIL;
+ }
+ }
}
-construct_cipher_name(tls_session);
+/* Figure out peer DN, and if authenticated, etc. */
+
+rc = peer_status(state);
+if (rc != OK) return rc;
+
+/* Sets various Exim expansion variables; always safe within server */
+
+extract_exim_vars_from_tls_state(state);
/* TLS has been set up. Adjust the input functions to read via TLS,
and initialize appropriately. */
-ssl_xfer_buffer = store_malloc(ssl_xfer_buffer_size);
-ssl_xfer_buffer_lwm = ssl_xfer_buffer_hwm = 0;
-ssl_xfer_eof = ssl_xfer_error = 0;
+state->xfer_buffer = store_malloc(ssl_xfer_buffer_size);
receive_getc = tls_getc;
receive_ungetc = tls_ungetc;
@@ -1034,8 +1330,6 @@ receive_feof = tls_feof;
receive_ferror = tls_ferror;
receive_smtp_buffered = tls_smtp_buffered;
-tls_active = fileno(smtp_out);
-
return OK;
}
@@ -1052,16 +1346,13 @@ Arguments:
fd the fd of the connection
host connected host (for messages)
addr the first address (not used)
- dhparam DH parameter file
+ dhparam DH parameter file (ignored, we're a client)
certificate certificate file
privatekey private key file
sni TLS SNI to send to remote host
verify_certs file for certificate verify
verify_crl CRL for verify
require_ciphers list of allowed ciphers or NULL
- require_mac list of allowed MACs or NULL
- require_kx list of allowed key_exchange methods or NULL
- require_proto list of allowed protocols or NULL
timeout startup timeout
Returns: OK/DEFER/FAIL (because using common functions),
@@ -1069,114 +1360,116 @@ Returns: OK/DEFER/FAIL (because using common functions),
*/
int
-tls_client_start(int fd, host_item *host, address_item *addr, uschar *dhparam,
- uschar *certificate, uschar *privatekey, uschar *sni ARG_UNUSED,
- uschar *verify_certs, uschar *verify_crl,
- uschar *require_ciphers, uschar *require_mac,
- uschar *require_kx, uschar *require_proto, int timeout)
+tls_client_start(int fd, host_item *host,
+ address_item *addr ARG_UNUSED, uschar *dhparam ARG_UNUSED,
+ uschar *certificate, uschar *privatekey, uschar *sni,
+ uschar *verify_certs, uschar *verify_crl,
+ uschar *require_ciphers, int timeout)
{
-const gnutls_datum *server_certs;
-uschar *expciphers = NULL;
-uschar *expmac = NULL;
-uschar *expkx = NULL;
-uschar *expproto = NULL;
-const char *error;
-unsigned int server_certs_size;
int rc;
+const char *error;
+exim_gnutls_state_st *state = NULL;
-DEBUG(D_tls) debug_printf("initializing GnuTLS as a client\n");
+DEBUG(D_tls) debug_printf("initialising GnuTLS as a client on fd %d\n", fd);
-verify_requirement = (verify_certs == NULL)? VERIFY_NONE : VERIFY_REQUIRED;
-rc = tls_init(host, certificate, privatekey, verify_certs, verify_crl);
+rc = tls_init(host, certificate, privatekey,
+ sni, verify_certs, verify_crl, require_ciphers, &state);
if (rc != OK) return rc;
-if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers) ||
- !expand_check(require_mac, US"gnutls_require_mac", &expmac) ||
- !expand_check(require_kx, US"gnutls_require_kx", &expkx) ||
- !expand_check(require_proto, US"gnutls_require_proto", &expproto))
- return FAIL;
-
-tls_session = tls_session_init(GNUTLS_CLIENT, expciphers, expmac, expkx,
- expproto);
+gnutls_dh_set_prime_bits(state->session, EXIM_CLIENT_DH_MIN_BITS);
-if (tls_session == NULL)
- return tls_error(US "tls_session_init", host,
- gnutls_strerror(GNUTLS_E_MEMORY_ERROR));
+if (verify_certs == NULL)
+ {
+ DEBUG(D_tls) debug_printf("TLS: server certificate verification not required\n");
+ state->verify_requirement = VERIFY_NONE;
+ /* we still ask for it, to log it, etc */
+ gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUEST);
+ }
+else
+ {
+ DEBUG(D_tls) debug_printf("TLS: server certificate verification required\n");
+ state->verify_requirement = VERIFY_REQUIRED;
+ gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUIRE);
+ }
-gnutls_transport_set_ptr(tls_session, (gnutls_transport_ptr)fd);
+gnutls_transport_set_ptr(state->session, (gnutls_transport_ptr)fd);
+state->fd_in = fd;
+state->fd_out = fd;
/* There doesn't seem to be a built-in timeout on connection. */
sigalrm_seen = FALSE;
alarm(timeout);
-rc = gnutls_handshake(tls_session);
+do
+ {
+ rc = gnutls_handshake(state->session);
+ } while ((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED));
alarm(0);
-if (rc < 0)
- return tls_error(US "gnutls_handshake", host,
- sigalrm_seen ? "timed out" : gnutls_strerror(rc));
+DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n");
-server_certs = gnutls_certificate_get_peers(tls_session, &server_certs_size);
+/* Verify late */
-if (server_certs != NULL)
- {
- uschar buff[1024];
- gnutls_x509_crt gcert;
+if (state->verify_requirement != VERIFY_NONE &&
+ !verify_certificate(state, &error))
+ return tls_error(US"certificate verification failed", error, state->host);
- gnutls_x509_crt_init(&gcert);
- tls_peerdn = US"unknown";
+/* Figure out peer DN, and if authenticated, etc. */
- if (gnutls_x509_crt_import(gcert, server_certs, GNUTLS_X509_FMT_DER) == 0)
- {
- size_t bufsize = sizeof(buff);
- if (gnutls_x509_crt_get_dn(gcert, CS buff, &bufsize) >= 0)
- tls_peerdn = string_copy_malloc(buff);
- }
- }
+rc = peer_status(state);
+if (rc != OK) return rc;
-/* Should we also verify the hostname here? */
+/* Sets various Exim expansion variables; always safe within server */
-if (verify_requirement != VERIFY_NONE &&
- !verify_certificate(tls_session, &error))
- return tls_error(US"certificate verification failed", host, error);
+extract_exim_vars_from_tls_state(state);
-construct_cipher_name(tls_session); /* Sets tls_cipher */
-tls_active = fd;
return OK;
}
+
/*************************************************
-* Deal with logging errors during I/O *
+* Close down a TLS session *
*************************************************/
-/* We have to get the identity of the peer from saved data.
-
-Argument:
- ec the GnuTLS error code, or 0 if it's a local error
- when text identifying read or write
- text local error text when ec is 0
+/* This is also called from within a delivery subprocess forked from the
+daemon, to shut down the TLS library, without actually doing a shutdown (which
+would tamper with the TLS session in the parent process).
-Returns: nothing
+Arguments: TRUE if gnutls_bye is to be called
+Returns: nothing
*/
-static void
-record_io_error(int ec, uschar *when, uschar *text)
+void
+tls_close(BOOL shutdown)
{
-const char *msg;
+exim_gnutls_state_st *state = current_global_tls_state;
-if (ec == GNUTLS_E_FATAL_ALERT_RECEIVED)
- msg = string_sprintf("%s: %s", gnutls_strerror(ec),
- gnutls_alert_get_name(gnutls_alert_get(tls_session)));
-else
- msg = gnutls_strerror(ec);
+if (tls_active < 0) return; /* TLS was not active */
+
+if (shutdown)
+ {
+ DEBUG(D_tls) debug_printf("tls_close(): shutting down TLS\n");
+ gnutls_bye(state->session, GNUTLS_SHUT_WR);
+ }
+
+gnutls_deinit(state->session);
+
+memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
+
+if ((state_server.session == NULL) && (state_client.session == NULL))
+ {
+ gnutls_global_deinit();
+ exim_gnutls_base_init_done = FALSE;
+ }
-tls_error(when, client_host, msg);
+tls_active = -1;
}
+
/*************************************************
* TLS version of getc *
*************************************************/
@@ -1184,6 +1477,8 @@ tls_error(when, client_host, msg);
/* This gets the next byte from the TLS input buffer. If the buffer is empty,
it refills the buffer via the GnuTLS reading function.
+This feeds DKIM and should be used for all message-body reads.
+
Arguments: none
Returns: the next character or EOF
*/
@@ -1191,15 +1486,16 @@ Returns: the next character or EOF
int
tls_getc(void)
{
-if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
+exim_gnutls_state_st *state = current_global_tls_state;
+if (state->xfer_buffer_lwm >= state->xfer_buffer_hwm)
{
- int inbytes;
+ ssize_t inbytes;
- DEBUG(D_tls) debug_printf("Calling gnutls_record_recv(%lx, %lx, %u)\n",
- (long) tls_session, (long) ssl_xfer_buffer, ssl_xfer_buffer_size);
+ DEBUG(D_tls) debug_printf("Calling gnutls_record_recv(%p, %p, %u)\n",
+ state->session, state->xfer_buffer, ssl_xfer_buffer_size);
if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout);
- inbytes = gnutls_record_recv(tls_session, CS ssl_xfer_buffer,
+ inbytes = gnutls_record_recv(state->session, state->xfer_buffer,
ssl_xfer_buffer_size);
alarm(0);
@@ -1217,9 +1513,12 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
receive_ferror = smtp_ferror;
receive_smtp_buffered = smtp_buffered;
- gnutls_deinit(tls_session);
- tls_session = NULL;
+ gnutls_deinit(state->session);
+ state->session = NULL;
tls_active = -1;
+ tls_bits = 0;
+ tls_certificate_verified = FALSE;
+ tls_channelbinding_b64 = NULL;
tls_cipher = NULL;
tls_peerdn = NULL;
@@ -1230,30 +1529,31 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
else if (inbytes < 0)
{
- record_io_error(inbytes, US"recv", NULL);
- ssl_xfer_error = 1;
+ record_io_error(state, (int) inbytes, US"recv", NULL);
+ state->xfer_error = 1;
return EOF;
}
#ifndef DISABLE_DKIM
- dkim_exim_verify_feed(ssl_xfer_buffer, inbytes);
+ dkim_exim_verify_feed(state->xfer_buffer, inbytes);
#endif
- ssl_xfer_buffer_hwm = inbytes;
- ssl_xfer_buffer_lwm = 0;
+ state->xfer_buffer_hwm = (int) inbytes;
+ state->xfer_buffer_lwm = 0;
}
-
/* Something in the buffer; return next uschar */
-return ssl_xfer_buffer[ssl_xfer_buffer_lwm++];
+return state->xfer_buffer[state->xfer_buffer_lwm++];
}
+
/*************************************************
* Read bytes from TLS channel *
*************************************************/
-/*
+/* This does not feed DKIM, so if the caller uses this for reading message body,
+then the caller must feed DKIM.
Arguments:
buff buffer of data
len size of buffer
@@ -1265,24 +1565,36 @@ Returns: the number of bytes read
int
tls_read(uschar *buff, size_t len)
{
-int inbytes;
+exim_gnutls_state_st *state = current_global_tls_state;
+ssize_t inbytes;
-DEBUG(D_tls) debug_printf("Calling gnutls_record_recv(%lx, %lx, %u)\n",
- (long) tls_session, (long) buff, len);
+if (len > INT_MAX)
+ len = INT_MAX;
-inbytes = gnutls_record_recv(tls_session, CS buff, len);
+if (state->xfer_buffer_lwm < state->xfer_buffer_hwm)
+ DEBUG(D_tls)
+ debug_printf("*** PROBABLY A BUG *** " \
+ "tls_read() called with data in the tls_getc() buffer, %d ignored\n",
+ state->xfer_buffer_hwm - state->xfer_buffer_lwm);
+
+DEBUG(D_tls)
+ debug_printf("Calling gnutls_record_recv(%p, %p, " SIZE_T_FMT ")\n",
+ state->session, buff, len);
+
+inbytes = gnutls_record_recv(state->session, buff, len);
if (inbytes > 0) return inbytes;
if (inbytes == 0)
{
DEBUG(D_tls) debug_printf("Got TLS_EOF\n");
}
-else record_io_error(inbytes, US"recv", NULL);
+else record_io_error(state, (int)inbytes, US"recv", NULL);
return -1;
}
+
/*************************************************
* Write bytes down TLS channel *
*************************************************/
@@ -1299,25 +1611,26 @@ Returns: the number of bytes after a successful write,
int
tls_write(const uschar *buff, size_t len)
{
-int outbytes;
-int left = len;
+ssize_t outbytes;
+size_t left = len;
+exim_gnutls_state_st *state = current_global_tls_state;
-DEBUG(D_tls) debug_printf("tls_do_write(%lx, %d)\n", (long) buff, left);
+DEBUG(D_tls) debug_printf("tls_do_write(%p, " SIZE_T_FMT ")\n", buff, left);
while (left > 0)
{
- DEBUG(D_tls) debug_printf("gnutls_record_send(SSL, %lx, %d)\n", (long)buff,
- left);
- outbytes = gnutls_record_send(tls_session, CS buff, left);
+ DEBUG(D_tls) debug_printf("gnutls_record_send(SSL, %p, " SIZE_T_FMT ")\n",
+ buff, left);
+ outbytes = gnutls_record_send(state->session, buff, left);
- DEBUG(D_tls) debug_printf("outbytes=%d\n", outbytes);
+ DEBUG(D_tls) debug_printf("outbytes=" SSIZE_T_FMT "\n", outbytes);
if (outbytes < 0)
{
- record_io_error(outbytes, US"send", NULL);
+ record_io_error(state, outbytes, US"send", NULL);
return -1;
}
if (outbytes == 0)
{
- record_io_error(0, US"send", US"TLS channel closed on write");
+ record_io_error(state, 0, US"send", US"TLS channel closed on write");
return -1;
}
@@ -1325,39 +1638,71 @@ while (left > 0)
buff += outbytes;
}
-return len;
+if (len > INT_MAX)
+ {
+ DEBUG(D_tls)
+ debug_printf("Whoops! Wrote more bytes (" SIZE_T_FMT ") than INT_MAX\n",
+ len);
+ len = INT_MAX;
+ }
+
+return (int) len;
}
+
/*************************************************
-* Close down a TLS session *
+* Random number generation *
*************************************************/
-/* This is also called from within a delivery subprocess forked from the
-daemon, to shut down the TLS library, without actually doing a shutdown (which
-would tamper with the TLS session in the parent process).
+/* Pseudo-random number generation. The result is not expected to be
+cryptographically strong but not so weak that someone will shoot themselves
+in the foot using it as a nonce in input in some email header scheme or
+whatever weirdness they'll twist this into. The result should handle fork()
+and avoid repeating sequences. OpenSSL handles that for us.
-Arguments: TRUE if gnutls_bye is to be called
-Returns: nothing
+Arguments:
+ max range maximum
+Returns a random number in range [0, max-1]
*/
-void
-tls_close(BOOL shutdown)
+int
+vaguely_random_number(int max)
{
-if (tls_active < 0) return; /* TLS was not active */
-
-if (shutdown)
+unsigned int r;
+int i, needed_len;
+uschar *p;
+uschar smallbuf[sizeof(r)];
+
+if (max <= 1)
+ return 0;
+
+needed_len = sizeof(r);
+/* Don't take 8 times more entropy than needed if int is 8 octets and we were
+ * asked for a number less than 10. */
+for (r = max, i = 0; r; ++i)
+ r >>= 1;
+i = (i + 7) / 8;
+if (i < needed_len)
+ needed_len = i;
+
+i = gnutls_rnd(GNUTLS_RND_NONCE, smallbuf, needed_len);
+if (i < 0)
{
- DEBUG(D_tls) debug_printf("tls_close(): shutting down TLS\n");
- gnutls_bye(tls_session, GNUTLS_SHUT_WR);
+ DEBUG(D_all) debug_printf("gnutls_rnd() failed, using fallback.\n");
+ return vaguely_random_number_fallback(max);
+ }
+r = 0;
+for (p = smallbuf; needed_len; --needed_len, ++p)
+ {
+ r *= 256;
+ r += *p;
}
-gnutls_deinit(tls_session);
-tls_session = NULL;
-gnutls_global_deinit();
-
-tls_active = -1;
+/* We don't particularly care about weighted results; if someone wants
+ * smooth distribution and cares enough then they should submit a patch then. */
+return r % max;
}
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index e609670ee..316fd5a90 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -779,11 +779,6 @@ a TLS session.
Arguments:
require_ciphers allowed ciphers
- ------------------------------------------------------
- require_mac list of allowed MACs ) Not used
- require_kx list of allowed key_exchange methods ) for
- require_proto list of allowed protocols ) OpenSSL
- ------------------------------------------------------
Returns: OK on success
DEFER for errors before the start of the negotiation
@@ -792,8 +787,7 @@ Returns: OK on success
*/
int
-tls_server_start(uschar *require_ciphers, uschar *require_mac,
- uschar *require_kx, uschar *require_proto)
+tls_server_start(const uschar *require_ciphers)
{
int rc;
uschar *expciphers;
@@ -819,8 +813,9 @@ if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers))
return FAIL;
/* In OpenSSL, cipher components are separated by hyphens. In GnuTLS, they
-are separated by underscores. So that I can use either form in my tests, and
-also for general convenience, we turn underscores into hyphens here. */
+were historically separated by underscores. So that I can use either form in my
+tests, and also for general convenience, we turn underscores into hyphens here.
+*/
if (expciphers != NULL)
{
@@ -954,11 +949,6 @@ Argument:
verify_certs file for certificate verify
crl file containing CRL
require_ciphers list of allowed ciphers
- ------------------------------------------------------
- require_mac list of allowed MACs ) Not used
- require_kx list of allowed key_exchange methods ) for
- require_proto list of allowed protocols ) OpenSSL
- ------------------------------------------------------
timeout startup timeout
Returns: OK on success
@@ -970,8 +960,7 @@ int
tls_client_start(int fd, host_item *host, address_item *addr, uschar *dhparam,
uschar *certificate, uschar *privatekey, uschar *sni,
uschar *verify_certs, uschar *crl,
- uschar *require_ciphers, uschar *require_mac, uschar *require_kx,
- uschar *require_proto, int timeout)
+ uschar *require_ciphers, int timeout)
{
static uschar txt[256];
uschar *expciphers;
@@ -1292,7 +1281,7 @@ fprintf(f, "Library version: OpenSSL: Compile: %s\n"
/*************************************************
-* Pseudo-random number generation *
+* Random number generation *
*************************************************/
/* Pseudo-random number generation. The result is not expected to be
@@ -1307,7 +1296,7 @@ Returns a random number in range [0, max-1]
*/
int
-pseudo_random_number(int max)
+vaguely_random_number(int max)
{
unsigned int r;
int i, needed_len;
@@ -1343,7 +1332,14 @@ if (i < needed_len)
needed_len = i;
/* We do not care if crypto-strong */
-(void) RAND_pseudo_bytes(smallbuf, needed_len);
+i = RAND_pseudo_bytes(smallbuf, needed_len);
+if (i < 0)
+ {
+ DEBUG(D_all)
+ debug_printf("OpenSSL RAND_pseudo_bytes() not supported by RAND method, using fallback.\n");
+ return vaguely_random_number_fallback(max);
+ }
+
r = 0;
for (p = smallbuf; needed_len; --needed_len, ++p)
{
diff --git a/src/src/tls.c b/src/src/tls.c
index d975a2c89..f0eba37f8 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -34,7 +34,7 @@ functions and the common functions below. */
static uschar *ssl_xfer_buffer = NULL;
-static int ssl_xfer_buffer_size = 4096;
+static const int ssl_xfer_buffer_size = 4096;
static int ssl_xfer_buffer_lwm = 0;
static int ssl_xfer_buffer_hwm = 0;
static int ssl_xfer_eof = 0;
@@ -59,11 +59,11 @@ Returns: TRUE if OK; result may still be NULL after forced failure
*/
static BOOL
-expand_check(uschar *s, uschar *name, uschar **result)
+expand_check(const uschar *s, const uschar *name, uschar **result)
{
if (s == NULL) *result = NULL; else
{
- *result = expand_string(s);
+ *result = expand_string(US s); /* need to clean up const some more */
if (*result == NULL && !expand_string_forcedfail)
{
log_write(0, LOG_MAIN|LOG_PANIC, "expansion of %s failed: %s", name,
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 6ec94d5da..b01322662 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -62,6 +62,9 @@ optionlist smtp_transport_options[] = {
{ "gethostbyname", opt_bool,
(void *)offsetof(smtp_transport_options_block, gethostbyname) },
#ifdef SUPPORT_TLS
+ /* These are no longer honoured, as of Exim 4.78; for now, we silently
+ ignore; a later release will warn, and a later-still release will remove
+ these options, so that using them becomes an error. */
{ "gnutls_require_kx", opt_stringptr,
(void *)offsetof(smtp_transport_options_block, gnutls_require_kx) },
{ "gnutls_require_mac", opt_stringptr,
@@ -1133,9 +1136,6 @@ if (tls_offered && !suppress_tls &&
ob->tls_verify_certificates,
ob->tls_crl,
ob->tls_require_ciphers,
- ob->gnutls_require_mac,
- ob->gnutls_require_kx,
- ob->gnutls_require_proto,
ob->command_timeout);
/* TLS negotiation failed; give an error. From outside, this function may