SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / tls.c
index ffcc8598c173b5b2ee70966322bfd39fa4f6f7fd..9e20b5bca977fae7525b13e6c93bc973f426fd8c 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module provides TLS (aka SSL) support for Exim. The code for OpenSSL is
 based on a patch that was originally contributed by Steve Haslam. It was
 
 /* This module provides TLS (aka SSL) support for Exim. The code for OpenSSL is
 based on a patch that was originally contributed by Steve Haslam. It was
@@ -25,6 +26,11 @@ functions from the OpenSSL or GNU TLS libraries. */
 #endif
 
 
 #endif
 
 
+/* Forward decl. */
+static void tls_client_resmption_key(tls_support *, smtp_connect_args *,
+  smtp_transport_options_block *);
+
+
 #if defined(MACRO_PREDEF) && !defined(DISABLE_TLS)
 # include "macro_predef.h"
 # ifdef USE_GNUTLS
 #if defined(MACRO_PREDEF) && !defined(DISABLE_TLS)
 # include "macro_predef.h"
 # ifdef USE_GNUTLS
@@ -38,10 +44,13 @@ functions from the OpenSSL or GNU TLS libraries. */
 
 static void tls_per_lib_daemon_init(void);
 static void tls_per_lib_daemon_tick(void);
 
 static void tls_per_lib_daemon_init(void);
 static void tls_per_lib_daemon_tick(void);
-static void tls_server_creds_init(void);
+static unsigned  tls_server_creds_init(void);
 static void tls_server_creds_invalidate(void);
 static void tls_client_creds_init(transport_instance *, BOOL);
 static void tls_client_creds_invalidate(transport_instance *);
 static void tls_server_creds_invalidate(void);
 static void tls_client_creds_init(transport_instance *, BOOL);
 static void tls_client_creds_invalidate(transport_instance *);
+static void tls_daemon_creds_reload(void);
+static BOOL opt_set_and_noexpand(const uschar *);
+static BOOL opt_unset_or_noexpand(const uschar *);
 
 
 
 
 
 
@@ -73,6 +82,13 @@ static int ssl_xfer_eof = FALSE;
 static BOOL ssl_xfer_error = FALSE;
 #endif
 
 static BOOL ssl_xfer_error = FALSE;
 #endif
 
+#ifdef EXIM_HAVE_KEVENT
+# define KEV_SIZE 16   /* Eight file,dir pairs */
+static struct kevent kev[KEV_SIZE];
+static int kev_used = 0;
+#endif
+
+static unsigned tls_creds_expire = 0;
 
 /*************************************************
 *       Expand string; give error on failure     *
 
 /*************************************************
 *       Expand string; give error on failure     *
@@ -107,7 +123,7 @@ return TRUE;
 }
 
 
 }
 
 
-#ifdef EXIM_HAVE_INOTIFY
+#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)
 /* Add the directory for a filename to the inotify handle, creating that if
 needed.  This is enough to see changes to files in that dir.
 Return boolean success.
 /* Add the directory for a filename to the inotify handle, creating that if
 needed.  This is enough to see changes to files in that dir.
 Return boolean success.
@@ -118,33 +134,129 @@ directory it implies nor if the TLS library handles a watch for us.
 The string "system,cache" is recognised and explicitly accepted without
 setting a watch.  This permits the system CA bundle to be cached even though
 we have no way to tell when it gets modified by an update.
 The string "system,cache" is recognised and explicitly accepted without
 setting a watch.  This permits the system CA bundle to be cached even though
 we have no way to tell when it gets modified by an update.
-
-We *might* try to run "openssl version -d" and set watches on the dir
-indicated in its output, plus the "certs" subdir of it (following
-synlimks for both).  But this is undocumented even for OpenSSL, and
-who knows what GnuTLS might be doing.
+The call chain for OpenSSL uses a (undocumented) call into the library
+to discover the actual file.  We don't know what GnuTLS uses.
 
 A full set of caching including the CAs takes 35ms output off of the
 server tls_init() (GnuTLS, Fedora 32, 2018-class x86_64 laptop hardware).
 */
 static BOOL
 tls_set_one_watch(const uschar * filename)
 
 A full set of caching including the CAs takes 35ms output off of the
 server tls_init() (GnuTLS, Fedora 32, 2018-class x86_64 laptop hardware).
 */
 static BOOL
 tls_set_one_watch(const uschar * filename)
