From bf4054f8ecb7ce88118a4e3631a617dba0600da0 Mon Sep 17 00:00:00 2001 From: Glenn Strauss Date: Thu, 14 May 2020 02:03:14 -0400 Subject: [PATCH] [mod_gnutls] GnuTLS option for TLS (fixes #109) (experimental) mod_gnutls supports most ssl.* config options supported by mod_openssl x-ref: "GnuTLS support for the mod_ssl" https://redmine.lighttpd.net/issues/109 --- SConstruct | 14 + configure.ac | 40 + meson_options.txt | 5 + src/CMakeLists.txt | 15 + src/Makefile.am | 8 + src/SConscript | 3 + src/algo_sha1.h | 20 + src/configfile.c | 6 +- src/meson.build | 17 + src/mod_auth.c | 17 + src/mod_authn_file.c | 18 + src/mod_gnutls.c | 3092 +++++++++++++++++++++++++++++++++++++++++ src/mod_secdownload.c | 26 + src/rand.c | 20 + src/server.c | 13 +- src/sys-crypto.h | 5 + 16 files changed, 3317 insertions(+), 2 deletions(-) create mode 100644 src/mod_gnutls.c diff --git a/SConstruct b/SConstruct index 7e593528..4ade32da 100644 --- a/SConstruct +++ b/SConstruct @@ -252,6 +252,7 @@ vars.AddVariables( BoolVariable('with_memcached', 'enable memcached support', 'no'), PackageVariable('with_mysql', 'enable mysql support', 'no'), BoolVariable('with_openssl', 'enable openssl support', 'no'), + PackageVariable('with_gnutls', 'enable GnuTLS support', 'no'), PackageVariable('with_mbedtls', 'enable mbedTLS support', 'no'), PackageVariable('with_wolfssl', 'enable wolfSSL support', 'no'), BoolVariable('with_nettle', 'enable Nettle support', 'no'), @@ -328,6 +329,7 @@ if 1: LIBDL = '', LIBFCGI = '', LIBGDBM = '', + LIBGNUTLS = '', LIBGSSAPI_KRB5 = '', LIBKRB5 = '', LIBLBER = '', @@ -629,6 +631,18 @@ if 1: LIBCRYPTO = 'nettle', ) + if env['with_gnutls']: + if not autoconf.CheckLibWithHeader('gnutls', 'gnutls/crypto.h', 'C'): + fail("Couldn't find gnutls") + autoconf.env.Append( + CPPFLAGS = [ '-DHAVE_GNUTLS_CRYPTO_H' ], + LIBGNUTLS = 'gnutls', + ) + if not autoconf.env.exists('LIBCRYPTO'): + autoconf.env.Append( + LIBCRYPTO = 'gnutls', + ) + if env['with_pam']: if not autoconf.CheckLibWithHeader('pam', 'security/pam_appl.h', 'C'): fail("Couldn't find pam") diff --git a/configure.ac b/configure.ac index ab9ffaaf..f09610fc 100644 --- a/configure.ac +++ b/configure.ac @@ -815,6 +815,43 @@ if test "$WITH_NETTLE" != no; then AC_SUBST([CRYPTO_LIB]) fi +dnl Check for GnuTLS +AC_MSG_NOTICE([----------------------------------------]) +AC_MSG_CHECKING([for GnuTLS]) +AC_ARG_WITH([gnutls], + AC_HELP_STRING([--with-gnutls@<:@=DIR@:>@],[Include GnuTLS support. DIR points to the installation root. (default no)]), + [WITH_GNUTLS=$withval], + [WITH_GNUTLS=no] +) + +if test "$WITH_GNUTLS" != "no"; then + use_gnutls=yes +else + use_gnutls=no +fi +AC_MSG_RESULT([$use_gnutls]) +AM_CONDITIONAL(BUILD_WITH_GNUTLS, test ! $WITH_GNUTLS = no) + +GNUTLS_CFLAGS= +GNUTLS_LIBS= + +if test "x$use_gnutls" = "xyes"; then + if test "$WITH_GNUTLS" != "yes"; then + GNUTLS_CFLAGS="-I$WITH_GNUTLS/include" + GNUTLS_LIBS="-L$WITH_GNUTLS/lib" + else + dnl oldest GnuTLS supported release is 3.3.x at time this is being written + PKG_CHECK_MODULES([GNUTLS], [gnutls >= 3.3.0]) + fi + AC_SUBST([GNUTLS_CFLAGS]) + AC_SUBST([GNUTLS_LIBS]) + AC_DEFINE([HAVE_GNUTLS_CRYPTO_H], [1], [gnutls/crypto.h]) + if test "$WITH_OPENSSL" = no && test "$WITH_WOLFSSL" = no && \ + test "$WITH_MBEDTLS" = no && test "$WITH_NETTLE" = no; then + AC_SUBST([CRYPTO_LIB],[$GNUTLS_LIBS]) + fi +fi + dnl pcre support AC_MSG_NOTICE([----------------------------------------]) @@ -1612,6 +1649,9 @@ lighty_track_feature "network-openssl" "mod_openssl" \ lighty_track_feature "network-mbedtls" "mod_mbedtls" \ 'test "$WITH_MBEDTLS" != no' +lighty_track_feature "network-gnutls" "mod_gnutls" \ + 'test "$WITH_GNUTLS" != no' + lighty_track_feature "auth-crypt" "" \ 'test "$found_crypt" != no' diff --git a/meson_options.txt b/meson_options.txt index 17918e8b..960e44c6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,6 +23,11 @@ option('with_geoip', value: false, description: 'with GeoIP-support mod_geoip [default: off]', ) +option('with_gnutls', + type: 'string', + value: 'false', + description: 'with GnuTLS-support [default: off]', +) option('with_krb5', type: 'boolean', value: false, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b1043e05..c05be6e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ option(WITH_XATTR "with xattr-support for the stat-cache [default: off]") option(WITH_MYSQL "with mysql-support for mod_vhostdb_mysql [default: off]") option(WITH_PGSQL "with postgres-support for mod_vhostdb_pgsql [default: off]") option(WITH_DBI "with dbi-support for mod_vhostdb_dbi [default: off]") +option(WITH_GNUTLS "with GnuTLS-support [default: off]") option(WITH_MBEDTLS "with mbedTLS-support [default: off]") option(WITH_OPENSSL "with openssl-support [default: off]") option(WITH_WOLFSSL "with wolfSSL-support [default: off]") @@ -410,6 +411,15 @@ else() unset(HAVE_NETTLE_NETTLE_TYPES_H) endif() +if(WITH_GNUTLS) + pkg_check_modules(LIBGNUTLS REQUIRED gnutls) + set(HAVE_LIBGNUTLS 1) + add_definitions(-DHAVE_GNUTLS_CRYPTO_H) + if(NOT WITH_OPENSSL AND NOT WITH_WOLFSSL AND NOT WITH_MBEDTLS AND NOT WITH_NETTLE) + set(CRYPTO_LIBRARY gnutls) + endif() +endif() + if(WITH_PCRE) ## if we have pcre-config, use it xconfig(pcre-config PCRE_INCDIR PCRE_LIBDIR PCRE_LDFLAGS PCRE_CFLAGS) @@ -1091,6 +1101,11 @@ if(NOT ${CRYPTO_LIBRARY} EQUAL "") target_link_libraries(mod_wstunnel ${CRYPTO_LIBRARY}) endif() +if(HAVE_LIBGNUTLS) + add_and_install_library(mod_gnutls "mod_gnutls.c") + target_link_libraries(mod_gnutls gnutls) +endif() + if(HAVE_LIBMBEDTLS AND HAVE_LIBMEDCRYPTO AND HAVE_LIBMEDX509) target_link_libraries(lighttpd mbedtls) target_link_libraries(lighttpd mbedcrypto) diff --git a/src/Makefile.am b/src/Makefile.am index e9267c08..1e2977c7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -398,6 +398,14 @@ mod_mbedtls_la_LDFLAGS = $(common_module_ldflags) mod_mbedtls_la_LIBADD = $(MTLS_LIB) $(common_libadd) endif +if BUILD_WITH_GNUTLS +lib_LTLIBRARIES += mod_gnutls.la +mod_gnutls_la_SOURCES = mod_gnutls.c +mod_gnutls_la_LDFLAGS = $(common_module_ldflags) +mod_gnutls_la_LIBADD = $(GNUTLS_LIBS) $(common_libadd) +mod_gnutls_la_CPPFLAGS = $(GNUTLS_CFLAGS) +endif + lib_LTLIBRARIES += mod_rewrite.la mod_rewrite_la_SOURCES = mod_rewrite.c diff --git a/src/SConscript b/src/SConscript index c540fc48..4969d871 100644 --- a/src/SConscript +++ b/src/SConscript @@ -182,6 +182,9 @@ if env['with_wolfssl']: if env['with_mbedtls']: modules['mod_mbedtls'] = { 'src' : [ 'mod_mbedtls.c' ], 'lib' : [ env['LIBSSL'], env['LIBX509'], env['LIBCRYPTO'] ] } +if env['with_gnutls']: + modules['mod_gnutls'] = { 'src' : [ 'mod_gnutls.c' ], 'lib' : [ env['LIBGNUTLS'] ] } + staticenv = env.Clone(CPPFLAGS=[ env['CPPFLAGS'], '-DLIGHTTPD_STATIC' ]) ## all the core-sources + the modules diff --git a/src/algo_sha1.h b/src/algo_sha1.h index 34b9780d..6caabe5b 100644 --- a/src/algo_sha1.h +++ b/src/algo_sha1.h @@ -48,6 +48,26 @@ SHA1_Update(SHA_CTX *ctx, const void *data, size_t length) #include +#elif defined(USE_GNUTLS_CRYPTO) + +#include +#ifndef SHA_DIGEST_LENGTH +#define SHA_DIGEST_LENGTH 20 +#endif +typedef gnutls_hash_hd_t SHA_CTX; +#define SHA1_Init(ctx) \ + do { \ + if (gnutls_hash_init((ctx), GNUTLS_DIG_SHA1) < 0) \ + SEGFAULT(); \ + } while (0) +#define SHA1_Final(digest, ctx) \ + gnutls_hash_deinit(*(ctx),(digest)) +static void +SHA1_Update(SHA_CTX *ctx, const void *data, size_t length) +{ + gnutls_hash(*ctx, data, length); +} + #endif #else /* ! USE_LIB_CRYPTO */ diff --git a/src/configfile.c b/src/configfile.c index 8e415045..7862ca7b 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -307,6 +307,8 @@ static void config_compat_module_load (server *srv) { append_mod_staticfile = 0; else if (buffer_eq_slen(m, CONST_STR_LEN("mod_dirlisting"))) append_mod_dirlisting = 0; + else if (buffer_eq_slen(m, CONST_STR_LEN("mod_gnutls"))) + append_mod_openssl = 0; else if (buffer_eq_slen(m, CONST_STR_LEN("mod_mbedtls"))) append_mod_openssl = 0; else if (buffer_eq_slen(m, CONST_STR_LEN("mod_openssl"))) @@ -704,7 +706,9 @@ static int config_insert_srvconf(server *srv) { break; case 30:/* ssl.engine */ ssl_enabled = (0 != cpv->v.u); - #if !defined(USE_OPENSSL_CRYPTO) && !defined(USE_MBEDTLS_CRYPTO) + #if !defined(USE_OPENSSL_CRYPTO) \ + && !defined(USE_MBEDTLS_CRYPTO) \ + && !defined(USE_GNUTLS_CRYPTO) if (ssl_enabled) { log_error(srv->errh, __FILE__, __LINE__, "ssl support is missing; " diff --git a/src/meson.build b/src/meson.build index 9dc48db6..74ddc574 100644 --- a/src/meson.build +++ b/src/meson.build @@ -397,6 +397,7 @@ endif libssl = [] libx509 = [] libcrypto = [] +libgnutls = [] if get_option('with_openssl') # manual search: # header: openssl/ssl.h @@ -439,6 +440,16 @@ if get_option('with_nettle') != 'false' libcrypto = [ dependency('libnettle') ] conf_data.set('HAVE_NETTLE_NETTLE_TYPES_H', true) endif +if get_option('with_gnutls') + # manual search: + # header: gnutls/gnutls.h + # function: gnutls_check_version (-lgnutls) + libgnutls = [ dependency('libgnutls') ] + conf_data.set('HAVE_GNUTLS_CRYPTO_H', true) + if not(get_option('with_openssl')) and not(get_option('with_wolfssl')) and not(get_option('with_mbedtls')) and not(getoption('with_nettle')) + libcrypto = [ dependency('libgnutls') ] + endif +endif libpcre = [] if get_option('with_pcre') @@ -1002,6 +1013,12 @@ if get_option('with_mbedtls') != 'false' ] endif +if get_option('with_gnutls') != 'false' + modules += [ + [ 'mod_gnutls', [ 'mod_gnutls.c' ], libgnutls ], + ] +endif + if get_option('with_pam') modules += [ [ 'mod_authn_pam', [ 'mod_authn_pam.c' ], libpam ], diff --git a/src/mod_auth.c b/src/mod_auth.c index 7a3fceef..812b8f5d 100644 --- a/src/mod_auth.c +++ b/src/mod_auth.c @@ -57,6 +57,23 @@ SHA256_Update(SHA256_CTX *ctx, const void *data, size_t length) #include +#elif defined(USE_GNUTLS_CRYPTO) + +#include +typedef gnutls_hash_hd_t SHA256_CTX; +#define SHA256_Init(ctx) \ + do { \ + if (gnutls_hash_init((ctx), GNUTLS_DIG_SHA256) < 0) \ + SEGFAULT(); \ + } while (0) +#define SHA256_Final(digest, ctx) \ + gnutls_hash_deinit(*(ctx),(digest)) +static void +SHA256_Update(SHA256_CTX *ctx, const void *data, size_t length) +{ + gnutls_hash(*ctx, data, length); +} + #endif #endif /* USE_LIB_CRYPTO */ diff --git a/src/mod_authn_file.c b/src/mod_authn_file.c index 7d8de7b2..bbad3b12 100644 --- a/src/mod_authn_file.c +++ b/src/mod_authn_file.c @@ -95,6 +95,24 @@ SHA256_Update(SHA256_CTX *ctx, const void *data, size_t length) #include #include +#elif defined(USE_GNUTLS_CRYPTO) + +#include +#define NO_MD4 +typedef gnutls_hash_hd_t SHA256_CTX; +#define SHA256_Init(ctx) \ + do { \ + if (gnutls_hash_init((ctx), GNUTLS_DIG_SHA256) < 0) \ + SEGFAULT(); \ + } while (0) +#define SHA256_Final(digest, ctx) \ + gnutls_hash_deinit(*(ctx),(digest)) +static void +SHA256_Update(SHA256_CTX *ctx, const void *data, size_t length) +{ + gnutls_hash(*ctx, data, length); +} + #endif #endif /* USE_LIB_CRYPTO */ diff --git a/src/mod_gnutls.c b/src/mod_gnutls.c new file mode 100644 index 00000000..cc5d7829 --- /dev/null +++ b/src/mod_gnutls.c @@ -0,0 +1,3092 @@ +/* + * mod_gnutls - GnuTLS support for lighttpd + * + * Copyright(c) 2020 Glenn Strauss gstrauss()gluelogic.com All rights reserved + * License: BSD 3-clause (same as lighttpd) + */ +/* + * GnuTLS manual: https://www.gnutls.org/documentation.html + * + * future possible enhancements: OCSP stapling + * + * Note: session tickets are disabled by default. If enabled, + * mod_gnutls rotates server ticket encryption key (STEK) every 24 hours. + * This is fine for use with a single lighttpd instance, but with multiple + * lighttpd workers, no coordinated STEK (server ticket encryption key) + * rotation occurs other than by (some external job) restarting lighttpd. + * Restarting lighttpd generates a new key that is shared by lighttpd workers + * for the lifetime of the new key. If the rotation period expires and + * lighttpd has not been restarted, lighttpd workers will generate new + * independent keys, making session tickets less effective for session + * resumption, since clients have a lower chance for future connections to + * reach the same lighttpd worker. However, things will still work, and a new + * session will be created if session resumption fails. Admins should plan to + * restart lighttpd at least every 24 hours if session tickets are enabled and + * multiple lighttpd workers are configured. + */ +#include "first.h" + +#include +#include +#include +#include +#include /* vsnprintf() */ +#include +#include + +#include +#include +#include + +#ifdef GNUTLS_SKIP_GLOBAL_INIT +GNUTLS_SKIP_GLOBAL_INIT +#endif + +#include "base.h" +#include "http_header.h" +#include "log.h" +#include "plugin.h" + +typedef struct { + /* SNI per host: with COMP_SERVER_SOCKET, COMP_HTTP_SCHEME, COMP_HTTP_HOST */ + gnutls_certificate_credentials_t ssl_cred; + char trust_inited; + gnutls_datum_t *ssl_pemfile_x509; + gnutls_privkey_t ssl_pemfile_pkey; +} plugin_cert; + +typedef struct { + int8_t ssl_session_ticket; + /*(preserved here for deinit at server shutdown)*/ + gnutls_priority_t priority_cache; + #if GNUTLS_VERSION_NUMBER < 0x030600 + gnutls_dh_params_t dh_params; + #endif +} plugin_ssl_ctx; + +typedef struct { + plugin_cert *pc; + + /*(used only during startup; not patched)*/ + unsigned char ssl_enabled; /* only interesting for setting up listening sockets. don't use at runtime */ + unsigned char ssl_honor_cipher_order; /* determine SSL cipher in server-preferred order, not client-order */ + unsigned char ssl_empty_fragments; + unsigned char ssl_use_sslv2; + unsigned char ssl_use_sslv3; + const buffer *ssl_cipher_list; + const buffer *ssl_dh_file; + const buffer *ssl_ec_curve; + array *ssl_conf_cmd; + + /*(copied from plugin_data for socket ssl_ctx config)*/ + gnutls_priority_t priority_cache; + unsigned char ssl_session_ticket; + unsigned char ssl_verifyclient; + unsigned char ssl_verifyclient_enforce; + unsigned char ssl_verifyclient_depth; + + const char *priority_base; + buffer priority_str; + #if GNUTLS_VERSION_NUMBER < 0x030600 + gnutls_dh_params_t dh_params; + #endif +} plugin_config_socket; /*(used at startup during configuration)*/ + +typedef struct { + /* SNI per host: w/ COMP_SERVER_SOCKET, COMP_HTTP_SCHEME, COMP_HTTP_HOST */ + plugin_cert *pc; + gnutls_datum_t *ssl_ca_file; /* .data is (gnutls_x509_crt_t) */ + gnutls_datum_t *ssl_ca_dn_file; /* .data is (gnutls_x509_crt_t) */ + gnutls_datum_t *ssl_ca_crl_file;/* .data is (gnutls_x509_crl_t) */ + + unsigned char ssl_verifyclient; + unsigned char ssl_verifyclient_enforce; + unsigned char ssl_verifyclient_depth; + unsigned char ssl_verifyclient_export_cert; + unsigned char ssl_read_ahead; + unsigned char ssl_log_noise; + unsigned char ssl_disable_client_renegotiation; + const buffer *ssl_verifyclient_username; + const buffer *ssl_acme_tls_1; + #if GNUTLS_VERSION_NUMBER < 0x030600 + gnutls_dh_params_t dh_params; + #endif +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_ssl_ctx *ssl_ctxs; + plugin_config defaults; + server *srv; +} plugin_data; + +static int ssl_is_init; +/* need assigned p->id for deep access of module handler_ctx for connection + * i.e. handler_ctx *hctx = r->plugin_ctx[plugin_data_singleton->id]; */ +static plugin_data *plugin_data_singleton; +#define LOCAL_SEND_BUFSIZE 16384 /* DEFAULT_MAX_RECORD_SIZE */ +static char *local_send_buffer; + +typedef struct { + gnutls_session_t ssl; /* gnutls request/connection context */ + request_st *r; + connection *con; + int8_t request_env_patched; + int8_t close_notify; + uint8_t alpn; + int8_t ssl_session_ticket; + int handshake; + size_t pending_write; + plugin_config conf; + unsigned int verify_status; + buffer *tmp_buf; + gnutls_certificate_credentials_t acme_tls_1_cred; +} handler_ctx; + + +static handler_ctx * +handler_ctx_init (void) +{ + handler_ctx *hctx = calloc(1, sizeof(*hctx)); + force_assert(hctx); + return hctx; +} + + +static void +handler_ctx_free (handler_ctx *hctx) +{ + gnutls_deinit(hctx->ssl); + if (hctx->acme_tls_1_cred) + gnutls_certificate_free_credentials(hctx->acme_tls_1_cred); + free(hctx); +} + + +__attribute_cold__ +static void elog(log_error_st * const errh, + const char * const file, const int line, + const int rc, const char * const msg) +{ + /* error logging convenience function that decodes gnutls result codes */ + log_error(errh, file, line, "GnuTLS: %s: (%s) %s", + msg, gnutls_strerror_name(rc), gnutls_strerror(rc)); +} + + +__attribute_cold__ +__attribute_format__((__printf__, 5, 6)) +static void elogf(log_error_st * const errh, + const char * const file, const int line, + const int rc, const char * const fmt, ...) +{ + char msg[1024]; + va_list ap; + va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + elog(errh, file, line, rc, msg); +} + + +/* gnutls func gnutls_load_file() loads file contents into a gnutls_datum_t. + * The data loaded could be sensitive, e.g. a private key included in a pemfile. + * However, gnutls_load_file() is not careful about zeroizing memory on error, + * might use realloc() (which does not guarantee to zeroize memory released), + * and silently continues on short read, so provide our own. Updates to + * gnutls 3.6.14 may be more careful with private key files, but do not + * extend the same care to pemfiles which might contain private keys. + * Related, see also mod_gnutls_datum_wipe() below. + * https://gitlab.com/gnutls/gnutls/-/issues/1001 + * https://gitlab.com/gnutls/gnutls/-/issues/1002 + * https://gitlab.com/gnutls/gnutls/-/merge_requests/1270 + */ +#include +#include +#include +#include "fdevent.h" +static int +mod_gnutls_load_file (const char * const fn, gnutls_datum_t * const d, log_error_st *errh) +{ + #if 0 + int rc = gnutls_load_file(fn, d); + if (rc < 0) + elogf(errh, __FILE__, __LINE__, rc, "%s() %s", __func__, fn); + return rc; + #else + int fd = -1; + unsigned int sz = 0; + char *buf = NULL; + do { + fd = fdevent_open_cloexec(fn, 1, O_RDONLY, 0); /*(1: follows symlinks)*/ + if (fd < 0) break; + + struct stat st; + if (0 != fstat(fd, &st)) break; + if (st.st_size >= UINT_MAX) { /*(file too large for gnutls_datum_t)*/ + errno = EOVERFLOW; + break; + } + + sz = (unsigned int)st.st_size; + buf = gnutls_malloc(sz+1); /*(+1 trailing '\0' for gnutls_load_file())*/ + if (NULL == buf) break; + + ssize_t rd = 0; + unsigned int off = 0; + do { + rd = read(fd, buf+off, sz-off); + } while (rd > 0 ? (off += (unsigned int)rd) != sz : errno == EINTR); + if (off != sz) { /*(file truncated?)*/ + if (rd >= 0) errno = EIO; + break; + } + + buf[sz] = '\0'; + d->data = (unsigned char *)buf; + d->size = sz; + close(fd); + return 0; + } while (0); + int errnum = errno; + log_perror(errh, __FILE__, __LINE__, "%s() %s", __func__, fn); + if (fd >= 0) close(fd); + if (buf) { + gnutls_memset(buf, 0, sz); + gnutls_free(buf); + } + errno = errnum; + return GNUTLS_E_FILE_ERROR; + #endif +} + + +/* GnuTLS does not expose _gnutls_free_key_datum() so provide our own */ +static void +mod_gnutls_datum_wipe (gnutls_datum_t * const d) +{ + if (NULL == d) return; + if (d->data) { + if (d->size) gnutls_memset(d->data, 0, d->size); + gnutls_free(d->data); + d->data = NULL; + } + d->size = 0; +} + + +static time_t stek_rotate_ts; +static gnutls_datum_t session_ticket_key; +static void +mod_gnutls_session_ticket_key_free (void) +{ + mod_gnutls_datum_wipe(&session_ticket_key); +} +static void +mod_gnutls_session_ticket_key_init (server *srv) +{ + int rc = gnutls_session_ticket_key_generate(&session_ticket_key); + if (rc < 0) { + /*(should not happen, but if it does, disable session ticket)*/ + session_ticket_key.size = 0; + elog(srv->errh, __FILE__, __LINE__, rc, + "gnutls_session_ticket_key_generate()"); + } +} +static void +mod_gnutls_session_ticket_key_rotate (server *srv) +{ + mod_gnutls_session_ticket_key_free(); + mod_gnutls_session_ticket_key_init(srv); +} + + +INIT_FUNC(mod_gnutls_init) +{ + plugin_data_singleton = (plugin_data *)calloc(1, sizeof(plugin_data)); + return plugin_data_singleton; +} + + +static int mod_gnutls_init_once_gnutls (void) +{ + if (ssl_is_init) return 1; + ssl_is_init = 1; + + /* Note: on systems with support for weak symbols, GNUTLS_SKIP_GLOBAL_INIT + * is set near top of this file to inhibit GnuTLS implicit initialization + * in a library constructor. On systems without support for weak symbols, + * set GNUTLS_NO_EXPLICIT_INIT=1 in the environment before starting lighttpd + * (GnuTLS 3.3.0 or later) */ + + if (gnutls_global_init() != GNUTLS_E_SUCCESS) + return 0; + + local_send_buffer = malloc(LOCAL_SEND_BUFSIZE); + force_assert(NULL != local_send_buffer); + + return 1; +} + + +static void mod_gnutls_free_gnutls (void) +{ + if (!ssl_is_init) return; + + mod_gnutls_session_ticket_key_free(); + + gnutls_global_deinit(); + + free(local_send_buffer); + ssl_is_init = 0; +} + + +static void +mod_gnutls_free_config_crts (gnutls_datum_t *d) +{ + if (NULL == d) return; + gnutls_x509_crt_t *crts = (gnutls_x509_crt_t *)(void *)d->data; + unsigned int u = d->size; + for (unsigned int i = 0; i < u; ++i) + gnutls_x509_crt_deinit(crts[i]); + gnutls_free(crts); + gnutls_free(d); +} + + +static void +mod_gnutls_free_config_crls (gnutls_datum_t *d) +{ + if (NULL == d) return; + gnutls_x509_crl_t *crls = (gnutls_x509_crl_t *)(void *)d->data; + unsigned int u = d->size; + for (unsigned int i = 0; i < u; ++i) + gnutls_x509_crl_deinit(crls[i]); + gnutls_free(crls); + gnutls_free(d); +} + + +static gnutls_datum_t * +mod_gnutls_load_config_crts (const char *fn, log_error_st *errh) +{ + /*(very similar to other mod_gnutls_load_config_*())*/ + if (!mod_gnutls_init_once_gnutls()) return NULL; + + gnutls_datum_t f = { NULL, 0 }; + int rc = mod_gnutls_load_file(fn, &f, errh); + if (rc < 0) return NULL; + gnutls_datum_t *d = gnutls_malloc(sizeof(gnutls_datum_t)); + if (d == NULL) { + mod_gnutls_datum_wipe(&f); + return NULL; + } + d->data = NULL; + d->size = 0; + rc = gnutls_x509_crt_list_import2((gnutls_x509_crt_t **)&d->data, &d->size, + &f, GNUTLS_X509_FMT_PEM, + GNUTLS_X509_CRT_LIST_SORT); + mod_gnutls_datum_wipe(&f); + if (rc < 0) { + elogf(errh, __FILE__, __LINE__, rc, + "gnutls_x509_crt_list_import2() %s", fn); + mod_gnutls_free_config_crts(d); + return NULL; + } + + return d; +} + + +static gnutls_datum_t * +mod_gnutls_load_config_crls (const char *fn, log_error_st *errh) +{ + /*(very similar to other mod_gnutls_load_config_*())*/ + if (!mod_gnutls_init_once_gnutls()) return NULL; + + gnutls_datum_t f = { NULL, 0 }; + int rc = mod_gnutls_load_file(fn, &f, errh); + if (rc < 0) return NULL; + gnutls_datum_t *d = gnutls_malloc(sizeof(gnutls_datum_t)); + if (d == NULL) { + mod_gnutls_datum_wipe(&f); + return NULL; + } + d->data = NULL; + d->size = 0; + rc = gnutls_x509_crl_list_import2((gnutls_x509_crl_t **)&d->data, &d->size, + &f, GNUTLS_X509_FMT_PEM, 0); + mod_gnutls_datum_wipe(&f); + if (rc < 0) { + elogf(errh, __FILE__, __LINE__, rc, + "gnutls_x509_crl_list_import2() %s", fn); + mod_gnutls_free_config_crls(d); + return NULL; + } + + return d; +} + + +static gnutls_privkey_t +mod_gnutls_load_config_pkey (const char *fn, log_error_st *errh) +{ + /*(very similar to other mod_gnutls_load_config_*())*/ + if (!mod_gnutls_init_once_gnutls()) return NULL; + + gnutls_datum_t f = { NULL, 0 }; + int rc = mod_gnutls_load_file(fn, &f, errh); + if (rc < 0) return NULL; + gnutls_privkey_t pkey; + rc = gnutls_privkey_init(&pkey); + if (rc < 0) { + mod_gnutls_datum_wipe(&f); + return NULL; + } + rc = gnutls_privkey_import_x509_raw(pkey, &f, GNUTLS_X509_FMT_PEM, NULL, 0); + mod_gnutls_datum_wipe(&f); + if (rc < 0) { + elogf(errh, __FILE__, __LINE__, rc, + "gnutls_privkey_import_x509_raw() %s", fn); + gnutls_privkey_deinit(pkey); + return NULL; + } + + return pkey; +} + + +static void +mod_gnutls_free_config (server *srv, plugin_data * const p) +{ + if (NULL != p->ssl_ctxs) { + gnutls_priority_t pcache_global_scope = p->ssl_ctxs->priority_cache; + /* free from $SERVER["socket"] (if not copy of global scope) */ + for (uint32_t i = 1; i < srv->config_context->used; ++i) { + plugin_ssl_ctx * const s = p->ssl_ctxs + i; + if (s->priority_cache && s->priority_cache != pcache_global_scope) { + if (s->priority_cache) + gnutls_priority_deinit(s->priority_cache); + #if GNUTLS_VERSION_NUMBER < 0x030600 + if (s->dh_params) + gnutls_dh_params_deinit(s->dh_params); + #endif + } + } + /* free from global scope */ + if (pcache_global_scope) { + if (p->ssl_ctxs[0].priority_cache) + gnutls_priority_deinit(p->ssl_ctxs[0].priority_cache); + #if GNUTLS_VERSION_NUMBER < 0x030600 + if (p->ssl_ctxs[0].dh_params) + gnutls_dh_params_deinit(p->ssl_ctxs[0].dh_params); + #endif + } + free(p->ssl_ctxs); + } + + if (NULL == p->cvlist) return; + /* (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* ssl.pemfile */ + if (cpv->vtype == T_CONFIG_LOCAL) { + plugin_cert *pc = cpv->v.v; + gnutls_certificate_free_credentials(pc->ssl_cred); + mod_gnutls_free_config_crts(pc->ssl_pemfile_x509); + gnutls_privkey_deinit(pc->ssl_pemfile_pkey); + free(pc); + } + break; + case 2: /* ssl.ca-file */ + case 3: /* ssl.ca-dn-file */ + if (cpv->vtype == T_CONFIG_LOCAL) + mod_gnutls_free_config_crts(cpv->v.v); + break; + case 4: /* ssl.ca-crl-file */ + if (cpv->vtype == T_CONFIG_LOCAL) + mod_gnutls_free_config_crls(cpv->v.v); + break; + default: + break; + } + } + } +} + + +FREE_FUNC(mod_gnutls_free) +{ + plugin_data *p = p_d; + if (NULL == p->srv) return; + mod_gnutls_free_config(p->srv, p); + mod_gnutls_free_gnutls(); +} + + +static void +mod_gnutls_merge_config_cpv (plugin_config * const pconf, const config_plugin_value_t * const cpv) +{ + switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ + case 0: /* ssl.pemfile */ + if (cpv->vtype == T_CONFIG_LOCAL) + pconf->pc = cpv->v.v; + break; + case 1: /* ssl.privkey */ + break; + case 2: /* ssl.ca-file */ + if (cpv->vtype == T_CONFIG_LOCAL) + pconf->ssl_ca_file = cpv->v.v; + break; + case 3: /* ssl.ca-dn-file */ + if (cpv->vtype == T_CONFIG_LOCAL) + pconf->ssl_ca_dn_file = cpv->v.v; + break; + case 4: /* ssl.ca-crl-file */ + if (cpv->vtype == T_CONFIG_LOCAL) + pconf->ssl_ca_crl_file = cpv->v.v; + break; + case 5: /* ssl.read-ahead */ + pconf->ssl_read_ahead = (0 != cpv->v.u); + break; + case 6: /* ssl.disable-client-renegotiation */ + pconf->ssl_disable_client_renegotiation = (0 != cpv->v.u); + break; + case 7: /* ssl.verifyclient.activate */ + pconf->ssl_verifyclient = (0 != cpv->v.u); + break; + case 8: /* ssl.verifyclient.enforce */ + pconf->ssl_verifyclient_enforce = (0 != cpv->v.u); + break; + case 9: /* ssl.verifyclient.depth */ + pconf->ssl_verifyclient_depth = (unsigned char)cpv->v.shrt; + break; + case 10:/* ssl.verifyclient.username */ + pconf->ssl_verifyclient_username = cpv->v.b; + break; + case 11:/* ssl.verifyclient.exportcert */ + pconf->ssl_verifyclient_export_cert = (0 != cpv->v.u); + break; + case 12:/* ssl.acme-tls-1 */ + pconf->ssl_acme_tls_1 = cpv->v.b; + break; + case 13:/* debug.log-ssl-noise */ + pconf->ssl_log_noise = (unsigned char)cpv->v.shrt; + break; + default:/* should not happen */ + return; + } +} + + +static void +mod_gnutls_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) +{ + do { + mod_gnutls_merge_config_cpv(pconf, cpv); + } while ((++cpv)->k_id != -1); +} + + +static void +mod_gnutls_patch_config (request_st * const r, plugin_config * const pconf) +{ + plugin_data * const p = plugin_data_singleton; + memcpy(pconf, &p->defaults, sizeof(plugin_config)); + for (int i = 1, used = p->nconfig; i < used; ++i) { + if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) + mod_gnutls_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]); + } +} + + +static int +mod_gnutls_verify_set_tlist (handler_ctx *hctx, int req) +{ + /* XXX GnuTLS interfaces appear to be better for client-side use than for + * server-side use. If gnutls_x509_trust_list_t were available to attach + * to a gnutls_session_t (without copying), then there would not be race + * conditions swapping trust lists on the credential *shared* between + * connections when ssl.ca-dn-file and ssl.ca-file are both set. If both + * are set, current code attempts to set ssl.ca-dn-file right before sending + * client cert request, and sets ssl.ca-file right before client cert verify + * + * Architecture would be cleaner if the trust list for verifying client cert + * (gnutls_x509_trust_list_t) were available to attach to a gnutls_session_t + * instead of attaching to a gnutls_certificate_credentials_t. + */ + if (hctx->conf.pc->trust_inited) return GNUTLS_E_SUCCESS; + + gnutls_datum_t *d; + int rc; + + /* set trust list using ssl_ca_dn_file, if set, for client cert request + * (when req is true) (for CAs sent by server to client in cert request) + * (trust is later replaced by ssl_ca_file for client cert verification) */ + d = req && hctx->conf.ssl_ca_dn_file + ? hctx->conf.ssl_ca_dn_file + : hctx->conf.ssl_ca_file; + if (NULL == d) { + log_error(hctx->r->conf.errh, __FILE__, __LINE__, + "GnuTLS: can't verify client without ssl.ca-file " + "for TLS server name %s", + hctx->r->uri.authority.ptr); /*(might not be set yet if no SNI)*/ + return GNUTLS_E_INTERNAL_ERROR; + } + + gnutls_x509_trust_list_t tlist = NULL; + rc = gnutls_x509_trust_list_init(&tlist, 0); + if (rc < 0) { + elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "gnutls_x509_trust_list_init()"); + return rc; + } + + gnutls_x509_crt_t *clist = (gnutls_x509_crt_t *)(void *)d->data; + rc = gnutls_x509_trust_list_add_cas(tlist, clist, d->size, 0); + if (rc < 0) { + elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "gnutls_x509_trust_list_add_cas()"); + gnutls_x509_trust_list_deinit(tlist, 0); + return rc; + } + + d = hctx->conf.ssl_ca_crl_file; + if (NULL != d && req && hctx->conf.ssl_ca_dn_file) { + /*(check req and ssl_ca_dn_file to see if tlist will be replaced later, + * and, if so, defer setting crls until later)*/ + gnutls_x509_crl_t *crl_list = (gnutls_x509_crl_t *)(void *)d->data; + rc = gnutls_x509_trust_list_add_crls(tlist, crl_list, d->size, 0, 0); + if (rc < 0) { + elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "gnutls_x509_trust_list_add_crls()"); + gnutls_x509_trust_list_deinit(tlist, 0); + return rc; + } + } + + /* gnutls limitation; wasteful to have to copy into each cred */ + /* (would be better to share list with session, instead of with cred) */ + gnutls_certificate_credentials_t ssl_cred = hctx->conf.pc->ssl_cred; + gnutls_certificate_set_trust_list(ssl_cred, tlist, 0); /* transfer tlist */ + + /* (must flip trust lists back and forth b/w DN names and verify CAs) */ + if (NULL == hctx->conf.ssl_ca_dn_file) + hctx->conf.pc->trust_inited = 1; + + return GNUTLS_E_SUCCESS; +} + + +static int +mod_gnutls_verify_cb (gnutls_session_t ssl) +{ + handler_ctx * const hctx = gnutls_session_get_ptr(ssl); + if (!hctx->conf.ssl_verifyclient) return 0; + + if (gnutls_auth_client_get_type(ssl) != GNUTLS_CRD_CERTIFICATE) + return GNUTLS_E_SUCCESS; + + int rc; + + /* gnutls limitation; wasteful to have to copy into each cred */ + /* (would be better to share list with session, instead of with cred) */ + if (hctx->conf.ssl_ca_dn_file) { + rc = mod_gnutls_verify_set_tlist(hctx, 0); /* for client cert verify */ + if (rc < 0) return rc; + } + + /* gnutls_certificate_verify_peers2() includes processing OCSP staping, + * as well as certificate depth verification before getting internal + * flags and calling gnutls_x509_trust_list_verify_crt2() + * advanced reference: + * gnutls lib/cert-sessions.c:_gnutls_x509_cert_verify_peers() + * XXX: if GnuTLS provided a more advanced interface which permitted + * providing trust list, verify depth, and flags, we could avoid copying + * ca chain and crls into each credential, using + * gnutls_x509_trust_list_add_cas() + * gnutls_x509_trust_list_add_crls() + * gnutls_x509_trust_list_verify_crt2() + * See also GnuTLS manual Section 7.3.4 Advanced certificate verification + */ + + rc = gnutls_certificate_verify_peers2(ssl, &hctx->verify_status); + if (rc < 0) return rc; + + if (hctx->verify_status == 0 && hctx->conf.ssl_ca_dn_file) { + /* verify that client cert is issued by CA in ssl.ca-dn-file + * if both ssl.ca-dn-file and ssl.ca-file were configured */ + gnutls_x509_crt_t *CA_list = + (gnutls_x509_crt_t *)&hctx->conf.ssl_ca_dn_file->data; + unsigned int len = hctx->conf.ssl_ca_dn_file->size; + unsigned int i; + gnutls_x509_dn_t issuer, subject; + unsigned int crt_size = 0; + const gnutls_datum_t *crts; + gnutls_x509_crt_t crt = NULL; + crts = gnutls_certificate_get_peers(ssl, &crt_size); + if (0 == crt_size + || gnutls_x509_crt_init(&crt) < 0 + || gnutls_x509_crt_import(crt, &crts[0], GNUTLS_X509_FMT_DER) < 0 + || gnutls_x509_crt_get_subject(crt, &issuer) < 0) + len = 0; /*(trigger failure path below)*/ + for (i = 0; i < len; ++i) { + if (gnutls_x509_crt_get_subject(CA_list[i], &subject) >= 0 + && 0 == memcmp(&subject, &issuer, sizeof(issuer))) + break; + } + if (i == len) + hctx->verify_status |= GNUTLS_CERT_SIGNER_NOT_CA; + if (crt) gnutls_x509_crt_deinit(crt); + } + + if (hctx->verify_status != 0 && hctx->conf.ssl_verifyclient_enforce) { + /* (not looping on GNUTLS_E_INTERRUPTED or GNUTLS_E_AGAIN + * since we do not want to block here (and not expecting to have to))*/ + (void)gnutls_alert_send(ssl, GNUTLS_AL_FATAL, GNUTLS_A_ACCESS_DENIED); + return GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR; + } + + return GNUTLS_E_SUCCESS; +} + + +static int +mod_gnutls_construct_crt_chain (plugin_cert *pc, gnutls_datum_t *d, log_error_st *errh) +{ + /* Historically, openssl will use the cert chain in (SSL_CTX *) if a cert + * does not have a chain configured in (SSL *). Attempt to provide + * compatible behavior here. This may be called after startup since + * ssl.pemfile might be defined in a config scope without ssl.ca-file, + * and at runtime is when ssl.ca-file (e.g. from matching $SERVER["socket"]) + * would be known along with ssl.pemfile (e.g. from $HTTP["host"]) */ + + gnutls_certificate_credentials_t ssl_cred; + int rc = gnutls_certificate_allocate_credentials(&ssl_cred); + if (rc < 0) return rc; + unsigned int ncrt = (d ? d->size : 0); + unsigned int off = (d == pc->ssl_pemfile_x509) ? 0 : 1; + gnutls_pcert_st * const pcert_list = + gnutls_malloc(sizeof(gnutls_pcert_st) * (off+ncrt)); + if (NULL == pcert_list) { + gnutls_certificate_free_credentials(ssl_cred); + return GNUTLS_E_MEMORY_ERROR; + } + memset(pcert_list, 0, sizeof(gnutls_pcert_st) * (off+ncrt)); + rc = 0; + + if (off) { /*(add crt to chain if different from d)*/ + /*assert(pc->ssl_pemfile_x509->size == 1)*/ + gnutls_x509_crt_t *crts = + (gnutls_x509_crt_t *)(void *)pc->ssl_pemfile_x509->data; + rc = gnutls_pcert_import_x509(pcert_list, crts[0], 0); + } + + if (0 == rc && ncrt) { + gnutls_x509_crt_t *crts = (gnutls_x509_crt_t *)(void *)d->data; + #if GNUTLS_VERSION_NUMBER < 0x030400 + /*(GNUTLS_X509_CRT_LIST_SORT not needed; crts sorted when file read)*/ + rc = gnutls_pcert_import_x509_list(pcert_list+off, crts, &ncrt, 0); + #else /* manually import list, but note that sorting is not implemented */ + rc = 0; + for (unsigned int i = 0; i < ncrt; ++i) { + rc = gnutls_pcert_import_x509(pcert_list+off+i, crts[i], 0); + if (rc < 0) break; + } + #endif + } + ncrt += off; + if (0 == rc) + rc = gnutls_certificate_set_key(ssl_cred, NULL, 0, pcert_list, ncrt, + pc->ssl_pemfile_pkey); + if (rc < 0) { + for (unsigned int i = 0; i < ncrt; ++i) + gnutls_pcert_deinit(pcert_list+i); + gnutls_free(pcert_list); + gnutls_certificate_free_credentials(ssl_cred); + elog(errh, __FILE__, __LINE__, rc, "gnutls_certificate_set_key()"); + return rc; + } + /* XXX: gnutls_certificate_set_key() has an inconsistent implementation. + * On success, key ownership is transferred, and so should not be freed, + * but pcert_list is a shallow memcpy(), so gnutls_pcert_deinit() should + * not be run on gnutls_pcert_st in list, though top-level list storage + * should be freed. On failure, ownership is not transferred for either. */ + gnutls_free(pcert_list); + pc->ssl_pemfile_pkey = NULL; + pc->ssl_cred = ssl_cred; + + /* release lists used to configure pc->ssl_cred */ + mod_gnutls_free_config_crts(pc->ssl_pemfile_x509); + pc->ssl_pemfile_x509 = NULL; + + return 0; +} + + +static void * +network_gnutls_load_pemfile (server *srv, const buffer *pemfile, const buffer *privkey) +{ + #if 0 /* see comments in mod_gnutls_construct_crt_chain() above */ + + gnutls_certificate_credentials_t ssl_cred = NULL; + int rc; + + rc = gnutls_certificate_allocate_credentials(&ssl_cred); + if (rc < 0) return NULL; + + rc = gnutls_certificate_set_x509_key_file2(ssl_cred, + pemfile->ptr, privkey->ptr, + GNUTLS_X509_FMT_PEM, NULL, 0); + if (rc < 0) { + elogf(srv->errh, __FILE__, __LINE__, rc, + "gnutls_certificate_set_x509_key_file2(%s, %s)", + pemfile->ptr, privkey->ptr); + gnutls_certificate_free_credentials(ssl_cred); + return NULL; + } + + plugin_cert *pc = malloc(sizeof(plugin_cert)); + force_assert(pc); + pc->ssl_cred = ssl_cred; + pc->trust_inited = 0; + + return pc; + + #else + + gnutls_datum_t *d = mod_gnutls_load_config_crts(pemfile->ptr, srv->errh); + if (NULL == d) return NULL; + if (0 == d->size) { + mod_gnutls_free_config_crts(d); + return NULL; + } + + gnutls_privkey_t pkey = mod_gnutls_load_config_pkey(privkey->ptr,srv->errh); + if (NULL == pkey) { + mod_gnutls_free_config_crts(d); + return NULL; + } + + plugin_cert *pc = malloc(sizeof(plugin_cert)); + force_assert(pc); + pc->ssl_cred = NULL; + pc->trust_inited = 0; + pc->ssl_pemfile_x509 = d; + pc->ssl_pemfile_pkey = pkey; + + if (d->size > 1) { /*(certificate chain provided)*/ + int rc = mod_gnutls_construct_crt_chain(pc, d, srv->errh); + if (rc < 0) { + mod_gnutls_free_config_crts(d); + gnutls_privkey_deinit(pkey); + free(pc); + return NULL; + } + } + + return pc; + + #endif +} + + +#if GNUTLS_VERSION_NUMBER >= 0x030200 + +static int +mod_gnutls_acme_tls_1 (handler_ctx *hctx) +{ + buffer * const b = hctx->tmp_buf; + const buffer * const name = &hctx->r->uri.authority; + log_error_st * const errh = hctx->r->conf.errh; + int rc = GNUTLS_E_INVALID_REQUEST; + + /* check if acme-tls/1 protocol is enabled (path to dir of cert(s) is set)*/ + if (buffer_string_is_empty(hctx->conf.ssl_acme_tls_1)) + return 0; /*(should not happen)*/ + buffer_copy_buffer(b, hctx->conf.ssl_acme_tls_1); + buffer_append_slash(b); + + /* check if SNI set server name (required for acme-tls/1 protocol) + * and perform simple path checks for no '/' + * and no leading '.' (e.g. ignore "." or ".." or anything beginning '.') */ + if (buffer_string_is_empty(name)) return rc; + if (NULL != strchr(name->ptr, '/')) return rc; + if (name->ptr[0] == '.') return rc; + #if 0 + if (0 != http_request_host_policy(name, hctx->r->conf.http_parseopts, 443)) + return rc; + #endif + buffer_append_string_buffer(b, name); + + #if 0 + + buffer *privkey = buffer_init_buffer(b); + buffer_append_string_len(b, CONST_STR_LEN(".crt.pem")); + buffer_append_string_len(privkey, CONST_STR_LEN(".key.pem")); + + /*(similar to network_gnutls_load_pemfile() but propagates rc)*/ + gnutls_certificate_credentials_t ssl_cred = NULL; + rc = gnutls_certificate_allocate_credentials(&ssl_cred); + if (rc < 0) { buffer_free(privkey); return rc; } + rc = gnutls_certificate_set_x509_key_file2(ssl_cred, + b->ptr, privkey->ptr, + GNUTLS_X509_FMT_PEM, NULL, 0); + if (rc < 0) { + elogf(errh, __FILE__, __LINE__, rc, + "failed to load acme-tls/1 cert (%s, %s)", + b->ptr, privkey->ptr); + buffer_free(privkey); + gnutls_certificate_free_credentials(ssl_cred); + return rc; + } + buffer_free(privkey); + + #else + + /* gnutls_certificate_set_x509_key_file2() does not securely wipe + * sensitive data from memory, so take a few extra steps */ + + /* similar to network_gnutls_load_pemfile() */ + + uint32_t len = buffer_string_length(b); + buffer_append_string_len(b, CONST_STR_LEN(".crt.pem")); + + gnutls_datum_t *d = mod_gnutls_load_config_crts(b->ptr, errh); + if (NULL == d) return GNUTLS_E_FILE_ERROR; + if (0 == d->size) { + mod_gnutls_free_config_crts(d); + return GNUTLS_E_FILE_ERROR; + } + + buffer_string_set_length(b, len); + buffer_append_string_len(b, CONST_STR_LEN(".key.pem")); + + gnutls_privkey_t pkey = mod_gnutls_load_config_pkey(b->ptr, errh); + if (NULL == pkey) { + mod_gnutls_free_config_crts(d); + return GNUTLS_E_FILE_ERROR; + } + + plugin_cert pc; + pc.ssl_cred = NULL; + pc.trust_inited = 0; + pc.ssl_pemfile_x509 = d; + pc.ssl_pemfile_pkey = pkey; + + rc = mod_gnutls_construct_crt_chain(&pc, d, errh); + if (rc < 0) { + mod_gnutls_free_config_crts(d); + gnutls_privkey_deinit(pkey); + return rc; + } + + gnutls_certificate_credentials_t ssl_cred = pc.ssl_cred; + + #endif + + hctx->acme_tls_1_cred = ssl_cred; /*(save ptr and free later)*/ + + rc = gnutls_credentials_set(hctx->ssl, GNUTLS_CRD_CERTIFICATE, ssl_cred); + if (rc < 0) { + elogf(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "failed to set acme-tls/1 certificate for TLS server name %s", + hctx->r->uri.authority.ptr); + return rc; + } + + return GNUTLS_E_SUCCESS; /* 0 */ +} + + +enum { + MOD_GNUTLS_ALPN_HTTP11 = 1 + ,MOD_GNUTLS_ALPN_HTTP10 = 2 + ,MOD_GNUTLS_ALPN_H2 = 3 + ,MOD_GNUTLS_ALPN_ACME_TLS_1 = 4 +}; + + +static int +mod_gnutls_alpn_select_cb (gnutls_session_t ssl, handler_ctx *hctx) +{ + const int i = 0; + uint8_t proto; + + gnutls_datum_t d = { NULL, 0 }; + if (gnutls_alpn_get_selected_protocol(ssl, &d) < 0) + return 0; /* ignore GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE */ + const char *in = (const char *)d.data; + const int n = (int)d.size; + + switch (n) { + #if 0 + case 2: /* "h2" */ + if (in[i] == 'h' && in[i+1] == '2') { + proto = MOD_GNUTLS_ALPN_H2; + break; + } + return 0; + #endif + case 8: /* "http/1.1" "http/1.0" */ + if (0 == memcmp(in+i, "http/1.", 7)) { + if (in[i+7] == '1') { + proto = MOD_GNUTLS_ALPN_HTTP11; + break; + } + if (in[i+7] == '0') { + proto = MOD_GNUTLS_ALPN_HTTP10; + break; + } + } + return 0; + case 10: /* "acme-tls/1" */ + if (0 == memcmp(in+i, "acme-tls/1", 10)) { + int rc = mod_gnutls_acme_tls_1(hctx); + if (0 == rc) { + proto = MOD_GNUTLS_ALPN_ACME_TLS_1; + break; + } + return rc; + } + return 0; + default: + return 0; + } + + hctx->alpn = proto; + return 0; +} + +#endif /* GNUTLS_VERSION_NUMBER >= 0x030200 */ + + +static int +mod_gnutls_SNI(void *ctx, unsigned int tls_id, + const unsigned char *servername, unsigned int len) +{ + if (tls_id != 0) return 0; + + /* server name */ + + /* https://www.gnutls.org/manual/gnutls.html#Virtual-hosts-and-credentials + * figure the advertized name - the following hack relies on the fact that + * this extension only supports DNS names, and due to a protocol bug cannot + * be extended to support anything else. */ + if (len < 5) return 0; + len -= 5; + servername += 5; + handler_ctx * const hctx = (handler_ctx *) ctx; + request_st * const r = hctx->r; + buffer_copy_string(&r->uri.scheme, "https"); + + if (len >= 1024) { /*(expecting < 256; TLSEXT_MAXLEN_host_name is 255)*/ + log_error(r->conf.errh, __FILE__, __LINE__, + "GnuTLS: SNI name too long %.*s", (int)len, servername); + return GNUTLS_E_INVALID_REQUEST; + } + + /* use SNI to patch mod_gnutls config and then reset COMP_HTTP_HOST */ + buffer_copy_string_len(&r->uri.authority, (const char *)servername, len); + buffer_to_lower(&r->uri.authority); + #if 0 + /*(r->uri.authority used below for configuration before request read; + * revisit for h2)*/ + if (0 != http_request_host_policy(&r->uri.authority, + r->conf.http_parseopts, 443)) + return GNUTLS_E_INVALID_REQUEST; + #endif + + r->conditional_is_valid |= (1 << COMP_HTTP_SCHEME) + | (1 << COMP_HTTP_HOST); + + mod_gnutls_patch_config(r, &hctx->conf); + /* reset COMP_HTTP_HOST so that conditions re-run after request hdrs read */ + /*(done in configfile-glue.c:config_cond_cache_reset() after request hdrs read)*/ + /*config_cond_cache_reset_item(r, COMP_HTTP_HOST);*/ + /*buffer_clear(&r->uri.authority);*/ + + return 0; +} + + +static int +mod_gnutls_client_hello_hook_post(gnutls_session_t ssl) +{ + #if GNUTLS_VERSION_NUMBER >= 0x030200 + handler_ctx * const hctx = gnutls_session_get_ptr(ssl); + int rc = mod_gnutls_alpn_select_cb(ssl, hctx); + if (rc < 0) + return rc; + + /* check if acme-tls/1 protocol is enabled (path to dir of cert(s) is set)*/ + if (hctx->alpn == MOD_GNUTLS_ALPN_ACME_TLS_1) { + /*(acme-tls/1 is separate from certificate auth access to website)*/ + gnutls_certificate_server_set_request(ssl, GNUTLS_CERT_IGNORE); + return GNUTLS_E_SUCCESS; /* 0 */ /*(skip further session config below)*/ + } + #endif + + return GNUTLS_E_SUCCESS; /* 0 */ +} + + +static int +mod_gnutls_client_hello_hook(gnutls_session_t ssl, unsigned int htype, + unsigned when, unsigned int incoming, + const gnutls_datum_t *msg) +{ + /*assert(htype == GNUTLS_HANDSHAKE_CLIENT_HELLO);*/ + UNUSED(htype); + UNUSED(incoming); + + if (when == GNUTLS_HOOK_POST) + return mod_gnutls_client_hello_hook_post(ssl); + + handler_ctx * const hctx = gnutls_session_get_ptr(ssl); + #if GNUTLS_VERSION_NUMBER < 0x030600 + gnutls_dh_params_t dh_params = hctx->conf.dh_params; + #endif + int rc = gnutls_ext_raw_parse(hctx, mod_gnutls_SNI, msg, + GNUTLS_EXT_RAW_FLAG_TLS_CLIENT_HELLO); + if (rc < 0) { + log_error_st *errh = hctx->r->conf.errh; + elog(errh, __FILE__, __LINE__, rc, "gnutls_ext_raw_parse()"); + return rc; + } + + #if GNUTLS_VERSION_NUMBER >= 0x030200 + /* https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids */ + static const gnutls_datum_t alpn_protos_http_acme[] = { + { (unsigned char *)CONST_STR_LEN("http/1.1") } + ,{ (unsigned char *)CONST_STR_LEN("http/1.0") } + ,{ (unsigned char *)CONST_STR_LEN("acme-tls/1") } + }; + unsigned int n = !buffer_string_is_empty(hctx->conf.ssl_acme_tls_1) ? 3 : 2; + /*unsigned int flags = GNUTLS_ALPN_SERVER_PRECEDENCE;*/ + rc = gnutls_alpn_set_protocols(hctx->ssl, alpn_protos_http_acme, n, 0); + if (rc < 0) { + log_error_st *errh = hctx->r->conf.errh; + elog(errh, __FILE__, __LINE__, rc, "gnutls_alpn_set_protocols()"); + return rc; + } + #if 0 + /* XXX: might have to manually process client hello for ALPN "acme-tls/1" + * and handle here (GNUTLS_HOOK_PRE) before setting certificate */ + if (!buffer_string_is_empty(hctx->conf.ssl_acme_tls_1)) { + } + #endif + #endif + + #if 0 /* must enable before GnuTLS client hello hook */ + /* GnuTLS returns an error here if TLSv1.3 (? key already set ?) */ + /* see mod_gnutls_handle_con_accept() */ + if (hctx->ssl_session_ticket && session_ticket_key.size) { + rc = gnutls_session_ticket_enable_server(ssl, &session_ticket_key); + if (rc < 0) { + elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "gnutls_session_ticket_enable_server()"); + return rc; + } + } + #endif + + if (NULL == hctx->conf.pc->ssl_cred) { + rc = mod_gnutls_construct_crt_chain(hctx->conf.pc, + hctx->conf.ssl_ca_file, + hctx->r->conf.errh); + if (rc < 0) return rc; + } + + gnutls_certificate_credentials_t ssl_cred = hctx->conf.pc->ssl_cred; + + hctx->verify_status = ~0u; + gnutls_certificate_request_t req = GNUTLS_CERT_IGNORE; + if (hctx->conf.ssl_verifyclient) { + /*(cred shared; settings should be consistent across site using cred)*/ + /*(i.e. settings for client certs must not differ under this cred)*/ + req = hctx->conf.ssl_verifyclient_enforce + ? GNUTLS_CERT_REQUIRE + : GNUTLS_CERT_REQUEST; + gnutls_certificate_set_verify_function(ssl_cred, mod_gnutls_verify_cb); + gnutls_certificate_set_verify_limits(ssl_cred, 8200 /*(default)*/, + hctx->conf.ssl_verifyclient_depth); + rc = mod_gnutls_verify_set_tlist(hctx, 1); /* for client cert request */ + if (rc < 0) return rc; + } + gnutls_certificate_server_set_request(ssl, req); + + #if GNUTLS_VERSION_NUMBER < 0x030600 + if (dh_params) + gnutls_certificate_set_dh_params(ssl_cred, dh_params); + #if GNUTLS_VERSION_NUMBER >= 0x030506 + else + gnutls_certificate_set_known_dh_params(ssl_cred, GNUTLS_SEC_PARAM_HIGH); + #endif + #endif + + rc = gnutls_credentials_set(ssl, GNUTLS_CRD_CERTIFICATE, ssl_cred); + if (rc < 0) { + elogf(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "failed to set SNI certificate for TLS server name %s", + hctx->r->uri.authority.ptr); + return rc; + } + + return GNUTLS_E_SUCCESS; /* 0 */ +} + + +static int +mod_gnutls_ssl_conf_ciphersuites (server *srv, plugin_config_socket *s, buffer *ciphersuites, const buffer *cipherstring); + + +static int +mod_gnutls_ssl_conf_curves(server *srv, plugin_config_socket *s, const buffer *curvelist); + + +static void +mod_gnutls_ssl_conf_proto (server *srv, plugin_config_socket *s, const buffer *minb, const buffer *maxb); + + +static int +mod_gnutls_ssl_conf_cmd (server *srv, plugin_config_socket *s) +{ + /* reference: + * https://www.openssl.org/docs/man1.1.1/man3/SSL_CONF_cmd.html */ + int rc = 0; + buffer *cipherstring = NULL; + buffer *ciphersuites = NULL; + buffer *minb = NULL; + buffer *maxb = NULL; + buffer *curves = NULL; + + for (size_t i = 0; i < s->ssl_conf_cmd->used; ++i) { + data_string *ds = (data_string *)s->ssl_conf_cmd->data[i]; + if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("CipherString"))) + cipherstring = &ds->value; + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Ciphersuites"))) + ciphersuites = &ds->value; + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Curves")) + || buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Groups"))) + curves = &ds->value; + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("MaxProtocol"))) + maxb = &ds->value; + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("MinProtocol"))) + minb = &ds->value; + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Protocol"))) { + /* openssl config for Protocol=... is complex and deprecated */ + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.openssl.ssl-conf-cmd %s ignored; " + "use MinProtocol=... and MaxProtocol=... instead", + ds->key.ptr); + } + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Options"))) { + for (char *v = ds->value.ptr, *e; *v; v = e) { + while (*v == ' ' || *v == '\t' || *v == ',') ++v; + int flag = 1; + if (*v == '-') { + flag = 0; + ++v; + } + for (e = v; light_isalpha(*e); ++e) ; + switch ((int)(e-v)) { + case 11: + if (buffer_eq_icase_ssn(v, "Compression", 11)) { + /* GnuTLS 3.6.0+ no longer implements + * any support for compression */ + if (!flag) continue; + } + break; + case 13: + if (buffer_eq_icase_ssn(v, "SessionTicket", 13)) { + /*(translates to "%NO_TICKETS" priority str if !flag)*/ + s->ssl_session_ticket = flag; + continue; + } + break; + case 16: + if (buffer_eq_icase_ssn(v, "ServerPreference", 16)) { + /*(translates to "%SERVER_PRECEDENCE" priority string)*/ + s->ssl_honor_cipher_order = flag; + continue; + } + break; + default: + break; + } + /* warn if not explicitly handled or ignored above */ + if (!flag) --v; + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.openssl.ssl-conf-cmd Options %.*s " + "ignored", (int)(e-v), v); + } + } + #if 0 + else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("..."))) { + } + #endif + else { + /* warn if not explicitly handled or ignored above */ + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.openssl.ssl-conf-cmd %s ignored", + ds->key.ptr); + } + } + + if (minb || maxb) /*(if at least one was set)*/ + mod_gnutls_ssl_conf_proto(srv, s, minb, maxb); + + if (!mod_gnutls_ssl_conf_ciphersuites(srv, s, ciphersuites, cipherstring)) + rc = -1; + + if (curves) { + if (buffer_string_is_empty(s->ssl_ec_curve)) + buffer_append_string_len(&s->priority_str, + CONST_STR_LEN("-CURVE-ALL:")); + if (!mod_gnutls_ssl_conf_curves(srv, s, curves)) + rc = -1; + } + + return rc; +} + + +static int +network_init_ssl (server *srv, plugin_config_socket *s, plugin_data *p) +{ + int rc; + UNUSED(p); + + /* construct GnuTLS "priority" string + * + * XXX: might make config option to allow user to define priority string + * (which would short-circuit any related config directives) + * + * default: NORMAL (since GnuTLS 3.3.0, could also use NULL for defaults) + * SUITEB128 and SUITEB192 are stricter than NORMAL + * (and are attempted to be supported in mod_gnutls_ssl_conf_ciphersuites()) + * + * gnutls defaults to %PARTIAL_RENEGOTIATION (see manual) + * mod_gnutls does not attempt to support + * hctx->conf.ssl_disable_client_renegotiation == 0 + * though possible with %UNSAFE_RENEGOTATION + */ + + #if GNUTLS_VERSION_NUMBER < 0x030600 + /* GnuTLS 3.6.0+ no longer implements any support for compression, + * but we still explicitly disable for earlier versions */ + buffer_append_string_len(&s->priority_str, + CONST_STR_LEN("-COMP_ALL:+COMP-NULL:")); + #endif + + if (!buffer_string_is_empty(s->ssl_cipher_list)) { + if (!mod_gnutls_ssl_conf_ciphersuites(srv,s,NULL,s->ssl_cipher_list)) + return -1; + } + + #if GNUTLS_VERSION_NUMBER < 0x030600 + if (!buffer_string_is_empty(s->ssl_dh_file)) { + /* "Prior to GnuTLS 3.6.0 for the ephemeral or anonymous Diffie-Hellman + * (DH) TLS ciphersuites the application was required to generate or + * provide DH parameters. That is no longer necessary as GnuTLS utilizes + * DH parameters and negotiation from [RFC7919]." + */ + /* In other words, the following should not be used in 3.6.0 or later: + * gnutls_certificate_set_dh_params() + * gnutls_certificate_set_known_dh_params() + * However, if support is implemented in mod_gnutls, must also free in + * mod_gnutls_free_config() as gnutls_certificate_free_credentials() + * does not free RSA or DH params manually associated with credential */ + gnutls_datum_t f = { NULL, 0 }; + rc = gnutls_dh_params_init(&s->dh_params); + if (rc < 0) return -1; + rc = mod_gnutls_load_file(s->ssl_dh_file->ptr, &f, srv->errh); + if (rc < 0) return -1; + rc = gnutls_dh_params_import_pkcs3(s->dh_params,&f,GNUTLS_X509_FMT_PEM); + mod_gnutls_datum_wipe(&f); + if (rc < 0) { + elogf(srv->errh, __FILE__, __LINE__, rc, + "gnutls_dh_params_import_pkcs3() %s", s->ssl_dh_file->ptr); + return -1; + } + } + else { + #if GNUTLS_VERSION_NUMBER < 0x030506 + /* (this might take a while; you should upgrade to newer gnutls) */ + rc = gnutls_dh_params_init(&s->dh_params); + if (rc < 0) return -1; + unsigned int bits = + gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_HIGH); + rc = gnutls_dh_params_generate2(s->dh_params, bits); + if (rc < 0) { + elogf(srv->errh, __FILE__, __LINE__, rc, + "gnutls_dh_params_generate2() %s", s->ssl_dh_file->ptr); + return -1; + } + #endif + } + #endif /* GNUTLS_VERSION_NUMBER < 0x030600 */ + + if (!buffer_string_is_empty(s->ssl_ec_curve)) { + buffer_append_string_len(&s->priority_str, + CONST_STR_LEN("-CURVE-ALL:")); + if (!mod_gnutls_ssl_conf_curves(srv, s, s->ssl_ec_curve)) + return -1; + } + + if (s->ssl_conf_cmd && s->ssl_conf_cmd->used) { + if (0 != mod_gnutls_ssl_conf_cmd(srv, s)) return -1; + } + + if (s->ssl_honor_cipher_order) + buffer_append_string_len(&s->priority_str, + CONST_STR_LEN("%SERVER_PRECEDENCE:")); + + if (!s->ssl_session_ticket) + buffer_append_string_len(&s->priority_str, + CONST_STR_LEN("%NO_TICKETS:")); + + if (!s->ssl_use_sslv3 && !s->ssl_use_sslv2 + && NULL == strstr(s->priority_str.ptr, "-VERS-ALL:")) + mod_gnutls_ssl_conf_proto(srv, s, NULL, NULL); + + /* explicitly disable SSLv3 unless enabled in config + * (gnutls library would also need to be compiled with legacy support) */ + if (!s->ssl_use_sslv3) + buffer_append_string_len(&s->priority_str, + CONST_STR_LEN("-VERS-SSL3.0:")); + + /* gnutls_priority_init2() is available since GnuTLS 3.6.3 and could be + * called once with s->priority_base, and a second time with s->priority_str + * and GNUTLS_PRIORITY_INIT_DEF_APPEND, but preserve compat with earlier + * GnuTLS by concatenating into a single priority string */ + + buffer *b = srv->tmp_buf; + if (NULL == s->priority_base) s->priority_base = "NORMAL"; + buffer_copy_string_len(b, s->priority_base, strlen(s->priority_base)); + if (!buffer_string_is_empty(&s->priority_str)) { + buffer_append_string_len(b, CONST_STR_LEN(":")); + uint32_t len = buffer_string_length(&s->priority_str); + if (s->priority_str.ptr[len-1] == ':') + --len; /* remove trailing ':' */ + buffer_append_string_len(b, s->priority_str.ptr, len); + } + const char *err_pos; + rc = gnutls_priority_init(&s->priority_cache, b->ptr, &err_pos); + if (rc < 0) { + elogf(srv->errh, __FILE__, __LINE__, rc, + "gnutls_priority_init() error near %s", err_pos); + return -1; + } + + return 0; +} + + +static int +mod_gnutls_set_defaults_sockets(server *srv, plugin_data *p) +{ + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("ssl.engine"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.cipher-list"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.honor-cipher-order"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.dh-file"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.ec-curve"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.openssl.ssl-conf-cmd"), + T_CONFIG_ARRAY_KVSTRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.pemfile"), /* included to process global scope */ + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.empty-fragments"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.use-sslv2"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.use-sslv3"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + static const buffer default_ssl_cipher_list = { CONST_STR_LEN("HIGH"), 0 }; + + p->ssl_ctxs = calloc(srv->config_context->used, sizeof(plugin_ssl_ctx)); + force_assert(p->ssl_ctxs); + + int rc = HANDLER_GO_ON; + plugin_data_base srvplug; + memset(&srvplug, 0, sizeof(srvplug)); + plugin_data_base * const ps = &srvplug; + if (!config_plugin_values_init(srv, ps, cpk, "mod_gnutls")) + return HANDLER_ERROR; + + plugin_config_socket defaults; + memset(&defaults, 0, sizeof(defaults)); + defaults.ssl_honor_cipher_order = 1; /* default server preference for PFS */ + defaults.ssl_session_ticket = 0; /* disabled by default */ + defaults.ssl_cipher_list = &default_ssl_cipher_list; + + /* process and validate config directives for global and $SERVER["socket"] + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !ps->cvlist[0].v.u2[1]; i < ps->nconfig; ++i) { + config_cond_info cfginfo; + config_get_config_cond_info(&cfginfo, (uint32_t)ps->cvlist[i].k_id); + int is_socket_scope = (0 == i || cfginfo.comp == COMP_SERVER_SOCKET); + int count_not_engine = 0; + + plugin_config_socket conf; + memcpy(&conf, &defaults, sizeof(conf)); + + /*(preserve prior behavior; not inherited)*/ + /*(forcing inheritance might break existing configs where SSL is enabled + * by default in the global scope, but not $SERVER["socket"]=="*:80") */ + conf.ssl_enabled = 0; + + config_plugin_value_t *cpv = ps->cvlist + ps->cvlist[i].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + /* ignore ssl.pemfile (k_id=10); included to process global scope */ + if (!is_socket_scope && cpv->k_id != 10) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: %s is valid only in global scope or " + "$SERVER[\"socket\"] condition", cpk[cpv->k_id].k); + continue; + } + ++count_not_engine; + switch (cpv->k_id) { + case 0: /* ssl.engine */ + conf.ssl_enabled = (0 != cpv->v.u); + --count_not_engine; + break; + case 1: /* ssl.cipher-list */ + conf.ssl_cipher_list = cpv->v.b; + break; + case 2: /* ssl.honor-cipher-order */ + conf.ssl_honor_cipher_order = (0 != cpv->v.u); + break; + case 3: /* ssl.dh-file */ + #if GNUTLS_VERSION_NUMBER < 0x030600 + conf.ssl_dh_file = cpv->v.b; + #else + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ignoring ssl.dh-file; " + "obsoleted in GnuTLS 3.6.0 and later implementing RFC7919"); + #endif + break; + case 4: /* ssl.ec-curve */ + conf.ssl_ec_curve = cpv->v.b; + break; + case 5: /* ssl.openssl.ssl-conf-cmd */ + *(const array **)&conf.ssl_conf_cmd = cpv->v.a; + break; + case 6: /* ssl.pemfile */ + /* ignore here; included to process global scope when + * ssl.pemfile is set, but ssl.engine is not "enable" */ + break; + case 7: /* ssl.empty-fragments */ + conf.ssl_empty_fragments = (0 != cpv->v.u); + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ignoring ssl.empty-fragments; openssl-specific " + "counter-measure against a SSL 3.0/TLS 1.0 protocol " + "vulnerability affecting CBC ciphers, which cannot be handled" + " by some broken (Microsoft) SSL implementations."); + break; + case 8: /* ssl.use-sslv2 */ + conf.ssl_use_sslv2 = (0 != cpv->v.u); + log_error(srv->errh, __FILE__, __LINE__, "GnuTLS: " + "ssl.use-sslv2 is deprecated and will soon be removed. " + "Many modern TLS libraries no longer support SSLv2."); + break; + case 9: /* ssl.use-sslv3 */ + conf.ssl_use_sslv3 = (0 != cpv->v.u); + log_error(srv->errh, __FILE__, __LINE__, "GnuTLS: " + "ssl.use-sslv3 is deprecated and will soon be removed. " + "Many modern TLS libraries no longer support SSLv3. " + "If needed, use: " + "ssl.openssl.ssl-conf-cmd = (\"MinProtocol\" => \"SSLv3\")"); + break; + default:/* should not happen */ + break; + } + } + if (HANDLER_GO_ON != rc) break; + if (0 == i) memcpy(&defaults, &conf, sizeof(conf)); + + if (0 != i && !conf.ssl_enabled) continue; + + /* fill plugin_config_socket with global context then $SERVER["socket"] + * only for directives directly in current $SERVER["socket"] condition*/ + + /*conf.pc = p->defaults.pc;*/ + conf.ssl_verifyclient = p->defaults.ssl_verifyclient; + conf.ssl_verifyclient_enforce = p->defaults.ssl_verifyclient_enforce; + conf.ssl_verifyclient_depth = p->defaults.ssl_verifyclient_depth; + + int sidx = ps->cvlist[i].k_id; + for (int j = !p->cvlist[0].v.u2[1]; j < p->nconfig; ++j) { + if (p->cvlist[j].k_id != sidx) continue; + /*if (0 == sidx) break;*//*(repeat to get ssl_pemfile,ssl_privkey)*/ + cpv = p->cvlist + p->cvlist[j].v.u2[0]; + for (; -1 != cpv->k_id; ++cpv) { + ++count_not_engine; + switch (cpv->k_id) { + case 0: /* ssl.pemfile */ + if (cpv->vtype == T_CONFIG_LOCAL) + conf.pc = cpv->v.v; + break; + case 7: /* ssl.verifyclient.activate */ + conf.ssl_verifyclient = (0 != cpv->v.u); + break; + case 8: /* ssl.verifyclient.enforce */ + conf.ssl_verifyclient_enforce = (0 != cpv->v.u); + break; + case 9: /* ssl.verifyclient.depth */ + conf.ssl_verifyclient_depth = (unsigned char)cpv->v.shrt; + break; + default: + break; + } + } + break; + } + + if (NULL == conf.pc) { + if (0 == i && !conf.ssl_enabled) continue; + if (0 != i) { + /* inherit ssl settings from global scope + * (if only ssl.engine = "enable" and no other ssl.* settings) + * (This is for convenience when defining both IPv4 and IPv6 + * and desiring to inherit the ssl config from global context + * without having to duplicate the directives)*/ + if (count_not_engine) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.pemfile has to be set in same " + "$SERVER[\"socket\"] scope as other ssl.* directives, " + "unless only ssl.engine is set, inheriting ssl.* from " + "global scope"); + rc = HANDLER_ERROR; + continue; + } + plugin_ssl_ctx * const s = p->ssl_ctxs + sidx; + *s = *p->ssl_ctxs;/*(copy struct of ssl_ctx from global scope)*/ + continue; + } + /* PEM file is required */ + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.pemfile has to be set when ssl.engine = \"enable\""); + rc = HANDLER_ERROR; + continue; + } + + /* (initialize once if module enabled) */ + if (!mod_gnutls_init_once_gnutls()) { + rc = HANDLER_ERROR; + break; + } + + /* configure ssl_ctx for socket */ + + /*conf.ssl_ctx = NULL;*//*(filled by network_init_ssl() even on error)*/ + if (0 == network_init_ssl(srv, &conf, p)) { + plugin_ssl_ctx * const s = p->ssl_ctxs + sidx; + s->ssl_session_ticket = conf.ssl_session_ticket; + s->priority_cache = conf.priority_cache; + #if GNUTLS_VERSION_NUMBER < 0x030600 + s->dh_params = conf.dh_params; + #endif + } + else { + gnutls_priority_deinit(conf.priority_cache); + #if GNUTLS_VERSION_NUMBER < 0x030600 + gnutls_dh_params_deinit(conf.dh_params); + #endif + rc = HANDLER_ERROR; + } + free(conf.priority_str.ptr); + } + + stek_rotate_ts = log_epoch_secs; + + free(srvplug.cvlist); + return rc; +} + + +SETDEFAULTS_FUNC(mod_gnutls_set_defaults) +{ + static const config_plugin_keys_t cpk[] = { + { CONST_STR_LEN("ssl.pemfile"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.privkey"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.ca-file"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.ca-dn-file"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.ca-crl-file"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.read-ahead"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.disable-client-renegotiation"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.verifyclient.activate"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.verifyclient.enforce"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.verifyclient.depth"), + T_CONFIG_SHORT, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.verifyclient.username"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.verifyclient.exportcert"), + T_CONFIG_BOOL, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.acme-tls-1"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("debug.log-ssl-noise"), + T_CONFIG_SHORT, + T_CONFIG_SCOPE_CONNECTION } + ,{ NULL, 0, + T_CONFIG_UNSET, + T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = p_d; + p->srv = srv; + if (!config_plugin_values_init(srv, p, cpk, "mod_gnutls")) + return HANDLER_ERROR; + + /* process and validate config directives + * (init i to 0 if global context; to 1 to skip empty global context) */ + for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { + config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + config_plugin_value_t *pemfile = NULL; + config_plugin_value_t *privkey = NULL; + for (; -1 != cpv->k_id; ++cpv) { + switch (cpv->k_id) { + case 0: /* ssl.pemfile */ + if (!buffer_string_is_empty(cpv->v.b)) pemfile = cpv; + break; + case 1: /* ssl.privkey */ + if (!buffer_string_is_empty(cpv->v.b)) privkey = cpv; + break; + case 2: /* ssl.ca-file */ + case 3: /* ssl.ca-dn-file */ + if (!buffer_string_is_empty(cpv->v.b)) { + gnutls_datum_t *d = + mod_gnutls_load_config_crts(cpv->v.b->ptr, srv->errh); + if (d != NULL) { + cpv->vtype = T_CONFIG_LOCAL; + cpv->v.v = d; + } + else { + log_error(srv->errh, __FILE__, __LINE__, + "%s = %s", cpk[cpv->k_id].k, cpv->v.b->ptr); + return HANDLER_ERROR; + } + } + break; + case 4: /* ssl.ca-crl-file */ + if (!buffer_string_is_empty(cpv->v.b)) { + gnutls_datum_t *d = + mod_gnutls_load_config_crls(cpv->v.b->ptr, srv->errh); + if (d != NULL) { + cpv->vtype = T_CONFIG_LOCAL; + cpv->v.v = d; + } + else { + log_error(srv->errh, __FILE__, __LINE__, + "%s = %s", cpk[cpv->k_id].k, cpv->v.b->ptr); + return HANDLER_ERROR; + } + } + break; + case 5: /* ssl.read-ahead */ + case 6: /* ssl.disable-client-renegotiation */ + case 7: /* ssl.verifyclient.activate */ + case 8: /* ssl.verifyclient.enforce */ + break; + case 9: /* ssl.verifyclient.depth */ + if (cpv->v.shrt > 255) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: %s is absurdly large (%hu); limiting to 255", + cpk[cpv->k_id].k, cpv->v.shrt); + cpv->v.shrt = 255; + } + break; + case 10:/* ssl.verifyclient.username */ + case 11:/* ssl.verifyclient.exportcert */ + break; + case 12:/* ssl.acme-tls-1 */ + case 13:/* debug.log-ssl-noise */ + break; + default:/* should not happen */ + break; + } + } + + if (pemfile) { + if (NULL == privkey) privkey = pemfile; + pemfile->v.v = + network_gnutls_load_pemfile(srv, pemfile->v.b, privkey->v.b); + if (pemfile->v.v) + pemfile->vtype = T_CONFIG_LOCAL; + else + return HANDLER_ERROR; + } + } + + p->defaults.ssl_verifyclient = 0; + p->defaults.ssl_verifyclient_enforce = 1; + p->defaults.ssl_verifyclient_depth = 9; + p->defaults.ssl_verifyclient_export_cert = 0; + p->defaults.ssl_disable_client_renegotiation = 1; + p->defaults.ssl_read_ahead = 0; + + /* initialize p->defaults from global config context */ + if (p->nconfig > 0 && p->cvlist->v.u2[1]) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; + if (-1 != cpv->k_id) + mod_gnutls_merge_config(&p->defaults, cpv); + } + + int rc = mod_gnutls_set_defaults_sockets(srv, p); + + if (ssl_is_init) + mod_gnutls_session_ticket_key_rotate(srv); + + return rc; +} + + +static int +load_next_chunk (request_st * const r, chunkqueue * const cq, off_t max_bytes, + const char ** const data, size_t * const data_len) +{ + chunk *c = cq->first; + + /* local_send_buffer is a static buffer of size (LOCAL_SEND_BUFSIZE) + * + * buffer is allocated once, is NOT realloced (note: not thread-safe) + * */ + + force_assert(NULL != c); + + switch (c->type) { + case MEM_CHUNK: + *data = NULL; + *data_len = 0; + do { + size_t have; + + force_assert(c->offset >= 0 + && c->offset <= (off_t)buffer_string_length(c->mem)); + + have = buffer_string_length(c->mem) - c->offset; + + /* copy small mem chunks into single large buffer + * before gnutls_record_send() to reduce number times + * write() called underneath gnutls_record_send() and + * potentially reduce number of packets generated if TCP_NODELAY + * Alternatively, GnuTLS provides gnutls_record_cork() and + * gnutls_record_uncork(), not currently used by mod_gnutls */ + if (*data_len) { + size_t space = LOCAL_SEND_BUFSIZE - *data_len; + if (have > space) + have = space; + if (have > (size_t)max_bytes - *data_len) + have = (size_t)max_bytes - *data_len; + if (*data != local_send_buffer) { + memcpy(local_send_buffer, *data, *data_len); + *data = local_send_buffer; + } + memcpy(local_send_buffer+*data_len,c->mem->ptr+c->offset,have); + *data_len += have; + continue; + } + + if ((off_t) have > max_bytes) have = max_bytes; + + *data = c->mem->ptr + c->offset; + *data_len = have; + } while ((c = c->next) && c->type == MEM_CHUNK + && *data_len < LOCAL_SEND_BUFSIZE + && (off_t) *data_len < max_bytes); + return 0; + + case FILE_CHUNK: + if (0 != chunkqueue_open_file_chunk(cq, r->conf.errh)) return -1; + + { + off_t offset, toSend; + + force_assert(c->offset >= 0 && c->offset <= c->file.length); + offset = c->file.start + c->offset; + toSend = c->file.length - c->offset; + + if (toSend > LOCAL_SEND_BUFSIZE) toSend = LOCAL_SEND_BUFSIZE; + if (toSend > max_bytes) toSend = max_bytes; + + if (-1 == lseek(c->file.fd, offset, SEEK_SET)) { + log_perror(r->conf.errh, __FILE__, __LINE__, "lseek"); + return -1; + } + if (-1 == (toSend = read(c->file.fd, local_send_buffer, toSend))) { + log_perror(r->conf.errh, __FILE__, __LINE__, "read"); + return -1; + } + + *data = local_send_buffer; + *data_len = toSend; + } + return 0; + } + + return -1; +} + + +__attribute_cold__ +static int +mod_gnutls_write_err(connection *con, handler_ctx *hctx, int wr, size_t wr_len) +{ + switch (wr) { + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + if (gnutls_record_get_direction(hctx->ssl)) + con->is_writable = -1; + else + con->is_readable = -1; + break; /* try again later */ + default: + #if 0 + /* ??? how to check for EPIPE or ECONNRESET and skip logging ??? */ + if (hctx->conf.ssl_log_noise) + elog(hctx->r->conf.errh, __FILE__, __LINE__, wr, __func__); + #endif + elog(hctx->r->conf.errh, __FILE__, __LINE__, wr, __func__); + return -1; + } + + /* partial write; save attempted wr_len */ + hctx->pending_write = wr_len; + + return 0; /* try again later */ +} + + +__attribute_cold__ +static int +mod_gnutls_read_err(connection *con, handler_ctx *hctx, int rc) +{ + switch (rc) { + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + if (gnutls_record_get_direction(hctx->ssl)) + con->is_writable = -1; + con->is_readable = 0; + return 0; + #if 0 + case GNUTLS_E_SESSION_EOF: /*(not exposed by library)*/ + /* XXX: future: save state to avoid future read after response? */ + con->is_readable = 0; + r->keep_alive = 0; + return (hctx->handshake ? -2 : -1); /*(-1 error if during handshake)*/ + #endif + case GNUTLS_E_REHANDSHAKE: + if (!hctx->handshake) return -1; /*(not expected during handshake)*/ + if (hctx->conf.ssl_disable_client_renegotiation)/*(mod_gnutls default)*/ + return -1; + #if 0 + if (gnutls_safe_renegotiation_status(hctx->ssl)) { + hctx->handshake = 0; + return con->network_read(con, cq, max_bytes); + } + #else + return 0; /*(ignore client renegotiation request; generally unsafe)*/ + #endif + case GNUTLS_E_WARNING_ALERT_RECEIVED: + case GNUTLS_E_FATAL_ALERT_RECEIVED: + { + const char *str; + gnutls_alert_description_t alert = gnutls_alert_get(hctx->ssl); + switch (alert) { + case GNUTLS_A_NO_RENEGOTIATION: + return 0; /*(ignore non-fatal alert from client)*/ + case GNUTLS_A_HANDSHAKE_FAILURE: + case GNUTLS_A_CLOSE_NOTIFY: /*(not exposed by library)*/ + case GNUTLS_A_UNKNOWN_CA: + case GNUTLS_A_CERTIFICATE_UNKNOWN: + case GNUTLS_A_BAD_CERTIFICATE: + if (!hctx->conf.ssl_log_noise) return -1; + __attribute_fallthrough__ + default: + str = gnutls_alert_get_name(alert); + elogf(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "%s(): alert %s", __func__, str ? str : "(unknown)"); + return -1; + } + } + case GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET: + case GNUTLS_E_UNKNOWN_CIPHER_SUITE: /* GNUTLS_A_HANDSHAKE_FAILURE */ + case GNUTLS_E_PREMATURE_TERMINATION: + if (!hctx->conf.ssl_log_noise) return -1; + __attribute_fallthrough__ + case GNUTLS_E_GOT_APPLICATION_DATA: /*(not expected with current use)*/ + /*if (hctx->handshake) return -1;*//*(accept only during handshake)*/ + default: + elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, __func__); + return -1; + } +} + + +static int +mod_gnutls_close_notify(handler_ctx *hctx); + + +static int +connection_write_cq_ssl (connection *con, chunkqueue *cq, off_t max_bytes) +{ + request_st * const r = &con->request; + handler_ctx *hctx = r->plugin_ctx[plugin_data_singleton->id]; + gnutls_session_t ssl = hctx->ssl; + + if (hctx->pending_write) { + int wr = gnutls_record_send(ssl, NULL, 0); + if (wr <= 0) + return mod_gnutls_write_err(con, hctx, wr, hctx->pending_write); + max_bytes -= wr; + hctx->pending_write = 0; + chunkqueue_mark_written(cq, wr); + } + + if (0 != hctx->close_notify) return mod_gnutls_close_notify(hctx); + + chunkqueue_remove_finished_chunks(cq); + + const size_t lim = gnutls_record_get_max_size(ssl); + + /* future: for efficiency/performance might consider using GnuTLS corking + * gnutls_record_cork() + * gnutls_record_uncork() + * gnutls_record_check_corked() + * or might enhance load_next_chunk() to read a mix of MEM_CHUNK and + * FILE_CHUNK into the buffer before sending, e.g. to send header response + * headers and beginning of files before uncorking. + */ + + while (max_bytes > 0 && NULL != cq->first) { + const char *data; + size_t data_len; + int wr; + + if (0 != load_next_chunk(r, cq, max_bytes, &data, &data_len)) return -1; + + /* gnutls_record_send() copies the data, up to max record size, but if + * (temporarily) unable to write the entire record, it is documented + * that the caller must call gnutls_record_send() again, later, with the + * same arguments, or with NULL ptr and 0 data_len. The func may return + * GNUTLS_E_AGAIN or GNUTLS_E_INTERRUPTED to indicate that caller should + * wait for fd to be readable/writable before calling the func again, + * which is why those (temporary) errors are returned instead of telling + * the caller that the data was successfully copied. + * Additionally, to be accurate, the size must fit into a record which + * is why we restrict ourselves to sending max out record payload each + * iteration. + * XXX: above comments modified from mod_mbedtls; should be verified + */ + + int wr_total = 0; + do { + size_t wr_len = (data_len > lim) ? lim : data_len; + wr = gnutls_record_send(ssl, data, wr_len); + if (wr <= 0) { + if (wr_total) chunkqueue_mark_written(cq, wr_total); + return mod_gnutls_write_err(con, hctx, wr, wr_len); + } + wr_total += wr; + data += wr; + } while ((data_len -= wr)); + chunkqueue_mark_written(cq, wr_total); + max_bytes -= wr_total; + } + + return 0; +} + + +static int +mod_gnutls_ssl_handshake (handler_ctx *hctx) +{ + int rc = gnutls_handshake(hctx->ssl); + if (rc < 0) + return mod_gnutls_read_err(hctx->con, hctx, rc); + + /*(rc == GNUTLS_E_SUCCESS)*/ + + hctx->handshake = 1; + #if GNUTLS_VERSION_NUMBER >= 0x030200 + if (hctx->alpn == MOD_GNUTLS_ALPN_ACME_TLS_1) { + /* Once TLS handshake is complete, return -1 to result in + * CON_STATE_ERROR so that socket connection is quickly closed */ + return -1; + } + hctx->alpn = 0; + #endif + return 1; /* continue reading */ +} + + +static int +connection_read_cq_ssl (connection *con, chunkqueue *cq, off_t max_bytes) +{ + request_st * const r = &con->request; + handler_ctx *hctx = r->plugin_ctx[plugin_data_singleton->id]; + + UNUSED(max_bytes); + + if (0 != hctx->close_notify) return mod_gnutls_close_notify(hctx); + + if (!hctx->handshake) { + int rc = mod_gnutls_ssl_handshake(hctx); + if (1 != rc) return rc; /* !hctx->handshake; not done, or error */ + } + + gnutls_session_t ssl = hctx->ssl; + ssize_t len; + char *mem = NULL; + size_t mem_len = 0; + size_t pend = gnutls_record_check_pending(ssl); + do { + mem_len = pend < 2048 ? 2048 : pend; + chunk * const ckpt = cq->last; + mem = chunkqueue_get_memory(cq, &mem_len); + + len = gnutls_record_recv(ssl, mem, mem_len); + if (len > 0) { + chunkqueue_use_memory(cq, ckpt, len); + con->bytes_read += len; + } else { + chunkqueue_use_memory(cq, ckpt, 0); + } + } while (len > 0 && (pend = gnutls_record_check_pending(ssl))); + + if (len < 0) { + return mod_gnutls_read_err(con, hctx, (int)len); + } else if (len == 0) { + con->is_readable = 0; + /* the other end closed the connection -> KEEP-ALIVE */ + + return -2; + } else { + return 0; + } +} + + +static void +mod_gnutls_debug_cb(int level, const char *str) +{ + UNUSED(level); + log_error_st *errh = plugin_data_singleton->srv->errh; + log_error(errh, __FILE__, __LINE__, "GnuTLS: %s", str); +} + + +CONNECTION_FUNC(mod_gnutls_handle_con_accept) +{ + server_socket *srv_sock = con->srv_socket; + if (!srv_sock->is_ssl) return HANDLER_GO_ON; + + plugin_data *p = p_d; + handler_ctx * const hctx = handler_ctx_init(); + request_st * const r = &con->request; + hctx->r = r; + hctx->con = con; + hctx->tmp_buf = con->srv->tmp_buf; + r->plugin_ctx[p->id] = hctx; + + plugin_ssl_ctx * const s = p->ssl_ctxs + srv_sock->sidx; + hctx->ssl_session_ticket = s->ssl_session_ticket; + int flags = GNUTLS_SERVER | GNUTLS_NO_SIGNAL | GNUTLS_NONBLOCK; + /* ??? add feature: GNUTLS_ENABLE_EARLY_START ??? */ + int rc = gnutls_init(&hctx->ssl, flags); + if (rc < 0) { + elog(r->conf.errh, __FILE__, __LINE__, rc, "gnutls_init()"); + return HANDLER_ERROR; + } + + rc = gnutls_priority_set(hctx->ssl, s->priority_cache); + if (rc < 0) { + elog(r->conf.errh, __FILE__, __LINE__, rc, "gnutls_priority_set()"); + return HANDLER_ERROR; + } + + /* generic func replaces gnutls_handshake_set_post_client_hello_function()*/ + gnutls_handshake_set_hook_function(hctx->ssl, GNUTLS_HANDSHAKE_CLIENT_HELLO, + GNUTLS_HOOK_BOTH, + mod_gnutls_client_hello_hook); + + gnutls_session_set_ptr(hctx->ssl, hctx); + gnutls_transport_set_int(hctx->ssl, con->fd); + + con->network_read = connection_read_cq_ssl; + con->network_write = connection_write_cq_ssl; + con->proto_default_port = 443; /* "https" */ + mod_gnutls_patch_config(r, &hctx->conf); + + #if GNUTLS_VERSION_NUMBER < 0x030600 + hctx->conf.dh_params = s->dh_params; + #endif + + /* debug logging is global. Once enabled, debug hook will remain so, though + * different connection might overwrite level, if configured differently. + * If GNUTLS_DEBUG_LEVEL is set in environment (and ssl_log_noise not set), + * then debugging will go to stderr */ + if (hctx->conf.ssl_log_noise) {/* volume level for debug message callback */ + gnutls_global_set_log_function(mod_gnutls_debug_cb); + gnutls_global_set_log_level(hctx->conf.ssl_log_noise); + } + + /* GnuTLS limitation: must set session ticket encryption key before GnuTLS + * client hello hook runs if TLSv1.3 (? key already set by then ?) */ + if (hctx->ssl_session_ticket && session_ticket_key.size) { + rc = gnutls_session_ticket_enable_server(hctx->ssl,&session_ticket_key); + if (rc < 0) { + elog(r->conf.errh, __FILE__, __LINE__, rc, + "gnutls_session_ticket_enable_server()"); + return HANDLER_ERROR; + } + } + + return HANDLER_GO_ON; +} + + +static void +mod_gnutls_detach(handler_ctx *hctx) +{ + /* step aside from further SSL processing + * (used after handle_connection_shut_wr hook) */ + /* future: might restore prior network_read and network_write fn ptrs */ + hctx->con->is_ssl_sock = 0; + /* if called after handle_connection_shut_wr hook, shutdown SHUT_WR */ + if (-1 == hctx->close_notify) shutdown(hctx->con->fd, SHUT_WR); + hctx->close_notify = 1; +} + + +CONNECTION_FUNC(mod_gnutls_handle_con_shut_wr) +{ + request_st * const r = &con->request; + plugin_data *p = p_d; + handler_ctx *hctx = r->plugin_ctx[p->id]; + if (NULL == hctx) return HANDLER_GO_ON; + + hctx->close_notify = -2; + if (hctx->handshake) { + mod_gnutls_close_notify(hctx); + } + else { + mod_gnutls_detach(hctx); + } + + return HANDLER_GO_ON; +} + + +static int +mod_gnutls_close_notify (handler_ctx *hctx) +{ + if (1 == hctx->close_notify) return -2; + + int rc = gnutls_bye(hctx->ssl, GNUTLS_SHUT_WR); + switch (rc) { + case GNUTLS_E_SUCCESS: + mod_gnutls_detach(hctx); + return -2; + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + return 0; + default: + elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, + "mod_gnutls_close_notify()"); + mod_gnutls_detach(hctx); + return -1; + } +} + + +CONNECTION_FUNC(mod_gnutls_handle_con_close) +{ + request_st * const r = &con->request; + plugin_data *p = p_d; + handler_ctx *hctx = r->plugin_ctx[p->id]; + if (NULL != hctx) { + if (1 != hctx->close_notify) + mod_gnutls_close_notify(hctx); /*(one final try)*/ + handler_ctx_free(hctx); + r->plugin_ctx[p->id] = NULL; + } + + return HANDLER_GO_ON; +} + + +__attribute_noinline__ +static void +https_add_ssl_client_cert (request_st * const r, const gnutls_x509_crt_t peer) +{ + gnutls_datum_t d; + if (gnutls_x509_crt_export2(peer, GNUTLS_X509_FMT_PEM, &d) >= 0) + http_header_env_set(r, + CONST_STR_LEN("SSL_CLIENT_CERT"), + (char *)d.data, d.size); + if (d.data) gnutls_free(d.data); +} + + +/* modified from gnutls tests/dn.c:print_dn() */ +static void +https_add_ssl_client_subject (request_st * const r, gnutls_x509_dn_t dn) +{ + buffer * const tb = r->tmp_buf; + int irdn = 0, i, rc; + gnutls_x509_ava_st ava; + char buf[512]; /*(expecting element value len <= 256)*/ + + /* add components of client Subject DN */ + buffer_copy_string_len(tb, CONST_STR_LEN("SSL_CLIENT_S_DN_")); + + /* man gnutls_x509_dn_get_rdn_ava() + * The X.509 distinguished name is a sequence of sequences of strings and + * this is what the irdn and iava indexes model. + * This is a low-level function that requires the caller to do the value + * conversions when necessary (e.g. from UCS-2). + * XXX: value conversions not done below; unprintable chars replaced w/ '?' + */ + + do { + for (i=0; (rc = gnutls_x509_dn_get_rdn_ava(dn,irdn,i,&ava)) == 0; ++i) { + const char *name = + gnutls_x509_dn_oid_name((char *)ava.oid.data, + GNUTLS_X509_DN_OID_RETURN_OID); + buffer_string_set_length(tb, sizeof("SSL_CLIENT_S_DN_")-1); + buffer_append_string_len(tb, name, strlen(name)); + + unsigned int v, n = 0; + for (v = 0; v < ava.value.size && n < sizeof(buf)-1; ++n) { + unsigned char c = ava.value.data[v]; + buf[n] = (c < 32 || c == 127 || (c > 128 && c < 160)) ? '?' : c; + } + + http_header_env_set(r, CONST_BUF_LEN(tb), buf, n); + } + ++irdn; + } while (rc == GNUTLS_E_ASN1_ELEMENT_NOT_FOUND && i > 0); + + if (rc != GNUTLS_E_ASN1_ELEMENT_NOT_FOUND) + elog(r->conf.errh, __FILE__, __LINE__, rc, + "gnutls_x509_dn_get_rdn_ava()"); +} + +static void +https_add_ssl_client_entries (request_st * const r, handler_ctx * const hctx) +{ + gnutls_session_t ssl = hctx->ssl; + unsigned int crt_size = 0; + const gnutls_datum_t *crts; + gnutls_x509_crt_t crt; + buffer * const tb = r->tmp_buf; + char buf[513]; /*(512+1 for li_tohex_uc())*/ + size_t sz; + + if (hctx->verify_status != ~0u) + crts = gnutls_certificate_get_peers(ssl, &crt_size); + if (0 == crt_size) { /* || hctx->verify_status == ~0u) */ + /*(e.g. no cert, or verify result not available)*/ + http_header_env_set(r, + CONST_STR_LEN("SSL_CLIENT_VERIFY"), + CONST_STR_LEN("NONE")); + return; + } + else if (0 != hctx->verify_status) { + buffer_copy_string_len(tb, CONST_STR_LEN("FAILED:")); + #if GNUTLS_VERSION_NUMBER >= 0x030104 + /* get failure string and translate newline to ':', removing last one */ + /* (preserving behavior from mod_openssl) */ + gnutls_datum_t msg = { NULL, 0 }; + if (gnutls_certificate_verification_status_print(hctx->verify_status, + GNUTLS_CRT_X509, + &msg, 0) >= 0) { + sz = msg.size-1; /* '\0'-terminated string */ + for (char *nl=(char *)msg.data; NULL != (nl=strchr(nl, '\n')); ++nl) + nl[0] = ('\0' == nl[1] ? (--sz, '\0') : ':'); + buffer_append_string_len(tb, (char *)msg.data, sz); + } + if (msg.data) gnutls_free(msg.data); + #endif + http_header_env_set(r, + CONST_STR_LEN("SSL_CLIENT_VERIFY"), + CONST_BUF_LEN(tb)); + return; + } + else { + http_header_env_set(r, + CONST_STR_LEN("SSL_CLIENT_VERIFY"), + CONST_STR_LEN("SUCCESS")); + } + + if (gnutls_x509_crt_init(&crt) < 0) + return; + if (gnutls_x509_crt_import(crt, &crts[0], GNUTLS_X509_FMT_DER) < 0) { + gnutls_x509_crt_deinit(crt); + return; + } + + int rc; + gnutls_datum_t d = { NULL, 0 }; + /*rc = gnutls_x509_crt_print(cert, GNUTLS_CRT_PRINT_ONELINE, &d);*//* ??? */ + #if GNUTLS_VERSION_NUMBER < 0x030507 + d.data = buf; + d.size = sizeof(buf); + rc = gnutls_x509_crt_get_dn(crt, buf, &d.size); + #else + rc = gnutls_x509_crt_get_dn3(crt, &d, 0); + #endif + if (rc >= 0) + http_header_env_set(r, + CONST_STR_LEN("SSL_CLIENT_S_DN"), + (char *)d.data, d.size); + if (d.data && d.data != (void *)buf) gnutls_free(d.data); + + gnutls_x509_dn_t dn; + rc = gnutls_x509_crt_get_subject(crt, &dn); + if (rc >= 0) + https_add_ssl_client_subject(r, dn); + + sz = sizeof(buf)/2; + if (gnutls_x509_crt_get_serial(crt, buf+sz, &sz) >= 0) { + li_tohex_uc(buf, sizeof(buf), buf+(sizeof(buf)/2), sz); + http_header_env_set(r, + CONST_STR_LEN("SSL_CLIENT_M_SERIAL"), + buf, sz*2); + } + + if (!buffer_string_is_empty(hctx->conf.ssl_verifyclient_username)) { + /* pick one of the exported values as "REMOTE_USER", for example + * ssl.verifyclient.username = "SSL_CLIENT_S_DN_UID" + * or + * ssl.verifyclient.username = "SSL_CLIENT_S_DN_emailAddress" + */ + const buffer *varname = hctx->conf.ssl_verifyclient_username; + const buffer *vb = http_header_env_get(r, CONST_BUF_LEN(varname)); + if (vb) { /* same as http_auth.c:http_auth_setenv() */ + http_header_env_set(r, + CONST_STR_LEN("REMOTE_USER"), + CONST_BUF_LEN(vb)); + http_header_env_set(r, + CONST_STR_LEN("AUTH_TYPE"), + CONST_STR_LEN("SSL_CLIENT_VERIFY")); + } + } + + /* if (NULL != crt) (e.g. not PSK-based ciphersuite) */ + if (hctx->conf.ssl_verifyclient_export_cert && NULL != crt) + https_add_ssl_client_cert(r, crt); + + gnutls_x509_crt_deinit(crt); +} + + +static void +http_cgi_ssl_env (request_st * const r, handler_ctx * const hctx) +{ + gnutls_session_t ssl = hctx->ssl; + gnutls_protocol_t version = gnutls_protocol_get_version(ssl); + gnutls_cipher_algorithm_t cipher = gnutls_cipher_get(ssl); + gnutls_kx_algorithm_t kx = gnutls_kx_get(ssl); + gnutls_mac_algorithm_t mac = gnutls_mac_get(ssl); + const char *s; + + s = gnutls_protocol_get_name(version); + if (s) http_header_env_set(r, CONST_STR_LEN("SSL_PROTOCOL"), s, strlen(s)); + + s = gnutls_cipher_suite_get_name(kx, cipher, mac); + if (s) http_header_env_set(r, CONST_STR_LEN("SSL_CIPHER"), s, strlen(s)); + + /* SSL_CIPHER_ALGKEYSIZE - Number of cipher bits (possible) */ + /* SSL_CIPHER_USEKEYSIZE - Number of cipher bits (actually used) */ + /* (always the same in extra/gnutls_openssl.c:SSL_CIPHER_get_bits()) + * (The values are the same except for very old, weak ciphers, i.e. if you + * care about this, then you instead ought to be using stronger ciphers)*/ + /* ??? gnutls_x509_crt_get_pk_algorithm(crt, &usekeysize); ??? */ + size_t algkeysize = 8 * gnutls_cipher_get_key_size(cipher); + size_t usekeysize = algkeysize; + char buf[LI_ITOSTRING_LENGTH]; + http_header_env_set(r, CONST_STR_LEN("SSL_CIPHER_USEKEYSIZE"), + buf, li_utostrn(buf, sizeof(buf), usekeysize)); + http_header_env_set(r, CONST_STR_LEN("SSL_CIPHER_ALGKEYSIZE"), + buf, li_utostrn(buf, sizeof(buf), algkeysize)); +} + + +REQUEST_FUNC(mod_gnutls_handle_request_env) +{ + plugin_data *p = p_d; + handler_ctx *hctx = r->plugin_ctx[p->id]; + if (NULL == hctx) return HANDLER_GO_ON; + if (hctx->request_env_patched) return HANDLER_GO_ON; + hctx->request_env_patched = 1; + + http_cgi_ssl_env(r, hctx); + if (hctx->conf.ssl_verifyclient) { + https_add_ssl_client_entries(r, hctx); + } + + return HANDLER_GO_ON; +} + + +REQUEST_FUNC(mod_gnutls_handle_uri_raw) +{ + /* mod_gnutls must be loaded prior to mod_auth + * if mod_gnutls is configured to set REMOTE_USER based on client cert */ + /* mod_gnutls must be loaded after mod_extforward + * if mod_gnutls config is based on lighttpd.conf remote IP conditional + * using remote IP address set by mod_extforward, *unless* PROXY protocol + * is enabled with extforward.hap-PROXY = "enable", in which case the + * reverse is true: mod_extforward must be loaded after mod_gnutls */ + plugin_data *p = p_d; + handler_ctx *hctx = r->plugin_ctx[p->id]; + if (NULL == hctx) return HANDLER_GO_ON; + + mod_gnutls_patch_config(r, &hctx->conf); + if (hctx->conf.ssl_verifyclient) { + mod_gnutls_handle_request_env(r, p); + } + + return HANDLER_GO_ON; +} + + +REQUEST_FUNC(mod_gnutls_handle_request_reset) +{ + plugin_data *p = p_d; + handler_ctx *hctx = r->plugin_ctx[p->id]; + if (NULL == hctx) return HANDLER_GO_ON; + + hctx->request_env_patched = 0; + return HANDLER_GO_ON; +} + + +TRIGGER_FUNC(mod_gnutls_handle_trigger) { + const plugin_data * const p = p_d; + const time_t cur_ts = log_epoch_secs; + if (cur_ts & 0x3f) return HANDLER_GO_ON; /*(continue once each 64 sec)*/ + UNUSED(p); + + if (cur_ts - 86400 >= stek_rotate_ts) { /*(24 hours)*/ + mod_gnutls_session_ticket_key_rotate(srv); + stek_rotate_ts = cur_ts; + } + + return HANDLER_GO_ON; +} + + +int mod_gnutls_plugin_init (plugin *p); +int mod_gnutls_plugin_init (plugin *p) +{ + p->version = LIGHTTPD_VERSION_ID; + p->name = "gnutls"; + p->init = mod_gnutls_init; + p->cleanup = mod_gnutls_free; + p->priv_defaults= mod_gnutls_set_defaults; + + p->handle_connection_accept = mod_gnutls_handle_con_accept; + p->handle_connection_shut_wr = mod_gnutls_handle_con_shut_wr; + p->handle_connection_close = mod_gnutls_handle_con_close; + p->handle_uri_raw = mod_gnutls_handle_uri_raw; + p->handle_request_env = mod_gnutls_handle_request_env; + p->connection_reset = mod_gnutls_handle_request_reset; + p->handle_trigger = mod_gnutls_handle_trigger; + + return 0; +} + + +/* cipher suites + * + * (extremely coarse (and very possibly incorrect) mapping to openssl labels) + */ + +/* TLSv1.3 cipher list (supported in gnutls) */ +static const char suite_TLSv13[] = + "+CHACHA20-POLY1305:" + "+AES-256-GCM:" + "+AES-256-CCM:" + "+AES-256-CCM-8:" + "+AES-128-GCM:" + "+AES-128-CCM:" + "+AES-128-CCM-8:" +; + +/* TLSv1.2 cipher list (supported in gnutls) */ +static const char suite_TLSv12[] = + "+CHACHA20-POLY1305:" + "+AES-256-GCM:" + "+AES-256-CCM:" + "+AES-256-CBC:" + "+AES-256-CCM-8:" + "+CAMELLIA-256-GCM:" + "+CAMELLIA-256-CBC:" + "+AES-128-GCM:" + "+AES-128-CCM:" + "+AES-128-CBC:" + "+AES-128-CCM-8:" + "+CAMELLIA-128-GCM:" + "+CAMELLIA-128-CBC:" +; + +/* TLSv1.0 cipher list (supported in gnutls) */ +/* XXX: intentionally not including overlapping eNULL ciphers */ +static const char suite_TLSv10[] = + "+AES-256-CBC:" + "+AES-128-CBC:" + "+CAMELLIA-256-CBC:" + "+CAMELLIA-128-CBC:" +; + +/* SSLv3 cipher list (supported in gnutls) */ +/* XXX: intentionally not including overlapping eNULL ciphers */ +static const char suite_SSLv3[] = + "+AES-256-CBC:" + "+AES-128-CBC:" + "+CAMELLIA-256-CBC:" + "+CAMELLIA-128-CBC:" + "+3DES-CBC:" + "+DES-CBC:" +; + +/* HIGH cipher list (mapped from openssl list to gnutls) */ +static const char suite_HIGH[] = + "+CHACHA20-POLY1305:" + "+AES-256-GCM:" + "+AES-256-CCM:" + "+AES-256-CBC:" + "+AES-256-CCM-8:" + "+CAMELLIA-256-CBC:" + "+AES-128-GCM:" + "+AES-128-CCM:" + "+AES-128-CBC:" + "+AES-128-CCM-8:" + "+CAMELLIA-128-CBC:" +; + + +static int +mod_gnutls_ssl_conf_ciphersuites (server *srv, plugin_config_socket *s, buffer *ciphersuites, const buffer *cipherstring) +{ + /* reference: https://www.openssl.org/docs/man1.1.1/man1/ciphers.html + * Attempt to parse *some* keywords from Ciphersuites and CipherString + * !!! openssl uses a *different* naming scheme than does GnuTLS !!! + * Ciphersuites in openssl takes only TLSv1.3 suites. + * Note that CipherString does allow cipher suites to be listed, + * and this code does not currently attempt to provide mapping */ + + buffer * const plist = &s->priority_str; + char n[128]; /*(most ciphersuite names are about 40 chars)*/ + + if (ciphersuites) { + buffer *b = ciphersuites; + buffer_to_upper(b); /*(ciphersuites are all uppercase (currently))*/ + for (const char *p, *e = b->ptr-1; e && (e = strchr((p = e+1),':')); ) { + size_t len = e ? (size_t)(e - p) : strlen(p); + + if (buffer_eq_icase_ss(p, len, + CONST_STR_LEN("TLS_CHACHA20_POLY1305_SHA256"))) + buffer_append_string_len(plist, + CONST_STR_LEN("+CHACHA20-POLY1305:")); + else if (buffer_eq_icase_ss(p, len, + CONST_STR_LEN("TLS_AES_256_GCM_SHA384"))) + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-256-GCM:")); + else if (buffer_eq_icase_ss(p, len, + CONST_STR_LEN("TLS_AES_128_GCM_SHA256"))) + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-128-GCM:")); + else if (buffer_eq_icase_ss(p, len, + CONST_STR_LEN("TLS_AES_128_CCM_SHA256"))) + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-128-CCM:")); + else if (buffer_eq_icase_ss(p, len, + CONST_STR_LEN("TLS_AES_128_CCM_8_SHA256"))) + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-128-CCM-8:")); + else + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: skipped ciphersuite; not recognized: %.*s", + (int)len, p); + } + } + + /* XXX: openssl config for CipherString=... is excessively complex. + * If there is a need to enable specific ciphersuites, then that + * can be accomplished with mod_gnutls by specifying the list in + * Ciphersuites=... in the ssl.openssl.ssl-conf-cmd directive. + * + * The tokens parsed here are a quick attempt to handle a few cases + * + * XXX: not done: could make a list of ciphers with bitflag of attributes + * to make future combining easier */ + if (cipherstring) { + const buffer *b = cipherstring; + const char *e = b->ptr; + + /* XXX: not done: no checking for duplication of ciphersuites + * even if tokens overlap or are repeated */ + + /* XXX: not done: might walk string and build up exclude list of !xxxxx + * ciphersuites and walk string again, excluding as result list built */ + + /* manually handle first token, since one-offs apply */ + /* (openssl syntax NOT fully supported) */ + if (0 == strncmp(e, "!ALL", 4) || 0 == strncmp(e, "-ALL", 4)) { + /* "!ALL" excluding all ciphers is empty list */ + e += sizeof("!ALL")-1; /* same as sizeof("-ALL")-1 */ + buffer_append_string_len(plist, CONST_STR_LEN("-CIPHER-ALL:")); + } + else if (0 == strncmp(e, CONST_STR_LEN("!DEFAULT")) + || 0 == strncmp(e, CONST_STR_LEN("-DEFAULT"))) { + /* "!DEFAULT" excluding default ciphers is empty list */ + e += sizeof("!DEFAULT")-1; /* same as sizeof("-DEFAULT")-1 */ + buffer_append_string_len(plist, CONST_STR_LEN("-CIPHER-ALL:")); + } + else if (0 == strncmp(e, CONST_STR_LEN("DEFAULT"))) { + e += sizeof("DEFAULT")-1; + s->priority_base = "NORMAL"; + } + else if (0 == /* effectively the same as "DEFAULT" */ + strncmp(e, CONST_STR_LEN("ALL:!COMPLEMENTOFDEFAULT:!eNULL"))) { + e += sizeof("ALL:!COMPLEMENTOFDEFAULT:!eNULL")-1; + s->priority_base = "NORMAL"; + } + else if (0 == strncmp(e, CONST_STR_LEN("SUITEB128")) + || 0 == strncmp(e, CONST_STR_LEN("SUITEB128ONLY")) + || 0 == strncmp(e, CONST_STR_LEN("SUITEB192"))) { + s->priority_base = (0 == strncmp(e, CONST_STR_LEN("SUITEB192"))) + ? "SUITEB192" + : "SUITEB128"; + e += (0 == strncmp(e, CONST_STR_LEN("SUITEB128ONLY"))) + ? sizeof("SUITEB128ONLY")-1 + : sizeof("SUITEB128")-1; + if (*e) + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ignoring cipher string after SUITEB: %s", e); + return 1; + } + + if (e != b->ptr && *e != ':' && *e != '\0') { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: error: missing support for cipher list: %s", b->ptr); + return 0; + } + + /* not handled: "ALL" is "DEFAULT" and "RC4" */ + /* not handled: "COMPLEMENTOFALL" is "eNULL" */ + + int rc = 1; + if (e == b->ptr || *e == '\0') --e; /*initial condition for loop below*/ + for (const char *p; e && (e = strchr((p = e+1),':')); ) { + size_t len = e ? (size_t)(e - p) : strlen(p); + if (len >= sizeof(n)) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: skipped ciphersuite; too long: %.*s", + (int)len, p); + continue; + } + char c = (*p == '!' || *p == '-' || *p == '+') ? *p : 0; + size_t nlen = c ? len-1 : len; + memcpy(n, c ? p+1 : p, nlen); + n[nlen] = '\0'; + + /* not handled: !xxxxx -xxxxx and most +xxxxx */ + if (c) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: error: missing support for cipher list: %s", b->ptr); + } + + /* ignore @STRENGTH sorting and ignore @SECLEVEL=n */ + char *a = strchr(n, '@'); + if (a) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ignored %s in %.*s", a, (int)len, p); + *a = '\0'; + nlen = (size_t)(a - n); + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("TLSv1.3"))) { + buffer_append_string_len(plist, + CONST_STR_LEN(suite_TLSv13)); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("TLSv1.2"))) { + buffer_append_string_len(plist, + CONST_STR_LEN(suite_TLSv12)); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("TLSv1.0"))) { + buffer_append_string_len(plist, + CONST_STR_LEN(suite_TLSv10)); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("SSLv3"))) { + buffer_append_string_len(plist, + CONST_STR_LEN(suite_SSLv3)); + continue; + } + + /* handle a popular recommendations + * ssl.cipher-list = "EECDH+AESGCM:EDH+AESGCM" + * ssl.cipher-list = "AES256+EECDH:AES256+EDH" + * which uses AES hardware acceleration built into popular CPUs */ + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("ECDHE+AESGCM")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("EECDH+AESGCM"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-256-GCM:+AES-128-GCM:")); + continue; + } + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("DHE+AESGCM")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("EDH+AESGCM"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-256-GCM:+AES-128-GCM:")); + continue; + } + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("AES256+EECDH"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-256-GCM:+AES-256-CCM:+AES-256-CBC:+AES-256-CCM-8:")); + continue; + } + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("AES256+EDH"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-256-GCM:+AES-256-CCM:+AES-256-CBC:+AES-256-CCM-8:")); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("HIGH"))) { + buffer_append_string_len(plist, + CONST_STR_LEN(suite_HIGH)); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("AES256")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("AES"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-256-GCM:+AES-256-CCM:+AES-256-CBC:+AES-256-CCM-8:")); + if (nlen == sizeof("AES256")-1) continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("AES128")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("AES"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+AES-128-GCM:+AES-128-CCM:+AES-128-CBC:+AES-128-CCM-8:")); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("CAMELLIA256")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("CAMELLIA"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+CAMELLIA-256-GCM:+CAMELLIA-256-CBC:")); + if (nlen == sizeof("CAMELLIA256")-1) continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("CAMELLIA128")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("CAMELLIA"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+CAMELLIA-128-GCM:+CAMELLIA-128-CBC:")); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("CHACHA20"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+CHACHA20-POLY1305:")); + continue; + } + + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("3DES"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+3DES-CBC:")); + continue; + } + + /* not recommended, but permitted if explicitly requested */ + if (buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("NULL")) + || buffer_eq_icase_ss(n, nlen, CONST_STR_LEN("eNULL"))) { + buffer_append_string_len(plist, + CONST_STR_LEN("+NULL:")); + continue; + } + + if (*e != ':' && *e != '\0') { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: error: missing support for cipher list: %.*s", + (int)len, p); + rc = 0; + continue; + } + } + if (0 == rc) return 0; + } + + return 1; +} + + +static int +mod_gnutls_ssl_conf_curves(server *srv, plugin_config_socket *s, const buffer *curvelist) +{ + /* map NIST name (e.g. P-256) or OpenSSL OID name (e.g. prime256v1) + * to GnuTLS name of supported curves/groups */ + static const char *names[] = { + "P-192", "CURVE-SECP192R1", + "P-224", "CURVE-SECP224R1", + "P-256", "GROUP-SECP256R1", /* CURVE-SECP256R1 */ + "P-384", "GROUP-SECP384R1", /* CURVE-SECP384R1 */ + "P-521", "GROUP-SECP521R1", /* CURVE-SECP521R1 */ + "X25519", "GROUP-X25519", /* CURVE-X25519 */ + "X448", "GROUP-X448", /* CURVE-X448 */ + "prime192v1", "CURVE-SECP192R1", + "secp224r1", "CURVE-SECP224R1", + "prime256v1", "GROUP-SECP256R1", /* CURVE-SECP256R1 */ + "secp384r1", "GROUP-SECP384R1", /* CURVE-SECP384R1 */ + "secp521r1", "GROUP-SECP521R1", /* CURVE-SECP521R1 */ + "ffdhe2048", "GROUP-FFDHE2048", + "ffdhe3072", "GROUP-FFDHE3072", + "ffdhe4096", "GROUP-FFDHE4096", + "ffdhe6144", "GROUP-FFDHE6144", + "ffdhe8192", "GROUP-FFDHE8192", + }; + + buffer * const plist = &s->priority_str; + const buffer * const b = curvelist; + for (const char *n, *e = b->ptr-1; e && (e = strchr((n = e+1),':')); ) { + size_t len = e ? (size_t)(e - n) : strlen(n); + uint32_t i; + for (i = 0; i < sizeof(names)/sizeof(*names)/2; i += 2) { + if (0 == strncmp(names[i], n, len) && names[i][len] == '\0') + break; + } + if (i == sizeof(names)/sizeof(*names)/2) { + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: unrecognized curve: %.*s; ignored", (int)len, n); + continue; + } + + buffer_append_string_len(plist, CONST_STR_LEN("+")); + buffer_append_string_len(plist, names[i+1], strlen(names[i+1])); + buffer_append_string_len(plist, CONST_STR_LEN(":")); + } + + return 1; +} + + +static int +mod_gnutls_ssl_conf_proto_val (server *srv, plugin_config_socket *s, const buffer *b, int max) +{ + if (NULL == b) /* default: min TLSv1.2, max TLSv1.3 */ + return max ? GNUTLS_TLS1_3 : GNUTLS_TLS1_2; + else if (buffer_eq_icase_slen(b, CONST_STR_LEN("None"))) /*"disable" limit*/ + return max + ? GNUTLS_TLS1_3 + : (s->ssl_use_sslv3 ? GNUTLS_SSL3 : GNUTLS_TLS1_0); + else if (buffer_eq_icase_slen(b, CONST_STR_LEN("SSLv3"))) + return GNUTLS_SSL3; + else if (buffer_eq_icase_slen(b, CONST_STR_LEN("TLSv1.0"))) + return GNUTLS_TLS1_0; + else if (buffer_eq_icase_slen(b, CONST_STR_LEN("TLSv1.1"))) + return GNUTLS_TLS1_1; + else if (buffer_eq_icase_slen(b, CONST_STR_LEN("TLSv1.2"))) + return GNUTLS_TLS1_2; + else if (buffer_eq_icase_slen(b, CONST_STR_LEN("TLSv1.3"))) + return GNUTLS_TLS1_3; + else { + if (buffer_eq_icase_slen(b, CONST_STR_LEN("DTLSv1")) + || buffer_eq_icase_slen(b, CONST_STR_LEN("DTLSv1.2"))) + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.openssl.ssl-conf-cmd %s %s ignored", + max ? "MaxProtocol" : "MinProtocol", b->ptr); + else + log_error(srv->errh, __FILE__, __LINE__, + "GnuTLS: ssl.openssl.ssl-conf-cmd %s %s invalid; ignored", + max ? "MaxProtocol" : "MinProtocol", b->ptr); + } + return max ? GNUTLS_TLS1_3 : GNUTLS_TLS1_2; +} + + +static void +mod_gnutls_ssl_conf_proto (server *srv, plugin_config_socket *s, const buffer *minb, const buffer *maxb) +{ + /* use of SSL v3 should be avoided, and SSL v2 is not supported */ + int n = mod_gnutls_ssl_conf_proto_val(srv, s, minb, 0); + int x = mod_gnutls_ssl_conf_proto_val(srv, s, maxb, 1); + if (n < 0) return; + if (x < 0) return; + buffer * const b = &s->priority_str; + buffer_append_string_len(b, CONST_STR_LEN("-VERS-ALL:")); + switch (n) { + case GNUTLS_SSL3: + buffer_append_string_len(b, CONST_STR_LEN("+VERS-SSL3.0:")); + __attribute_fallthrough__ + case GNUTLS_TLS1_0: + if (x < GNUTLS_TLS1_0) break; + buffer_append_string_len(b, CONST_STR_LEN("+VERS-TLS1.0:")); + __attribute_fallthrough__ + case GNUTLS_TLS1_1: + if (x < GNUTLS_TLS1_1) break; + buffer_append_string_len(b, CONST_STR_LEN("+VERS-TLS1.1:")); + __attribute_fallthrough__ + case GNUTLS_TLS1_2: + if (x < GNUTLS_TLS1_2) break; + buffer_append_string_len(b, CONST_STR_LEN("+VERS-TLS1.2:")); + __attribute_fallthrough__ + case GNUTLS_TLS1_3: + if (x < GNUTLS_TLS1_3) break; + buffer_append_string_len(b, CONST_STR_LEN("+VERS-TLS1.3:")); + break; + } +} diff --git a/src/mod_secdownload.c b/src/mod_secdownload.c index 4b594e28..0e907a1f 100644 --- a/src/mod_secdownload.c +++ b/src/mod_secdownload.c @@ -20,6 +20,8 @@ #elif defined(USE_OPENSSL_CRYPTO) #include #include +#elif defined(USE_GNUTLS_CRYPTO) +#include #endif #endif @@ -215,6 +217,18 @@ static int secdl_verify_mac(plugin_config *config, const char* protected_path, c "hmac-sha1: HMAC() failed"); return 0; } + #elif defined(USE_GNUTLS_CRYPTO) + int rc = + gnutls_hmac_fast(GNUTLS_MAC_SHA1, + (const unsigned char *)config->secret->ptr, + buffer_string_length(config->secret), + (const unsigned char *)protected_path, + strlen(protected_path), digest); + if (0 != rc) { + log_error(errh, __FILE__, __LINE__, + "hmac-sha1: HMAC() failed"); + return 0; + } #else #error "unexpected; crypto lib not configured for use by mod_secdownload" #endif @@ -256,6 +270,18 @@ static int secdl_verify_mac(plugin_config *config, const char* protected_path, c "hmac-sha256: HMAC() failed"); return 0; } + #elif defined(USE_GNUTLS_CRYPTO) + int rc = + gnutls_hmac_fast(GNUTLS_MAC_SHA256, + (const unsigned char *)config->secret->ptr, + buffer_string_length(config->secret), + (const unsigned char *)protected_path, + strlen(protected_path), digest); + if (0 != rc) { + log_error(errh, __FILE__, __LINE__, + "hmac-sha256: HMAC() failed"); + return 0; + } #else #error "unexpected; crypto lib not configured for use by mod_secdownload" #endif diff --git a/src/rand.c b/src/rand.c index ffdd15f0..6cf2da6a 100644 --- a/src/rand.c +++ b/src/rand.c @@ -18,19 +18,25 @@ #ifdef USE_NETTLE_CRYPTO #undef USE_MBEDTLS_CRYPTO #undef USE_OPENSSL_CRYPTO +#undef USE_GNUTLS_CRYPTO #include #include #include #endif #ifdef USE_MBEDTLS_CRYPTO #undef USE_OPENSSL_CRYPTO +#undef USE_GNUTLS_CRYPTO #include #include #endif #ifdef USE_OPENSSL_CRYPTO +#undef USE_GNUTLS_CRYPTO #include /* OPENSSL_VERSION_NUMBER */ #include #endif +#ifdef USE_GNUTLS_CRYPTO +#include +#endif #ifdef HAVE_GETENTROPY #include #endif @@ -238,6 +244,10 @@ static void li_rand_init (void) void li_rand_reseed (void) { + #ifdef USE_GNUTLS_CRYPTO + gnutls_rnd_refresh(); + return; + #endif #ifdef USE_MBEDTLS_CRYPTO if (li_rand_inited) { #ifdef MBEDTLS_ENTROPY_C @@ -253,6 +263,10 @@ void li_rand_reseed (void) int li_rand_pseudo (void) { + #ifdef USE_GNUTLS_CRYPTO + int i; + if (0 == gnutls_rnd(GNUTLS_RND_NONCE, &i, sizeof(i))) return i; + #endif if (!li_rand_inited) li_rand_init(); /* randomness *is not* cryptographically strong */ /* (attempt to use better mechanisms to replace the more portable rand()) */ @@ -291,6 +305,9 @@ int li_rand_pseudo (void) void li_rand_pseudo_bytes (unsigned char *buf, int num) { + #ifdef USE_GNUTLS_CRYPTO + if (0 == gnutls_rnd(GNUTLS_RND_NONCE, buf, (size_t)num)) return; + #endif #ifdef USE_MBEDTLS_CRYPTO #ifdef MBEDTLS_CTR_DRBG_C if (0 == mbedtls_ctr_drbg_random(&ctr_drbg, buf, (size_t)num)) return; @@ -302,6 +319,9 @@ void li_rand_pseudo_bytes (unsigned char *buf, int num) int li_rand_bytes (unsigned char *buf, int num) { + #ifdef USE_GNUTLS_CRYPTO /* should use GNUTLS_RND_KEY for long-term keys */ + if (0 == gnutls_rnd(GNUTLS_RND_RANDOM, buf, (size_t)num)) return 1; + #endif #ifdef USE_NETTLE_CRYPTO #if 0 /* not implemented: periodic nettle_yarrow256_update() and reseed */ if (!nettle_yarrow256_is_seeded(&yarrow256_ctx)) { diff --git a/src/server.c b/src/server.c index 0c74a1c7..c1ba5bc3 100644 --- a/src/server.c +++ b/src/server.c @@ -72,7 +72,8 @@ static const buffer default_server_tag = { CONST_STR_LEN(PACKAGE_DESC), 0 }; #include "sys-crypto.h" #if defined(USE_OPENSSL_CRYPTO) \ - || defined(USE_MBEDTLS_CRYPTO) + || defined(USE_MBEDTLS_CRYPTO) \ + || defined(USE_GNUTLS_CRYPTO) #define TEXT_SSL " (ssl)" #else #define TEXT_SSL @@ -440,6 +441,16 @@ static void show_features (void) { #else "\t- mbedTLS support\n" #endif +#ifdef USE_GNUTLS_CRYPTO + "\t+ GnuTLS support\n" +#else + "\t- GnuTLS support\n" +#endif +#ifdef USE_NETTLE_CRYPTO + "\t+ Nettle support\n" +#else + "\t- Nettle support\n" +#endif #ifdef HAVE_LIBPCRE "\t+ PCRE support\n" #else diff --git a/src/sys-crypto.h b/src/sys-crypto.h index ec321cc4..c2e6fed9 100644 --- a/src/sys-crypto.h +++ b/src/sys-crypto.h @@ -29,4 +29,9 @@ #define USE_NETTLE_CRYPTO #endif +#ifdef HAVE_GNUTLS_CRYPTO_H +#define USE_LIB_CRYPTO +#define USE_GNUTLS_CRYPTO +#endif + #endif