From dc4ab0a186edc8b270c8fa486104fabc567d25e7 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Thu, 8 Oct 2020 13:30:41 +0100 Subject: [PATCH] FreeBSD: TLS: preload configuration items --- doc/doc-docbook/spec.xfpt | 4 +- src/OS/os.h-FreeBSD | 2 + src/src/daemon.c | 9 ++- src/src/exim.h | 3 + src/src/functions.h | 14 ++-- src/src/tls-gnu.c | 8 +-- src/src/tls-openssl.c | 9 ++- src/src/tls.c | 146 +++++++++++++++++++++++++++++++++----- test/confs/1103 | 1 + test/runtest | 10 ++- 10 files changed, 165 insertions(+), 41 deletions(-) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 31c8c5653..74c9b083c 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -29308,7 +29308,7 @@ then the associated information is loaded at daemon startup. It is made available to child processes forked for handling received SMTP connections. -This caching is currently only supported under Linux. +This caching is currently only supported under Linux and FreeBSD. If caching is not possible, for example if an item has to be dependent on the peer host so contains a &$sender_host_name$& expansion, the load @@ -29320,7 +29320,7 @@ containing files specified by these options. The information specified by the main option &%tls_verify_certificates%& is similarly cached so long as it specifies files explicitly or (under GnuTLS) is the string &"system,cache"&. -The latter case is not automatically invaludated; +The latter case is not automatically invalidated; it is the operator's responsibility to arrange for a daemon restart any time the system certificate authority bundle is updated. A HUP signal is sufficient for this. diff --git a/src/OS/os.h-FreeBSD b/src/OS/os.h-FreeBSD index c1720a796..0083642b4 100644 --- a/src/OS/os.h-FreeBSD +++ b/src/OS/os.h-FreeBSD @@ -70,4 +70,6 @@ extern ssize_t os_sendfile(int, int, off_t *, size_t); /*******************/ +#define EXIM_HAVE_KEVENT + /* End */ diff --git a/src/src/daemon.c b/src/src/daemon.c index 4e90799e6..899aa0dab 100644 --- a/src/src/daemon.c +++ b/src/src/daemon.c @@ -963,9 +963,8 @@ daemon_die(void) { int pid; -#ifndef DISABLE_TLS -if (tls_watch_fd >= 0) - { close(tls_watch_fd); tls_watch_fd = -1; } +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) +tls_watch_invalidate(); #endif if (daemon_notifier_fd >= 0) @@ -2353,12 +2352,12 @@ for (;;) if (!select_failed) { -#if defined(EXIM_HAVE_INOTIFY) && !defined(DISABLE_TLS) +#if !defined(DISABLE_TLS) && (defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)) if (tls_watch_fd >= 0 && FD_ISSET(tls_watch_fd, &select_listen)) { FD_CLR(tls_watch_fd, &select_listen); tls_watch_trigger_time = time(NULL); /* Set up delayed event */ - (void) read(tls_watch_fd, big_buffer, big_buffer_size); + tls_watch_discard_event(tls_watch_fd); break; /* to top of daemon loop */ } #endif diff --git a/src/src/exim.h b/src/src/exim.h index 6669e809e..f27ed5cd8 100644 --- a/src/src/exim.h +++ b/src/src/exim.h @@ -90,6 +90,9 @@ making unique names. */ #ifdef EXIM_HAVE_INOTIFY # include #endif +#ifdef EXIM_HAVE_KEVENT +# include +#endif /* C99 integer types, figure out how to undo this if needed for older systems */ diff --git a/src/src/functions.h b/src/src/functions.h index c69851962..38309e7dd 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -63,23 +63,27 @@ extern BOOL tls_dropprivs_validate_require_cipher(BOOL); extern BOOL tls_export_cert(uschar *, size_t, void *); extern int tls_feof(void); extern int tls_ferror(void); +extern uschar *tls_field_from_dn(uschar *, const uschar *); extern void tls_free_cert(void **); extern int tls_getc(unsigned); extern uschar *tls_getbuf(unsigned *); extern void tls_get_cache(void); extern BOOL tls_import_cert(const uschar *, void **); +extern BOOL tls_is_name_for_cert(const uschar *, void *); +# ifdef USE_OPENSSL +extern BOOL tls_openssl_options_parse(uschar *, long *); +# endif extern int tls_read(void *, uschar *, size_t); extern int tls_server_start(uschar **); extern BOOL tls_smtp_buffered(void); extern int tls_ungetc(int); +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) +extern void tls_watch_discard_event(int); +extern void tls_watch_invalidate(void); +#endif extern int tls_write(void *, const uschar *, size_t, BOOL); extern uschar *tls_validate_require_cipher(void); extern void tls_version_report(FILE *); -# ifdef USE_OPENSSL -extern BOOL tls_openssl_options_parse(uschar *, long *); -# endif -extern uschar * tls_field_from_dn(uschar *, const uschar *); -extern BOOL tls_is_name_for_cert(const uschar *, void *); # ifdef SUPPORT_DANE extern int tlsa_lookup(const host_item *, dns_answer *, BOOL); diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index b6354b4b2..9b684e3cd 100644 --- a/src/src/tls-gnu.c +++ b/src/src/tls-gnu.c @@ -145,7 +145,7 @@ builtin_macro_create(US"_HAVE_TLS_OCSP"); # ifdef SUPPORT_SRV_OCSP_STACK builtin_macro_create(US"_HAVE_TLS_OCSP_LIST"); # endif -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) builtin_macro_create(US"_HAVE_TLS_CA_CACHE"); # endif } @@ -1435,7 +1435,7 @@ if (gnutls_certificate_allocate_credentials( } creds_basic_init(state_server.lib_state.x509_cred, TRUE); -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) /* If tls_certificate has any $ indicating expansions, it is not good. If tls_privatekey is set but has $, not good. Likewise for tls_ocsp_file. If all good (and tls_certificate set), load the cert(s). Do not try @@ -1550,7 +1550,7 @@ creds_basic_init(ob->tls_preload.x509_cred, FALSE); tpt_dummy_state.session = NULL; tpt_dummy_state.lib_state = ob->tls_preload; -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) if ( opt_set_and_noexpand(ob->tls_certificate) && opt_unset_or_noexpand(ob->tls_privatekey)) { @@ -1614,7 +1614,7 @@ depends on DANE or plain usage. */ } -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) /* Invalidate the creds cached, by dropping the current ones. Call when we notice one of the source files has changed. */ diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c index b0b99779d..050b36c40 100644 --- a/src/src/tls-openssl.c +++ b/src/src/tls-openssl.c @@ -1648,7 +1648,7 @@ if (opt_unset_or_noexpand(tls_eccurve)) state_server.lib_state.ecdh = TRUE; } -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) /* If we can, preload the server-side cert, key and ocsp */ if ( opt_set_and_noexpand(tls_certificate) @@ -1658,8 +1658,7 @@ if ( opt_set_and_noexpand(tls_certificate) && opt_unset_or_noexpand(tls_privatekey)) { /* Set watches on the filenames. The implementation does de-duplication - so we can just blindly do them all. - */ + so we can just blindly do them all. */ if ( tls_set_watch(tls_certificate, TRUE) # ifndef DISABLE_OCSP @@ -1759,7 +1758,7 @@ if (opt_unset_or_noexpand(tls_eccurve)) ob->tls_preload.ecdh = TRUE; } -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) if ( opt_set_and_noexpand(ob->tls_certificate) && opt_unset_or_noexpand(ob->tls_privatekey)) { @@ -1812,7 +1811,7 @@ else } -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) /* Invalidate the creds cached, by dropping the current ones. Call when we notice one of the source files has changed. */ diff --git a/src/src/tls.c b/src/src/tls.c index a12211859..a1e78d7cd 100644 --- a/src/src/tls.c +++ b/src/src/tls.c @@ -76,6 +76,11 @@ static int ssl_xfer_eof = FALSE; 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 /************************************************* * Expand string; give error on failure * @@ -110,7 +115,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. @@ -121,26 +126,26 @@ 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. - -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) +# ifdef EXIM_HAVE_INOTIFY { uschar * s; if (Ustrcmp(filename, "system,cache") == 0) return TRUE; if (!(s = Ustrrchr(filename, '/'))) return FALSE; -s = string_copyn(filename, s - filename); +s = string_copyn(filename, s - filename); /* mem released by tls_set_watch */ DEBUG(D_tls) debug_printf("watch dir '%s'\n", s); +/*XXX unclear what effect symlinked files will have for inotify */ + 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) @@ -148,6 +153,66 @@ if (inotify_add_watch(tls_watch_fd, CCS s, DEBUG(D_tls) debug_printf("add_watch: %s\n", strerror(errno)); return FALSE; } +# endif +# ifdef EXIM_HAVE_KEVENT +{ +uschar * s; +int fd1, fd2, i, cnt = 0; +struct stat sb; + +if (Ustrcmp(filename, "system,cache") == 0) return TRUE; + +for (;;) + { + if (!(s = Ustrrchr(filename, '/'))) return FALSE; + if ((lstat(filename, &sb)) < 0) return FALSE; + if (kev_used > KEV_SIZE-2) return FALSE; + + /* 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(s, O_RDONLY | O_NOFOLLOW)) < 0) return FALSE; + + if (!S_ISLNK(sb.st_mode)) + { + if ((fd1 = open(filename, O_RDONLY | O_NOFOLLOW)) < 0) return FALSE; + DEBUG(D_tls) debug_printf("watch file '%s'\n", filename); + 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'\n", s); + 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; + + s = store_get(1024, FALSE); + if ((i = readlink(filename, s, 1024)) < 0) return FALSE; + filename = s; + *(s += i) = '\0'; + store_release_above(s+1); + } + +if (kevent(tls_watch_fd, &kev[kev_used-cnt], cnt, NULL, 0, NULL) >= 0) + return TRUE; +DEBUG(D_tls) debug_printf("add_watch: %d, %s\n", strerror(errno)); +return FALSE; +} +# endif /*EXIM_HAVE_KEVENT*/ /* Create an inotify facility if needed. @@ -160,13 +225,23 @@ tls_set_watch(const uschar * filename, BOOL list) 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 (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(); @@ -180,10 +255,24 @@ else 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; } +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 +} + /* Called, after a delay for multiple file ops to get done, from the daemon when any of the watches added (above) fire. @@ -194,12 +283,10 @@ static void tls_watch_triggered(void) { DEBUG(D_tls) debug_printf("watch triggered\n"); -close(tls_watch_fd); -tls_watch_fd = -1; tls_daemon_creds_reload(); } -#endif /* EXIM_HAVE_INOTIFY */ +#endif /*EXIM_HAVE_INOTIFY*/ void @@ -213,9 +300,34 @@ for(transport_instance * t = transports; t; t = t->next) } } + +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 fd, i = 0; i < kev_used; i++) + { + (void) close((int) kev[i].ident); + kev[i].ident = (uintptr_t)-1; + } +kev_used = 0; +#endif + +close(tls_watch_fd); +tls_watch_fd = -1; +} + + static void tls_daemon_creds_reload(void) { +#ifdef EXIM_HAVE_KEVENT +tls_watch_invalidate(); +#endif + tls_server_creds_invalidate(); tls_server_creds_init(); @@ -240,7 +352,7 @@ void tls_daemon_tick(void) { tls_per_lib_daemon_tick(); -#ifdef EXIM_HAVE_INOTIFY +#if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT) if (tls_watch_trigger_time && time(NULL) >= tls_watch_trigger_time + 5) { tls_watch_trigger_time = 0; diff --git a/test/confs/1103 b/test/confs/1103 index b937ee99c..52f471b79 100644 --- a/test/confs/1103 +++ b/test/confs/1103 @@ -36,6 +36,7 @@ smtp: hosts = 127.0.0.1 allow_localhost port = PORT_D + hosts_try_fastopen = : tls_certificate = DIR/aux-fixed/cert2 tls_verify_certificates = DIR/aux-fixed/cert1 tls_verify_cert_hostnames = : diff --git a/test/runtest b/test/runtest index 8a1e46edd..84227b9d3 100755 --- a/test/runtest +++ b/test/runtest @@ -1072,12 +1072,12 @@ RESET_AFTER_EXTRA_LINE_READ: # TLS preload # only OpenSSL speaks of these - next if /^TLS: preloading DH params for server/; + next if /^TLS: preloading (DH params|ECDH curve|CA bundle) for server/; next if /^Diffie-Hellman initialized from default/; - next if /^TLS: preloading ECDH curve for server/; next if /^ECDH OpenSSL [< ]?[\d.+]+ temp key parameter settings:/; next if /^ECDH: .'*prime256v1'/; - next if /^watch dir/; + next if /^tls_verify_certificates: system$/; + next if /^tls_set_watch: .*\/cert.pem/; # TLS preload # only GnuTLS speaks of these @@ -1087,6 +1087,10 @@ RESET_AFTER_EXTRA_LINE_READ: s/^GnuTLS using default session cipher\/priority "NORMAL"$/TLS: not preloading cipher list for server/; next if /^GnuTLS<2>: added \d+ protocols, \d+ ciphersuites, \d+ sig algos and \d+ groups into priority list$/; + # only kevent platforms (FreeBSD) say this + next if /^watch dir/; + next if /^watch file .*\/usr\/local/; + # TLS preload # there happen in different orders for OpenSSL/GnuTLS/noTLS next if /^TLS: not preloading (CA bundle|cipher list) for server$/; -- 2.30.2