+# ifdef EXIM_HAVE_INOTIFY
 {
 {
+uschar buf[PATH_MAX];
+ssize_t len;
 uschar * s;
 
 if (Ustrcmp(filename, "system,cache") == 0) return TRUE;
 uschar * s;
 
 if (Ustrcmp(filename, "system,cache") == 0) return TRUE;
-
 if (!(s = Ustrrchr(filename, '/'))) return FALSE;
 if (!(s = Ustrrchr(filename, '/'))) return FALSE;
-s = string_copyn(filename, s - filename);
+
+for (unsigned loop = 20;
+     (len = readlink(CCS filename, CS buf, sizeof(buf))) >= 0; )
+  {                                            /* a symlink */
+  if (--loop == 0) { errno = ELOOP; return FALSE; }
+  filename = buf[0] == '/'
+    ? string_copyn(buf, (unsigned)len) /* mem released by tls_set_watch */
+    : string_sprintf("%.*s/%.*s", (int)(s - filename), filename, (int)len, buf);
+  s = Ustrrchr(filename, '/');
+  }
+if (errno != EINVAL)
+  return FALSE;                                        /* other error */
+
+/* not a symlink */
+s = string_copyn(filename, s - filename);      /* mem released by tls_set_watch */
+
 DEBUG(D_tls) debug_printf("watch dir '%s'\n", s);
 
 if (inotify_add_watch(tls_watch_fd, CCS s,
       IN_ONESHOT | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF
       | IN_MOVED_FROM | IN_MOVED_TO | IN_MOVE_SELF) >= 0)
   return TRUE;
 DEBUG(D_tls) debug_printf("watch dir '%s'\n", s);
 
 if (inotify_add_watch(tls_watch_fd, CCS s,
       IN_ONESHOT | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF
       | IN_MOVED_FROM | IN_MOVED_TO | IN_MOVE_SELF) >= 0)
   return TRUE;
-DEBUG(D_tls) debug_printf("add_watch: %s\n", strerror(errno));
+DEBUG(D_tls) debug_printf("notify_add_watch: %s\n", strerror(errno));
+return FALSE;
+}
+# endif
+# ifdef EXIM_HAVE_KEVENT
+{
+uschar * s, * t;
+int fd1, fd2, i, j, cnt = 0;
+struct stat sb;
+#ifdef OpenBSD
+struct kevent k_dummy;
+struct timespec ts = {0};
+#endif
+
+errno = 0;
+if (Ustrcmp(filename, "system,cache") == 0) return TRUE;
+
+for (;;)
+  {
+  if (kev_used > KEV_SIZE-2) { s = US"out of kev space"; goto bad; }
+  if (!(s = Ustrrchr(filename, '/'))) return FALSE;
+  s = string_copyn(filename, s - filename);    /* mem released by tls_set_watch */
+
+  /* The dir open will fail if there is a symlink on the path. Fine; it's too
+  much effort to handle all possible cases; just refuse the preload. */
+
+  if ((fd2 = open(CCS s, O_RDONLY | O_NOFOLLOW)) < 0) { s = US"open dir"; goto bad; }
+
+  if ((lstat(CCS filename, &sb)) < 0) { s = US"lstat"; goto bad; }
+  if (!S_ISLNK(sb.st_mode))
+    {
+    if ((fd1 = open(CCS filename, O_RDONLY | O_NOFOLLOW)) < 0)
+      { s = US"open file"; goto bad; }
+    DEBUG(D_tls) debug_printf("watch file '%s':\t%d\n", filename, fd1);
+    EV_SET(&kev[kev_used++],
+       (uintptr_t)fd1,
+       EVFILT_VNODE,
+       EV_ADD | EV_ENABLE | EV_ONESHOT,
+       NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND
+       | NOTE_ATTRIB | NOTE_RENAME | NOTE_REVOKE,
+       0,
+       NULL);
+    cnt++;
+    }
+  DEBUG(D_tls) debug_printf("watch dir  '%s':\t%d\n", s, fd2);
+  EV_SET(&kev[kev_used++],
+       (uintptr_t)fd2,
+       EVFILT_VNODE,
+       EV_ADD | EV_ENABLE | EV_ONESHOT,
+       NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND
+       | NOTE_ATTRIB | NOTE_RENAME | NOTE_REVOKE,
+       0,
+       NULL);
+  cnt++;
+
+  if (!(S_ISLNK(sb.st_mode))) break;
+
+  t = store_get(1024, GET_UNTAINTED);
+  Ustrncpy(t, s, 1022);
+  j = Ustrlen(s);
+  t[j++] = '/';
+  if ((i = readlink(CCS filename, (void *)(t+j), 1023-j)) < 0) { s = US"readlink"; goto bad; }
+  filename = t;
+  *(t += i+j) = '\0';
+  store_release_above(t+1);
+  }
+
+#ifdef OpenBSD
+if (kevent(tls_watch_fd, &kev[kev_used-cnt], cnt, &k_dummy, 1, &ts) >= 0)
+  return TRUE;
+#else
+if (kevent(tls_watch_fd, &kev[kev_used-cnt], cnt, NULL, 0, NULL) >= 0)
+  return TRUE;
+#endif
+s = US"kevent";
+
+bad:
+DEBUG(D_tls)
+  if (errno)
+    debug_printf("%s: %s: %s\n", __FUNCTION__, s, strerror(errno));
+  else
+    debug_printf("%s: %s\n", __FUNCTION__, s);
 return FALSE;
 }
 return FALSE;
 }
