|
|
|
/*
|
|
|
|
* mod_gnutls - ssl support
|
|
|
|
*
|
|
|
|
* Description:
|
|
|
|
* mod_gnutls listens on separate sockets for ssl connections (https://...)
|
|
|
|
*
|
|
|
|
* Setups:
|
|
|
|
* gnutls - setup a ssl socket; takes a hash/key-value list of following parameters:
|
|
|
|
* listen - (mandatory) the socket address (same as standard listen)
|
|
|
|
* pemfile - (mandatory) contains key and certificate and intermediate certificates ("chain") for the key (PEM format)
|
|
|
|
* priority - contains priority string (specifying ciphers and gnutls options), default: "NORMAL"
|
|
|
|
* protect-against-beast - whether to append ":-CIPHER-ALL:+ARCFOUR-128" for SSL3/TLS1.0 connections to priority
|
|
|
|
* dh-params - file with genereated dh-params. default: pre generated 4096-bit params included in the source
|
|
|
|
* session-db-size - size of session db (TLS session cookies). set to <= 0 to disable. default: 256
|
|
|
|
* when SNI was enabled
|
|
|
|
* sni-backend - "fetch" backend name to search certificates in with the SNI servername as key
|
|
|
|
* sni-fallback-pemfile - certificate to use if request contained SNI servername, but the sni-backend didn't find anything
|
|
|
|
* if request didn't contain SNI the standard "pemfile"(s) are used
|
|
|
|
* NOTES:
|
|
|
|
* * gnutls has some SNI support builtin - you can just load all certificates with multiple "pemfile" parameters,
|
|
|
|
* and gnutls will try to pick the right one.
|
|
|
|
* * listen and pemfile can be specified more than once
|
|
|
|
* * certificates in a file have to be ordered from bottom to top (each certificate is followed by the one that signed it)
|
|
|
|
*
|
|
|
|
* Example config:
|
|
|
|
* setup gnutls ( "listen" => "0.0.0.0:8443", "listen" => "[::]:8443", "pemfile" => "server.pem" ];
|
|
|
|
*
|
|
|
|
* setup {
|
|
|
|
* fetch.files_static "sni" => "/etc/lighttpd2/certs/sni_*_server.pem";
|
|
|
|
* gnutls ( "listen" => "0.0.0.0:8443", "listen" => "[::]:8443", "pemfile" => "server.pem", "sni-backend" => "sni" );
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* TODO:
|
|
|
|
* * support client certificate authentication: http://www.gnutls.org/manual/gnutls.html#Client-certificate-authentication
|
|
|
|
* gnutls_certificate_set_x509_system_trust (available since 3.0 (docs) or 3.0.19 (weechat ??))
|
|
|
|
* gnutls_certificate_set_x509_trust_file
|
|
|
|
* * TLS session tickets are always activated with gnutls >= 2.10 - option to disable
|
|
|
|
* * OCSP stapling
|
|
|
|
*
|
|
|
|
* Author:
|
|
|
|
* Copyright (c) 2013 Stefan Bühler
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <lighttpd/base.h>
|
|
|
|
#include <lighttpd/throttle.h>
|
|
|
|
|
|
|
|
#include "gnutls_filter.h"
|
|
|
|
#include "ssl_client_hello_parser.h"
|
|
|
|
#include "ssl-session-db.h"
|
|
|
|
|
|
|
|
#include <gnutls/gnutls.h>
|
|
|
|
#include <glib-2.0/glib/galloca.h>
|
|
|
|
|
|
|
|
#if GNUTLS_VERSION_NUMBER >= 0x020a00
|
|
|
|
#define HAVE_SESSION_TICKET
|
|
|
|
#endif
|
|
|
|
|
|
|
|
LI_API gboolean mod_gnutls_init(liModules *mods, liModule *mod);
|
|
|
|
LI_API gboolean mod_gnutls_free(liModules *mods, liModule *mod);
|
|
|
|
|
|
|
|
static int load_dh_params_4096(gnutls_dh_params_t *dh_params);
|
|
|
|
|
|
|
|
typedef struct mod_connection_ctx mod_connection_ctx;
|
|
|
|
typedef struct mod_context mod_context;
|
|
|
|
|
|
|
|
struct mod_connection_ctx {
|
|
|
|
gnutls_session_t session;
|
|
|
|
liConnection *con;
|
|
|
|
mod_context *ctx;
|
|
|
|
|
|
|
|
liGnuTLSFilter *tls_filter;
|
|
|
|
|
|
|
|
liIOStream *sock_stream;
|
|
|
|
gpointer simple_socket_data;
|
|
|
|
|
|
|
|
liStream *client_hello_stream;
|
|
|
|
#ifdef USE_SNI
|
|
|
|
liJob sni_job;
|
|
|
|
liJobRef *sni_jobref;
|
|
|
|
liFetchEntry *sni_entry;
|
|
|
|
liFetchWait *sni_db_wait;
|
|
|
|
GString *sni_server_name;
|
|
|
|
#endif
|
|
|
|
};
|
|
|
|
|
|
|
|
struct mod_context {
|
|
|
|
gint refcount;
|
|
|
|
|
|
|
|
liSSLSessionDB *session_db;
|
|
|
|
|
|
|
|
gnutls_certificate_credentials_t server_cert;
|
|
|
|
gnutls_dh_params_t dh_params;
|
|
|
|
gnutls_priority_t server_priority;
|
|
|
|
gnutls_priority_t server_priority_beast;
|
|
|
|
#ifdef HAVE_SESSION_TICKET
|
|
|
|
gnutls_datum_t ticket_key;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
liFetchDatabase *sni_db, *sni_backend_db;
|
|
|
|
gnutls_certificate_credentials_t sni_fallback_cert;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
unsigned int protect_against_beast:1;
|
|
|
|
};
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
typedef struct fetch_cert_backend_lookup fetch_cert_backend_lookup;
|
|
|
|
struct fetch_cert_backend_lookup {
|
|
|
|
liFetchWait *wait;
|
|
|
|
liFetchEntry *entry;
|
|
|
|
mod_context *ctx;
|
|
|
|
};
|
|
|
|
#endif
|
|
|
|
|
|
|
|
static void mod_gnutls_context_release(mod_context *ctx);
|
|
|
|
static void mod_gnutls_context_acquire(mod_context *ctx);
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
static gnutls_certificate_credentials_t creds_from_gstring(mod_context *ctx, GString *str) {
|
|
|
|
gnutls_certificate_credentials_t creds = NULL;
|
|
|
|
gnutls_datum_t pemfile;
|
|
|
|
int r;
|
|
|
|
|
|
|
|
if (NULL == str) return NULL;
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_certificate_allocate_credentials(&creds))) return NULL;
|
|
|
|
|
|
|
|
pemfile.data = (unsigned char*) str->str;
|
|
|
|
pemfile.size = str->len;
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_certificate_set_x509_key_mem(creds, &pemfile, &pemfile, GNUTLS_X509_FMT_PEM))) {
|
|
|
|
goto error_free_creds;
|
|
|
|
}
|
|
|
|
|
|
|
|
gnutls_certificate_set_dh_params(creds, ctx->dh_params);
|
|
|
|
|
|
|
|
return creds;
|
|
|
|
|
|
|
|
error_free_creds:
|
|
|
|
gnutls_certificate_free_credentials(creds);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void fetch_cert_backend_ready(gpointer data) {
|
|
|
|
liFetchEntry *be;
|
|
|
|
fetch_cert_backend_lookup *lookup = (fetch_cert_backend_lookup*) data;
|
|
|
|
|
|
|
|
be = li_fetch_get2(lookup->ctx->sni_backend_db, lookup->entry->key, fetch_cert_backend_ready, lookup, &lookup->wait);
|
|
|
|
if (NULL != be) {
|
|
|
|
liFetchEntry *entry = lookup->entry;
|
|
|
|
mod_context *ctx = lookup->ctx;
|
|
|
|
g_slice_free(fetch_cert_backend_lookup, lookup);
|
|
|
|
|
|
|
|
entry->backend_data = be;
|
|
|
|
entry->data = creds_from_gstring(ctx, (GString*) be->data);
|
|
|
|
li_fetch_entry_ready(entry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void fetch_cert_lookup(liFetchDatabase* db, gpointer data, liFetchEntry *entry) {
|
|
|
|
fetch_cert_backend_lookup *lookup = g_slice_new0(fetch_cert_backend_lookup);
|
|
|
|
UNUSED(db);
|
|
|
|
|
|
|
|
lookup->ctx = (mod_context*) data;
|
|
|
|
lookup->entry = entry;
|
|
|
|
fetch_cert_backend_ready(lookup);
|
|
|
|
}
|
|
|
|
static gboolean fetch_cert_revalidate(liFetchDatabase* db, gpointer data, liFetchEntry *entry) {
|
|
|
|
UNUSED(db); UNUSED(data);
|
|
|
|
return li_fetch_entry_revalidate((liFetchEntry*) entry->backend_data);
|
|
|
|
}
|
|
|
|
static void fetch_cert_refresh(liFetchDatabase* db, gpointer data, liFetchEntry *cur_entry, liFetchEntry *new_entry) {
|
|
|
|
UNUSED(db); UNUSED(data);
|
|
|
|
li_fetch_entry_refresh((liFetchEntry*) cur_entry->backend_data);
|
|
|
|
li_fetch_entry_refresh_skip(new_entry);
|
|
|
|
}
|
|
|
|
static void fetch_cert_free_entry(gpointer data, liFetchEntry *entry) {
|
|
|
|
gnutls_certificate_credentials_t creds = entry->data;
|
|
|
|
UNUSED(data);
|
|
|
|
|
|
|
|
if (NULL != creds) gnutls_certificate_free_credentials(creds);
|
|
|
|
li_fetch_entry_release((liFetchEntry*) entry->backend_data);
|
|
|
|
}
|
|
|
|
static void fetch_cert_free_db(gpointer data) {
|
|
|
|
mod_context *ctx = (mod_context*) data;
|
|
|
|
mod_gnutls_context_release(ctx);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static const liFetchCallbacks fetch_cert_callbacks = {
|
|
|
|
fetch_cert_lookup,
|
|
|
|
fetch_cert_revalidate,
|
|
|
|
fetch_cert_refresh,
|
|
|
|
fetch_cert_free_entry,
|
|
|
|
fetch_cert_free_db
|
|
|
|
};
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
static void mod_gnutls_context_release(mod_context *ctx) {
|
|
|
|
if (!ctx) return;
|
|
|
|
assert(g_atomic_int_get(&ctx->refcount) > 0);
|
|
|
|
if (g_atomic_int_dec_and_test(&ctx->refcount)) {
|
|
|
|
gnutls_priority_deinit(ctx->server_priority_beast);
|
|
|
|
gnutls_priority_deinit(ctx->server_priority);
|
|
|
|
gnutls_certificate_free_credentials(ctx->server_cert);
|
|
|
|
gnutls_dh_params_deinit(ctx->dh_params);
|
|
|
|
#ifdef HAVE_SESSION_TICKET
|
|
|
|
/* wtf. why is there no function in gnutls for this... */
|
|
|
|
if (NULL != ctx->ticket_key.data) {
|
|
|
|
gnutls_free(ctx->ticket_key.data);
|
|
|
|
ctx->ticket_key.data = NULL;
|
|
|
|
ctx->ticket_key.size = 0;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
li_ssl_session_db_free(ctx->session_db);
|
|
|
|
ctx->session_db = NULL;
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
if (NULL != ctx->sni_db) {
|
|
|
|
li_fetch_database_release(ctx->sni_db);
|
|
|
|
ctx->sni_db = NULL;
|
|
|
|
}
|
|
|
|
if (NULL != ctx->sni_backend_db) {
|
|
|
|
li_fetch_database_release(ctx->sni_backend_db);
|
|
|
|
ctx->sni_backend_db = NULL;
|
|
|
|
}
|
|
|
|
if (NULL != ctx->sni_fallback_cert) {
|
|
|
|
gnutls_certificate_free_credentials(ctx->sni_fallback_cert);
|
|
|
|
ctx->sni_fallback_cert = NULL;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
g_slice_free(mod_context, ctx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void mod_gnutls_context_acquire(mod_context *ctx) {
|
|
|
|
assert(g_atomic_int_get(&ctx->refcount) > 0);
|
|
|
|
g_atomic_int_inc(&ctx->refcount);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static mod_context *mod_gnutls_context_new(liServer *srv) {
|
|
|
|
mod_context *ctx = g_slice_new0(mod_context);
|
|
|
|
int r;
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_certificate_allocate_credentials(&ctx->server_cert))) {
|
|
|
|
ERROR(srv, "gnutls_certificate_allocate_credentials failed(%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_priority_init(&ctx->server_priority, "NORMAL", NULL))) {
|
|
|
|
ERROR(srv, "gnutls_priority_init failed(%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_priority_init(&ctx->server_priority_beast, "NORMAL:-CIPHER-ALL:+ARCFOUR-128", NULL))) {
|
|
|
|
ERROR(srv, "gnutls_priority_init failed(%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error2;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef HAVE_SESSION_TICKET
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_session_ticket_key_generate(&ctx->ticket_key))) {
|
|
|
|
ERROR(srv, "gnutls_session_ticket_key_generate failed(%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error3;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
ctx->refcount = 1;
|
|
|
|
ctx->protect_against_beast = 1;
|
|
|
|
|
|
|
|
return ctx;
|
|
|
|
|
|
|
|
error3:
|
|
|
|
gnutls_priority_deinit(ctx->server_priority_beast);
|
|
|
|
|
|
|
|
error2:
|
|
|
|
gnutls_priority_deinit(ctx->server_priority);
|
|
|
|
|
|
|
|
error1:
|
|
|
|
gnutls_certificate_free_credentials(ctx->server_cert);
|
|
|
|
|
|
|
|
error0:
|
|
|
|
g_slice_free(mod_context, ctx);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tcp_io_cb(liIOStream *stream, liIOStreamEvent event) {
|
|
|
|
mod_connection_ctx *conctx = stream->data;
|
|
|
|
assert(NULL == conctx->sock_stream || conctx->sock_stream == stream);
|
|
|
|
|
|
|
|
if (LI_IOSTREAM_DESTROY == event) {
|
|
|
|
li_stream_simple_socket_close(stream, TRUE); /* kill it, ssl sent an close alert message */
|
|
|
|
}
|
|
|
|
|
|
|
|
li_connection_simple_tcp(&conctx->con, stream, &conctx->simple_socket_data, event);
|
|
|
|
|
|
|
|
if (NULL != conctx->con && conctx->con->out_has_all_data
|
|
|
|
&& (NULL == stream->stream_out.out || 0 == stream->stream_out.out->length)
|
|
|
|
&& li_streams_empty(conctx->con->con_sock.raw_out, NULL)) {
|
|
|
|
li_stream_simple_socket_flush(stream);
|
|
|
|
li_connection_request_done(conctx->con);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (event) {
|
|
|
|
case LI_IOSTREAM_DESTROY:
|
|
|
|
assert(NULL == conctx->sock_stream);
|
|
|
|
assert(NULL == conctx->tls_filter);
|
|
|
|
assert(NULL == conctx->con);
|
|
|
|
stream->data = NULL;
|
|
|
|
g_slice_free(mod_connection_ctx, conctx);
|
|
|
|
return;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void handshake_cb(liGnuTLSFilter *f, gpointer data, liStream *plain_source, liStream *plain_drain) {
|
|
|
|
mod_connection_ctx *conctx = data;
|
|
|
|
liConnection *con = conctx->con;
|
|
|
|
UNUSED(f);
|
|
|
|
|
|
|
|
if (NULL != con) {
|
|
|
|
li_stream_connect(plain_source, con->con_sock.raw_in);
|
|
|
|
li_stream_connect(con->con_sock.raw_out, plain_drain);
|
|
|
|
} else {
|
|
|
|
li_stream_reset(plain_source);
|
|
|
|
li_stream_reset(plain_drain);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void close_cb(liGnuTLSFilter *f, gpointer data) {
|
|
|
|
mod_connection_ctx *conctx = data;
|
|
|
|
liConnection *con = conctx->con;
|
|
|
|
assert(conctx->tls_filter == f);
|
|
|
|
|
|
|
|
conctx->tls_filter = NULL;
|
|
|
|
li_gnutls_filter_free(f);
|
|
|
|
gnutls_deinit(conctx->session);
|
|
|
|
|
|
|
|
if (NULL != conctx->ctx) {
|
|
|
|
mod_gnutls_context_release(conctx->ctx);
|
|
|
|
conctx->ctx = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (NULL != conctx->con) {
|
|
|
|
liStream *raw_out = con->con_sock.raw_out, *raw_in = con->con_sock.raw_in;
|
|
|
|
assert(con->con_sock.data == conctx);
|
|
|
|
conctx->con = NULL;
|
|
|
|
con->con_sock.data = NULL;
|
|
|
|
con->con_sock.callbacks = NULL;
|
|
|
|
li_stream_acquire(raw_in);
|
|
|
|
li_stream_reset(raw_out);
|
|
|
|
li_stream_reset(raw_in);
|
|
|
|
li_stream_release(raw_in);
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
if (NULL != conctx->sni_db_wait) {
|
|
|
|
li_fetch_cancel(&conctx->sni_db_wait);
|
|
|
|
}
|
|
|
|
if (NULL != conctx->sni_entry) {
|
|
|
|
li_fetch_entry_release(conctx->sni_entry);
|
|
|
|
conctx->sni_entry = NULL;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if (NULL != conctx->client_hello_stream) {
|
|
|
|
li_ssl_client_hello_stream_ready(conctx->client_hello_stream);
|
|
|
|
li_stream_release(conctx->client_hello_stream);
|
|
|
|
conctx->client_hello_stream = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
if (NULL != conctx->sni_jobref) {
|
|
|
|
li_job_ref_release(conctx->sni_jobref);
|
|
|
|
conctx->sni_jobref = NULL;
|
|
|
|
}
|
|
|
|
li_job_clear(&conctx->sni_job);
|
|
|
|
|
|
|
|
if (NULL != conctx->sni_server_name) {
|
|
|
|
g_string_free(conctx->sni_server_name, TRUE);
|
|
|
|
conctx->sni_server_name = NULL;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
assert(NULL != conctx->sock_stream);
|
|
|
|
li_iostream_safe_release(&conctx->sock_stream);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int session_db_store_cb(void *_sdb, gnutls_datum_t key, gnutls_datum_t data) {
|
|
|
|
liSSLSessionDB *sdb = _sdb;
|
|
|
|
li_ssl_session_db_store(sdb, key.data, key.size, data.data, data.size);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
static int session_db_remove_cb(void *_sdb, gnutls_datum_t key) {
|
|
|
|
liSSLSessionDB *sdb = _sdb;
|
|
|
|
li_ssl_session_db_remove(sdb, key.data, key.size);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
static gnutls_datum_t session_db_retrieve_cb(void *_sdb, gnutls_datum_t key) {
|
|
|
|
liSSLSessionDB *sdb = _sdb;
|
|
|
|
liSSLSessionDBData *data = li_ssl_session_db_lookup(sdb, key.data, key.size);
|
|
|
|
gnutls_datum_t result = { NULL, 0 };
|
|
|
|
if (NULL != data) {
|
|
|
|
result.size = data->size;
|
|
|
|
result.data = gnutls_malloc(result.size);
|
|
|
|
memcpy(result.data, data->data, result.size);
|
|
|
|
li_ssl_session_db_data_release(data);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const liGnuTLSFilterCallbacks filter_callbacks = {
|
|
|
|
handshake_cb,
|
|
|
|
close_cb,
|
|
|
|
NULL
|
|
|
|
};
|
|
|
|
|
|
|
|
static void gnutls_tcp_finished(liConnection *con, gboolean aborted) {
|
|
|
|
mod_connection_ctx *conctx = con->con_sock.data;
|
|
|
|
UNUSED(aborted);
|
|
|
|
|
|
|
|
con->info.is_ssl = FALSE;
|
|
|
|
con->con_sock.callbacks = NULL;
|
|
|
|
|
|
|
|
if (NULL != conctx) {
|
|
|
|
assert(con == conctx->con);
|
|
|
|
close_cb(conctx->tls_filter, conctx);
|
|
|
|
assert(NULL == con->con_sock.data);
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
liStream *raw_out = con->con_sock.raw_out, *raw_in = con->con_sock.raw_in;
|
|
|
|
con->con_sock.raw_out = con->con_sock.raw_in = NULL;
|
|
|
|
if (NULL != raw_out) { li_stream_reset(raw_out); li_stream_release(raw_out); }
|
|
|
|
if (NULL != raw_in) { li_stream_reset(raw_in); li_stream_release(raw_in); }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static liThrottleState* gnutls_tcp_throttle_out(liConnection *con) {
|
|
|
|
mod_connection_ctx *conctx = con->con_sock.data;
|
|
|
|
if (NULL == conctx) return NULL;
|
|
|
|
if (NULL == conctx->sock_stream->throttle_out) conctx->sock_stream->throttle_out = li_throttle_new();
|
|
|
|
return conctx->sock_stream->throttle_out;
|
|
|
|
}
|
|
|
|
|
|
|
|
static liThrottleState* gnutls_tcp_throttle_in(liConnection *con) {
|
|
|
|
mod_connection_ctx *conctx = con->con_sock.data;
|
|
|
|
if (NULL == conctx) return NULL;
|
|
|
|
if (NULL == conctx->sock_stream->throttle_in) conctx->sock_stream->throttle_in = li_throttle_new();
|
|
|
|
return conctx->sock_stream->throttle_in;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const liConnectionSocketCallbacks gnutls_tcp_cbs = {
|
|
|
|
gnutls_tcp_finished,
|
|
|
|
gnutls_tcp_throttle_out,
|
|
|
|
gnutls_tcp_throttle_in
|
|
|
|
};
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
static void sni_job_cb(liJob *job) {
|
|
|
|
mod_connection_ctx *conctx = LI_CONTAINER_OF(job, mod_connection_ctx, sni_job);
|
|
|
|
|
|
|
|
assert(NULL != conctx->client_hello_stream);
|
|
|
|
|
|
|
|
conctx->sni_entry = li_fetch_get(conctx->ctx->sni_db, conctx->sni_server_name, conctx->sni_jobref, &conctx->sni_db_wait);
|
|
|
|
if (conctx->sni_entry != NULL) {
|
|
|
|
gnutls_certificate_credentials_t creds = conctx->sni_entry->data;
|
|
|
|
if (NULL != creds) {
|
|
|
|
gnutls_credentials_set(conctx->session, GNUTLS_CRD_CERTIFICATE, creds);
|
|
|
|
} else if (NULL != conctx->ctx->sni_fallback_cert) {
|
|
|
|
gnutls_credentials_set(conctx->session, GNUTLS_CRD_CERTIFICATE, conctx->ctx->sni_fallback_cert);
|
|
|
|
}
|
|
|
|
li_ssl_client_hello_stream_ready(conctx->client_hello_stream);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
static void gnutls_client_hello_cb(gpointer data, gboolean success, GString *server_name, guint16 protocol) {
|
|
|
|
mod_connection_ctx *conctx = data;
|
|
|
|
|
|
|
|
if (conctx->ctx->protect_against_beast) {
|
|
|
|
if (!success || protocol <= 0x0301) { /* SSL3: 0x0300, TLS1.0: 0x0301 */
|
|
|
|
gnutls_priority_set(conctx->session, conctx->ctx->server_priority_beast);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
if (success && NULL != server_name && NULL != conctx->ctx->sni_db && server_name->len > 0) {
|
|
|
|
conctx->sni_server_name = g_string_new_len(GSTR_LEN(server_name));
|
|
|
|
sni_job_cb(&conctx->sni_job);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
UNUSED(server_name);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
li_ssl_client_hello_stream_ready(conctx->client_hello_stream);
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean mod_gnutls_con_new(liConnection *con, int fd) {
|
|
|
|
liEventLoop *loop = &con->wrk->loop;
|
|
|
|
liServer *srv = con->srv;
|
|
|
|
mod_context *ctx = con->srv_sock->data;
|
|
|
|
mod_connection_ctx *conctx;
|
|
|
|
gnutls_session_t session;
|
|
|
|
int r;
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_init(&session, GNUTLS_SERVER))) {
|
|
|
|
ERROR(srv, "gnutls_init (%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
mod_gnutls_context_acquire(ctx);
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_priority_set(session, ctx->server_priority))) {
|
|
|
|
ERROR(srv, "gnutls_priority_set (%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, ctx->server_cert))) {
|
|
|
|
ERROR(srv, "gnutls_credentials_set (%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (NULL != ctx->session_db) {
|
|
|
|
gnutls_db_set_ptr(session, ctx->session_db);
|
|
|
|
gnutls_db_set_remove_function(session, session_db_remove_cb);
|
|
|
|
gnutls_db_set_retrieve_function(session, session_db_retrieve_cb);
|
|
|
|
gnutls_db_set_store_function(session, session_db_store_cb);
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef HAVE_SESSION_TICKET
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_session_ticket_enable_server(session, &ctx->ticket_key))) {
|
|
|
|
ERROR(srv, "gnutls_session_ticket_enable_server (%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef GNUTLS_ALPN_MAND
|
|
|
|
{
|
|
|
|
static const gnutls_datum_t proto_http1 = { (unsigned char*) CONST_STR_LEN("http/1.1") };
|
|
|
|
gnutls_alpn_set_protocols(session, &proto_http1, 1, 0);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
conctx = g_slice_new0(mod_connection_ctx);
|
|
|
|
conctx->session = session;
|
|
|
|
conctx->sock_stream = li_iostream_new(con->wrk, fd, tcp_io_cb, conctx);
|
|
|
|
|
|
|
|
conctx->client_hello_stream = li_ssl_client_hello_stream(&con->wrk->loop, gnutls_client_hello_cb, conctx);
|
|
|
|
#ifdef USE_SNI
|
|
|
|
li_job_init(&conctx->sni_job, sni_job_cb);
|
|
|
|
conctx->sni_jobref = li_job_ref(&con->wrk->loop.jobqueue, &conctx->sni_job);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
li_stream_connect(&conctx->sock_stream->stream_in, conctx->client_hello_stream);
|
|
|
|
|
|
|
|
conctx->tls_filter = li_gnutls_filter_new(srv, con->wrk, &filter_callbacks, conctx, conctx->session,
|
|
|
|
conctx->client_hello_stream, &conctx->sock_stream->stream_out);
|
|
|
|
|
|
|
|
conctx->con = con;
|
|
|
|
conctx->ctx = ctx;
|
|
|
|
|
|
|
|
con->con_sock.data = conctx;
|
|
|
|
con->con_sock.callbacks = &gnutls_tcp_cbs;
|
|
|
|
con->con_sock.raw_out = li_stream_plug_new(loop);
|
|
|
|
con->con_sock.raw_in = li_stream_plug_new(loop);
|
|
|
|
con->info.is_ssl = TRUE;
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
fail:
|
|
|
|
gnutls_deinit(session);
|
|
|
|
mod_gnutls_context_release(ctx);
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void mod_gnutls_sock_release(liServerSocket *srv_sock) {
|
|
|
|
mod_context *ctx = srv_sock->data;
|
|
|
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
|
|
|
mod_gnutls_context_release(ctx);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gnutls_setup_listen_cb(liServer *srv, int fd, gpointer data) {
|
|
|
|
mod_context *ctx = data;
|
|
|
|
liServerSocket *srv_sock;
|
|
|
|
UNUSED(data);
|
|
|
|
|
|
|
|
if (-1 == fd) {
|
|
|
|
mod_gnutls_context_release(ctx);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
srv_sock = li_server_listen(srv, fd);
|
|
|
|
|
|
|
|
srv_sock->data = ctx; /* transfer ownership, no refcount change */
|
|
|
|
|
|
|
|
srv_sock->new_cb = mod_gnutls_con_new;
|
|
|
|
srv_sock->release_cb = mod_gnutls_sock_release;
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean gnutls_setup(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
|
|
|
|
mod_context *ctx;
|
|
|
|
int r;
|
|
|
|
|
|
|
|
/* setup defaults */
|
|
|
|
gboolean have_listen_parameter = FALSE;
|
|
|
|
gboolean have_pemfile_parameter = FALSE;
|
|
|
|
gboolean have_protect_beast_parameter = FALSE;
|
|
|
|
gboolean have_session_db_size_parameter = FALSE;
|
|
|
|
const char
|
|
|
|
*priority = NULL, *dh_params_file = NULL
|
|
|
|
#ifdef USE_SNI
|
|
|
|
,*sni_backend = NULL, *sni_fallback_pemfile = NULL
|
|
|
|
#endif
|
|
|
|
;
|
|
|
|
gboolean
|
|
|
|
protect_against_beast = TRUE;
|
|
|
|
gint64 session_db_size = 256;
|
|
|
|
|
|
|
|
UNUSED(p); UNUSED(userdata);
|
|
|
|
|
|
|
|
val = li_value_get_single_argument(val);
|
|
|
|
|
|
|
|
if (NULL == (val = li_value_to_key_value_list(val))) {
|
|
|
|
ERROR(srv, "%s", "gnutls expects a hash/key-value list as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
LI_VALUE_FOREACH(entry, val)
|
|
|
|
liValue *entryKey = li_value_list_at(entry, 0);
|
|
|
|
liValue *entryValue = li_value_list_at(entry, 1);
|
|
|
|
GString *entryKeyStr;
|
|
|
|
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryKey)) {
|
|
|
|
ERROR(srv, "%s", "gnutls doesn't take default keys");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
entryKeyStr = entryKey->data.string; /* keys are either NONE or STRING */
|
|
|
|
|
|
|
|
if (g_str_equal(entryKeyStr->str, "listen")) {
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls listen expects a string as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
have_listen_parameter = TRUE;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "pemfile")) {
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls pemfile expects a string as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
have_pemfile_parameter = TRUE;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "dh-params")) {
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls dh-params expects a string as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (NULL != dh_params_file) {
|
|
|
|
ERROR(srv, "gnutls unexpected duplicate parameter %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
dh_params_file = entryValue->data.string->str;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "priority")) {
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls priority expects a string as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (NULL != priority) {
|
|
|
|
ERROR(srv, "gnutls unexpected duplicate parameter %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
priority = entryValue->data.string->str;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "protect-against-beast")) {
|
|
|
|
if (LI_VALUE_BOOLEAN != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls protect-against-beast expects a boolean as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (have_protect_beast_parameter) {
|
|
|
|
ERROR(srv, "gnutls unexpected duplicate parameter %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
have_protect_beast_parameter = TRUE;
|
|
|
|
protect_against_beast = entryValue->data.boolean;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "session-db-size")) {
|
|
|
|
if (LI_VALUE_NUMBER != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls session-db-size expects an integer as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (have_session_db_size_parameter) {
|
|
|
|
ERROR(srv, "gnutls unexpected duplicate parameter %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
have_session_db_size_parameter = TRUE;
|
|
|
|
session_db_size = entryValue->data.number;
|
|
|
|
#ifdef USE_SNI
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "sni-backend")) {
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls sni-backend expects a string as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (NULL != sni_backend) {
|
|
|
|
ERROR(srv, "gnutls unexpected duplicate parameter %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
sni_backend = entryValue->data.string->str;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "sni-fallback-pemfile")) {
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryValue)) {
|
|
|
|
ERROR(srv, "%s", "gnutls sni-fallback-pemfile expects a string as parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (NULL != sni_fallback_pemfile) {
|
|
|
|
ERROR(srv, "gnutls unexpected duplicate parameter %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
sni_fallback_pemfile = entryValue->data.string->str;
|
|
|
|
#else
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "sni-backend")) {
|
|
|
|
ERROR(srv, "%s", "mod_gnutls was build without SNI support, invalid option sni-backend");
|
|
|
|
return FALSE;
|
|
|
|
} else if (g_str_equal(entryKeyStr->str, "sni-fallback-pemfile")) {
|
|
|
|
ERROR(srv, "%s", "mod_gnutls was build without SNI support, invalid option gnutls sni-fallback-pemfile");
|
|
|
|
return FALSE;
|
|
|
|
#endif
|
|
|
|
} else {
|
|
|
|
ERROR(srv, "invalid parameter for gnutls: %s", entryKeyStr->str);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
LI_VALUE_END_FOREACH()
|
|
|
|
|
|
|
|
if (!have_listen_parameter) {
|
|
|
|
ERROR(srv, "%s", "gnutls needs a listen parameter");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!have_pemfile_parameter) {
|
|
|
|
ERROR(srv, "%s", "gnutls needs a pemfile");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(ctx = mod_gnutls_context_new(srv))) return FALSE;
|
|
|
|
|
|
|
|
ctx->protect_against_beast = protect_against_beast;
|
|
|
|
|
|
|
|
#ifdef USE_SNI
|
|
|
|
if (NULL != sni_backend) {
|
|
|
|
liFetchDatabase *backend = li_server_get_fetch_database(srv, sni_backend);
|
|
|
|
if (NULL == backend) {
|
|
|
|
ERROR(srv, "gnutls: no fetch backend with name '%s' registered", sni_backend);
|
|
|
|
goto error_free_ctx;
|
|
|
|
}
|
|
|
|
ctx->sni_backend_db = backend;
|
|
|
|
mod_gnutls_context_acquire(ctx);
|
|
|
|
ctx->sni_db = li_fetch_database_new(&fetch_cert_callbacks, ctx, 64, 16);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (NULL != sni_fallback_pemfile) {
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_certificate_allocate_credentials(&ctx->sni_fallback_cert))) {
|
|
|
|
ERROR(srv, "gnutls_certificate_allocate_credentials failed(%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error_free_ctx;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_certificate_set_x509_key_file(ctx->sni_fallback_cert, sni_fallback_pemfile, sni_fallback_pemfile, GNUTLS_X509_FMT_PEM))) {
|
|
|
|
ERROR(srv, "gnutls_certificate_set_x509_key_file failed(certfile '%s', keyfile '%s', PEM) (%s): %s",
|
|
|
|
sni_fallback_pemfile, sni_fallback_pemfile,
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error_free_ctx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
LI_VALUE_FOREACH(entry, val)
|
|
|
|
liValue *entryKey = li_value_list_at(entry, 0);
|
|
|
|
liValue *entryValue = li_value_list_at(entry, 1);
|
|
|
|
GString *entryKeyStr;
|
|
|
|
|
|
|
|
if (LI_VALUE_STRING != li_value_type(entryKey)) continue;
|
|
|
|
entryKeyStr = entryKey->data.string; /* keys are either NONE or STRING */
|
|
|
|
|
|
|
|
if (g_str_equal(entryKeyStr->str, "pemfile")) {
|
|
|
|
const char *pemfile = entryValue->data.string->str;
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_certificate_set_x509_key_file(ctx->server_cert, pemfile, pemfile, GNUTLS_X509_FMT_PEM))) {
|
|
|
|
ERROR(srv, "gnutls_certificate_set_x509_key_file failed(certfile '%s', keyfile '%s', PEM) (%s): %s",
|
|
|
|
pemfile, pemfile,
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error_free_ctx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
LI_VALUE_END_FOREACH()
|
|
|
|
|
|
|
|
if (session_db_size > 0) ctx->session_db = li_ssl_session_db_new(session_db_size);
|
|
|
|
|
|
|
|
if (NULL != dh_params_file) {
|
|
|
|
gchar *contents = NULL;
|
|
|
|
gsize length = 0;
|
|
|
|
GError *error = NULL;
|
|
|
|
gnutls_datum_t pkcs3_params;
|
|
|
|
|
|
|
|
if (GNUTLS_E_SUCCESS != (r = gnutls_dh_params_init(&ctx->dh_params))) {
|
|
|
|
ERROR(srv, "gnutls_dh_params_init failed (%s): %s",
|
|
|
|
gnutls_strerror_name(r), gnutls_strerror(r));
|
|
|
|
goto error_free_ctx;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!g_file_get_contents(dh_params_file, &contents, &length, &error)) {
|
|
|
|
ERROR(srv, "Unable to read dh-params file '%s': %s", dh_params_file, error->message);
|
|
|
|
g_error_free(error);
|
|
|
|
goto error_free_ctx;
|
|
|
|
|