/* * Copyright (C) Igor Sysoev * Copyright (C) NGINX, Inc. */ #include #include 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)); }