lighttpd 1.4.x
https://www.lighttpd.net/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3092 lines
108 KiB
3092 lines
108 KiB
/* |
|
* mod_gnutls - GnuTLS support for lighttpd |
|
* |
|
* Copyright(c) 2020 Glenn Strauss gstrauss()gluelogic.com All rights reserved |
|
* License: BSD 3-clause (same as lighttpd) |
|
*/ |
|
/* |
|
* GnuTLS manual: https://www.gnutls.org/documentation.html |
|
* |
|
* future possible enhancements: OCSP stapling |
|
* |
|
* Note: session tickets are disabled by default. If enabled, |
|
* mod_gnutls rotates server ticket encryption key (STEK) every 24 hours. |
|
* This is fine for use with a single lighttpd instance, but with multiple |
|
* lighttpd workers, no coordinated STEK (server ticket encryption key) |
|
* rotation occurs other than by (some external job) restarting lighttpd. |
|
* Restarting lighttpd generates a new key that is shared by lighttpd workers |
|
* for the lifetime of the new key. If the rotation period expires and |
|
* lighttpd has not been restarted, lighttpd workers will generate new |
|
* independent keys, making session tickets less effective for session |
|
* resumption, since clients have a lower chance for future connections to |
|
* reach the same lighttpd worker. However, things will still work, and a new |
|
* session will be created if session resumption fails. Admins should plan to |
|
* restart lighttpd at least every 24 hours if session tickets are enabled and |
|
* multiple lighttpd workers are configured. |
|
*/ |
|
#include "first.h" |
|
|
|
#include <sys/types.h> |
|
#include <errno.h> |
|
#include <stdarg.h> |
|
#include <stdlib.h> |
|
#include <stdio.h> /* vsnprintf() */ |
|
#include <string.h> |
|
#include <unistd.h> |
|
|
|
#include <gnutls/gnutls.h> |
|
#include <gnutls/x509.h> |
|
#include <gnutls/abstract.h> |
|
|
|
#ifdef GNUTLS_SKIP_GLOBAL_INIT |
|
GNUTLS_SKIP_GLOBAL_INIT |
|
#endif |
|
|
|
#include "base.h" |
|
#include "http_header.h" |
|
#include "log.h" |
|
#include "plugin.h" |
|
|
|
typedef struct { |
|
/* SNI per host: with COMP_SERVER_SOCKET, COMP_HTTP_SCHEME, COMP_HTTP_HOST */ |
|
gnutls_certificate_credentials_t ssl_cred; |
|
char trust_inited; |
|
gnutls_datum_t *ssl_pemfile_x509; |
|
gnutls_privkey_t ssl_pemfile_pkey; |
|
} plugin_cert; |
|
|
|
typedef struct { |
|
int8_t ssl_session_ticket; |
|
/*(preserved here for deinit at server shutdown)*/ |
|
gnutls_priority_t priority_cache; |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
gnutls_dh_params_t dh_params; |
|
#endif |
|
} plugin_ssl_ctx; |
|
|
|
typedef struct { |
|
plugin_cert *pc; |
|
|
|
/*(used only during startup; not patched)*/ |
|
unsigned char ssl_enabled; /* only interesting for setting up listening sockets. don't use at runtime */ |
|
unsigned char ssl_honor_cipher_order; /* determine SSL cipher in server-preferred order, not client-order */ |
|
unsigned char ssl_empty_fragments; |
|
unsigned char ssl_use_sslv2; |
|
unsigned char ssl_use_sslv3; |
|
const buffer *ssl_cipher_list; |
|
const buffer *ssl_dh_file; |
|
const buffer *ssl_ec_curve; |
|
array *ssl_conf_cmd; |
|
|
|
/*(copied from plugin_data for socket ssl_ctx config)*/ |
|
gnutls_priority_t priority_cache; |
|
unsigned char ssl_session_ticket; |
|
unsigned char ssl_verifyclient; |
|
unsigned char ssl_verifyclient_enforce; |
|
unsigned char ssl_verifyclient_depth; |
|
|
|
const char *priority_base; |
|
buffer priority_str; |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
gnutls_dh_params_t dh_params; |
|
#endif |
|
} plugin_config_socket; /*(used at startup during configuration)*/ |
|
|
|
typedef struct { |
|
/* SNI per host: w/ COMP_SERVER_SOCKET, COMP_HTTP_SCHEME, COMP_HTTP_HOST */ |
|
plugin_cert *pc; |
|
gnutls_datum_t *ssl_ca_file; /* .data is (gnutls_x509_crt_t) */ |
|
gnutls_datum_t *ssl_ca_dn_file; /* .data is (gnutls_x509_crt_t) */ |
|
gnutls_datum_t *ssl_ca_crl_file;/* .data is (gnutls_x509_crl_t) */ |
|
|
|
unsigned char ssl_verifyclient; |
|
unsigned char ssl_verifyclient_enforce; |
|
unsigned char ssl_verifyclient_depth; |
|
unsigned char ssl_verifyclient_export_cert; |
|
unsigned char ssl_read_ahead; |
|
unsigned char ssl_log_noise; |
|
unsigned char ssl_disable_client_renegotiation; |
|
const buffer *ssl_verifyclient_username; |
|
const buffer *ssl_acme_tls_1; |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
gnutls_dh_params_t dh_params; |
|
#endif |
|
} plugin_config; |
|
|
|
typedef struct { |
|
PLUGIN_DATA; |
|
plugin_ssl_ctx *ssl_ctxs; |
|
plugin_config defaults; |
|
server *srv; |
|
} plugin_data; |
|
|
|
static int ssl_is_init; |
|
/* need assigned p->id for deep access of module handler_ctx for connection |
|
* i.e. handler_ctx *hctx = r->plugin_ctx[plugin_data_singleton->id]; */ |
|
static plugin_data *plugin_data_singleton; |
|
#define LOCAL_SEND_BUFSIZE 16384 /* DEFAULT_MAX_RECORD_SIZE */ |
|
static char *local_send_buffer; |
|
|
|
typedef struct { |
|
gnutls_session_t ssl; /* gnutls request/connection context */ |
|
request_st *r; |
|
connection *con; |
|
int8_t request_env_patched; |
|
int8_t close_notify; |
|
uint8_t alpn; |
|
int8_t ssl_session_ticket; |
|
int handshake; |
|
size_t pending_write; |
|
plugin_config conf; |
|
unsigned int verify_status; |
|
buffer *tmp_buf; |
|
gnutls_certificate_credentials_t acme_tls_1_cred; |
|
} handler_ctx; |
|
|
|
|
|
static handler_ctx * |
|
handler_ctx_init (void) |
|
{ |
|
handler_ctx *hctx = calloc(1, sizeof(*hctx)); |
|
force_assert(hctx); |
|
return hctx; |
|
} |
|
|
|
|
|
static void |
|
handler_ctx_free (handler_ctx *hctx) |
|
{ |
|
gnutls_deinit(hctx->ssl); |
|
if (hctx->acme_tls_1_cred) |
|
gnutls_certificate_free_credentials(hctx->acme_tls_1_cred); |
|
free(hctx); |
|
} |
|
|
|
|
|
__attribute_cold__ |
|
static void elog(log_error_st * const errh, |
|
const char * const file, const int line, |
|
const int rc, const char * const msg) |
|
{ |
|
/* error logging convenience function that decodes gnutls result codes */ |
|
log_error(errh, file, line, "GnuTLS: %s: (%s) %s", |
|
msg, gnutls_strerror_name(rc), gnutls_strerror(rc)); |
|
} |
|
|
|
|
|
__attribute_cold__ |
|
__attribute_format__((__printf__, 5, 6)) |
|
static void elogf(log_error_st * const errh, |
|
const char * const file, const int line, |
|
const int rc, const char * const fmt, ...) |
|
{ |
|
char msg[1024]; |
|
va_list ap; |
|
va_start(ap, fmt); |
|
vsnprintf(msg, sizeof(msg), fmt, ap); |
|
va_end(ap); |
|
elog(errh, file, line, rc, msg); |
|
} |
|
|
|
|
|
/* gnutls func gnutls_load_file() loads file contents into a gnutls_datum_t. |
|
* The data loaded could be sensitive, e.g. a private key included in a pemfile. |
|
* However, gnutls_load_file() is not careful about zeroizing memory on error, |
|
* might use realloc() (which does not guarantee to zeroize memory released), |
|
* and silently continues on short read, so provide our own. Updates to |
|
* gnutls 3.6.14 may be more careful with private key files, but do not |
|
* extend the same care to pemfiles which might contain private keys. |
|
* Related, see also mod_gnutls_datum_wipe() below. |
|
* https://gitlab.com/gnutls/gnutls/-/issues/1001 |
|
* https://gitlab.com/gnutls/gnutls/-/issues/1002 |
|
* https://gitlab.com/gnutls/gnutls/-/merge_requests/1270 |
|
*/ |
|
#include <sys/stat.h> |
|
#include <fcntl.h> |
|
#include <limits.h> |
|
#include "fdevent.h" |
|
static int |
|
mod_gnutls_load_file (const char * const fn, gnutls_datum_t * const d, log_error_st *errh) |
|
{ |
|
#if 0 |
|
int rc = gnutls_load_file(fn, d); |
|
if (rc < 0) |
|
elogf(errh, __FILE__, __LINE__, rc, "%s() %s", __func__, fn); |
|
return rc; |
|
#else |
|
int fd = -1; |
|
unsigned int sz = 0; |
|
char *buf = NULL; |
|
do { |
|
fd = fdevent_open_cloexec(fn, 1, O_RDONLY, 0); /*(1: follows symlinks)*/ |
|
if (fd < 0) break; |
|
|
|
struct stat st; |
|
if (0 != fstat(fd, &st)) break; |
|
if (st.st_size >= UINT_MAX) { /*(file too large for gnutls_datum_t)*/ |
|
errno = EOVERFLOW; |
|
break; |
|
} |
|
|
|
sz = (unsigned int)st.st_size; |
|
buf = gnutls_malloc(sz+1); /*(+1 trailing '\0' for gnutls_load_file())*/ |
|
if (NULL == buf) break; |
|
|
|
ssize_t rd = 0; |
|
unsigned int off = 0; |
|
do { |
|
rd = read(fd, buf+off, sz-off); |
|
} while (rd > 0 ? (off += (unsigned int)rd) != sz : errno == EINTR); |
|
if (off != sz) { /*(file truncated?)*/ |
|
if (rd >= 0) errno = EIO; |
|
break; |
|
} |
|
|
|
buf[sz] = '\0'; |
|
d->data = (unsigned char *)buf; |
|
d->size = sz; |
|
close(fd); |
|
return 0; |
|
} while (0); |
|
int errnum = errno; |
|
log_perror(errh, __FILE__, __LINE__, "%s() %s", __func__, fn); |
|
if (fd >= 0) close(fd); |
|
if (buf) { |
|
gnutls_memset(buf, 0, sz); |
|
gnutls_free(buf); |
|
} |
|
errno = errnum; |
|
return GNUTLS_E_FILE_ERROR; |
|
#endif |
|
} |
|
|
|
|
|
/* GnuTLS does not expose _gnutls_free_key_datum() so provide our own */ |
|
static void |
|
mod_gnutls_datum_wipe (gnutls_datum_t * const d) |
|
{ |
|
if (NULL == d) return; |
|
if (d->data) { |
|
if (d->size) gnutls_memset(d->data, 0, d->size); |
|
gnutls_free(d->data); |
|
d->data = NULL; |
|
} |
|
d->size = 0; |
|
} |
|
|
|
|
|
static time_t stek_rotate_ts; |
|
static gnutls_datum_t session_ticket_key; |
|
static void |
|
mod_gnutls_session_ticket_key_free (void) |
|
{ |
|
mod_gnutls_datum_wipe(&session_ticket_key); |
|
} |
|
static void |
|
mod_gnutls_session_ticket_key_init (server *srv) |
|
{ |
|
int rc = gnutls_session_ticket_key_generate(&session_ticket_key); |
|
if (rc < 0) { |
|
/*(should not happen, but if it does, disable session ticket)*/ |
|
session_ticket_key.size = 0; |
|
elog(srv->errh, __FILE__, __LINE__, rc, |
|
"gnutls_session_ticket_key_generate()"); |
|
} |
|
} |
|
static void |
|
mod_gnutls_session_ticket_key_rotate (server *srv) |
|
{ |
|
mod_gnutls_session_ticket_key_free(); |
|
mod_gnutls_session_ticket_key_init(srv); |
|
} |
|
|
|
|
|
INIT_FUNC(mod_gnutls_init) |
|
{ |
|
plugin_data_singleton = (plugin_data *)calloc(1, sizeof(plugin_data)); |
|
return plugin_data_singleton; |
|
} |
|
|
|
|
|
static int mod_gnutls_init_once_gnutls (void) |
|
{ |
|
if (ssl_is_init) return 1; |
|
ssl_is_init = 1; |
|
|
|
/* Note: on systems with support for weak symbols, GNUTLS_SKIP_GLOBAL_INIT |
|
* is set near top of this file to inhibit GnuTLS implicit initialization |
|
* in a library constructor. On systems without support for weak symbols, |
|
* set GNUTLS_NO_EXPLICIT_INIT=1 in the environment before starting lighttpd |
|
* (GnuTLS 3.3.0 or later) */ |
|
|
|
if (gnutls_global_init() != GNUTLS_E_SUCCESS) |
|
return 0; |
|
|
|
local_send_buffer = malloc(LOCAL_SEND_BUFSIZE); |
|
force_assert(NULL != local_send_buffer); |
|
|
|
return 1; |
|
} |
|
|
|
|
|
static void mod_gnutls_free_gnutls (void) |
|
{ |
|
if (!ssl_is_init) return; |
|
|
|
mod_gnutls_session_ticket_key_free(); |
|
|
|
gnutls_global_deinit(); |
|
|
|
free(local_send_buffer); |
|
ssl_is_init = 0; |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_free_config_crts (gnutls_datum_t *d) |
|
{ |
|
if (NULL == d) return; |
|
gnutls_x509_crt_t *crts = (gnutls_x509_crt_t *)(void *)d->data; |
|
unsigned int u = d->size; |
|
for (unsigned int i = 0; i < u; ++i) |
|
gnutls_x509_crt_deinit(crts[i]); |
|
gnutls_free(crts); |
|
gnutls_free(d); |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_free_config_crls (gnutls_datum_t *d) |
|
{ |
|
if (NULL == d) return; |
|
gnutls_x509_crl_t *crls = (gnutls_x509_crl_t *)(void *)d->data; |
|
unsigned int u = d->size; |
|
for (unsigned int i = 0; i < u; ++i) |
|
gnutls_x509_crl_deinit(crls[i]); |
|
gnutls_free(crls); |
|
gnutls_free(d); |
|
} |
|
|
|
|
|
static gnutls_datum_t * |
|
mod_gnutls_load_config_crts (const char *fn, log_error_st *errh) |
|
{ |
|
/*(very similar to other mod_gnutls_load_config_*())*/ |
|
if (!mod_gnutls_init_once_gnutls()) return NULL; |
|
|
|
gnutls_datum_t f = { NULL, 0 }; |
|
int rc = mod_gnutls_load_file(fn, &f, errh); |
|
if (rc < 0) return NULL; |
|
gnutls_datum_t *d = gnutls_malloc(sizeof(gnutls_datum_t)); |
|
if (d == NULL) { |
|
mod_gnutls_datum_wipe(&f); |
|
return NULL; |
|
} |
|
d->data = NULL; |
|
d->size = 0; |
|
rc = gnutls_x509_crt_list_import2((gnutls_x509_crt_t **)&d->data, &d->size, |
|
&f, GNUTLS_X509_FMT_PEM, |
|
GNUTLS_X509_CRT_LIST_SORT); |
|
mod_gnutls_datum_wipe(&f); |
|
if (rc < 0) { |
|
elogf(errh, __FILE__, __LINE__, rc, |
|
"gnutls_x509_crt_list_import2() %s", fn); |
|
mod_gnutls_free_config_crts(d); |
|
return NULL; |
|
} |
|
|
|
return d; |
|
} |
|
|
|
|
|
static gnutls_datum_t * |
|
mod_gnutls_load_config_crls (const char *fn, log_error_st *errh) |
|
{ |
|
/*(very similar to other mod_gnutls_load_config_*())*/ |
|
if (!mod_gnutls_init_once_gnutls()) return NULL; |
|
|
|
gnutls_datum_t f = { NULL, 0 }; |
|
int rc = mod_gnutls_load_file(fn, &f, errh); |
|
if (rc < 0) return NULL; |
|
gnutls_datum_t *d = gnutls_malloc(sizeof(gnutls_datum_t)); |
|
if (d == NULL) { |
|
mod_gnutls_datum_wipe(&f); |
|
return NULL; |
|
} |
|
d->data = NULL; |
|
d->size = 0; |
|
rc = gnutls_x509_crl_list_import2((gnutls_x509_crl_t **)&d->data, &d->size, |
|
&f, GNUTLS_X509_FMT_PEM, 0); |
|
mod_gnutls_datum_wipe(&f); |
|
if (rc < 0) { |
|
elogf(errh, __FILE__, __LINE__, rc, |
|
"gnutls_x509_crl_list_import2() %s", fn); |
|
mod_gnutls_free_config_crls(d); |
|
return NULL; |
|
} |
|
|
|
return d; |
|
} |
|
|
|
|
|
static gnutls_privkey_t |
|
mod_gnutls_load_config_pkey (const char *fn, log_error_st *errh) |
|
{ |
|
/*(very similar to other mod_gnutls_load_config_*())*/ |
|
if (!mod_gnutls_init_once_gnutls()) return NULL; |
|
|
|
gnutls_datum_t f = { NULL, 0 }; |
|
int rc = mod_gnutls_load_file(fn, &f, errh); |
|
if (rc < 0) return NULL; |
|
gnutls_privkey_t pkey; |
|
rc = gnutls_privkey_init(&pkey); |
|
if (rc < 0) { |
|
mod_gnutls_datum_wipe(&f); |
|
return NULL; |
|
} |
|
rc = gnutls_privkey_import_x509_raw(pkey, &f, GNUTLS_X509_FMT_PEM, NULL, 0); |
|
mod_gnutls_datum_wipe(&f); |
|
if (rc < 0) { |
|
elogf(errh, __FILE__, __LINE__, rc, |
|
"gnutls_privkey_import_x509_raw() %s", fn); |
|
gnutls_privkey_deinit(pkey); |
|
return NULL; |
|
} |
|
|
|
return pkey; |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_free_config (server *srv, plugin_data * const p) |
|
{ |
|
if (NULL != p->ssl_ctxs) { |
|
gnutls_priority_t pcache_global_scope = p->ssl_ctxs->priority_cache; |
|
/* free from $SERVER["socket"] (if not copy of global scope) */ |
|
for (uint32_t i = 1; i < srv->config_context->used; ++i) { |
|
plugin_ssl_ctx * const s = p->ssl_ctxs + i; |
|
if (s->priority_cache && s->priority_cache != pcache_global_scope) { |
|
if (s->priority_cache) |
|
gnutls_priority_deinit(s->priority_cache); |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
if (s->dh_params) |
|
gnutls_dh_params_deinit(s->dh_params); |
|
#endif |
|
} |
|
} |
|
/* free from global scope */ |
|
if (pcache_global_scope) { |
|
if (p->ssl_ctxs[0].priority_cache) |
|
gnutls_priority_deinit(p->ssl_ctxs[0].priority_cache); |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
if (p->ssl_ctxs[0].dh_params) |
|
gnutls_dh_params_deinit(p->ssl_ctxs[0].dh_params); |
|
#endif |
|
} |
|
free(p->ssl_ctxs); |
|
} |
|
|
|
if (NULL == p->cvlist) return; |
|
/* (init i to 0 if global context; to 1 to skip empty global context) */ |
|
for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { |
|
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; |
|
for (; -1 != cpv->k_id; ++cpv) { |
|
switch (cpv->k_id) { |
|
case 0: /* ssl.pemfile */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) { |
|
plugin_cert *pc = cpv->v.v; |
|
gnutls_certificate_free_credentials(pc->ssl_cred); |
|
mod_gnutls_free_config_crts(pc->ssl_pemfile_x509); |
|
gnutls_privkey_deinit(pc->ssl_pemfile_pkey); |
|
free(pc); |
|
} |
|
break; |
|
case 2: /* ssl.ca-file */ |
|
case 3: /* ssl.ca-dn-file */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
mod_gnutls_free_config_crts(cpv->v.v); |
|
break; |
|
case 4: /* ssl.ca-crl-file */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
mod_gnutls_free_config_crls(cpv->v.v); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
FREE_FUNC(mod_gnutls_free) |
|
{ |
|
plugin_data *p = p_d; |
|
if (NULL == p->srv) return; |
|
mod_gnutls_free_config(p->srv, p); |
|
mod_gnutls_free_gnutls(); |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_merge_config_cpv (plugin_config * const pconf, const config_plugin_value_t * const cpv) |
|
{ |
|
switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ |
|
case 0: /* ssl.pemfile */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
pconf->pc = cpv->v.v; |
|
break; |
|
case 1: /* ssl.privkey */ |
|
break; |
|
case 2: /* ssl.ca-file */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
pconf->ssl_ca_file = cpv->v.v; |
|
break; |
|
case 3: /* ssl.ca-dn-file */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
pconf->ssl_ca_dn_file = cpv->v.v; |
|
break; |
|
case 4: /* ssl.ca-crl-file */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
pconf->ssl_ca_crl_file = cpv->v.v; |
|
break; |
|
case 5: /* ssl.read-ahead */ |
|
pconf->ssl_read_ahead = (0 != cpv->v.u); |
|
break; |
|
case 6: /* ssl.disable-client-renegotiation */ |
|
pconf->ssl_disable_client_renegotiation = (0 != cpv->v.u); |
|
break; |
|
case 7: /* ssl.verifyclient.activate */ |
|
pconf->ssl_verifyclient = (0 != cpv->v.u); |
|
break; |
|
case 8: /* ssl.verifyclient.enforce */ |
|
pconf->ssl_verifyclient_enforce = (0 != cpv->v.u); |
|
break; |
|
case 9: /* ssl.verifyclient.depth */ |
|
pconf->ssl_verifyclient_depth = (unsigned char)cpv->v.shrt; |
|
break; |
|
case 10:/* ssl.verifyclient.username */ |
|
pconf->ssl_verifyclient_username = cpv->v.b; |
|
break; |
|
case 11:/* ssl.verifyclient.exportcert */ |
|
pconf->ssl_verifyclient_export_cert = (0 != cpv->v.u); |
|
break; |
|
case 12:/* ssl.acme-tls-1 */ |
|
pconf->ssl_acme_tls_1 = cpv->v.b; |
|
break; |
|
case 13:/* debug.log-ssl-noise */ |
|
pconf->ssl_log_noise = (unsigned char)cpv->v.shrt; |
|
break; |
|
default:/* should not happen */ |
|
return; |
|
} |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) |
|
{ |
|
do { |
|
mod_gnutls_merge_config_cpv(pconf, cpv); |
|
} while ((++cpv)->k_id != -1); |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_patch_config (request_st * const r, plugin_config * const pconf) |
|
{ |
|
plugin_data * const p = plugin_data_singleton; |
|
memcpy(pconf, &p->defaults, sizeof(plugin_config)); |
|
for (int i = 1, used = p->nconfig; i < used; ++i) { |
|
if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) |
|
mod_gnutls_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]); |
|
} |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_verify_set_tlist (handler_ctx *hctx, int req) |
|
{ |
|
/* XXX GnuTLS interfaces appear to be better for client-side use than for |
|
* server-side use. If gnutls_x509_trust_list_t were available to attach |
|
* to a gnutls_session_t (without copying), then there would not be race |
|
* conditions swapping trust lists on the credential *shared* between |
|
* connections when ssl.ca-dn-file and ssl.ca-file are both set. If both |
|
* are set, current code attempts to set ssl.ca-dn-file right before sending |
|
* client cert request, and sets ssl.ca-file right before client cert verify |
|
* |
|
* Architecture would be cleaner if the trust list for verifying client cert |
|
* (gnutls_x509_trust_list_t) were available to attach to a gnutls_session_t |
|
* instead of attaching to a gnutls_certificate_credentials_t. |
|
*/ |
|
if (hctx->conf.pc->trust_inited) return GNUTLS_E_SUCCESS; |
|
|
|
gnutls_datum_t *d; |
|
int rc; |
|
|
|
/* set trust list using ssl_ca_dn_file, if set, for client cert request |
|
* (when req is true) (for CAs sent by server to client in cert request) |
|
* (trust is later replaced by ssl_ca_file for client cert verification) */ |
|
d = req && hctx->conf.ssl_ca_dn_file |
|
? hctx->conf.ssl_ca_dn_file |
|
: hctx->conf.ssl_ca_file; |
|
if (NULL == d) { |
|
log_error(hctx->r->conf.errh, __FILE__, __LINE__, |
|
"GnuTLS: can't verify client without ssl.ca-file " |
|
"for TLS server name %s", |
|
hctx->r->uri.authority.ptr); /*(might not be set yet if no SNI)*/ |
|
return GNUTLS_E_INTERNAL_ERROR; |
|
} |
|
|
|
gnutls_x509_trust_list_t tlist = NULL; |
|
rc = gnutls_x509_trust_list_init(&tlist, 0); |
|
if (rc < 0) { |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"gnutls_x509_trust_list_init()"); |
|
return rc; |
|
} |
|
|
|
gnutls_x509_crt_t *clist = (gnutls_x509_crt_t *)(void *)d->data; |
|
rc = gnutls_x509_trust_list_add_cas(tlist, clist, d->size, 0); |
|
if (rc < 0) { |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"gnutls_x509_trust_list_add_cas()"); |
|
gnutls_x509_trust_list_deinit(tlist, 0); |
|
return rc; |
|
} |
|
|
|
d = hctx->conf.ssl_ca_crl_file; |
|
if (NULL != d && req && hctx->conf.ssl_ca_dn_file) { |
|
/*(check req and ssl_ca_dn_file to see if tlist will be replaced later, |
|
* and, if so, defer setting crls until later)*/ |
|
gnutls_x509_crl_t *crl_list = (gnutls_x509_crl_t *)(void *)d->data; |
|
rc = gnutls_x509_trust_list_add_crls(tlist, crl_list, d->size, 0, 0); |
|
if (rc < 0) { |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"gnutls_x509_trust_list_add_crls()"); |
|
gnutls_x509_trust_list_deinit(tlist, 0); |
|
return rc; |
|
} |
|
} |
|
|
|
/* gnutls limitation; wasteful to have to copy into each cred */ |
|
/* (would be better to share list with session, instead of with cred) */ |
|
gnutls_certificate_credentials_t ssl_cred = hctx->conf.pc->ssl_cred; |
|
gnutls_certificate_set_trust_list(ssl_cred, tlist, 0); /* transfer tlist */ |
|
|
|
/* (must flip trust lists back and forth b/w DN names and verify CAs) */ |
|
if (NULL == hctx->conf.ssl_ca_dn_file) |
|
hctx->conf.pc->trust_inited = 1; |
|
|
|
return GNUTLS_E_SUCCESS; |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_verify_cb (gnutls_session_t ssl) |
|
{ |
|
handler_ctx * const hctx = gnutls_session_get_ptr(ssl); |
|
if (!hctx->conf.ssl_verifyclient) return 0; |
|
|
|
if (gnutls_auth_client_get_type(ssl) != GNUTLS_CRD_CERTIFICATE) |
|
return GNUTLS_E_SUCCESS; |
|
|
|
int rc; |
|
|
|
/* gnutls limitation; wasteful to have to copy into each cred */ |
|
/* (would be better to share list with session, instead of with cred) */ |
|
if (hctx->conf.ssl_ca_dn_file) { |
|
rc = mod_gnutls_verify_set_tlist(hctx, 0); /* for client cert verify */ |
|
if (rc < 0) return rc; |
|
} |
|
|
|
/* gnutls_certificate_verify_peers2() includes processing OCSP staping, |
|
* as well as certificate depth verification before getting internal |
|
* flags and calling gnutls_x509_trust_list_verify_crt2() |
|
* advanced reference: |
|
* gnutls lib/cert-sessions.c:_gnutls_x509_cert_verify_peers() |
|
* XXX: if GnuTLS provided a more advanced interface which permitted |
|
* providing trust list, verify depth, and flags, we could avoid copying |
|
* ca chain and crls into each credential, using |
|
* gnutls_x509_trust_list_add_cas() |
|
* gnutls_x509_trust_list_add_crls() |
|
* gnutls_x509_trust_list_verify_crt2() |
|
* See also GnuTLS manual Section 7.3.4 Advanced certificate verification |
|
*/ |
|
|
|
rc = gnutls_certificate_verify_peers2(ssl, &hctx->verify_status); |
|
if (rc < 0) return rc; |
|
|
|
if (hctx->verify_status == 0 && hctx->conf.ssl_ca_dn_file) { |
|
/* verify that client cert is issued by CA in ssl.ca-dn-file |
|
* if both ssl.ca-dn-file and ssl.ca-file were configured */ |
|
gnutls_x509_crt_t *CA_list = |
|
(gnutls_x509_crt_t *)&hctx->conf.ssl_ca_dn_file->data; |
|
unsigned int len = hctx->conf.ssl_ca_dn_file->size; |
|
unsigned int i; |
|
gnutls_x509_dn_t issuer, subject; |
|
unsigned int crt_size = 0; |
|
const gnutls_datum_t *crts; |
|
gnutls_x509_crt_t crt = NULL; |
|
crts = gnutls_certificate_get_peers(ssl, &crt_size); |
|
if (0 == crt_size |
|
|| gnutls_x509_crt_init(&crt) < 0 |
|
|| gnutls_x509_crt_import(crt, &crts[0], GNUTLS_X509_FMT_DER) < 0 |
|
|| gnutls_x509_crt_get_subject(crt, &issuer) < 0) |
|
len = 0; /*(trigger failure path below)*/ |
|
for (i = 0; i < len; ++i) { |
|
if (gnutls_x509_crt_get_subject(CA_list[i], &subject) >= 0 |
|
&& 0 == memcmp(&subject, &issuer, sizeof(issuer))) |
|
break; |
|
} |
|
if (i == len) |
|
hctx->verify_status |= GNUTLS_CERT_SIGNER_NOT_CA; |
|
if (crt) gnutls_x509_crt_deinit(crt); |
|
} |
|
|
|
if (hctx->verify_status != 0 && hctx->conf.ssl_verifyclient_enforce) { |
|
/* (not looping on GNUTLS_E_INTERRUPTED or GNUTLS_E_AGAIN |
|
* since we do not want to block here (and not expecting to have to))*/ |
|
(void)gnutls_alert_send(ssl, GNUTLS_AL_FATAL, GNUTLS_A_ACCESS_DENIED); |
|
return GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR; |
|
} |
|
|
|
return GNUTLS_E_SUCCESS; |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_construct_crt_chain (plugin_cert *pc, gnutls_datum_t *d, log_error_st *errh) |
|
{ |
|
/* Historically, openssl will use the cert chain in (SSL_CTX *) if a cert |
|
* does not have a chain configured in (SSL *). Attempt to provide |
|
* compatible behavior here. This may be called after startup since |
|
* ssl.pemfile might be defined in a config scope without ssl.ca-file, |
|
* and at runtime is when ssl.ca-file (e.g. from matching $SERVER["socket"]) |
|
* would be known along with ssl.pemfile (e.g. from $HTTP["host"]) */ |
|
|
|
gnutls_certificate_credentials_t ssl_cred; |
|
int rc = gnutls_certificate_allocate_credentials(&ssl_cred); |
|
if (rc < 0) return rc; |
|
unsigned int ncrt = (d ? d->size : 0); |
|
unsigned int off = (d == pc->ssl_pemfile_x509) ? 0 : 1; |
|
gnutls_pcert_st * const pcert_list = |
|
gnutls_malloc(sizeof(gnutls_pcert_st) * (off+ncrt)); |
|
if (NULL == pcert_list) { |
|
gnutls_certificate_free_credentials(ssl_cred); |
|
return GNUTLS_E_MEMORY_ERROR; |
|
} |
|
memset(pcert_list, 0, sizeof(gnutls_pcert_st) * (off+ncrt)); |
|
rc = 0; |
|
|
|
if (off) { /*(add crt to chain if different from d)*/ |
|
/*assert(pc->ssl_pemfile_x509->size == 1)*/ |
|
gnutls_x509_crt_t *crts = |
|
(gnutls_x509_crt_t *)(void *)pc->ssl_pemfile_x509->data; |
|
rc = gnutls_pcert_import_x509(pcert_list, crts[0], 0); |
|
} |
|
|
|
if (0 == rc && ncrt) { |
|
gnutls_x509_crt_t *crts = (gnutls_x509_crt_t *)(void *)d->data; |
|
#if GNUTLS_VERSION_NUMBER < 0x030400 |
|
/*(GNUTLS_X509_CRT_LIST_SORT not needed; crts sorted when file read)*/ |
|
rc = gnutls_pcert_import_x509_list(pcert_list+off, crts, &ncrt, 0); |
|
#else /* manually import list, but note that sorting is not implemented */ |
|
rc = 0; |
|
for (unsigned int i = 0; i < ncrt; ++i) { |
|
rc = gnutls_pcert_import_x509(pcert_list+off+i, crts[i], 0); |
|
if (rc < 0) break; |
|
} |
|
#endif |
|
} |
|
ncrt += off; |
|
if (0 == rc) |
|
rc = gnutls_certificate_set_key(ssl_cred, NULL, 0, pcert_list, ncrt, |
|
pc->ssl_pemfile_pkey); |
|
if (rc < 0) { |
|
for (unsigned int i = 0; i < ncrt; ++i) |
|
gnutls_pcert_deinit(pcert_list+i); |
|
gnutls_free(pcert_list); |
|
gnutls_certificate_free_credentials(ssl_cred); |
|
elog(errh, __FILE__, __LINE__, rc, "gnutls_certificate_set_key()"); |
|
return rc; |
|
} |
|
/* XXX: gnutls_certificate_set_key() has an inconsistent implementation. |
|
* On success, key ownership is transferred, and so should not be freed, |
|
* but pcert_list is a shallow memcpy(), so gnutls_pcert_deinit() should |
|
* not be run on gnutls_pcert_st in list, though top-level list storage |
|
* should be freed. On failure, ownership is not transferred for either. */ |
|
gnutls_free(pcert_list); |
|
pc->ssl_pemfile_pkey = NULL; |
|
pc->ssl_cred = ssl_cred; |
|
|
|
/* release lists used to configure pc->ssl_cred */ |
|
mod_gnutls_free_config_crts(pc->ssl_pemfile_x509); |
|
pc->ssl_pemfile_x509 = NULL; |
|
|
|
return 0; |
|
} |
|
|
|
|
|
static void * |
|
network_gnutls_load_pemfile (server *srv, const buffer *pemfile, const buffer *privkey) |
|
{ |
|
#if 0 /* see comments in mod_gnutls_construct_crt_chain() above */ |
|
|
|
gnutls_certificate_credentials_t ssl_cred = NULL; |
|
int rc; |
|
|
|
rc = gnutls_certificate_allocate_credentials(&ssl_cred); |
|
if (rc < 0) return NULL; |
|
|
|
rc = gnutls_certificate_set_x509_key_file2(ssl_cred, |
|
pemfile->ptr, privkey->ptr, |
|
GNUTLS_X509_FMT_PEM, NULL, 0); |
|
if (rc < 0) { |
|
elogf(srv->errh, __FILE__, __LINE__, rc, |
|
"gnutls_certificate_set_x509_key_file2(%s, %s)", |
|
pemfile->ptr, privkey->ptr); |
|
gnutls_certificate_free_credentials(ssl_cred); |
|
return NULL; |
|
} |
|
|
|
plugin_cert *pc = malloc(sizeof(plugin_cert)); |
|
force_assert(pc); |
|
pc->ssl_cred = ssl_cred; |
|
pc->trust_inited = 0; |
|
|
|
return pc; |
|
|
|
#else |
|
|
|
gnutls_datum_t *d = mod_gnutls_load_config_crts(pemfile->ptr, srv->errh); |
|
if (NULL == d) return NULL; |
|
if (0 == d->size) { |
|
mod_gnutls_free_config_crts(d); |
|
return NULL; |
|
} |
|
|
|
gnutls_privkey_t pkey = mod_gnutls_load_config_pkey(privkey->ptr,srv->errh); |
|
if (NULL == pkey) { |
|
mod_gnutls_free_config_crts(d); |
|
return NULL; |
|
} |
|
|
|
plugin_cert *pc = malloc(sizeof(plugin_cert)); |
|
force_assert(pc); |
|
pc->ssl_cred = NULL; |
|
pc->trust_inited = 0; |
|
pc->ssl_pemfile_x509 = d; |
|
pc->ssl_pemfile_pkey = pkey; |
|
|
|
if (d->size > 1) { /*(certificate chain provided)*/ |
|
int rc = mod_gnutls_construct_crt_chain(pc, d, srv->errh); |
|
if (rc < 0) { |
|
mod_gnutls_free_config_crts(d); |
|
gnutls_privkey_deinit(pkey); |
|
free(pc); |
|
return NULL; |
|
} |
|
} |
|
|
|
return pc; |
|
|
|
#endif |
|
} |
|
|
|
|
|
#if GNUTLS_VERSION_NUMBER >= 0x030200 |
|
|
|
static int |
|
mod_gnutls_acme_tls_1 (handler_ctx *hctx) |
|
{ |
|
buffer * const b = hctx->tmp_buf; |
|
const buffer * const name = &hctx->r->uri.authority; |
|
log_error_st * const errh = hctx->r->conf.errh; |
|
int rc = GNUTLS_E_INVALID_REQUEST; |
|
|
|
/* check if acme-tls/1 protocol is enabled (path to dir of cert(s) is set)*/ |
|
if (buffer_string_is_empty(hctx->conf.ssl_acme_tls_1)) |
|
return 0; /*(should not happen)*/ |
|
buffer_copy_buffer(b, hctx->conf.ssl_acme_tls_1); |
|
buffer_append_slash(b); |
|
|
|
/* check if SNI set server name (required for acme-tls/1 protocol) |
|
* and perform simple path checks for no '/' |
|
* and no leading '.' (e.g. ignore "." or ".." or anything beginning '.') */ |
|
if (buffer_string_is_empty(name)) return rc; |
|
if (NULL != strchr(name->ptr, '/')) return rc; |
|
if (name->ptr[0] == '.') return rc; |
|
#if 0 |
|
if (0 != http_request_host_policy(name, hctx->r->conf.http_parseopts, 443)) |
|
return rc; |
|
#endif |
|
buffer_append_string_buffer(b, name); |
|
|
|
#if 0 |
|
|
|
buffer *privkey = buffer_init_buffer(b); |
|
buffer_append_string_len(b, CONST_STR_LEN(".crt.pem")); |
|
buffer_append_string_len(privkey, CONST_STR_LEN(".key.pem")); |
|
|
|
/*(similar to network_gnutls_load_pemfile() but propagates rc)*/ |
|
gnutls_certificate_credentials_t ssl_cred = NULL; |
|
rc = gnutls_certificate_allocate_credentials(&ssl_cred); |
|
if (rc < 0) { buffer_free(privkey); return rc; } |
|
rc = gnutls_certificate_set_x509_key_file2(ssl_cred, |
|
b->ptr, privkey->ptr, |
|
GNUTLS_X509_FMT_PEM, NULL, 0); |
|
if (rc < 0) { |
|
elogf(errh, __FILE__, __LINE__, rc, |
|
"failed to load acme-tls/1 cert (%s, %s)", |
|
b->ptr, privkey->ptr); |
|
buffer_free(privkey); |
|
gnutls_certificate_free_credentials(ssl_cred); |
|
return rc; |
|
} |
|
buffer_free(privkey); |
|
|
|
#else |
|
|
|
/* gnutls_certificate_set_x509_key_file2() does not securely wipe |
|
* sensitive data from memory, so take a few extra steps */ |
|
|
|
/* similar to network_gnutls_load_pemfile() */ |
|
|
|
uint32_t len = buffer_string_length(b); |
|
buffer_append_string_len(b, CONST_STR_LEN(".crt.pem")); |
|
|
|
gnutls_datum_t *d = mod_gnutls_load_config_crts(b->ptr, errh); |
|
if (NULL == d) return GNUTLS_E_FILE_ERROR; |
|
if (0 == d->size) { |
|
mod_gnutls_free_config_crts(d); |
|
return GNUTLS_E_FILE_ERROR; |
|
} |
|
|
|
buffer_string_set_length(b, len); |
|
buffer_append_string_len(b, CONST_STR_LEN(".key.pem")); |
|
|
|
gnutls_privkey_t pkey = mod_gnutls_load_config_pkey(b->ptr, errh); |
|
if (NULL == pkey) { |
|
mod_gnutls_free_config_crts(d); |
|
return GNUTLS_E_FILE_ERROR; |
|
} |
|
|
|
plugin_cert pc; |
|
pc.ssl_cred = NULL; |
|
pc.trust_inited = 0; |
|
pc.ssl_pemfile_x509 = d; |
|
pc.ssl_pemfile_pkey = pkey; |
|
|
|
rc = mod_gnutls_construct_crt_chain(&pc, d, errh); |
|
if (rc < 0) { |
|
mod_gnutls_free_config_crts(d); |
|
gnutls_privkey_deinit(pkey); |
|
return rc; |
|
} |
|
|
|
gnutls_certificate_credentials_t ssl_cred = pc.ssl_cred; |
|
|
|
#endif |
|
|
|
hctx->acme_tls_1_cred = ssl_cred; /*(save ptr and free later)*/ |
|
|
|
rc = gnutls_credentials_set(hctx->ssl, GNUTLS_CRD_CERTIFICATE, ssl_cred); |
|
if (rc < 0) { |
|
elogf(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"failed to set acme-tls/1 certificate for TLS server name %s", |
|
hctx->r->uri.authority.ptr); |
|
return rc; |
|
} |
|
|
|
return GNUTLS_E_SUCCESS; /* 0 */ |
|
} |
|
|
|
|
|
enum { |
|
MOD_GNUTLS_ALPN_HTTP11 = 1 |
|
,MOD_GNUTLS_ALPN_HTTP10 = 2 |
|
,MOD_GNUTLS_ALPN_H2 = 3 |
|
,MOD_GNUTLS_ALPN_ACME_TLS_1 = 4 |
|
}; |
|
|
|
|
|
static int |
|
mod_gnutls_alpn_select_cb (gnutls_session_t ssl, handler_ctx *hctx) |
|
{ |
|
const int i = 0; |
|
uint8_t proto; |
|
|
|
gnutls_datum_t d = { NULL, 0 }; |
|
if (gnutls_alpn_get_selected_protocol(ssl, &d) < 0) |
|
return 0; /* ignore GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE */ |
|
const char *in = (const char *)d.data; |
|
const int n = (int)d.size; |
|
|
|
switch (n) { |
|
#if 0 |
|
case 2: /* "h2" */ |
|
if (in[i] == 'h' && in[i+1] == '2') { |
|
proto = MOD_GNUTLS_ALPN_H2; |
|
break; |
|
} |
|
return 0; |
|
#endif |
|
case 8: /* "http/1.1" "http/1.0" */ |
|
if (0 == memcmp(in+i, "http/1.", 7)) { |
|
if (in[i+7] == '1') { |
|
proto = MOD_GNUTLS_ALPN_HTTP11; |
|
break; |
|
} |
|
if (in[i+7] == '0') { |
|
proto = MOD_GNUTLS_ALPN_HTTP10; |
|
break; |
|
} |
|
} |
|
return 0; |
|
case 10: /* "acme-tls/1" */ |
|
if (0 == memcmp(in+i, "acme-tls/1", 10)) { |
|
int rc = mod_gnutls_acme_tls_1(hctx); |
|
if (0 == rc) { |
|
proto = MOD_GNUTLS_ALPN_ACME_TLS_1; |
|
break; |
|
} |
|
return rc; |
|
} |
|
return 0; |
|
default: |
|
return 0; |
|
} |
|
|
|
hctx->alpn = proto; |
|
return 0; |
|
} |
|
|
|
#endif /* GNUTLS_VERSION_NUMBER >= 0x030200 */ |
|
|
|
|
|
static int |
|
mod_gnutls_SNI(void *ctx, unsigned int tls_id, |
|
const unsigned char *servername, unsigned int len) |
|
{ |
|
if (tls_id != 0) return 0; |
|
|
|
/* server name */ |
|
|
|
/* https://www.gnutls.org/manual/gnutls.html#Virtual-hosts-and-credentials |
|
* figure the advertized name - the following hack relies on the fact that |
|
* this extension only supports DNS names, and due to a protocol bug cannot |
|
* be extended to support anything else. */ |
|
if (len < 5) return 0; |
|
len -= 5; |
|
servername += 5; |
|
handler_ctx * const hctx = (handler_ctx *) ctx; |
|
request_st * const r = hctx->r; |
|
buffer_copy_string(&r->uri.scheme, "https"); |
|
|
|
if (len >= 1024) { /*(expecting < 256; TLSEXT_MAXLEN_host_name is 255)*/ |
|
log_error(r->conf.errh, __FILE__, __LINE__, |
|
"GnuTLS: SNI name too long %.*s", (int)len, servername); |
|
return GNUTLS_E_INVALID_REQUEST; |
|
} |
|
|
|
/* use SNI to patch mod_gnutls config and then reset COMP_HTTP_HOST */ |
|
buffer_copy_string_len(&r->uri.authority, (const char *)servername, len); |
|
buffer_to_lower(&r->uri.authority); |
|
#if 0 |
|
/*(r->uri.authority used below for configuration before request read; |
|
* revisit for h2)*/ |
|
if (0 != http_request_host_policy(&r->uri.authority, |
|
r->conf.http_parseopts, 443)) |
|
return GNUTLS_E_INVALID_REQUEST; |
|
#endif |
|
|
|
r->conditional_is_valid |= (1 << COMP_HTTP_SCHEME) |
|
| (1 << COMP_HTTP_HOST); |
|
|
|
mod_gnutls_patch_config(r, &hctx->conf); |
|
/* reset COMP_HTTP_HOST so that conditions re-run after request hdrs read */ |
|
/*(done in configfile-glue.c:config_cond_cache_reset() after request hdrs read)*/ |
|
/*config_cond_cache_reset_item(r, COMP_HTTP_HOST);*/ |
|
/*buffer_clear(&r->uri.authority);*/ |
|
|
|
return 0; |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_client_hello_hook_post(gnutls_session_t ssl) |
|
{ |
|
#if GNUTLS_VERSION_NUMBER >= 0x030200 |
|
handler_ctx * const hctx = gnutls_session_get_ptr(ssl); |
|
int rc = mod_gnutls_alpn_select_cb(ssl, hctx); |
|
if (rc < 0) |
|
return rc; |
|
|
|
/* check if acme-tls/1 protocol is enabled (path to dir of cert(s) is set)*/ |
|
if (hctx->alpn == MOD_GNUTLS_ALPN_ACME_TLS_1) { |
|
/*(acme-tls/1 is separate from certificate auth access to website)*/ |
|
gnutls_certificate_server_set_request(ssl, GNUTLS_CERT_IGNORE); |
|
return GNUTLS_E_SUCCESS; /* 0 */ /*(skip further session config below)*/ |
|
} |
|
#endif |
|
|
|
return GNUTLS_E_SUCCESS; /* 0 */ |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_client_hello_hook(gnutls_session_t ssl, unsigned int htype, |
|
unsigned when, unsigned int incoming, |
|
const gnutls_datum_t *msg) |
|
{ |
|
/*assert(htype == GNUTLS_HANDSHAKE_CLIENT_HELLO);*/ |
|
UNUSED(htype); |
|
UNUSED(incoming); |
|
|
|
if (when == GNUTLS_HOOK_POST) |
|
return mod_gnutls_client_hello_hook_post(ssl); |
|
|
|
handler_ctx * const hctx = gnutls_session_get_ptr(ssl); |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
gnutls_dh_params_t dh_params = hctx->conf.dh_params; |
|
#endif |
|
int rc = gnutls_ext_raw_parse(hctx, mod_gnutls_SNI, msg, |
|
GNUTLS_EXT_RAW_FLAG_TLS_CLIENT_HELLO); |
|
if (rc < 0) { |
|
log_error_st *errh = hctx->r->conf.errh; |
|
elog(errh, __FILE__, __LINE__, rc, "gnutls_ext_raw_parse()"); |
|
return rc; |
|
} |
|
|
|
#if GNUTLS_VERSION_NUMBER >= 0x030200 |
|
/* https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids */ |
|
static const gnutls_datum_t alpn_protos_http_acme[] = { |
|
{ (unsigned char *)CONST_STR_LEN("http/1.1") } |
|
,{ (unsigned char *)CONST_STR_LEN("http/1.0") } |
|
,{ (unsigned char *)CONST_STR_LEN("acme-tls/1") } |
|
}; |
|
unsigned int n = !buffer_string_is_empty(hctx->conf.ssl_acme_tls_1) ? 3 : 2; |
|
/*unsigned int flags = GNUTLS_ALPN_SERVER_PRECEDENCE;*/ |
|
rc = gnutls_alpn_set_protocols(hctx->ssl, alpn_protos_http_acme, n, 0); |
|
if (rc < 0) { |
|
log_error_st *errh = hctx->r->conf.errh; |
|
elog(errh, __FILE__, __LINE__, rc, "gnutls_alpn_set_protocols()"); |
|
return rc; |
|
} |
|
#if 0 |
|
/* XXX: might have to manually process client hello for ALPN "acme-tls/1" |
|
* and handle here (GNUTLS_HOOK_PRE) before setting certificate */ |
|
if (!buffer_string_is_empty(hctx->conf.ssl_acme_tls_1)) { |
|
} |
|
#endif |
|
#endif |
|
|
|
#if 0 /* must enable before GnuTLS client hello hook */ |
|
/* GnuTLS returns an error here if TLSv1.3 (? key already set ?) */ |
|
/* see mod_gnutls_handle_con_accept() */ |
|
if (hctx->ssl_session_ticket && session_ticket_key.size) { |
|
rc = gnutls_session_ticket_enable_server(ssl, &session_ticket_key); |
|
if (rc < 0) { |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"gnutls_session_ticket_enable_server()"); |
|
return rc; |
|
} |
|
} |
|
#endif |
|
|
|
if (NULL == hctx->conf.pc->ssl_cred) { |
|
rc = mod_gnutls_construct_crt_chain(hctx->conf.pc, |
|
hctx->conf.ssl_ca_file, |
|
hctx->r->conf.errh); |
|
if (rc < 0) return rc; |
|
} |
|
|
|
gnutls_certificate_credentials_t ssl_cred = hctx->conf.pc->ssl_cred; |
|
|
|
hctx->verify_status = ~0u; |
|
gnutls_certificate_request_t req = GNUTLS_CERT_IGNORE; |
|
if (hctx->conf.ssl_verifyclient) { |
|
/*(cred shared; settings should be consistent across site using cred)*/ |
|
/*(i.e. settings for client certs must not differ under this cred)*/ |
|
req = hctx->conf.ssl_verifyclient_enforce |
|
? GNUTLS_CERT_REQUIRE |
|
: GNUTLS_CERT_REQUEST; |
|
gnutls_certificate_set_verify_function(ssl_cred, mod_gnutls_verify_cb); |
|
gnutls_certificate_set_verify_limits(ssl_cred, 8200 /*(default)*/, |
|
hctx->conf.ssl_verifyclient_depth); |
|
rc = mod_gnutls_verify_set_tlist(hctx, 1); /* for client cert request */ |
|
if (rc < 0) return rc; |
|
} |
|
gnutls_certificate_server_set_request(ssl, req); |
|
|
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
if (dh_params) |
|
gnutls_certificate_set_dh_params(ssl_cred, dh_params); |
|
#if GNUTLS_VERSION_NUMBER >= 0x030506 |
|
else |
|
gnutls_certificate_set_known_dh_params(ssl_cred, GNUTLS_SEC_PARAM_HIGH); |
|
#endif |
|
#endif |
|
|
|
rc = gnutls_credentials_set(ssl, GNUTLS_CRD_CERTIFICATE, ssl_cred); |
|
if (rc < 0) { |
|
elogf(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"failed to set SNI certificate for TLS server name %s", |
|
hctx->r->uri.authority.ptr); |
|
return rc; |
|
} |
|
|
|
return GNUTLS_E_SUCCESS; /* 0 */ |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_ssl_conf_ciphersuites (server *srv, plugin_config_socket *s, buffer *ciphersuites, const buffer *cipherstring); |
|
|
|
|
|
static int |
|
mod_gnutls_ssl_conf_curves(server *srv, plugin_config_socket *s, const buffer *curvelist); |
|
|
|
|
|
static void |
|
mod_gnutls_ssl_conf_proto (server *srv, plugin_config_socket *s, const buffer *minb, const buffer *maxb); |
|
|
|
|
|
static int |
|
mod_gnutls_ssl_conf_cmd (server *srv, plugin_config_socket *s) |
|
{ |
|
/* reference: |
|
* https://www.openssl.org/docs/man1.1.1/man3/SSL_CONF_cmd.html */ |
|
int rc = 0; |
|
buffer *cipherstring = NULL; |
|
buffer *ciphersuites = NULL; |
|
buffer *minb = NULL; |
|
buffer *maxb = NULL; |
|
buffer *curves = NULL; |
|
|
|
for (size_t i = 0; i < s->ssl_conf_cmd->used; ++i) { |
|
data_string *ds = (data_string *)s->ssl_conf_cmd->data[i]; |
|
if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("CipherString"))) |
|
cipherstring = &ds->value; |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Ciphersuites"))) |
|
ciphersuites = &ds->value; |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Curves")) |
|
|| buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Groups"))) |
|
curves = &ds->value; |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("MaxProtocol"))) |
|
maxb = &ds->value; |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("MinProtocol"))) |
|
minb = &ds->value; |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Protocol"))) { |
|
/* openssl config for Protocol=... is complex and deprecated */ |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ssl.openssl.ssl-conf-cmd %s ignored; " |
|
"use MinProtocol=... and MaxProtocol=... instead", |
|
ds->key.ptr); |
|
} |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("Options"))) { |
|
for (char *v = ds->value.ptr, *e; *v; v = e) { |
|
while (*v == ' ' || *v == '\t' || *v == ',') ++v; |
|
int flag = 1; |
|
if (*v == '-') { |
|
flag = 0; |
|
++v; |
|
} |
|
for (e = v; light_isalpha(*e); ++e) ; |
|
switch ((int)(e-v)) { |
|
case 11: |
|
if (buffer_eq_icase_ssn(v, "Compression", 11)) { |
|
/* GnuTLS 3.6.0+ no longer implements |
|
* any support for compression */ |
|
if (!flag) continue; |
|
} |
|
break; |
|
case 13: |
|
if (buffer_eq_icase_ssn(v, "SessionTicket", 13)) { |
|
/*(translates to "%NO_TICKETS" priority str if !flag)*/ |
|
s->ssl_session_ticket = flag; |
|
continue; |
|
} |
|
break; |
|
case 16: |
|
if (buffer_eq_icase_ssn(v, "ServerPreference", 16)) { |
|
/*(translates to "%SERVER_PRECEDENCE" priority string)*/ |
|
s->ssl_honor_cipher_order = flag; |
|
continue; |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
/* warn if not explicitly handled or ignored above */ |
|
if (!flag) --v; |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ssl.openssl.ssl-conf-cmd Options %.*s " |
|
"ignored", (int)(e-v), v); |
|
} |
|
} |
|
#if 0 |
|
else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("..."))) { |
|
} |
|
#endif |
|
else { |
|
/* warn if not explicitly handled or ignored above */ |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ssl.openssl.ssl-conf-cmd %s ignored", |
|
ds->key.ptr); |
|
} |
|
} |
|
|
|
if (minb || maxb) /*(if at least one was set)*/ |
|
mod_gnutls_ssl_conf_proto(srv, s, minb, maxb); |
|
|
|
if (!mod_gnutls_ssl_conf_ciphersuites(srv, s, ciphersuites, cipherstring)) |
|
rc = -1; |
|
|
|
if (curves) { |
|
if (buffer_string_is_empty(s->ssl_ec_curve)) |
|
buffer_append_string_len(&s->priority_str, |
|
CONST_STR_LEN("-CURVE-ALL:")); |
|
if (!mod_gnutls_ssl_conf_curves(srv, s, curves)) |
|
rc = -1; |
|
} |
|
|
|
return rc; |
|
} |
|
|
|
|
|
static int |
|
network_init_ssl (server *srv, plugin_config_socket *s, plugin_data *p) |
|
{ |
|
int rc; |
|
UNUSED(p); |
|
|
|
/* construct GnuTLS "priority" string |
|
* |
|
* XXX: might make config option to allow user to define priority string |
|
* (which would short-circuit any related config directives) |
|
* |
|
* default: NORMAL (since GnuTLS 3.3.0, could also use NULL for defaults) |
|
* SUITEB128 and SUITEB192 are stricter than NORMAL |
|
* (and are attempted to be supported in mod_gnutls_ssl_conf_ciphersuites()) |
|
* |
|
* gnutls defaults to %PARTIAL_RENEGOTIATION (see manual) |
|
* mod_gnutls does not attempt to support |
|
* hctx->conf.ssl_disable_client_renegotiation == 0 |
|
* though possible with %UNSAFE_RENEGOTATION |
|
*/ |
|
|
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
/* GnuTLS 3.6.0+ no longer implements any support for compression, |
|
* but we still explicitly disable for earlier versions */ |
|
buffer_append_string_len(&s->priority_str, |
|
CONST_STR_LEN("-COMP_ALL:+COMP-NULL:")); |
|
#endif |
|
|
|
if (!buffer_string_is_empty(s->ssl_cipher_list)) { |
|
if (!mod_gnutls_ssl_conf_ciphersuites(srv,s,NULL,s->ssl_cipher_list)) |
|
return -1; |
|
} |
|
|
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
if (!buffer_string_is_empty(s->ssl_dh_file)) { |
|
/* "Prior to GnuTLS 3.6.0 for the ephemeral or anonymous Diffie-Hellman |
|
* (DH) TLS ciphersuites the application was required to generate or |
|
* provide DH parameters. That is no longer necessary as GnuTLS utilizes |
|
* DH parameters and negotiation from [RFC7919]." |
|
*/ |
|
/* In other words, the following should not be used in 3.6.0 or later: |
|
* gnutls_certificate_set_dh_params() |
|
* gnutls_certificate_set_known_dh_params() |
|
* However, if support is implemented in mod_gnutls, must also free in |
|
* mod_gnutls_free_config() as gnutls_certificate_free_credentials() |
|
* does not free RSA or DH params manually associated with credential */ |
|
gnutls_datum_t f = { NULL, 0 }; |
|
rc = gnutls_dh_params_init(&s->dh_params); |
|
if (rc < 0) return -1; |
|
rc = mod_gnutls_load_file(s->ssl_dh_file->ptr, &f, srv->errh); |
|
if (rc < 0) return -1; |
|
rc = gnutls_dh_params_import_pkcs3(s->dh_params,&f,GNUTLS_X509_FMT_PEM); |
|
mod_gnutls_datum_wipe(&f); |
|
if (rc < 0) { |
|
elogf(srv->errh, __FILE__, __LINE__, rc, |
|
"gnutls_dh_params_import_pkcs3() %s", s->ssl_dh_file->ptr); |
|
return -1; |
|
} |
|
} |
|
else { |
|
#if GNUTLS_VERSION_NUMBER < 0x030506 |
|
/* (this might take a while; you should upgrade to newer gnutls) */ |
|
rc = gnutls_dh_params_init(&s->dh_params); |
|
if (rc < 0) return -1; |
|
unsigned int bits = |
|
gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_HIGH); |
|
rc = gnutls_dh_params_generate2(s->dh_params, bits); |
|
if (rc < 0) { |
|
elogf(srv->errh, __FILE__, __LINE__, rc, |
|
"gnutls_dh_params_generate2() %s", s->ssl_dh_file->ptr); |
|
return -1; |
|
} |
|
#endif |
|
} |
|
#endif /* GNUTLS_VERSION_NUMBER < 0x030600 */ |
|
|
|
if (!buffer_string_is_empty(s->ssl_ec_curve)) { |
|
buffer_append_string_len(&s->priority_str, |
|
CONST_STR_LEN("-CURVE-ALL:")); |
|
if (!mod_gnutls_ssl_conf_curves(srv, s, s->ssl_ec_curve)) |
|
return -1; |
|
} |
|
|
|
if (s->ssl_conf_cmd && s->ssl_conf_cmd->used) { |
|
if (0 != mod_gnutls_ssl_conf_cmd(srv, s)) return -1; |
|
} |
|
|
|
if (s->ssl_honor_cipher_order) |
|
buffer_append_string_len(&s->priority_str, |
|
CONST_STR_LEN("%SERVER_PRECEDENCE:")); |
|
|
|
if (!s->ssl_session_ticket) |
|
buffer_append_string_len(&s->priority_str, |
|
CONST_STR_LEN("%NO_TICKETS:")); |
|
|
|
if (!s->ssl_use_sslv3 && !s->ssl_use_sslv2 |
|
&& NULL == strstr(s->priority_str.ptr, "-VERS-ALL:")) |
|
mod_gnutls_ssl_conf_proto(srv, s, NULL, NULL); |
|
|
|
/* explicitly disable SSLv3 unless enabled in config |
|
* (gnutls library would also need to be compiled with legacy support) */ |
|
if (!s->ssl_use_sslv3) |
|
buffer_append_string_len(&s->priority_str, |
|
CONST_STR_LEN("-VERS-SSL3.0:")); |
|
|
|
/* gnutls_priority_init2() is available since GnuTLS 3.6.3 and could be |
|
* called once with s->priority_base, and a second time with s->priority_str |
|
* and GNUTLS_PRIORITY_INIT_DEF_APPEND, but preserve compat with earlier |
|
* GnuTLS by concatenating into a single priority string */ |
|
|
|
buffer *b = srv->tmp_buf; |
|
if (NULL == s->priority_base) s->priority_base = "NORMAL"; |
|
buffer_copy_string_len(b, s->priority_base, strlen(s->priority_base)); |
|
if (!buffer_string_is_empty(&s->priority_str)) { |
|
buffer_append_string_len(b, CONST_STR_LEN(":")); |
|
uint32_t len = buffer_string_length(&s->priority_str); |
|
if (s->priority_str.ptr[len-1] == ':') |
|
--len; /* remove trailing ':' */ |
|
buffer_append_string_len(b, s->priority_str.ptr, len); |
|
} |
|
const char *err_pos; |
|
rc = gnutls_priority_init(&s->priority_cache, b->ptr, &err_pos); |
|
if (rc < 0) { |
|
elogf(srv->errh, __FILE__, __LINE__, rc, |
|
"gnutls_priority_init() error near %s", err_pos); |
|
return -1; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_set_defaults_sockets(server *srv, plugin_data *p) |
|
{ |
|
static const config_plugin_keys_t cpk[] = { |
|
{ CONST_STR_LEN("ssl.engine"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.cipher-list"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.honor-cipher-order"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.dh-file"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.ec-curve"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.openssl.ssl-conf-cmd"), |
|
T_CONFIG_ARRAY_KVSTRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.pemfile"), /* included to process global scope */ |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.empty-fragments"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.use-sslv2"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.use-sslv3"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ NULL, 0, |
|
T_CONFIG_UNSET, |
|
T_CONFIG_SCOPE_UNSET } |
|
}; |
|
static const buffer default_ssl_cipher_list = { CONST_STR_LEN("HIGH"), 0 }; |
|
|
|
p->ssl_ctxs = calloc(srv->config_context->used, sizeof(plugin_ssl_ctx)); |
|
force_assert(p->ssl_ctxs); |
|
|
|
int rc = HANDLER_GO_ON; |
|
plugin_data_base srvplug; |
|
memset(&srvplug, 0, sizeof(srvplug)); |
|
plugin_data_base * const ps = &srvplug; |
|
if (!config_plugin_values_init(srv, ps, cpk, "mod_gnutls")) |
|
return HANDLER_ERROR; |
|
|
|
plugin_config_socket defaults; |
|
memset(&defaults, 0, sizeof(defaults)); |
|
defaults.ssl_honor_cipher_order = 1; /* default server preference for PFS */ |
|
defaults.ssl_session_ticket = 0; /* disabled by default */ |
|
defaults.ssl_cipher_list = &default_ssl_cipher_list; |
|
|
|
/* process and validate config directives for global and $SERVER["socket"] |
|
* (init i to 0 if global context; to 1 to skip empty global context) */ |
|
for (int i = !ps->cvlist[0].v.u2[1]; i < ps->nconfig; ++i) { |
|
config_cond_info cfginfo; |
|
config_get_config_cond_info(&cfginfo, (uint32_t)ps->cvlist[i].k_id); |
|
int is_socket_scope = (0 == i || cfginfo.comp == COMP_SERVER_SOCKET); |
|
int count_not_engine = 0; |
|
|
|
plugin_config_socket conf; |
|
memcpy(&conf, &defaults, sizeof(conf)); |
|
|
|
/*(preserve prior behavior; not inherited)*/ |
|
/*(forcing inheritance might break existing configs where SSL is enabled |
|
* by default in the global scope, but not $SERVER["socket"]=="*:80") */ |
|
conf.ssl_enabled = 0; |
|
|
|
config_plugin_value_t *cpv = ps->cvlist + ps->cvlist[i].v.u2[0]; |
|
for (; -1 != cpv->k_id; ++cpv) { |
|
/* ignore ssl.pemfile (k_id=10); included to process global scope */ |
|
if (!is_socket_scope && cpv->k_id != 10) { |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: %s is valid only in global scope or " |
|
"$SERVER[\"socket\"] condition", cpk[cpv->k_id].k); |
|
continue; |
|
} |
|
++count_not_engine; |
|
switch (cpv->k_id) { |
|
case 0: /* ssl.engine */ |
|
conf.ssl_enabled = (0 != cpv->v.u); |
|
--count_not_engine; |
|
break; |
|
case 1: /* ssl.cipher-list */ |
|
conf.ssl_cipher_list = cpv->v.b; |
|
break; |
|
case 2: /* ssl.honor-cipher-order */ |
|
conf.ssl_honor_cipher_order = (0 != cpv->v.u); |
|
break; |
|
case 3: /* ssl.dh-file */ |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
conf.ssl_dh_file = cpv->v.b; |
|
#else |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ignoring ssl.dh-file; " |
|
"obsoleted in GnuTLS 3.6.0 and later implementing RFC7919"); |
|
#endif |
|
break; |
|
case 4: /* ssl.ec-curve */ |
|
conf.ssl_ec_curve = cpv->v.b; |
|
break; |
|
case 5: /* ssl.openssl.ssl-conf-cmd */ |
|
*(const array **)&conf.ssl_conf_cmd = cpv->v.a; |
|
break; |
|
case 6: /* ssl.pemfile */ |
|
/* ignore here; included to process global scope when |
|
* ssl.pemfile is set, but ssl.engine is not "enable" */ |
|
break; |
|
case 7: /* ssl.empty-fragments */ |
|
conf.ssl_empty_fragments = (0 != cpv->v.u); |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ignoring ssl.empty-fragments; openssl-specific " |
|
"counter-measure against a SSL 3.0/TLS 1.0 protocol " |
|
"vulnerability affecting CBC ciphers, which cannot be handled" |
|
" by some broken (Microsoft) SSL implementations."); |
|
break; |
|
case 8: /* ssl.use-sslv2 */ |
|
conf.ssl_use_sslv2 = (0 != cpv->v.u); |
|
log_error(srv->errh, __FILE__, __LINE__, "GnuTLS: " |
|
"ssl.use-sslv2 is deprecated and will soon be removed. " |
|
"Many modern TLS libraries no longer support SSLv2."); |
|
break; |
|
case 9: /* ssl.use-sslv3 */ |
|
conf.ssl_use_sslv3 = (0 != cpv->v.u); |
|
log_error(srv->errh, __FILE__, __LINE__, "GnuTLS: " |
|
"ssl.use-sslv3 is deprecated and will soon be removed. " |
|
"Many modern TLS libraries no longer support SSLv3. " |
|
"If needed, use: " |
|
"ssl.openssl.ssl-conf-cmd = (\"MinProtocol\" => \"SSLv3\")"); |
|
break; |
|
default:/* should not happen */ |
|
break; |
|
} |
|
} |
|
if (HANDLER_GO_ON != rc) break; |
|
if (0 == i) memcpy(&defaults, &conf, sizeof(conf)); |
|
|
|
if (0 != i && !conf.ssl_enabled) continue; |
|
|
|
/* fill plugin_config_socket with global context then $SERVER["socket"] |
|
* only for directives directly in current $SERVER["socket"] condition*/ |
|
|
|
/*conf.pc = p->defaults.pc;*/ |
|
conf.ssl_verifyclient = p->defaults.ssl_verifyclient; |
|
conf.ssl_verifyclient_enforce = p->defaults.ssl_verifyclient_enforce; |
|
conf.ssl_verifyclient_depth = p->defaults.ssl_verifyclient_depth; |
|
|
|
int sidx = ps->cvlist[i].k_id; |
|
for (int j = !p->cvlist[0].v.u2[1]; j < p->nconfig; ++j) { |
|
if (p->cvlist[j].k_id != sidx) continue; |
|
/*if (0 == sidx) break;*//*(repeat to get ssl_pemfile,ssl_privkey)*/ |
|
cpv = p->cvlist + p->cvlist[j].v.u2[0]; |
|
for (; -1 != cpv->k_id; ++cpv) { |
|
++count_not_engine; |
|
switch (cpv->k_id) { |
|
case 0: /* ssl.pemfile */ |
|
if (cpv->vtype == T_CONFIG_LOCAL) |
|
conf.pc = cpv->v.v; |
|
break; |
|
case 7: /* ssl.verifyclient.activate */ |
|
conf.ssl_verifyclient = (0 != cpv->v.u); |
|
break; |
|
case 8: /* ssl.verifyclient.enforce */ |
|
conf.ssl_verifyclient_enforce = (0 != cpv->v.u); |
|
break; |
|
case 9: /* ssl.verifyclient.depth */ |
|
conf.ssl_verifyclient_depth = (unsigned char)cpv->v.shrt; |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
break; |
|
} |
|
|
|
if (NULL == conf.pc) { |
|
if (0 == i && !conf.ssl_enabled) continue; |
|
if (0 != i) { |
|
/* inherit ssl settings from global scope |
|
* (if only ssl.engine = "enable" and no other ssl.* settings) |
|
* (This is for convenience when defining both IPv4 and IPv6 |
|
* and desiring to inherit the ssl config from global context |
|
* without having to duplicate the directives)*/ |
|
if (count_not_engine) { |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ssl.pemfile has to be set in same " |
|
"$SERVER[\"socket\"] scope as other ssl.* directives, " |
|
"unless only ssl.engine is set, inheriting ssl.* from " |
|
"global scope"); |
|
rc = HANDLER_ERROR; |
|
continue; |
|
} |
|
plugin_ssl_ctx * const s = p->ssl_ctxs + sidx; |
|
*s = *p->ssl_ctxs;/*(copy struct of ssl_ctx from global scope)*/ |
|
continue; |
|
} |
|
/* PEM file is required */ |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: ssl.pemfile has to be set when ssl.engine = \"enable\""); |
|
rc = HANDLER_ERROR; |
|
continue; |
|
} |
|
|
|
/* (initialize once if module enabled) */ |
|
if (!mod_gnutls_init_once_gnutls()) { |
|
rc = HANDLER_ERROR; |
|
break; |
|
} |
|
|
|
/* configure ssl_ctx for socket */ |
|
|
|
/*conf.ssl_ctx = NULL;*//*(filled by network_init_ssl() even on error)*/ |
|
if (0 == network_init_ssl(srv, &conf, p)) { |
|
plugin_ssl_ctx * const s = p->ssl_ctxs + sidx; |
|
s->ssl_session_ticket = conf.ssl_session_ticket; |
|
s->priority_cache = conf.priority_cache; |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
s->dh_params = conf.dh_params; |
|
#endif |
|
} |
|
else { |
|
gnutls_priority_deinit(conf.priority_cache); |
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
gnutls_dh_params_deinit(conf.dh_params); |
|
#endif |
|
rc = HANDLER_ERROR; |
|
} |
|
free(conf.priority_str.ptr); |
|
} |
|
|
|
stek_rotate_ts = log_epoch_secs; |
|
|
|
free(srvplug.cvlist); |
|
return rc; |
|
} |
|
|
|
|
|
SETDEFAULTS_FUNC(mod_gnutls_set_defaults) |
|
{ |
|
static const config_plugin_keys_t cpk[] = { |
|
{ CONST_STR_LEN("ssl.pemfile"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.privkey"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.ca-file"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.ca-dn-file"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.ca-crl-file"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.read-ahead"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.disable-client-renegotiation"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.verifyclient.activate"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.verifyclient.enforce"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.verifyclient.depth"), |
|
T_CONFIG_SHORT, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.verifyclient.username"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.verifyclient.exportcert"), |
|
T_CONFIG_BOOL, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("ssl.acme-tls-1"), |
|
T_CONFIG_STRING, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ CONST_STR_LEN("debug.log-ssl-noise"), |
|
T_CONFIG_SHORT, |
|
T_CONFIG_SCOPE_CONNECTION } |
|
,{ NULL, 0, |
|
T_CONFIG_UNSET, |
|
T_CONFIG_SCOPE_UNSET } |
|
}; |
|
|
|
plugin_data * const p = p_d; |
|
p->srv = srv; |
|
if (!config_plugin_values_init(srv, p, cpk, "mod_gnutls")) |
|
return HANDLER_ERROR; |
|
|
|
/* process and validate config directives |
|
* (init i to 0 if global context; to 1 to skip empty global context) */ |
|
for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { |
|
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; |
|
config_plugin_value_t *pemfile = NULL; |
|
config_plugin_value_t *privkey = NULL; |
|
for (; -1 != cpv->k_id; ++cpv) { |
|
switch (cpv->k_id) { |
|
case 0: /* ssl.pemfile */ |
|
if (!buffer_string_is_empty(cpv->v.b)) pemfile = cpv; |
|
break; |
|
case 1: /* ssl.privkey */ |
|
if (!buffer_string_is_empty(cpv->v.b)) privkey = cpv; |
|
break; |
|
case 2: /* ssl.ca-file */ |
|
case 3: /* ssl.ca-dn-file */ |
|
if (!buffer_string_is_empty(cpv->v.b)) { |
|
gnutls_datum_t *d = |
|
mod_gnutls_load_config_crts(cpv->v.b->ptr, srv->errh); |
|
if (d != NULL) { |
|
cpv->vtype = T_CONFIG_LOCAL; |
|
cpv->v.v = d; |
|
} |
|
else { |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"%s = %s", cpk[cpv->k_id].k, cpv->v.b->ptr); |
|
return HANDLER_ERROR; |
|
} |
|
} |
|
break; |
|
case 4: /* ssl.ca-crl-file */ |
|
if (!buffer_string_is_empty(cpv->v.b)) { |
|
gnutls_datum_t *d = |
|
mod_gnutls_load_config_crls(cpv->v.b->ptr, srv->errh); |
|
if (d != NULL) { |
|
cpv->vtype = T_CONFIG_LOCAL; |
|
cpv->v.v = d; |
|
} |
|
else { |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"%s = %s", cpk[cpv->k_id].k, cpv->v.b->ptr); |
|
return HANDLER_ERROR; |
|
} |
|
} |
|
break; |
|
case 5: /* ssl.read-ahead */ |
|
case 6: /* ssl.disable-client-renegotiation */ |
|
case 7: /* ssl.verifyclient.activate */ |
|
case 8: /* ssl.verifyclient.enforce */ |
|
break; |
|
case 9: /* ssl.verifyclient.depth */ |
|
if (cpv->v.shrt > 255) { |
|
log_error(srv->errh, __FILE__, __LINE__, |
|
"GnuTLS: %s is absurdly large (%hu); limiting to 255", |
|
cpk[cpv->k_id].k, cpv->v.shrt); |
|
cpv->v.shrt = 255; |
|
} |
|
break; |
|
case 10:/* ssl.verifyclient.username */ |
|
case 11:/* ssl.verifyclient.exportcert */ |
|
break; |
|
case 12:/* ssl.acme-tls-1 */ |
|
case 13:/* debug.log-ssl-noise */ |
|
break; |
|
default:/* should not happen */ |
|
break; |
|
} |
|
} |
|
|
|
if (pemfile) { |
|
if (NULL == privkey) privkey = pemfile; |
|
pemfile->v.v = |
|
network_gnutls_load_pemfile(srv, pemfile->v.b, privkey->v.b); |
|
if (pemfile->v.v) |
|
pemfile->vtype = T_CONFIG_LOCAL; |
|
else |
|
return HANDLER_ERROR; |
|
} |
|
} |
|
|
|
p->defaults.ssl_verifyclient = 0; |
|
p->defaults.ssl_verifyclient_enforce = 1; |
|
p->defaults.ssl_verifyclient_depth = 9; |
|
p->defaults.ssl_verifyclient_export_cert = 0; |
|
p->defaults.ssl_disable_client_renegotiation = 1; |
|
p->defaults.ssl_read_ahead = 0; |
|
|
|
/* initialize p->defaults from global config context */ |
|
if (p->nconfig > 0 && p->cvlist->v.u2[1]) { |
|
const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; |
|
if (-1 != cpv->k_id) |
|
mod_gnutls_merge_config(&p->defaults, cpv); |
|
} |
|
|
|
int rc = mod_gnutls_set_defaults_sockets(srv, p); |
|
|
|
if (ssl_is_init) |
|
mod_gnutls_session_ticket_key_rotate(srv); |
|
|
|
return rc; |
|
} |
|
|
|
|
|
static int |
|
load_next_chunk (request_st * const r, chunkqueue * const cq, off_t max_bytes, |
|
const char ** const data, size_t * const data_len) |
|
{ |
|
chunk *c = cq->first; |
|
|
|
/* local_send_buffer is a static buffer of size (LOCAL_SEND_BUFSIZE) |
|
* |
|
* buffer is allocated once, is NOT realloced (note: not thread-safe) |
|
* */ |
|
|
|
force_assert(NULL != c); |
|
|
|
switch (c->type) { |
|
case MEM_CHUNK: |
|
*data = NULL; |
|
*data_len = 0; |
|
do { |
|
size_t have; |
|
|
|
force_assert(c->offset >= 0 |
|
&& c->offset <= (off_t)buffer_string_length(c->mem)); |
|
|
|
have = buffer_string_length(c->mem) - c->offset; |
|
|
|
/* copy small mem chunks into single large buffer |
|
* before gnutls_record_send() to reduce number times |
|
* write() called underneath gnutls_record_send() and |
|
* potentially reduce number of packets generated if TCP_NODELAY |
|
* Alternatively, GnuTLS provides gnutls_record_cork() and |
|
* gnutls_record_uncork(), not currently used by mod_gnutls */ |
|
if (*data_len) { |
|
size_t space = LOCAL_SEND_BUFSIZE - *data_len; |
|
if (have > space) |
|
have = space; |
|
if (have > (size_t)max_bytes - *data_len) |
|
have = (size_t)max_bytes - *data_len; |
|
if (*data != local_send_buffer) { |
|
memcpy(local_send_buffer, *data, *data_len); |
|
*data = local_send_buffer; |
|
} |
|
memcpy(local_send_buffer+*data_len,c->mem->ptr+c->offset,have); |
|
*data_len += have; |
|
continue; |
|
} |
|
|
|
if ((off_t) have > max_bytes) have = max_bytes; |
|
|
|
*data = c->mem->ptr + c->offset; |
|
*data_len = have; |
|
} while ((c = c->next) && c->type == MEM_CHUNK |
|
&& *data_len < LOCAL_SEND_BUFSIZE |
|
&& (off_t) *data_len < max_bytes); |
|
return 0; |
|
|
|
case FILE_CHUNK: |
|
if (0 != chunkqueue_open_file_chunk(cq, r->conf.errh)) return -1; |
|
|
|
{ |
|
off_t offset, toSend; |
|
|
|
force_assert(c->offset >= 0 && c->offset <= c->file.length); |
|
offset = c->file.start + c->offset; |
|
toSend = c->file.length - c->offset; |
|
|
|
if (toSend > LOCAL_SEND_BUFSIZE) toSend = LOCAL_SEND_BUFSIZE; |
|
if (toSend > max_bytes) toSend = max_bytes; |
|
|
|
if (-1 == lseek(c->file.fd, offset, SEEK_SET)) { |
|
log_perror(r->conf.errh, __FILE__, __LINE__, "lseek"); |
|
return -1; |
|
} |
|
if (-1 == (toSend = read(c->file.fd, local_send_buffer, toSend))) { |
|
log_perror(r->conf.errh, __FILE__, __LINE__, "read"); |
|
return -1; |
|
} |
|
|
|
*data = local_send_buffer; |
|
*data_len = toSend; |
|
} |
|
return 0; |
|
} |
|
|
|
return -1; |
|
} |
|
|
|
|
|
__attribute_cold__ |
|
static int |
|
mod_gnutls_write_err(connection *con, handler_ctx *hctx, int wr, size_t wr_len) |
|
{ |
|
switch (wr) { |
|
case GNUTLS_E_AGAIN: |
|
case GNUTLS_E_INTERRUPTED: |
|
if (gnutls_record_get_direction(hctx->ssl)) |
|
con->is_writable = -1; |
|
else |
|
con->is_readable = -1; |
|
break; /* try again later */ |
|
default: |
|
#if 0 |
|
/* ??? how to check for EPIPE or ECONNRESET and skip logging ??? */ |
|
if (hctx->conf.ssl_log_noise) |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, wr, __func__); |
|
#endif |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, wr, __func__); |
|
return -1; |
|
} |
|
|
|
/* partial write; save attempted wr_len */ |
|
hctx->pending_write = wr_len; |
|
|
|
return 0; /* try again later */ |
|
} |
|
|
|
|
|
__attribute_cold__ |
|
static int |
|
mod_gnutls_read_err(connection *con, handler_ctx *hctx, int rc) |
|
{ |
|
switch (rc) { |
|
case GNUTLS_E_AGAIN: |
|
case GNUTLS_E_INTERRUPTED: |
|
if (gnutls_record_get_direction(hctx->ssl)) |
|
con->is_writable = -1; |
|
con->is_readable = 0; |
|
return 0; |
|
#if 0 |
|
case GNUTLS_E_SESSION_EOF: /*(not exposed by library)*/ |
|
/* XXX: future: save state to avoid future read after response? */ |
|
con->is_readable = 0; |
|
r->keep_alive = 0; |
|
return (hctx->handshake ? -2 : -1); /*(-1 error if during handshake)*/ |
|
#endif |
|
case GNUTLS_E_REHANDSHAKE: |
|
if (!hctx->handshake) return -1; /*(not expected during handshake)*/ |
|
if (hctx->conf.ssl_disable_client_renegotiation)/*(mod_gnutls default)*/ |
|
return -1; |
|
#if 0 |
|
if (gnutls_safe_renegotiation_status(hctx->ssl)) { |
|
hctx->handshake = 0; |
|
return con->network_read(con, cq, max_bytes); |
|
} |
|
#else |
|
return 0; /*(ignore client renegotiation request; generally unsafe)*/ |
|
#endif |
|
case GNUTLS_E_WARNING_ALERT_RECEIVED: |
|
case GNUTLS_E_FATAL_ALERT_RECEIVED: |
|
{ |
|
const char *str; |
|
gnutls_alert_description_t alert = gnutls_alert_get(hctx->ssl); |
|
switch (alert) { |
|
case GNUTLS_A_NO_RENEGOTIATION: |
|
return 0; /*(ignore non-fatal alert from client)*/ |
|
case GNUTLS_A_HANDSHAKE_FAILURE: |
|
case GNUTLS_A_CLOSE_NOTIFY: /*(not exposed by library)*/ |
|
case GNUTLS_A_UNKNOWN_CA: |
|
case GNUTLS_A_CERTIFICATE_UNKNOWN: |
|
case GNUTLS_A_BAD_CERTIFICATE: |
|
if (!hctx->conf.ssl_log_noise) return -1; |
|
__attribute_fallthrough__ |
|
default: |
|
str = gnutls_alert_get_name(alert); |
|
elogf(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"%s(): alert %s", __func__, str ? str : "(unknown)"); |
|
return -1; |
|
} |
|
} |
|
case GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET: |
|
case GNUTLS_E_UNKNOWN_CIPHER_SUITE: /* GNUTLS_A_HANDSHAKE_FAILURE */ |
|
case GNUTLS_E_PREMATURE_TERMINATION: |
|
if (!hctx->conf.ssl_log_noise) return -1; |
|
__attribute_fallthrough__ |
|
case GNUTLS_E_GOT_APPLICATION_DATA: /*(not expected with current use)*/ |
|
/*if (hctx->handshake) return -1;*//*(accept only during handshake)*/ |
|
default: |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, __func__); |
|
return -1; |
|
} |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_close_notify(handler_ctx *hctx); |
|
|
|
|
|
static int |
|
connection_write_cq_ssl (connection *con, chunkqueue *cq, off_t max_bytes) |
|
{ |
|
request_st * const r = &con->request; |
|
handler_ctx *hctx = r->plugin_ctx[plugin_data_singleton->id]; |
|
gnutls_session_t ssl = hctx->ssl; |
|
|
|
if (hctx->pending_write) { |
|
int wr = gnutls_record_send(ssl, NULL, 0); |
|
if (wr <= 0) |
|
return mod_gnutls_write_err(con, hctx, wr, hctx->pending_write); |
|
max_bytes -= wr; |
|
hctx->pending_write = 0; |
|
chunkqueue_mark_written(cq, wr); |
|
} |
|
|
|
if (0 != hctx->close_notify) return mod_gnutls_close_notify(hctx); |
|
|
|
chunkqueue_remove_finished_chunks(cq); |
|
|
|
const size_t lim = gnutls_record_get_max_size(ssl); |
|
|
|
/* future: for efficiency/performance might consider using GnuTLS corking |
|
* gnutls_record_cork() |
|
* gnutls_record_uncork() |
|
* gnutls_record_check_corked() |
|
* or might enhance load_next_chunk() to read a mix of MEM_CHUNK and |
|
* FILE_CHUNK into the buffer before sending, e.g. to send header response |
|
* headers and beginning of files before uncorking. |
|
*/ |
|
|
|
while (max_bytes > 0 && NULL != cq->first) { |
|
const char *data; |
|
size_t data_len; |
|
int wr; |
|
|
|
if (0 != load_next_chunk(r, cq, max_bytes, &data, &data_len)) return -1; |
|
|
|
/* gnutls_record_send() copies the data, up to max record size, but if |
|
* (temporarily) unable to write the entire record, it is documented |
|
* that the caller must call gnutls_record_send() again, later, with the |
|
* same arguments, or with NULL ptr and 0 data_len. The func may return |
|
* GNUTLS_E_AGAIN or GNUTLS_E_INTERRUPTED to indicate that caller should |
|
* wait for fd to be readable/writable before calling the func again, |
|
* which is why those (temporary) errors are returned instead of telling |
|
* the caller that the data was successfully copied. |
|
* Additionally, to be accurate, the size must fit into a record which |
|
* is why we restrict ourselves to sending max out record payload each |
|
* iteration. |
|
* XXX: above comments modified from mod_mbedtls; should be verified |
|
*/ |
|
|
|
int wr_total = 0; |
|
do { |
|
size_t wr_len = (data_len > lim) ? lim : data_len; |
|
wr = gnutls_record_send(ssl, data, wr_len); |
|
if (wr <= 0) { |
|
if (wr_total) chunkqueue_mark_written(cq, wr_total); |
|
return mod_gnutls_write_err(con, hctx, wr, wr_len); |
|
} |
|
wr_total += wr; |
|
data += wr; |
|
} while ((data_len -= wr)); |
|
chunkqueue_mark_written(cq, wr_total); |
|
max_bytes -= wr_total; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_ssl_handshake (handler_ctx *hctx) |
|
{ |
|
int rc = gnutls_handshake(hctx->ssl); |
|
if (rc < 0) |
|
return mod_gnutls_read_err(hctx->con, hctx, rc); |
|
|
|
/*(rc == GNUTLS_E_SUCCESS)*/ |
|
|
|
hctx->handshake = 1; |
|
#if GNUTLS_VERSION_NUMBER >= 0x030200 |
|
if (hctx->alpn == MOD_GNUTLS_ALPN_ACME_TLS_1) { |
|
/* Once TLS handshake is complete, return -1 to result in |
|
* CON_STATE_ERROR so that socket connection is quickly closed */ |
|
return -1; |
|
} |
|
hctx->alpn = 0; |
|
#endif |
|
return 1; /* continue reading */ |
|
} |
|
|
|
|
|
static int |
|
connection_read_cq_ssl (connection *con, chunkqueue *cq, off_t max_bytes) |
|
{ |
|
request_st * const r = &con->request; |
|
handler_ctx *hctx = r->plugin_ctx[plugin_data_singleton->id]; |
|
|
|
UNUSED(max_bytes); |
|
|
|
if (0 != hctx->close_notify) return mod_gnutls_close_notify(hctx); |
|
|
|
if (!hctx->handshake) { |
|
int rc = mod_gnutls_ssl_handshake(hctx); |
|
if (1 != rc) return rc; /* !hctx->handshake; not done, or error */ |
|
} |
|
|
|
gnutls_session_t ssl = hctx->ssl; |
|
ssize_t len; |
|
char *mem = NULL; |
|
size_t mem_len = 0; |
|
size_t pend = gnutls_record_check_pending(ssl); |
|
do { |
|
mem_len = pend < 2048 ? 2048 : pend; |
|
chunk * const ckpt = cq->last; |
|
mem = chunkqueue_get_memory(cq, &mem_len); |
|
|
|
len = gnutls_record_recv(ssl, mem, mem_len); |
|
if (len > 0) { |
|
chunkqueue_use_memory(cq, ckpt, len); |
|
con->bytes_read += len; |
|
} else { |
|
chunkqueue_use_memory(cq, ckpt, 0); |
|
} |
|
} while (len > 0 && (pend = gnutls_record_check_pending(ssl))); |
|
|
|
if (len < 0) { |
|
return mod_gnutls_read_err(con, hctx, (int)len); |
|
} else if (len == 0) { |
|
con->is_readable = 0; |
|
/* the other end closed the connection -> KEEP-ALIVE */ |
|
|
|
return -2; |
|
} else { |
|
return 0; |
|
} |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_debug_cb(int level, const char *str) |
|
{ |
|
UNUSED(level); |
|
log_error_st *errh = plugin_data_singleton->srv->errh; |
|
log_error(errh, __FILE__, __LINE__, "GnuTLS: %s", str); |
|
} |
|
|
|
|
|
CONNECTION_FUNC(mod_gnutls_handle_con_accept) |
|
{ |
|
server_socket *srv_sock = con->srv_socket; |
|
if (!srv_sock->is_ssl) return HANDLER_GO_ON; |
|
|
|
plugin_data *p = p_d; |
|
handler_ctx * const hctx = handler_ctx_init(); |
|
request_st * const r = &con->request; |
|
hctx->r = r; |
|
hctx->con = con; |
|
hctx->tmp_buf = con->srv->tmp_buf; |
|
r->plugin_ctx[p->id] = hctx; |
|
|
|
plugin_ssl_ctx * const s = p->ssl_ctxs + srv_sock->sidx; |
|
hctx->ssl_session_ticket = s->ssl_session_ticket; |
|
int flags = GNUTLS_SERVER | GNUTLS_NO_SIGNAL | GNUTLS_NONBLOCK; |
|
/* ??? add feature: GNUTLS_ENABLE_EARLY_START ??? */ |
|
int rc = gnutls_init(&hctx->ssl, flags); |
|
if (rc < 0) { |
|
elog(r->conf.errh, __FILE__, __LINE__, rc, "gnutls_init()"); |
|
return HANDLER_ERROR; |
|
} |
|
|
|
rc = gnutls_priority_set(hctx->ssl, s->priority_cache); |
|
if (rc < 0) { |
|
elog(r->conf.errh, __FILE__, __LINE__, rc, "gnutls_priority_set()"); |
|
return HANDLER_ERROR; |
|
} |
|
|
|
/* generic func replaces gnutls_handshake_set_post_client_hello_function()*/ |
|
gnutls_handshake_set_hook_function(hctx->ssl, GNUTLS_HANDSHAKE_CLIENT_HELLO, |
|
GNUTLS_HOOK_BOTH, |
|
mod_gnutls_client_hello_hook); |
|
|
|
gnutls_session_set_ptr(hctx->ssl, hctx); |
|
gnutls_transport_set_int(hctx->ssl, con->fd); |
|
|
|
con->network_read = connection_read_cq_ssl; |
|
con->network_write = connection_write_cq_ssl; |
|
con->proto_default_port = 443; /* "https" */ |
|
mod_gnutls_patch_config(r, &hctx->conf); |
|
|
|
#if GNUTLS_VERSION_NUMBER < 0x030600 |
|
hctx->conf.dh_params = s->dh_params; |
|
#endif |
|
|
|
/* debug logging is global. Once enabled, debug hook will remain so, though |
|
* different connection might overwrite level, if configured differently. |
|
* If GNUTLS_DEBUG_LEVEL is set in environment (and ssl_log_noise not set), |
|
* then debugging will go to stderr */ |
|
if (hctx->conf.ssl_log_noise) {/* volume level for debug message callback */ |
|
gnutls_global_set_log_function(mod_gnutls_debug_cb); |
|
gnutls_global_set_log_level(hctx->conf.ssl_log_noise); |
|
} |
|
|
|
/* GnuTLS limitation: must set session ticket encryption key before GnuTLS |
|
* client hello hook runs if TLSv1.3 (? key already set by then ?) */ |
|
if (hctx->ssl_session_ticket && session_ticket_key.size) { |
|
rc = gnutls_session_ticket_enable_server(hctx->ssl,&session_ticket_key); |
|
if (rc < 0) { |
|
elog(r->conf.errh, __FILE__, __LINE__, rc, |
|
"gnutls_session_ticket_enable_server()"); |
|
return HANDLER_ERROR; |
|
} |
|
} |
|
|
|
return HANDLER_GO_ON; |
|
} |
|
|
|
|
|
static void |
|
mod_gnutls_detach(handler_ctx *hctx) |
|
{ |
|
/* step aside from further SSL processing |
|
* (used after handle_connection_shut_wr hook) */ |
|
/* future: might restore prior network_read and network_write fn ptrs */ |
|
hctx->con->is_ssl_sock = 0; |
|
/* if called after handle_connection_shut_wr hook, shutdown SHUT_WR */ |
|
if (-1 == hctx->close_notify) shutdown(hctx->con->fd, SHUT_WR); |
|
hctx->close_notify = 1; |
|
} |
|
|
|
|
|
CONNECTION_FUNC(mod_gnutls_handle_con_shut_wr) |
|
{ |
|
request_st * const r = &con->request; |
|
plugin_data *p = p_d; |
|
handler_ctx *hctx = r->plugin_ctx[p->id]; |
|
if (NULL == hctx) return HANDLER_GO_ON; |
|
|
|
hctx->close_notify = -2; |
|
if (hctx->handshake) { |
|
mod_gnutls_close_notify(hctx); |
|
} |
|
else { |
|
mod_gnutls_detach(hctx); |
|
} |
|
|
|
return HANDLER_GO_ON; |
|
} |
|
|
|
|
|
static int |
|
mod_gnutls_close_notify (handler_ctx *hctx) |
|
{ |
|
if (1 == hctx->close_notify) return -2; |
|
|
|
int rc = gnutls_bye(hctx->ssl, GNUTLS_SHUT_WR); |
|
switch (rc) { |
|
case GNUTLS_E_SUCCESS: |
|
mod_gnutls_detach(hctx); |
|
return -2; |
|
case GNUTLS_E_AGAIN: |
|
case GNUTLS_E_INTERRUPTED: |
|
return 0; |
|
default: |
|
elog(hctx->r->conf.errh, __FILE__, __LINE__, rc, |
|
"mod_gnutls_close_notify()"); |
|
mod_gnutls_detach(hctx); |
|
return -1; |
|
} |
|
} |
|
|
|
|
|
CONNECTION_FUNC(mod_gnutls_handle_con_close) |
|
{ |
|
request_st * const r = &con->request; |
|
plugin_data *p = p_d; |
|
handler_ctx *hctx = r->plugin_ctx[p->id]; |
|
if (NULL != hctx) { |
|
if (1 != hctx->close_notify) |
|
mod_gnutls_close_notify(hctx); /*(one final try)*/ |
|
handler_ctx_free(hctx); |
|
r->plugin_ctx[p->id] = NULL; |
|
} |
|
|
|
return HANDLER_GO_ON; |
|
} |
|
|
|
|
|
__attribute_noinline__ |
|
static void |
|
https_add_ssl_client_cert (request_st * const r, const gnutls_x509_crt_t peer) |
|
{ |
|
gnutls_datum_t d; |
|
if (gnutls_x509_crt_export2(peer, GNUTLS_X509_FMT_PEM, &d) >= 0) |
|
http_header_env_set(r, |
|
CONST_STR_LEN("SSL_CLIENT_CERT"), |
|
(char *)d.data, d.size); |
|
if (d.data) gnutls_free(d.data); |
|
} |
|
|
|
|
|
/* modified from gnutls tests/dn.c:print_dn() */ |
|
static void |
|
https_add_ssl_client_subject (request_st * const r, gnutls_x509_dn_t dn) |
|
{ |
|
buffer * const tb = r->tmp_buf; |
|
int irdn = 0, i, rc; |
|
gnutls_x509_ava_st ava; |
|
char buf[512]; /*(expecting element value len <= 256)*/ |
|
|
|
/* add components of client Subject DN */ |
|
buffer_copy_string_len(tb, CONST_STR_LEN("SSL_CLIENT_S_DN_")); |
|
|