diff options
Diffstat (limited to '')
-rw-r--r-- | src/nxt_gnutls.c | 742 |
1 files changed, 742 insertions, 0 deletions
diff --git a/src/nxt_gnutls.c b/src/nxt_gnutls.c new file mode 100644 index 00000000..15db7fc8 --- /dev/null +++ b/src/nxt_gnutls.c @@ -0,0 +1,742 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) NGINX, Inc. + */ + +#include <nxt_main.h> +#include <gnutls/gnutls.h> + + +typedef struct { + gnutls_session_t session; + + uint8_t times; /* 2 bits */ + uint8_t no_shutdown; /* 1 bit */ + + nxt_buf_mem_t buffer; +} nxt_gnutls_conn_t; + + +typedef struct { + gnutls_priority_t ciphers; + gnutls_certificate_credentials_t certificate; +} nxt_gnutls_ctx_t; + + + +#if (NXT_HAVE_GNUTLS_SET_TIME) +time_t nxt_gnutls_time(time_t *tp); +#endif +static nxt_int_t nxt_gnutls_server_init(nxt_ssltls_conf_t *conf); +static nxt_int_t nxt_gnutls_set_ciphers(nxt_ssltls_conf_t *conf); + +static void nxt_gnutls_conn_init(nxt_thread_t *thr, nxt_ssltls_conf_t *conf, + nxt_event_conn_t *c); +static void nxt_gnutls_session_cleanup(void *data); +static ssize_t nxt_gnutls_pull(gnutls_transport_ptr_t data, void *buf, + size_t size); +static ssize_t nxt_gnutls_push(gnutls_transport_ptr_t data, const void *buf, + size_t size); +#if (NXT_HAVE_GNUTLS_VEC_PUSH) +static ssize_t nxt_gnutls_vec_push(gnutls_transport_ptr_t data, + const giovec_t *iov, int iovcnt); +#endif +static void nxt_gnutls_conn_handshake(nxt_thread_t *thr, void *obj, void *data); +static void nxt_gnutls_conn_io_read(nxt_thread_t *thr, void *obj, void *data); +static ssize_t nxt_gnutls_conn_io_write_chunk(nxt_thread_t *thr, + nxt_event_conn_t *c, nxt_buf_t *b, size_t limit); +static ssize_t nxt_gnutls_conn_io_send(nxt_event_conn_t *c, void *buf, + size_t size); +static void nxt_gnutls_conn_io_shutdown(nxt_thread_t *thr, void *obj, + void *data); +static nxt_int_t nxt_gnutls_conn_test_error(nxt_thread_t *thr, + nxt_event_conn_t *c, ssize_t err, nxt_work_handler_t handler); +static void nxt_cdecl nxt_gnutls_conn_log_error(nxt_event_conn_t *c, + ssize_t err, const char *fmt, ...); +static nxt_uint_t nxt_gnutls_log_error_level(nxt_event_conn_t *c, ssize_t err); +static void nxt_cdecl nxt_gnutls_log_error(nxt_uint_t level, nxt_log_t *log, + int err, const char *fmt, ...); + + +const nxt_ssltls_lib_t nxt_gnutls_lib = { + nxt_gnutls_server_init, + NULL, +}; + + +static nxt_event_conn_io_t nxt_gnutls_event_conn_io = { + NULL, + NULL, + + nxt_gnutls_conn_io_read, + NULL, + NULL, + + nxt_event_conn_io_write, + nxt_gnutls_conn_io_write_chunk, + NULL, + NULL, + nxt_gnutls_conn_io_send, + + nxt_gnutls_conn_io_shutdown, +}; + + +static nxt_int_t +nxt_gnutls_start(void) +{ + int ret; + static nxt_bool_t started; + + if (nxt_fast_path(started)) { + return NXT_OK; + } + + started = 1; + + /* TODO: gnutls_global_deinit */ + + ret = gnutls_global_init(); + if (ret != GNUTLS_E_SUCCESS) { + nxt_gnutls_log_error(NXT_LOG_CRIT, nxt_thread_log(), ret, + "gnutls_global_init() failed"); + return NXT_ERROR; + } + + nxt_thread_log_error(NXT_LOG_INFO, "GnuTLS version: %s", + gnutls_check_version(NULL)); + +#if (NXT_HAVE_GNUTLS_SET_TIME) + gnutls_global_set_time_function(nxt_gnutls_time); +#endif + + return NXT_OK; +} + + +#if (NXT_HAVE_GNUTLS_SET_TIME) + +/* GnuTLS 2.12.0 */ + +time_t +nxt_gnutls_time(time_t *tp) +{ + time_t t; + nxt_thread_t *thr; + + thr = nxt_thread(); + nxt_log_debug(thr->log, "gnutls time"); + + t = (time_t) nxt_thread_time(thr); + + if (tp != NULL) { + *tp = t; + } + + return t; +} + +#endif + + +static nxt_int_t +nxt_gnutls_server_init(nxt_ssltls_conf_t *conf) +{ + int ret; + char *certificate, *key, *ca_certificate; + nxt_thread_t *thr; + nxt_gnutls_ctx_t *ctx; + + if (nxt_slow_path(nxt_gnutls_start() != NXT_OK)) { + return NXT_ERROR; + } + + /* TODO: mem_pool, cleanup: gnutls_certificate_free_credentials, + gnutls_priority_deinit */ + + ctx = nxt_zalloc(sizeof(nxt_gnutls_ctx_t)); + if (ctx == NULL) { + return NXT_ERROR; + } + + conf->ctx = ctx; + conf->conn_init = nxt_gnutls_conn_init; + + thr = nxt_thread(); + + ret = gnutls_certificate_allocate_credentials(&ctx->certificate); + if (ret != GNUTLS_E_SUCCESS) { + nxt_gnutls_log_error(NXT_LOG_CRIT, thr->log, ret, + "gnutls_certificate_allocate_credentials() failed"); + return NXT_ERROR; + } + + certificate = conf->certificate; + key = conf->certificate_key; + + ret = gnutls_certificate_set_x509_key_file(ctx->certificate, certificate, + key, GNUTLS_X509_FMT_PEM); + if (ret != GNUTLS_E_SUCCESS) { + nxt_gnutls_log_error(NXT_LOG_CRIT, thr->log, ret, + "gnutls_certificate_set_x509_key_file(\"%s\", \"%s\") failed", + certificate, key); + goto certificate_fail; + } + + if (nxt_gnutls_set_ciphers(conf) != NXT_OK) { + goto ciphers_fail; + } + + if (conf->ca_certificate != NULL) { + ca_certificate = conf->ca_certificate; + + ret = gnutls_certificate_set_x509_trust_file(ctx->certificate, + ca_certificate, + GNUTLS_X509_FMT_PEM); + if (ret < 0) { + nxt_gnutls_log_error(NXT_LOG_CRIT, thr->log, ret, + "gnutls_certificate_set_x509_trust_file(\"%s\") failed", + ca_certificate); + goto ca_certificate_fail; + } + } + + return NXT_OK; + +ca_certificate_fail: + + gnutls_priority_deinit(ctx->ciphers); + +ciphers_fail: + +certificate_fail: + + gnutls_certificate_free_credentials(ctx->certificate); + + return NXT_ERROR; +} + + +static nxt_int_t +nxt_gnutls_set_ciphers(nxt_ssltls_conf_t *conf) +{ + int ret; + const char *ciphers; + const char *err; + nxt_gnutls_ctx_t *ctx; + + ciphers = (conf->ciphers != NULL) ? conf->ciphers : "NORMAL:!COMP-DEFLATE"; + ctx = conf->ctx; + + ret = gnutls_priority_init(&ctx->ciphers, ciphers, &err); + + switch (ret) { + + case GNUTLS_E_SUCCESS: + return NXT_OK; + + case GNUTLS_E_INVALID_REQUEST: + nxt_gnutls_log_error(NXT_LOG_CRIT, nxt_thread_log(), ret, + "gnutls_priority_init(\"%s\") failed at \"%s\"", + ciphers, err); + return NXT_ERROR; + + default: + nxt_gnutls_log_error(NXT_LOG_CRIT, nxt_thread_log(), ret, + "gnutls_priority_init() failed"); + return NXT_ERROR; + } +} + + +static void +nxt_gnutls_conn_init(nxt_thread_t *thr, nxt_ssltls_conf_t *conf, + nxt_event_conn_t *c) +{ + int ret; + gnutls_session_t sess; + nxt_gnutls_ctx_t *ctx; + nxt_gnutls_conn_t *ssltls; + nxt_mem_pool_cleanup_t *mpcl; + + nxt_log_debug(c->socket.log, "gnutls conn init"); + + ssltls = nxt_mem_zalloc(c->mem_pool, sizeof(nxt_gnutls_conn_t)); + if (ssltls == NULL) { + goto fail; + } + + c->u.ssltls = ssltls; + nxt_buf_mem_set_size(&ssltls->buffer, conf->buffer_size); + + mpcl = nxt_mem_pool_cleanup(c->mem_pool, 0); + if (mpcl == NULL) { + goto fail; + } + + ret = gnutls_init(&ssltls->session, GNUTLS_SERVER); + if (ret != GNUTLS_E_SUCCESS) { + nxt_gnutls_log_error(NXT_LOG_CRIT, c->socket.log, ret, + "gnutls_init() failed"); + goto fail; + } + + sess = ssltls->session; + mpcl->handler = nxt_gnutls_session_cleanup; + mpcl->data = ssltls; + + ctx = conf->ctx; + + ret = gnutls_priority_set(sess, ctx->ciphers); + if (ret != GNUTLS_E_SUCCESS) { + nxt_gnutls_log_error(NXT_LOG_CRIT, c->socket.log, ret, + "gnutls_priority_set() failed"); + goto fail; + } + + /* + * Disable TLS random padding of records in CBC ciphers, + * which may be up to 255 bytes. + */ + gnutls_record_disable_padding(sess); + + ret = gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, + ctx->certificate); + if (ret != GNUTLS_E_SUCCESS) { + nxt_gnutls_log_error(NXT_LOG_CRIT, c->socket.log, ret, + "gnutls_credentials_set() failed"); + goto fail; + } + + if (conf->ca_certificate != NULL) { + gnutls_certificate_server_set_request(sess, GNUTLS_CERT_REQUEST); + } + + gnutls_transport_set_ptr(sess, (gnutls_transport_ptr_t) c); + gnutls_transport_set_pull_function(sess, nxt_gnutls_pull); + gnutls_transport_set_push_function(sess, nxt_gnutls_push); +#if (NXT_HAVE_GNUTLS_VEC_PUSH) + gnutls_transport_set_vec_push_function(sess, nxt_gnutls_vec_push); +#endif + + c->io = &nxt_gnutls_event_conn_io; + c->sendfile = NXT_CONN_SENDFILE_OFF; + + nxt_gnutls_conn_handshake(thr, c, c->socket.data); + return; + +fail: + + nxt_event_conn_io_handle(thr, c->read_work_queue, + c->read_state->error_handler, c, c->socket.data); +} + + +static void +nxt_gnutls_session_cleanup(void *data) +{ + nxt_gnutls_conn_t *ssltls; + + ssltls = data; + + nxt_thread_log_debug("gnutls session cleanup"); + + nxt_free(ssltls->buffer.start); + + gnutls_deinit(ssltls->session); +} + + +static ssize_t +nxt_gnutls_pull(gnutls_transport_ptr_t data, void *buf, size_t size) +{ + ssize_t n; + nxt_thread_t *thr; + nxt_event_conn_t *c; + + c = data; + thr = nxt_thread(); + + n = thr->engine->event->io->recv(c, buf, size, 0); + + if (n == NXT_AGAIN) { + nxt_set_errno(NXT_EAGAIN); + return -1; + } + + return n; +} + + +static ssize_t +nxt_gnutls_push(gnutls_transport_ptr_t data, const void *buf, size_t size) +{ + ssize_t n; + nxt_thread_t *thr; + nxt_event_conn_t *c; + + c = data; + thr = nxt_thread(); + + n = thr->engine->event->io->send(c, (u_char *) buf, size); + + if (n == NXT_AGAIN) { + nxt_set_errno(NXT_EAGAIN); + return -1; + } + + return n; +} + + +#if (NXT_HAVE_GNUTLS_VEC_PUSH) + +/* GnuTLS 2.12.0 */ + +static ssize_t +nxt_gnutls_vec_push(gnutls_transport_ptr_t data, const giovec_t *iov, + int iovcnt) +{ + ssize_t n; + nxt_thread_t *thr; + nxt_event_conn_t *c; + + c = data; + thr = nxt_thread(); + + /* + * This code assumes that giovec_t is the same as "struct iovec" + * and nxt_iobuf_t. It is not true for Windows. + */ + n = thr->engine->event->io->writev(c, (nxt_iobuf_t *) iov, iovcnt); + + if (n == NXT_AGAIN) { + nxt_set_errno(NXT_EAGAIN); + return -1; + } + + return n; +} + +#endif + + +static void +nxt_gnutls_conn_handshake(nxt_thread_t *thr, void *obj, void *data) +{ + int err; + nxt_int_t ret; + nxt_event_conn_t *c; + nxt_gnutls_conn_t *ssltls; + + c = obj; + ssltls = c->u.ssltls; + + nxt_log_debug(thr->log, "gnutls conn handshake: %d", ssltls->times); + + /* "ssltls->times == 1" is suitable to run gnutls_handshake() in job. */ + + err = gnutls_handshake(ssltls->session); + + nxt_thread_time_debug_update(thr); + + nxt_log_debug(thr->log, "gnutls_handshake(): %d", err); + + if (err == GNUTLS_E_SUCCESS) { + nxt_gnutls_conn_io_read(thr, c, data); + return; + } + + ret = nxt_gnutls_conn_test_error(thr, c, err, nxt_gnutls_conn_handshake); + + if (ret == NXT_ERROR) { + nxt_gnutls_conn_log_error(c, err, "gnutls_handshake() failed"); + + nxt_event_conn_io_handle(thr, c->read_work_queue, + c->read_state->error_handler, c, data); + + } else if (err == GNUTLS_E_AGAIN + && ssltls->times < 2 + && gnutls_record_get_direction(ssltls->session) == 0) + { + ssltls->times++; + } +} + + +static void +nxt_gnutls_conn_io_read(nxt_thread_t *thr, void *obj, void *data) +{ + ssize_t n; + nxt_buf_t *b; + nxt_int_t ret; + nxt_event_conn_t *c; + nxt_gnutls_conn_t *ssltls; + nxt_work_handler_t handler; + + c = obj; + + nxt_log_debug(thr->log, "gnutls conn read"); + + handler = c->read_state->ready_handler; + b = c->read; + + /* b == NULL is used to test descriptor readiness. */ + + if (b != NULL) { + ssltls = c->u.ssltls; + + n = gnutls_record_recv(ssltls->session, b->mem.free, + b->mem.end - b->mem.free); + + nxt_log_debug(thr->log, "gnutls_record_recv(%d, %p, %uz): %z", + c->socket.fd, b->mem.free, b->mem.end - b->mem.free, n); + + if (n > 0) { + /* c->socket.read_ready is kept. */ + b->mem.free += n; + handler = c->read_state->ready_handler; + + } else if (n == 0) { + handler = c->read_state->close_handler; + + } else { + ret = nxt_gnutls_conn_test_error(thr, c, n, + nxt_gnutls_conn_io_read); + + if (nxt_fast_path(ret != NXT_ERROR)) { + return; + } + + nxt_gnutls_conn_log_error(c, n, + "gnutls_record_recv(%d, %p, %uz): failed", + c->socket.fd, b->mem.free, + b->mem.end - b->mem.free); + + handler = c->read_state->error_handler; + } + } + + nxt_event_conn_io_handle(thr, c->read_work_queue, handler, c, data); +} + + +static ssize_t +nxt_gnutls_conn_io_write_chunk(nxt_thread_t *thr, nxt_event_conn_t *c, + nxt_buf_t *b, size_t limit) +{ + nxt_gnutls_conn_t *ssltls; + + nxt_log_debug(thr->log, "gnutls conn write chunk"); + + ssltls = c->u.ssltls; + + return nxt_sendbuf_copy_coalesce(c, &ssltls->buffer, b, limit); +} + + +static ssize_t +nxt_gnutls_conn_io_send(nxt_event_conn_t *c, void *buf, size_t size) +{ + ssize_t n; + nxt_int_t ret; + nxt_gnutls_conn_t *ssltls; + + ssltls = c->u.ssltls; + + n = gnutls_record_send(ssltls->session, buf, size); + + nxt_log_debug(c->socket.log, "gnutls_record_send(%d, %p, %uz): %z", + c->socket.fd, buf, size, n); + + if (n > 0) { + return n; + } + + ret = nxt_gnutls_conn_test_error(nxt_thread(), c, n, + nxt_event_conn_io_write); + + if (nxt_slow_path(ret == NXT_ERROR)) { + nxt_gnutls_conn_log_error(c, n, + "gnutls_record_send(%d, %p, %uz): failed", + c->socket.fd, buf, size); + } + + return ret; +} + + +static void +nxt_gnutls_conn_io_shutdown(nxt_thread_t *thr, void *obj, void *data) +{ + int err; + nxt_int_t ret; + nxt_event_conn_t *c; + nxt_gnutls_conn_t *ssltls; + nxt_work_handler_t handler; + gnutls_close_request_t how; + + c = obj; + ssltls = c->u.ssltls; + + if (ssltls->session == NULL || ssltls->no_shutdown) { + handler = c->write_state->close_handler; + goto done; + } + + nxt_log_debug(c->socket.log, "gnutls conn shutdown"); + + if (c->socket.timedout || c->socket.error != 0) { + how = GNUTLS_SHUT_WR; + + } else if (c->socket.closed) { + how = GNUTLS_SHUT_RDWR; + + } else { + how = GNUTLS_SHUT_RDWR; + } + + err = gnutls_bye(ssltls->session, how); + + nxt_log_debug(c->socket.log, "gnutls_bye(%d, %d): %d", + c->socket.fd, how, err); + + if (err == GNUTLS_E_SUCCESS) { + handler = c->write_state->close_handler; + + } else { + ret = nxt_gnutls_conn_test_error(thr, c, err, + nxt_gnutls_conn_io_shutdown); + + if (ret != NXT_ERROR) { /* ret == NXT_AGAIN */ + c->socket.error_handler = c->read_state->error_handler; + nxt_event_timer_add(thr->engine, &c->read_timer, 5000); + return; + } + + nxt_gnutls_conn_log_error(c, err, "gnutls_bye(%d) failed", + c->socket.fd); + + handler = c->write_state->error_handler; + } + +done: + + nxt_event_conn_io_handle(thr, c->write_work_queue, handler, c, data); +} + + +static nxt_int_t +nxt_gnutls_conn_test_error(nxt_thread_t *thr, nxt_event_conn_t *c, ssize_t err, + nxt_work_handler_t handler) +{ + int ret; + nxt_gnutls_conn_t *ssltls; + + switch (err) { + + case GNUTLS_E_REHANDSHAKE: + case GNUTLS_E_AGAIN: + ssltls = c->u.ssltls; + ret = gnutls_record_get_direction(ssltls->session); + + nxt_log_debug(thr->log, "gnutls_record_get_direction(): %d", ret); + + if (ret == 0) { + /* A read direction. */ + + nxt_event_fd_block_write(thr->engine, &c->socket); + + c->socket.read_ready = 0; + c->socket.read_handler = handler; + + if (nxt_event_fd_is_disabled(c->socket.read)) { + nxt_event_fd_enable_read(thr->engine, &c->socket); + } + + } else { + /* A write direction. */ + + nxt_event_fd_block_read(thr->engine, &c->socket); + + c->socket.write_ready = 0; + c->socket.write_handler = handler; + + if (nxt_event_fd_is_disabled(c->socket.write)) { + nxt_event_fd_enable_write(thr->engine, &c->socket); + } + } + + return NXT_AGAIN; + + default: + c->socket.error = 1000; /* Nonexistent errno code. */ + return NXT_ERROR; + } +} + + +static void +nxt_gnutls_conn_log_error(nxt_event_conn_t *c, ssize_t err, + const char *fmt, ...) +{ + va_list args; + nxt_uint_t level; + u_char *p, msg[NXT_MAX_ERROR_STR]; + + level = nxt_gnutls_log_error_level(c, err); + + if (nxt_log_level_enough(c->socket.log, level)) { + + va_start(args, fmt); + p = nxt_vsprintf(msg, msg + sizeof(msg), fmt, args); + va_end(args); + + nxt_log_error(level, c->socket.log, "%*s (%d: %s)", + p - msg, msg, err, gnutls_strerror(err)); + } +} + + +static nxt_uint_t +nxt_gnutls_log_error_level(nxt_event_conn_t *c, ssize_t err) +{ + nxt_gnutls_conn_t *ssltls; + + switch (err) { + + case GNUTLS_E_UNKNOWN_CIPHER_SUITE: /* -21 */ + + /* Disable gnutls_bye(), because it returns GNUTLS_E_INTERNAL_ERROR. */ + ssltls = c->u.ssltls; + ssltls->no_shutdown = 1; + + /* Fall through. */ + + case GNUTLS_E_UNEXPECTED_PACKET_LENGTH: /* -9 */ + c->socket.error = 1000; /* Nonexistent errno code. */ + break; + + default: + return NXT_LOG_CRIT; + } + + return NXT_LOG_INFO; +} + + +static void +nxt_gnutls_log_error(nxt_uint_t level, nxt_log_t *log, int err, + const char *fmt, ...) +{ + va_list args; + u_char *p, msg[NXT_MAX_ERROR_STR]; + + va_start(args, fmt); + p = nxt_vsprintf(msg, msg + sizeof(msg), fmt, args); + va_end(args); + + nxt_log_error(level, log, "%*s (%d: %s)", + p - msg, msg, err, gnutls_strerror(err)); +} |