X-Git-Url: https://vcs.fsf.org/?p=exim.git;a=blobdiff_plain;f=src%2Fsrc%2Ftls-gnu.c;h=7e87dded0e0859f93dc6196ac047bb31e470fa91;hp=c81484c5b6c5defb38ca770cda5ae862ef9c99bf;hb=f675bf30a2ce6242cfc7c3e3997ec5d68a1fca7a;hpb=8e669ac162fe3b1040297f1d021de10778dce9d9 diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index c81484c5b..7e87dded0 100644 --- a/src/src/tls-gnu.c +++ b/src/src/tls-gnu.c @@ -1,10 +1,8 @@ -/* $Cambridge: exim/src/src/tls-gnu.c,v 1.5 2005/02/17 11:58:26 ph10 Exp $ */ - /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2005 */ +/* Copyright (c) University of Cambridge 1995 - 2009 */ /* See the file NOTICE for conditions of use and distribution. */ /* This module provides TLS (aka SSL) support for Exim using the GnuTLS @@ -14,6 +12,13 @@ is based on a patch that was contributed by Nikos Mavroyanopoulos. No cryptographic code is included in Exim. All this module does is to call functions from the GnuTLS library. */ +/* 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. */ /* Heading stuff for GnuTLS */ @@ -22,20 +27,18 @@ functions from the GnuTLS library. */ #define UNKNOWN_NAME "unknown" -#define DH_BITS 768 -#define RSA_BITS 512 +#define DH_BITS 1024 +#define PARAM_SIZE 2*1024 + -/* Values for verify_requirment and initialized */ +/* Values for verify_requirment */ enum { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED }; -enum { INITIALIZED_NOT, INITIALIZED_SERVER, INITIALIZED_CLIENT }; /* Local static variables for GNUTLS */ -static BOOL initialized = INITIALIZED_NOT; static host_item *client_host; -static gnutls_rsa_params rsa_params = NULL; static gnutls_dh_params dh_params = NULL; static gnutls_certificate_server_credentials x509_cred = NULL; @@ -46,18 +49,31 @@ static char ssl_errstring[256]; static int ssl_session_timeout = 200; static int verify_requirement; -/* Priorities for TLS algorithms to use. At present, only the cipher priority -vector can be altered. */ +/* Priorities for TLS algorithms to use. In each case there's a default table, +and space into which it can be copied and altered. */ + +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 }; -static const int protocol_priority[16] = { GNUTLS_TLS1, GNUTLS_SSL3, 0 }; +static int proto_priority[16]; -static const int kx_priority[16] = { +static const int default_kx_priority[16] = { GNUTLS_KX_RSA, GNUTLS_KX_DHE_DSS, GNUTLS_KX_DHE_RSA, - GNUTLS_KX_RSA_EXPORT, 0 }; +static int kx_priority[16]; + static int default_cipher_priority[16] = { GNUTLS_CIPHER_AES_256_CBC, GNUTLS_CIPHER_AES_128_CBC, @@ -67,21 +83,66 @@ static int default_cipher_priority[16] = { static int cipher_priority[16]; -static const int mac_priority[16] = { +static const int default_mac_priority[16] = { GNUTLS_MAC_SHA, GNUTLS_MAC_MD5, 0 }; +static int mac_priority[16]; + +/* These two are currently not changeable. */ + static const int comp_priority[16] = { GNUTLS_COMP_NULL, 0 }; static const int cert_type_priority[16] = { GNUTLS_CRT_X509, 0 }; -/* Tables of cipher names and equivalent numbers */ +/* Tables of priority names and equivalent numbers */ typedef struct pri_item { uschar *name; int *values; } pri_item; + +#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 } +}; + + +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 } +}; + + 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, @@ -103,6 +164,16 @@ static pri_item cipher_index[] = { }; +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 } +}; + + /************************************************* * Handle TLS error * @@ -119,27 +190,28 @@ Argument: prefix text to include in the logged error host NULL if setting up a server; the connected host if setting up a client - err a GnuTLS error number, or 0 if local error + msg additional error string (may be NULL) + usually obtained from gnutls_strerror() Returns: OK/DEFER/FAIL */ static int -tls_error(uschar *prefix, host_item *host, int err) +tls_error(uschar *prefix, host_item *host, const char *msg) { -uschar *errtext = US""; -if (err != 0) errtext = string_sprintf(": %s", gnutls_strerror(err)); if (host == NULL) { - log_write(0, LOG_MAIN, "TLS error on connection from %s (%s)%s", - (sender_fullhost != NULL)? sender_fullhost : US "local process", - prefix, errtext); + uschar *conn_info = smtp_get_connection_info(); + if (strncmp(conn_info, "SMTP ", 5) == 0) + conn_info += 5; + log_write(0, LOG_MAIN, "TLS error on %s (%s)%s%s", + conn_info, prefix, msg ? ": " : "", msg ? msg : ""); return DEFER; } else { - log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s", - host->name, host->address, prefix, errtext); + 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; } } @@ -161,12 +233,12 @@ Returns: TRUE/FALSE */ static BOOL -verify_certificate(gnutls_session session, uschar **error) +verify_certificate(gnutls_session session, const char **error) { -int verify; +int rc = -1; uschar *dn_string = US""; const gnutls_datum *cert; -unsigned int cert_size = 0; +unsigned int verify, cert_size = 0; *error = NULL; @@ -190,23 +262,23 @@ if (cert != NULL) dn_string = string_copy_malloc(buff); } - verify = gnutls_certificate_verify_peers(session); + rc = gnutls_certificate_verify_peers2(session, &verify); } else { DEBUG(D_tls) debug_printf("no peer certificate supplied\n"); verify = GNUTLS_CERT_INVALID; - *error = US"not supplied"; + *error = "not supplied"; } /* Handle the result of verification. INVALID seems to be set as well as REVOKED, but leave the test for both. */ -if ((verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0) +if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0) { tls_certificate_verified = FALSE; if (*error == NULL) *error = ((verify & GNUTLS_CERT_REVOKED) != 0)? - US"revoked" : US"invalid"; + "revoked" : "invalid"; if (verify_requirement == VERIFY_REQUIRED) { DEBUG(D_tls) debug_printf("TLS certificate verification failed (%s): " @@ -230,48 +302,11 @@ return TRUE; /* accept */ - /************************************************* -* Write/read datum to/from file * +* Setup up DH parameters * *************************************************/ -/* These functions are used for saving and restoring the RSA and D-H parameters -for use by all Exim processes. Data that is read is placed in malloc'd store -because that's what happens for newly generated data. - -Arguments: - fd the file descriptor - d points to the datum - -returns: FALSE on error (errno set) -*/ - -static BOOL -write_datum(int fd, gnutls_datum *d) -{ -if (write(fd, &(d->size), sizeof(d->size)) != sizeof(d->size)) return FALSE; -if (write(fd, d->data, d->size) != d->size) return FALSE; -return TRUE; -} - - -static BOOL -read_datum(int fd, gnutls_datum *d) -{ -if (read(fd, &(d->size), sizeof(d->size)) != sizeof(d->size)) return FALSE; -d->data = malloc(d->size); -if (d->data == NULL) return FALSE; -if (read(fd, d->data, d->size) != d->size) return FALSE; -return TRUE; -} - - - -/************************************************* -* Setup up RSA and DH parameters * -*************************************************/ - -/* Generating the RSA and D-H parameters takes a long time. They only need to +/* Generating the D-H parameters may take a long time. They only need to be re-generated every so often, depending on security policy. What we do is to keep these parameters in a file in the spool directory. If the file does not exist, we generate them. This means that it is easy to cause a regeneration. @@ -288,49 +323,81 @@ Returns: OK/DEFER/FAIL */ static int -init_rsa_dh(host_item *host) +init_dh(host_item *host) { -int fd, ret; -gnutls_datum m, e, d, p, q, u, prime, generator; +int fd; +int ret; +gnutls_datum m; uschar filename[200]; /* Initialize the data structures for holding the parameters */ -ret = gnutls_rsa_params_init(&rsa_params); -if (ret < 0) return tls_error(US"init rsa_params", host, ret); - ret = gnutls_dh_params_init(&dh_params); -if (ret < 0) return tls_error(US"init dh_params", host, ret); +if (ret < 0) return tls_error(US"init dh_params", host, gnutls_strerror(ret)); /* Set up the name of the cache file */ if (!string_format(filename, sizeof(filename), "%s/gnutls-params", spool_directory)) - return tls_error(US"overlong filename", host, 0); + return tls_error(US"overlong filename", host, NULL); -/* Open the cache file for reading. If this fails because of a non-existent -file, compute a new set of parameters, write them to a temporary file, and then -rename that file as the cache file. Other opening errors are bad. */ +/* Open the cache file for reading and if successful, read it and set up the +parameters. */ fd = Uopen(filename, O_RDONLY, 0); -if (fd < 0) +if (fd >= 0) { - unsigned int rsa_bits = RSA_BITS; - unsigned int dh_bits = DH_BITS; - uschar tempfilename[sizeof(filename) + 10]; + struct stat statbuf; + if (fstat(fd, &statbuf) < 0) + { + (void)close(fd); + return tls_error(US"TLS cache stat failed", host, strerror(errno)); + } - if (errno != ENOENT) - return tls_error(string_open_failed(errno, "%s for reading", filename), - host, 0); + 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); - DEBUG(D_tls) debug_printf("generating %d bit RSA key...\n", RSA_BITS); - ret = gnutls_rsa_params_generate2(rsa_params, RSA_BITS); - if (ret < 0) return tls_error(US"RSA key generation", host, ret); + 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"); + + free(m.data); + } + +/* If the file does not exist, fall through to compute new data and cache it. +If there was any other opening error, it is serious. */ + +else if (errno == ENOENT) + { + ret = -1; + DEBUG(D_tls) + debug_printf("parameter cache file %s does not exist\n", filename); + } +else + return tls_error(string_open_failed(errno, "%s for reading", filename), + host, 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 +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) + { + uschar tempfilename[sizeof(filename) + 10]; 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, ret); + if (ret < 0) return tls_error(US"D-H key generation", host, gnutls_strerror(ret)); /* Write the parameters to a file in the spool directory so that we can use them from other Exim processes. */ @@ -339,61 +406,43 @@ if (fd < 0) fd = Uopen(tempfilename, O_WRONLY|O_CREAT, 0400); if (fd < 0) return tls_error(string_open_failed(errno, "%s for writing", filename), - host, 0); + host, NULL); (void)fchown(fd, exim_uid, exim_gid); /* Probably not necessary */ - ret = gnutls_rsa_params_export_raw(rsa_params, &m, &e, &d, &p, &q, &u, - &rsa_bits); - if (ret < 0) return tls_error(US"RSA params export", host, ret); - - ret = gnutls_dh_params_export_raw(dh_params, &prime, &generator, &dh_bits); - if (ret < 0) return tls_error(US"DH params export", host, ret); - - if (!write_datum(fd, &m) || - !write_datum(fd, &e) || - !write_datum(fd, &d) || - !write_datum(fd, &p) || - !write_datum(fd, &q) || - !write_datum(fd, &u) || - !write_datum(fd, &prime) || - !write_datum(fd, &generator)) - return tls_error(US"TLS cache write failed", host, 0); - + /* 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; + 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)); + + free(m.data); (void)close(fd); if (rename(CS tempfilename, CS filename) < 0) - return tls_error(string_sprintf("failed to rename %s as %s: %s", - tempfilename, filename, strerror(errno)), host, 0); + return tls_error(string_sprintf("failed to rename %s as %s", + tempfilename, filename), host, strerror(errno)); - DEBUG(D_tls) debug_printf("wrote RSA and D-H parameters to file\n"); + DEBUG(D_tls) debug_printf("wrote D-H parameters to file %s\n", filename); } -/* File opened for reading; get the data */ - -else - { - if (!read_datum(fd, &m) || - !read_datum(fd, &e) || - !read_datum(fd, &d) || - !read_datum(fd, &p) || - !read_datum(fd, &q) || - !read_datum(fd, &u) || - !read_datum(fd, &prime) || - !read_datum(fd, &generator)) - return tls_error(US"TLS cache read failed", host, 0); - - (void)close(fd); - - ret = gnutls_rsa_params_import_raw(rsa_params, &m, &e, &d, &p, &q, &u); - if (ret < 0) return tls_error(US"RSA params import", host, ret); - - ret = gnutls_dh_params_import_raw(dh_params, &prime, &generator); - if (ret < 0) return tls_error(US"DH params import", host, ret); - - DEBUG(D_tls) debug_printf("read RSA and D-H parameters from file\n"); - } - -DEBUG(D_tls) debug_printf("initialized RSA and D-H parameters\n"); +DEBUG(D_tls) debug_printf("initialized D-H parameters\n"); return OK; } @@ -424,21 +473,23 @@ tls_init(host_item *host, uschar *certificate, uschar *privatekey, uschar *cas, int rc; uschar *cert_expanded, *key_expanded, *cas_expanded, *crl_expanded; -initialized = (host == NULL)? INITIALIZED_SERVER : INITIALIZED_CLIENT; +client_host = host; rc = gnutls_global_init(); -if (rc < 0) return tls_error(US"tls-init", host, rc); +if (rc < 0) return tls_error(US"tls-init", host, gnutls_strerror(rc)); -/* Create RSA and D-H parameters, or read them from the cache file. This -function does its own SMTP error messaging. */ +/* Create D-H parameters, or read them from the cache file. This function does +its own SMTP error messaging. */ -rc = init_rsa_dh(host); +rc = init_dh(host); if (rc != OK) return rc; /* Create the credentials structure */ rc = gnutls_certificate_allocate_credentials(&x509_cred); -if (rc < 0) return tls_error(US"certificate_allocate_credentials", host, rc); +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. */ @@ -446,12 +497,19 @@ may be required for different sessions. */ if (!expand_check(certificate, US"tls_certificate", &cert_expanded)) return DEFER; +key_expanded = NULL; if (privatekey != NULL) { if (!expand_check(privatekey, US"tls_privatekey", &key_expanded)) return DEFER; } -else key_expanded = cert_expanded; + +/* 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 */ @@ -465,7 +523,7 @@ if (cert_expanded != NULL) { uschar *msg = string_sprintf("cert/key setup: cert=%s key=%s", cert_expanded, key_expanded); - return tls_error(msg, host, rc); + return tls_error(msg, host, gnutls_strerror(rc)); } } @@ -474,7 +532,7 @@ if (cert_expanded != NULL) else { if (host == NULL) - return tls_error(US"no TLS server certificate is specified", host, 0); + return tls_error(US"no TLS server certificate is specified", NULL, NULL); DEBUG(D_tls) debug_printf("no TLS client certificate is specified\n"); } @@ -498,8 +556,8 @@ if (cas != NULL) return DEFER; } - DEBUG(D_tls) debug_printf("verify certificates = %s size=%d\n", - cas_expanded, (int)statbuf.st_size); + DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n", + cas_expanded, statbuf.st_size); /* If the cert file is empty, there's no point in loading the CRL file. */ @@ -507,7 +565,7 @@ if (cas != NULL) { 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, rc); + if (rc < 0) return tls_error(US"setup_certs", host, gnutls_strerror(rc)); if (crl != NULL && *crl != 0) { @@ -516,7 +574,7 @@ if (cas != NULL) 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, rc); + if (rc < 0) return tls_error(US"CRL setup", host, gnutls_strerror(rc)); } } } @@ -524,7 +582,6 @@ if (cas != NULL) /* Associate the parameters with the x509 credentials structure. */ gnutls_certificate_set_dh_params(x509_cred, dh_params); -gnutls_certificate_set_rsa_params(x509_cred, rsa_params); DEBUG(D_tls) debug_printf("initialized certificate stuff\n"); return OK; @@ -534,7 +591,7 @@ return OK; /************************************************* -* Remove ciphers from priority list * +* Remove from a priority list * *************************************************/ /* Cautiously written so that it will remove duplicates if present. @@ -547,7 +604,7 @@ Returns: nothing */ static void -remove_ciphers(int *list, int *remove_list) +remove_priority(int *list, int *remove_list) { for (; *remove_list != 0; remove_list++) { @@ -567,7 +624,7 @@ for (; *remove_list != 0; remove_list++) /************************************************* -* Add ciphers to priority list * +* Add to a priority list * *************************************************/ /* Cautiously written to check the list size @@ -581,7 +638,7 @@ Returns: TRUE if OK; FALSE if list overflows */ static BOOL -add_ciphers(int *list, int list_max, int *add_list) +add_priority(int *list, int list_max, int *add_list) { int next = 0; while (list[next] != 0) next++; @@ -596,6 +653,78 @@ 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 + + + which text for an error message + +Returns: FALSE if the table overflows, else TRUE +*/ + +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; + +while ((t = string_nextinlist(&s, &sep, big_buffer, big_buffer_size)) != NULL) + { + int i; + BOOL exclude = t[0] == '!'; + if (first && !exclude) plist[0] = 0; + first = FALSE; + for (i = 0; i < isize; i++) + { + 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) + { + int *ptr = plist; + debug_printf("adjusted %s priorities:", which); + while (*ptr != 0) debug_printf(" %d", *ptr++); + debug_printf("\n"); + } + +return TRUE; +} + + + + /************************************************* * Initialize a single GNUTLS session * *************************************************/ @@ -613,78 +742,60 @@ cipher. Arguments: side one of GNUTLS_SERVER, GNUTLS_CLIENT - expciphers expanded ciphers list + 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 Returns: a gnutls_session, or NULL if there is a problem */ static gnutls_session -tls_session_init(int side, uschar *expciphers) +tls_session_init(int side, uschar *expciphers, uschar *expmac, uschar *expkx, + uschar *expproto) { gnutls_session session; gnutls_init(&session, side); -/* Handle the list of permitted ciphers */ +/* Initialize the lists of permitted protocols, key-exchange methods, ciphers, +and MACs. */ 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)); + +/* 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 (expciphers != NULL) { - int sep = 0; - BOOL first = TRUE; - uschar *cipher; - - /* The names OpenSSL uses are of the form DES-CBC3-SHA, using hyphen - separators. GnuTLS uses underscore separators. So that I can use either form - in my tests, and also for general convenience, we turn hyphens into - underscores before scanning the list. */ - uschar *s = expciphers; while (*s != 0) { if (*s == '-') *s = '_'; s++; } + } - while ((cipher = string_nextinlist(&expciphers, &sep, big_buffer, - big_buffer_size)) != NULL) - { - int i; - BOOL exclude = cipher[0] == '!'; - if (first && !exclude) cipher_priority[0] = 0; - first = FALSE; - - for (i = 0; i < sizeof(cipher_index)/sizeof(pri_item); i++) - { - uschar *ss = strstric(cipher, cipher_index[i].name, FALSE); - if (ss != NULL) - { - uschar *endss = ss + Ustrlen(cipher_index[i].name); - if ((ss == cipher || !isalnum(ss[-1])) && !isalnum(*endss)) - { - if (exclude) - remove_ciphers(cipher_priority, cipher_index[i].values); - else - { - if (!add_ciphers(cipher_priority, - sizeof(cipher_priority)/sizeof(pri_item), - cipher_index[i].values)) - { - log_write(0, LOG_MAIN|LOG_PANIC, "GnuTLS init failed: cipher " - "priority table overflow"); - gnutls_deinit(session); - return NULL; - } - } - } - } - } - } - - DEBUG(D_tls) - { - int *ptr = cipher_priority; - debug_printf("adjusted cipher priorities:"); - while (*ptr != 0) debug_printf(" %d", *ptr++); - debug_printf("\n"); - } +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"))) + { + gnutls_deinit(session); + return NULL; } /* Define the various priorities */ @@ -692,7 +803,7 @@ if (expciphers != NULL) 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, protocol_priority); +gnutls_protocol_set_priority(session, proto_priority); gnutls_mac_set_priority(session, mac_priority); gnutls_cred_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred); @@ -709,6 +820,18 @@ if (verify_requirement != VERIFY_NONE) gnutls_db_set_cache_expiration(session, ssl_session_timeout); +/* Reduce security in favour of increased compatibility, if the admin +decides to make that trade-off. */ +if (gnutls_compat_mode) + { +#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 + } + DEBUG(D_tls) debug_printf("initialized GnuTLS session\n"); return session; } @@ -731,23 +854,43 @@ construct_cipher_name(gnutls_session session) { static uschar cipherbuf[256]; uschar *ver; -int bits, c, kx, mac; +int c, kx, mac; +#ifdef GNUTLS_CB_TLS_UNIQUE +int rc; +gnutls_datum_t channel; +#endif 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); -bits = gnutls_cipher_get_key_size(c); +/* returns size in "bytes" */ +tls_bits = gnutls_cipher_get_key_size(c) * 8; mac = gnutls_mac_get(session); kx = gnutls_kx_get(session); string_format(cipherbuf, sizeof(cipherbuf), "%s:%s:%u", ver, - gnutls_cipher_suite_get_name(kx, c, mac), bits); + gnutls_cipher_suite_get_name(kx, c, mac), tls_bits); tls_cipher = cipherbuf; DEBUG(D_tls) debug_printf("cipher: %s\n", cipherbuf); + +if (tls_channelbinding_b64) + free(tls_channelbinding_b64); +tls_channelbinding_b64 = NULL; + +#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 } @@ -761,7 +904,10 @@ the STARTTLS command. It must respond to that command, and then negotiate a TLS session. Arguments: - require_ciphers list of allowed ciphers + 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 @@ -770,19 +916,21 @@ Returns: OK on success */ int -tls_server_start(uschar *require_ciphers) +tls_server_start(uschar *require_ciphers, uschar *require_mac, + uschar *require_kx, uschar *require_proto) { int rc; -uschar *error; +const char *error; uschar *expciphers = NULL; +uschar *expmac = NULL; +uschar *expkx = NULL; +uschar *expproto = NULL; /* Check for previous activation */ if (tls_active >= 0) { - log_write(0, LOG_MAIN, "STARTTLS received in already encrypted " - "connection from %s", - (sender_fullhost != NULL)? sender_fullhost : US"local process"); + tls_error("STARTTLS received after TLS started", NULL, ""); smtp_printf("554 Already in TLS\r\n"); return FAIL; } @@ -796,7 +944,10 @@ rc = tls_init(NULL, tls_certificate, tls_privatekey, tls_verify_certificates, tls_crl); if (rc != OK) return rc; -if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers)) +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 @@ -812,9 +963,11 @@ else if (verify_check_host(&tls_try_verify_hosts) == OK) /* Prepare for new connection */ -tls_session = tls_session_init(GNUTLS_SERVER, expciphers); +tls_session = tls_session_init(GNUTLS_SERVER, expciphers, expmac, expkx, + expproto); if (tls_session == NULL) - return tls_error(US"tls_session_init", NULL, GNUTLS_E_MEMORY_ERROR); + return tls_error(US"tls_session_init", NULL, + gnutls_strerror(GNUTLS_E_MEMORY_ERROR)); /* 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 @@ -831,7 +984,8 @@ 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_ptr(tls_session, (gnutls_transport_ptr)fileno(smtp_out)); +gnutls_transport_set_ptr2(tls_session, (gnutls_transport_ptr)fileno(smtp_in), + (gnutls_transport_ptr)fileno(smtp_out)); sigalrm_seen = FALSE; if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout); @@ -840,14 +994,8 @@ alarm(0); if (rc < 0) { - if (sigalrm_seen) - Ustrcpy(ssl_errstring, "timed out"); - else - Ustrcpy(ssl_errstring, gnutls_strerror(rc)); - log_write(0, LOG_MAIN, - "TLS error on connection from %s (gnutls_handshake): %s", - (sender_fullhost != NULL)? sender_fullhost : US"local process", - ssl_errstring); + tls_error(US"gnutls_handshake", NULL, + sigalrm_seen ? "timed out" : gnutls_strerror(rc)); /* 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 @@ -855,8 +1003,8 @@ if (rc < 0) if (!sigalrm_seen) { - fclose(smtp_out); - fclose(smtp_in); + (void)fclose(smtp_out); + (void)fclose(smtp_in); } return FAIL; @@ -867,9 +1015,7 @@ DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n"); if (verify_requirement != VERIFY_NONE && !verify_certificate(tls_session, &error)) { - log_write(0, LOG_MAIN, - "TLS error on connection from %s: certificate verification failed (%s)", - (sender_fullhost != NULL)? sender_fullhost : US"local process", error); + tls_error(US"certificate verification failed", NULL, error); return FAIL; } @@ -886,6 +1032,7 @@ receive_getc = tls_getc; receive_ungetc = tls_ungetc; receive_feof = tls_feof; receive_ferror = tls_ferror; +receive_smtp_buffered = tls_smtp_buffered; tls_active = fileno(smtp_out); @@ -904,13 +1051,17 @@ return OK; Arguments: fd the fd of the connection host connected host (for messages) - addr + addr the first address (not used) dhparam DH parameter file 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 + 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), @@ -919,28 +1070,38 @@ 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 *verify_certs, - uschar *verify_crl, uschar *require_ciphers, int timeout) + 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) { const gnutls_datum *server_certs; uschar *expciphers = NULL; -uschar *error; +uschar *expmac = NULL; +uschar *expkx = NULL; +uschar *expproto = NULL; +const char *error; unsigned int server_certs_size; int rc; DEBUG(D_tls) debug_printf("initializing GnuTLS as a client\n"); -client_host = host; verify_requirement = (verify_certs == NULL)? VERIFY_NONE : VERIFY_REQUIRED; rc = tls_init(host, certificate, privatekey, verify_certs, verify_crl); if (rc != OK) return rc; -if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers)) +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); +tls_session = tls_session_init(GNUTLS_CLIENT, expciphers, expmac, expkx, + expproto); + if (tls_session == NULL) - return tls_error(US "tls_session_init", host, GNUTLS_E_MEMORY_ERROR); + return tls_error(US "tls_session_init", host, + gnutls_strerror(GNUTLS_E_MEMORY_ERROR)); gnutls_transport_set_ptr(tls_session, (gnutls_transport_ptr)fd); @@ -952,15 +1113,8 @@ rc = gnutls_handshake(tls_session); alarm(0); if (rc < 0) - { - if (sigalrm_seen) - { - log_write(0, LOG_MAIN, "TLS error on connection to %s [%s]: " - "gnutls_handshake timed out", host->name, host->address); - return FAIL; - } - else return tls_error(US "gnutls_handshake", host, rc); - } + return tls_error(US "gnutls_handshake", host, + sigalrm_seen ? "timed out" : gnutls_strerror(rc)); server_certs = gnutls_certificate_get_peers(tls_session, &server_certs_size); @@ -984,12 +1138,7 @@ if (server_certs != NULL) if (verify_requirement != VERIFY_NONE && !verify_certificate(tls_session, &error)) - { - log_write(0, LOG_MAIN, - "TLS error on connection to %s [%s]: certificate verification failed (%s)", - host->name, host->address, error); - return FAIL; - } + return tls_error(US"certificate verification failed", host, error); construct_cipher_name(tls_session); /* Sets tls_cipher */ tls_active = fd; @@ -1015,21 +1164,15 @@ Returns: nothing static void record_io_error(int ec, uschar *when, uschar *text) { -uschar *additional = US""; +const char *msg; if (ec == GNUTLS_E_FATAL_ALERT_RECEIVED) - additional = string_sprintf(": %s", + msg = string_sprintf("%s: %s", gnutls_strerror(ec), gnutls_alert_get_name(gnutls_alert_get(tls_session))); - -if (initialized == INITIALIZED_SERVER) - log_write(0, LOG_MAIN, "TLS %s error on connection from %s: %s%s", when, - (sender_fullhost != NULL)? sender_fullhost : US "local process", - (ec == 0)? text : US gnutls_strerror(ec), additional); - else - log_write(0, LOG_MAIN, "TLS %s error on connection to %s [%s]: %s%s", when, - client_host->name, client_host->address, - (ec == 0)? text : US gnutls_strerror(ec), additional); + msg = gnutls_strerror(ec); + +tls_error(when, client_host, msg); } @@ -1072,6 +1215,7 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm) receive_ungetc = smtp_ungetc; receive_feof = smtp_feof; receive_ferror = smtp_ferror; + receive_smtp_buffered = smtp_buffered; gnutls_deinit(tls_session); tls_session = NULL; @@ -1090,7 +1234,9 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm) ssl_xfer_error = 1; return EOF; } - +#ifndef DISABLE_DKIM + dkim_exim_verify_feed(ssl_xfer_buffer, inbytes); +#endif ssl_xfer_buffer_hwm = inbytes; ssl_xfer_buffer_lwm = 0; } @@ -1214,4 +1360,26 @@ gnutls_global_deinit(); tls_active = -1; } + + + +/************************************************* +* Report the library versions. * +*************************************************/ + +/* See a description in tls-openssl.c for an explanation of why this exists. + +Arguments: a FILE* to print the results to +Returns: nothing +*/ + +void +tls_version_report(FILE *f) +{ +fprintf(f, "Library version: GnuTLS: Compile: %s\n" + " Runtime: %s\n", + LIBGNUTLS_VERSION, + gnutls_check_version(NULL)); +} + /* End of tls-gnu.c */