+# endif        /*EXIM_HAVE_KEVENT*/
 
 
 /* Create an inotify facility if needed.
 
 
 /* Create an inotify facility if needed.
@@ -157,13 +269,23 @@ tls_set_watch(const uschar * filename, BOOL list)
 rmark r;
 BOOL rc = FALSE;
 
 rmark r;
 BOOL rc = FALSE;
 
-if (tls_watch_fd < 0 && (tls_watch_fd = inotify_init1(O_CLOEXEC)) < 0)
-  {
-  DEBUG(D_tls) debug_printf("inotify_init: %s\n", strerror(errno));
-  return FALSE;
-  }
-
 if (!filename || !*filename) return TRUE;
 if (!filename || !*filename) return TRUE;
+if (Ustrncmp(filename, "system", 6) == 0) return TRUE;
+
+DEBUG(D_tls) debug_printf("tls_set_watch: '%s'\n", filename);
+
+if (  tls_watch_fd < 0
+# ifdef EXIM_HAVE_INOTIFY
+   && (tls_watch_fd = inotify_init1(O_CLOEXEC)) < 0
+# endif
+# ifdef EXIM_HAVE_KEVENT
+   && (tls_watch_fd = kqueue()) < 0
+# endif
+   )
+    {
+    DEBUG(D_tls) debug_printf("inotify_init: %s\n", strerror(errno));
+    return FALSE;
+    }
 
 r = store_mark();
 
 
 r = store_mark();
 
@@ -177,10 +299,26 @@ else
   rc = tls_set_one_watch(filename);
 
 store_reset(r);
   rc = tls_set_one_watch(filename);
 
 store_reset(r);
+if (!rc) DEBUG(D_tls) debug_printf("tls_set_watch() fail on '%s': %s\n", filename, strerror(errno));
 return rc;
 }
 
 
 return rc;
 }
 
 
+void
+tls_watch_discard_event(int fd)
+{
+#ifdef EXIM_HAVE_INOTIFY
+(void) read(fd, big_buffer, big_buffer_size);
+#endif
+#ifdef EXIM_HAVE_KEVENT
+struct kevent kev;
+struct timespec t = {0};
+(void) kevent(fd, NULL, 0, &kev, 1, &t);
+#endif
+}
+#endif /*EXIM_HAVE_INOTIFY*/
+
+
 void
 tls_client_creds_reload(BOOL watch)
 {
 void
 tls_client_creds_reload(BOOL watch)
 {
@@ -192,30 +330,44 @@ for(transport_instance * t = transports; t; t = t->next)
     }
 }
 
     }
 }
 
