/* * 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 * * Note: If session tickets are -not- disabled with * ssl.openssl.ssl-conf-cmd = ("Options" => "-SessionTicket") * 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 unless ssl.stek-file is defined and maintained (preferred), * or if some external job restarts 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, and if * ssl.stek-file is not in use, then 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. Since that is likely disruptive, * if multiple lighttpd workers are configured, ssl.stek-file should be * defined and the file maintained externally. */ #include "first.h" #include #include #include #include #include #include #include /* vsnprintf() */ #include #include #include #include #include #include #include #ifdef GNUTLS_SKIP_GLOBAL_INIT GNUTLS_SKIP_GLOBAL_INIT #endif #include "base.h" #include "fdevent.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; char must_staple; gnutls_datum_t *ssl_pemfile_x509; gnutls_privkey_t ssl_pemfile_pkey; const buffer *ssl_stapling_file; time_t ssl_stapling_loadts; time_t ssl_stapling_nextts; } 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_override; 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; const char *ssl_stek_file; } 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 */ 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 off_t dlen = 512*1024*1024;/*(arbitrary limit: 512 MB file; expect < 1 MB)*/ char *data = fdevent_load_file(fn, &dlen, errh, gnutls_malloc, gnutls_free); if (NULL == data) return GNUTLS_E_FILE_ERROR; d->data = (unsigned char *)data; d->size = (unsigned int)dlen; return 0; #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; } /* session tickets * * gnutls expects 64 bytes of random data in gnutls_datum_t passed to * gnutls_session_ticket_enable_server() * * (private) session ticket definitions from lib/gnutls_int.h * #define TICKET_MASTER_KEY_SIZE (TICKET_KEY_NAME_SIZE+TICKET_CIPHER_KEY_SIZE+TICKET_MAC_SECRET_SIZE) * #define TICKET_KEY_NAME_SIZE 16 * #define TICKET_CIPHER_KEY_SIZE 32 * #define TICKET_MAC_SECRET_SIZE 16 */ #define TICKET_MASTER_KEY_SIZE 64 #define TLSEXT_KEYNAME_LENGTH 16 #define TLSEXT_TICK_KEY_LENGTH 32 /* construct our own session ticket encryption key structure * to store keys that are not yet active * (mirror from mod_openssl, even though not all bits are used here) */ typedef struct tlsext_ticket_key_st { time_t active_ts; /* tickets not issued w/ key until activation timestamp */ time_t expire_ts; /* key not valid after expiration timestamp */ unsigned char tick_key_name[TLSEXT_KEYNAME_LENGTH]; unsigned char tick_hmac_key[TLSEXT_TICK_KEY_LENGTH]; unsigned char tick_aes_key[TLSEXT_TICK_KEY_LENGTH]; } tlsext_ticket_key_t; static tlsext_ticket_key_t session_ticket_keys[1]; /* temp store until active */ 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); } static int mod_gnutls_session_ticket_key_file (const char *fn) { /* session ticket encryption key (STEK) * * STEK file should be stored in non-persistent storage, * e.g. /dev/shm/lighttpd/stek-file (in memory) * with appropriate permissions set to keep stek-file from being * read by other users. Where possible, systems should also be * configured without swap. * * admin should schedule an independent job to periodically * generate new STEK up to 3 times during key lifetime * (lighttpd stores up to 3 keys) * * format of binary file is: * 4-byte - format version (always 0; for use if format changes) * 4-byte - activation timestamp * 4-byte - expiration timestamp * 16-byte - session ticket key name * 32-byte - session ticket HMAC encrpytion key * 32-byte - session ticket AES encrpytion key * * STEK file can be created with a command such as: * dd if=/dev/random bs=1 count=80 status=none | \ * perl -e 'print pack("iii",0,time()+300,time()+86400),<>' \ * > STEK-file.$$ && mv STEK-file.$$ STEK-file * * The above delays activation time by 5 mins (+300 sec) to allow file to * be propagated to other machines. (admin must handle this independently) * If STEK generation is performed immediately prior to starting lighttpd, * admin should activate keys immediately (without +300). */ int buf[23]; /* 92 bytes */ int fd = fdevent_open_cloexec(fn, 1, O_RDONLY, 0); if (fd < 0) return 0; ssize_t rd = read(fd, buf, sizeof(buf)); close(fd); int rc = 0; /*(will retry on next check interval upon any error)*/ if (rd == sizeof(buf) && buf[0] == 0) { /*(format version 0)*/ session_ticket_keys[0].active_ts = buf[1]; session_ticket_keys[0].expire_ts = buf[2]; memcpy(&session_ticket_keys[0].tick_key_name, buf+3, 80); rc = 1; } gnutls_memset(buf, 0, sizeof(buf)); return rc; } static void mod_gnutls_session_ticket_key_check (server *srv, const plugin_data *p, const time_t cur_ts) { if (p->ssl_stek_file) { struct stat st; if (0 == stat(p->ssl_stek_file, &st) && st.st_mtime > stek_rotate_ts && mod_gnutls_session_ticket_key_file(p->ssl_stek_file)) { stek_rotate_ts = cur_ts; } tlsext_ticket_key_t *stek = session_ticket_keys; if (stek->active_ts != 0 && stek->active_ts - 63 <= cur_ts) { if (NULL == session_ticket_key.data) { session_ticket_key.data = gnutls_malloc(TICKET_MASTER_KEY_SIZE); if (NULL == session_ticket_key.data) return; session_ticket_key.size = TICKET_MASTER_KEY_SIZE; } memcpy(session_ticket_key.data, stek->tick_key_name, TICKET_MASTER_KEY_SIZE); gnutls_memset(stek->tick_key_name, 0, TICKET_MASTER_KEY_SIZE); } if (stek->expire_ts < cur_ts) mod_gnutls_session_ticket_key_free(); } else if (cur_ts - 86400 >= stek_rotate_ts) { /*(24 hours)*/ mod_gnutls_session_ticket_key_rotate(srv); stek_rotate_ts = cur_ts; } } 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; gnutls_memset(session_ticket_keys, 0, sizeof(session_ticket_keys)); mod_gnutls_session_ticket_key_free(); stek_rotate_ts = 0; 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:/* ssl.stapling-file */ break; case 14:/* 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; } #if GNUTLS_VERSION_NUMBER < 0x030603 static time_t mod_gnutls_ocsp_next_update (plugin_cert *pc, log_error_st *errh) { gnutls_datum_t f = { NULL, 0 }; int rc = mod_gnutls_load_file(pc->ssl_stapling_file->ptr, &f, errh); if (rc < 0) return (time_t)-1; gnutls_ocsp_resp_t resp = NULL; time_t nextupd; if ( gnutls_ocsp_resp_init(&resp) < 0 || gnutls_ocsp_resp_import(resp, &f) < 0 || gnutls_ocsp_resp_get_single(resp, 0, NULL, NULL, NULL, NULL, NULL, NULL, &nextupd, NULL, NULL) < 0) nextupd = (time_t)-1; gnutls_ocsp_resp_deinit(resp); mod_gnutls_datum_wipe(&f); return nextupd; } #endif static int mod_gnutls_reload_stapling_file (server *srv, plugin_cert *pc, const time_t cur_ts) { #if GNUTLS_VERSION_NUMBER < 0x030603 /* load file into gnutls_ocsp_resp_t before loading into * gnutls_certificate_credentials_t for safety. Still ToC-ToU since file * is loaded twice, but unlikely condition. (GnuTLS limitation does not * expose access to OCSP response from gnutls_certificate_credentials_t * before GnuTLS 3.6.3) */ time_t nextupd = mod_gnutls_ocsp_next_update(pc, srv->errh); #else UNUSED(srv); #endif /* GnuTLS 3.5.6 added the ability to include multiple OCSP responses for * certificate chain as allowed in TLSv1.3, but that is not utilized here. * If implemented, it will probably operate on a new directive, * e.g. ssl.stapling-pemfile * GnuTLS 3.6.3 added gnutls_certificate_set_ocsp_status_request_file2() * GnuTLS 3.6.3 added gnutls_certificate_set_ocsp_status_request_mem() * GnuTLS 3.6.3 added gnutls_certificate_get_ocsp_expiration() */ /* gnutls_certificate_get_ocsp_expiration() code comments: * Note that the credentials structure should be read-only when in * use, thus when reloading, either the credentials structure must not * be in use by any sessions, or a new credentials structure should be * allocated for new sessions. * XXX: lighttpd is not threaded, so this is probably not an issue (?) */ #if 0 gnutls_certificate_set_flags(pc->ssl_cred, GNUTLS_CERTIFICATE_SKIP_OCSP_RESPONSE_CHECK); #endif const char *fn = pc->ssl_stapling_file->ptr; int rc = gnutls_certificate_set_ocsp_status_request_file(pc->ssl_cred,fn,0); if (rc < 0) return rc; #if GNUTLS_VERSION_NUMBER >= 0x030603 time_t nextupd = gnutls_certificate_get_ocsp_expiration(pc->ssl_cred, 0, 0, 0); if (nextupd == (time_t)-2) nextupd = (time_t)-1; #endif pc->ssl_stapling_loadts = cur_ts; pc->ssl_stapling_nextts = nextupd; if (pc->ssl_stapling_nextts == (time_t)-1) { /* "Next Update" might not be provided by OCSP responder * Use 3600 sec (1 hour) in that case. */ /* retry in 1 hour if unable to determine Next Update */ pc->ssl_stapling_nextts = cur_ts + 3600; pc->ssl_stapling_loadts = 0; } return 0; } static int mod_gnutls_refresh_stapling_file (server *srv, plugin_cert *pc, const time_t cur_ts) { if (pc->ssl_stapling_nextts >= 256 && pc->ssl_stapling_nextts - 256 > cur_ts) return 0; /* skip check for refresh unless close to expire */ struct stat st; if (0 != stat(pc->ssl_stapling_file->ptr, &st) || st.st_mtime <= pc->ssl_stapling_loadts) { if (pc->ssl_stapling_nextts < cur_ts) { #if 0 /* discard expired OCSP stapling response */ /* Does GnuTLS detect expired OCSP response? */ /* or must we rebuild gnutls_certificate_credentials_t ? */ #endif if (pc->must_staple) { log_error(srv->errh, __FILE__, __LINE__, "certificate marked OCSP Must-Staple, " "but OCSP response expired from ssl.stapling-file %s", pc->ssl_stapling_file->ptr); } } return 0; } return mod_gnutls_reload_stapling_file(srv, pc, cur_ts); } static void mod_gnutls_refresh_stapling_files (server *srv, const plugin_data *p, const time_t cur_ts) { /* future: might construct array of (plugin_cert *) at startup * to avoid the need to search for them here */ for (int i = 0, used = p->nconfig; i < used; ++i) { const config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; for (; cpv->k_id != -1; ++cpv) { if (cpv->k_id != 0) continue; /* k_id == 0 for ssl.pemfile */ if (cpv->vtype != T_CONFIG_LOCAL) continue; plugin_cert *pc = cpv->v.v; if (!buffer_string_is_empty(pc->ssl_stapling_file)) mod_gnutls_refresh_stapling_file(srv, pc, cur_ts); } } } static int mod_gnutls_crt_must_staple (gnutls_x509_crt_t crt) { /* Look for TLS features X.509 extension with value 5 * RFC 7633 https://tools.ietf.org/html/rfc7633#appendix-A * 5 = OCSP Must-Staple (security mechanism) */ int rc; #if GNUTLS_VERSION_NUMBER < 0x030501 unsigned int i; char oid[128]; size_t oidsz; for (i = 0; ; ++i) { oidsz = sizeof(oid); rc = gnutls_x509_crt_get_extension_info(crt, i, oid, &oidsz, NULL); if (rc < 0 || 0 == strcmp(oid, GNUTLS_X509EXT_OID_TLSFEATURES)) break; } /* ext not found if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) */ if (rc < 0) return rc; gnutls_datum_t der = { NULL, 0 }; rc = gnutls_x509_crt_get_extension_data2(crt, i, &der); if (rc < 0) return rc; /* DER encoding (Tag, Length, Value (TLV)) expecting: 30:03:02:01:05 * [TL[TLV]] (30=Sequence, Len=03, (02=Integer, Len=01, Value=05)) * XXX: This is not future-proof if TLS feature list values are extended */ /*const char must_staple[] = { 0x30, 0x03, 0x02, 0x01, 0x05 };*/ /*rc = (der.size == 5 && 0 == memcmp(der.data, must_staple, 5))*/ rc = (der.size >= 5 && der.data[0] == 0x30 && der.data[1] >= 0x03 && der.data[2] == 0x2 && der.data[3] == 0x1 && der.data[4] == 0x5) ? 1 : 0; gnutls_free(der.data); #else gnutls_x509_tlsfeatures_t f; rc = gnutls_x509_tlsfeatures_init(&f); if (rc < 0) return rc; rc = gnutls_x509_tlsfeatures_add(f, 5); /* 5 = OCSP Must-Staple */ if (rc < 0) return rc; rc = (0 != gnutls_x509_tlsfeatures_check_crt(f, crt)); gnutls_x509_tlsfeatures_deinit(f); #endif return rc; /* 1 if OCSP Must-Staple found; 0 if not */ } 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, const buffer *ssl_stapling_file) { #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; pc->ssl_stapling_file= ssl_stapling_file; pc->ssl_stapling_loadts = 0; pc->ssl_stapling_nextts = 0; pc->must_staple = mod_gnutls_crt_must_staple(((gnutls_x509_crt_t *)(void *)d->data)[0]); 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; } } if (!buffer_string_is_empty(pc->ssl_stapling_file)) { if (mod_gnutls_reload_stapling_file(srv, pc, log_epoch_secs) < 0) { /* continue without OCSP response if there is an error */ } } else if (pc->must_staple) { log_error(srv->errh, __FILE__, __LINE__, "certificate %s marked OCSP Must-Staple, " "but ssl.stapling-file not provided", pemfile->ptr); } 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) { /* XXX: NOT done: parse client hello for session ticket extension * and choose from among multiple keys */ 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); } } else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("gnutls-override"))) { s->priority_override = &ds->value; } #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 * * 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); } if (!buffer_string_is_empty(s->priority_override)) { b = s->priority_override; s->ssl_session_ticket = (NULL == strstr(b->ptr, "%NO_TICKET")); } if (p->defaults.ssl_log_noise) log_error(srv->errh, __FILE__, __LINE__, "debug: GnuTLS priority string: %s", b->ptr); 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 } ,{ CONST_STR_LEN("ssl.stek-file"), T_CONFIG_STRING, T_CONFIG_SCOPE_SERVER } ,{ 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 = 1; /* enabled 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; case 10:/* ssl.stek-file */ if (!buffer_is_empty(cpv->v.b)) p->ssl_stek_file = cpv->v.b->ptr; 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); } if (rc == HANDLER_GO_ON && ssl_is_init) mod_gnutls_session_ticket_key_check(srv, p, 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("ssl.stapling-file"), 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; const buffer *ssl_stapling_file = 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 */ case 12:/* ssl.acme-tls-1 */ break; case 13:/* ssl.stapling-file */ ssl_stapling_file = cpv->v.b; break; case 14:/* 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, ssl_stapling_file); 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); } return mod_gnutls_set_defaults_sockets(srv, p); } 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)*/ mod_gnutls_session_ticket_key_check(srv, p, cur_ts); mod_gnutls_refresh_stapling_files(srv, p, 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; } }