-static void
-tls_daemon_creds_reload(void)
-{
-tls_server_creds_invalidate();
-tls_server_creds_init();
 
 
-tls_client_creds_reload(TRUE);
-}
+void
+tls_watch_invalidate(void)
+{
+if (tls_watch_fd < 0) return;
 
 
+#ifdef EXIM_HAVE_KEVENT
+/* Close the files we had open for kevent */
+for (int i = 0; i < kev_used; i++)
+  {
+  DEBUG(D_tls) debug_printf("closing watch fd: %d\n", (int) kev[i].ident);
+  (void) close((int) kev[i].ident);
+  kev[i].ident = (uintptr_t)-1;
+  }
+kev_used = 0;
+#endif
 
 
-/* Called, after a delay for multiple file ops to get done, from
-the daemon when any of the watches added (above) fire.
+close(tls_watch_fd);
+tls_watch_fd = -1;
+}
 
 
-Dump the set of watches and arrange to reload cached creds (which
-will set up new watches). */
 
 static void
 
 static void
-tls_watch_triggered(void)
+tls_daemon_creds_reload(void)
 {
 {
-DEBUG(D_tls) debug_printf("watch triggered\n");
-close(tls_watch_fd);
-tls_watch_fd = -1;
+unsigned lifetime;
+
+#ifdef EXIM_HAVE_KEVENT
+tls_watch_invalidate();
+#endif
+
+tls_server_creds_invalidate();
 
 
-tls_daemon_creds_reload();
+/* _expire is for a time-limited selfsign server cert */
+tls_creds_expire = (lifetime = tls_server_creds_init())
+  ? time(NULL) + lifetime : 0;
+
+tls_client_creds_reload(TRUE);
 }
 
 
 }
 
 
@@ -228,22 +380,47 @@ static BOOL
 opt_unset_or_noexpand(const uschar * opt)
 { return !opt || Ustrchr(opt, '$') == NULL; }
 
 opt_unset_or_noexpand(const uschar * opt)
 { return !opt || Ustrchr(opt, '$') == NULL; }
 
-#endif /* EXIM_HAVE_INOTIFY */
 
 
 
 
-/* Called every time round the daemon loop */
+/* Called every time round the daemon loop.
 
 
-void
+If we reloaded fd-watcher, return the old watch fd
+having modified the global for the new one. Otherwise
+return -1.
+*/
+
+int
 tls_daemon_tick(void)
 {
 tls_daemon_tick(void)
 {
+int old_watch_fd = tls_watch_fd;
+
 tls_per_lib_daemon_tick();
 tls_per_lib_daemon_tick();
-#ifdef EXIM_HAVE_INOTIFY
-if (tls_watch_trigger_time && time(NULL) >= tls_watch_trigger_time + 5)
+#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)
+if (tls_creds_expire && time(NULL) >= tls_creds_expire)
+  {
+  /* The server cert is a selfsign, with limited lifetime.  Dump it and
+  generate a new one.  Reload the rest of the creds also as the machinery
+  is all there. */
+
+  DEBUG(D_tls) debug_printf("selfsign cert rotate\n");
+  tls_creds_expire = 0;
+  tls_daemon_creds_reload();
+  return old_watch_fd;
+  }
+else if (tls_watch_trigger_time && time(NULL) >= tls_watch_trigger_time + 5)
   {
   {
-  tls_watch_trigger_time = 0;
-  tls_watch_triggered();
+  /* Called, after a delay for multiple file ops to get done, from
+  the daemon when any of the watches added (above) fire.
+  Dump the set of watches and arrange to reload cached creds (which
+  will set up new watches). */
+
+  DEBUG(D_tls) debug_printf("watch triggered\n");
+  tls_watch_trigger_time = tls_creds_expire = 0;
+  tls_daemon_creds_reload();
+  return old_watch_fd;
   }
 #endif
   }
 #endif
+return -1;
 }
 
 /* Called once at daemon startup */
 }
 
 /* Called once at daemon startup */
@@ -316,6 +493,9 @@ Returns:       the character
 int
 tls_ungetc(int ch)
 {
 int
 tls_ungetc(int ch)
 {
+if (ssl_xfer_buffer_lwm <= 0)
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "buffer underflow in tls_ungetc");
+
 ssl_xfer_buffer[--ssl_xfer_buffer_lwm] = ch;
 return ch;
 }
 ssl_xfer_buffer[--ssl_xfer_buffer_lwm] = ch;
 return ch;
 }
@@ -528,7 +708,6 @@ else if ((subjdn = tls_cert_subject(cert, NULL)))
 return FALSE;
 }
 
 return FALSE;
 }
 
-
 /* Environment cleanup: The GnuTLS library uses SSLKEYLOGFILE in the environment
 and writes a file by that name.  Our OpenSSL code does the same, using keying
 info from the library API.
 /* Environment cleanup: The GnuTLS library uses SSLKEYLOGFILE in the environment
 and writes a file by that name.  Our OpenSSL code does the same, using keying
 info from the library API.
@@ -634,6 +813,45 @@ return status == 0;
 
 
 
 
 
 
+static void
+tls_client_resmption_key(tls_support * tlsp, smtp_connect_args * conn_args,
+  smtp_transport_options_block * ob)
+{
+#ifndef DISABLE_TLS_RESUME
+hctx * h = &tlsp->resume_hctx;
+blob b;
+gstring * g;
+
+DEBUG(D_tls) if (conn_args->host_lbserver)
+  debug_printf("TLS: lbserver '%s'\n", conn_args->host_lbserver);
+
+# ifdef EXIM_HAVE_SHA2
+exim_sha_init(h, HASH_SHA2_256);
+# else
+exim_sha_init(h, HASH_SHA1);
+# endif
+exim_sha_update_string(h, conn_args->host_lbserver);
+# ifdef SUPPORT_DANE
+if (conn_args->dane)
+  exim_sha_update(h,  CUS &conn_args->tlsa_dnsa, sizeof(dns_answer));
+# endif
+exim_sha_update_string(h, conn_args->host->address);
+exim_sha_update(h,   CUS &conn_args->host->port, sizeof(conn_args->host->port));
+exim_sha_update_string(h, conn_args->sending_ip_address);
+exim_sha_update_string(h, openssl_options);
+exim_sha_update_string(h, ob->tls_require_ciphers);
+exim_sha_update_string(h, tlsp->sni);
+# ifdef EXIM_HAVE_ALPN
+exim_sha_update_string(h, ob->tls_alpn);
+# endif
+exim_sha_finish(h, &b);
+for (g = string_get(b.len*2+1); b.len-- > 0; )
+  g = string_fmt_append(g, "%02x", *b.data++);
+tlsp->resume_index = string_from_gstring(g);
+DEBUG(D_tls) debug_printf("TLS: resume session index %s\n", tlsp->resume_index);
+#endif
+}
+
 #endif /*!DISABLE_TLS*/
 #endif /*!MACRO_PREDEF*/
 
 #endif /*!DISABLE_TLS*/
 #endif /*!MACRO_PREDEF*/