From a34659351fde72fdde137df87b3de45cfae033f4 Mon Sep 17 00:00:00 2001
From: godzie44 <godzie@yandex.ru>
Date: Fri, 2 Feb 2024 17:04:38 +0300
Subject: [PATCH] feat: IPROTO traffic encryption
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add new transport for IPROTO connections: ssl.
SSL transport may be configured with (at client and server side):
- certificate (mandatory at server side)
- private key (mandatory at server side)
- password for PK
- certificate authorities (for peer certificate verification)
- cipher list

SSL transport also can be used in replication and net.box mechanisms.

@TarantoolBot document
Title: add IPROTO traffic encryption.

New ssl transport allows
creating a secure connection between two IPROTO peers.
TLS protocol using and openssl v1.1 or later required.

To configure traffic encryption, you need to set the
special URI parameters for a particular connection.
The parameters can be set for the following box.cfg
options and nex.box method:
- box.cfg.listen – on the server side.
- box.cfg.replication–on the client side.
- net_box_object.connect()–on the client side.
---
 CMakeLists.txt                                |   1 +
 changelogs/unreleased/encrypted-iproto.md     |  10 +
 src/lib/core/CMakeLists.txt                   |   2 +
 src/lib/core/iostream.h                       |   8 +
 src/lib/core/sio.c                            |  56 ++
 src/lib/core/sio.h                            |  22 +
 src/lib/core/ssl_error.cc                     |   4 -
 src/lib/core/ssl_error.h                      |   7 -
 src/lib/core/ssl_impl.c                       | 560 ++++++++++++++++++
 src/lib/core/ssl_impl.h                       |  66 +++
 test/box-luatest/transport_test.lua           |  25 -
 test/ssl-luatest/basic_test.lua               | 510 ++++++++++++++++
 test/ssl-luatest/certs/ca-sign-cert.crt       |  22 +
 test/ssl-luatest/certs/ca-sign-key.key        |  28 +
 test/ssl-luatest/certs/ca.pem                 |  21 +
 .../ssl-luatest/certs/client/ca-sign-cert.crt |  22 +
 test/ssl-luatest/certs/client/ca-sign-key.key |  28 +
 .../certs/client/self-sign-cert.pem           |  34 ++
 .../certs/client/self-sign-key.pem            |  52 ++
 test/ssl-luatest/certs/pw-wrong.txt           |   2 +
 test/ssl-luatest/certs/pw.txt                 |   2 +
 test/ssl-luatest/certs/self-sign-cert.pem     |  34 ++
 test/ssl-luatest/certs/self-sign-key.pem      |  52 ++
 test/ssl-luatest/certs/self-sign-pw-cert.crt  |  22 +
 test/ssl-luatest/certs/self-sign-pw-key.key   |  30 +
 .../ssl-luatest/certs/self-sign-wrong-key.pem |  28 +
 test/ssl-luatest/net_box_test.lua             | 301 ++++++++++
 test/ssl-luatest/replication_test.lua         | 190 ++++++
 test/ssl-luatest/suite.ini                    |   5 +
 29 files changed, 2108 insertions(+), 36 deletions(-)
 create mode 100644 changelogs/unreleased/encrypted-iproto.md
 create mode 100644 src/lib/core/ssl_impl.c
 create mode 100644 src/lib/core/ssl_impl.h
 create mode 100644 test/ssl-luatest/basic_test.lua
 create mode 100644 test/ssl-luatest/certs/ca-sign-cert.crt
 create mode 100644 test/ssl-luatest/certs/ca-sign-key.key
 create mode 100644 test/ssl-luatest/certs/ca.pem
 create mode 100644 test/ssl-luatest/certs/client/ca-sign-cert.crt
 create mode 100644 test/ssl-luatest/certs/client/ca-sign-key.key
 create mode 100644 test/ssl-luatest/certs/client/self-sign-cert.pem
 create mode 100644 test/ssl-luatest/certs/client/self-sign-key.pem
 create mode 100644 test/ssl-luatest/certs/pw-wrong.txt
 create mode 100644 test/ssl-luatest/certs/pw.txt
 create mode 100644 test/ssl-luatest/certs/self-sign-cert.pem
 create mode 100644 test/ssl-luatest/certs/self-sign-key.pem
 create mode 100644 test/ssl-luatest/certs/self-sign-pw-cert.crt
 create mode 100644 test/ssl-luatest/certs/self-sign-pw-key.key
 create mode 100644 test/ssl-luatest/certs/self-sign-wrong-key.pem
 create mode 100644 test/ssl-luatest/net_box_test.lua
 create mode 100644 test/ssl-luatest/replication_test.lua
 create mode 100644 test/ssl-luatest/suite.ini

diff --git a/CMakeLists.txt b/CMakeLists.txt
index c84a0a65a7..cbe4291f5c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -464,6 +464,7 @@ endif()
 #
 # OpenSSL
 #
+SET(ENABLE_SSL ON)
 
 option(OPENSSL_USE_STATIC_LIBS "Link OpenSSL statically"
     ${BUILD_STATIC})
diff --git a/changelogs/unreleased/encrypted-iproto.md b/changelogs/unreleased/encrypted-iproto.md
new file mode 100644
index 0000000000..c668623796
--- /dev/null
+++ b/changelogs/unreleased/encrypted-iproto.md
@@ -0,0 +1,10 @@
+## feature/iproto
+
+* Add new transport for iproto connection: 'ssl' with additional settings at server
+side and client side:
+  * certificates and private keys
+  * passwords for private keys
+  * client/server CA for certificate verification
+  * cipher list
+* New 'ssl' transport has compatibility with `net.box` and replication mechanisms
+* libssl 1.1 or later requires
diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index bd0d901fd0..2fb1a3494a 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -61,6 +61,8 @@ else()
     list(APPEND core_sources  tt_compression.c)
 endif()
 
+SET(SSL_SOURCES ssl_impl.c ssl_error.cc)
+
 if(ENABLE_SSL)
     list(APPEND core_sources ${SSL_SOURCES})
 else()
diff --git a/src/lib/core/iostream.h b/src/lib/core/iostream.h
index 760c691231..84badcfb3c 100644
--- a/src/lib/core/iostream.h
+++ b/src/lib/core/iostream.h
@@ -71,6 +71,14 @@ enum iostream_flag {
 	 * Set if the iostream is encrypted (e.g. with SSL/TLS).
 	 */
 	IOSTREAM_IS_ENCRYPTED = 1 << 0,
+	/**
+	 * Set if the encrypted iostream is ready for read/write.
+	 */
+	SSL_IOSTREAM_SESSION_READY = 1 << 1,
+	/**
+	 * Set if last read/write call on encrypted iostream return an error.
+	 */
+	SSL_IOSTREAM_POISON = 1 << 2,
 };
 
 /**
diff --git a/src/lib/core/sio.c b/src/lib/core/sio.c
index c9bfa13b56..5ee6bf736c 100644
--- a/src/lib/core/sio.c
+++ b/src/lib/core/sio.c
@@ -50,6 +50,17 @@
 static_assert(SMALL_STATIC_SIZE > NI_MAXHOST + NI_MAXSERV,
 	      "static buffer should fit host name");
 
+static inline bool
+ssl_is_want_error(SSL *ssl, int err)
+{
+	int ssl_err = SSL_get_error(ssl, err);
+	return ssl_err == SSL_ERROR_WANT_READ ||
+	       ssl_err == SSL_ERROR_WANT_WRITE ||
+	       ssl_err == SSL_ERROR_WANT_ACCEPT ||
+	       ssl_err == SSL_ERROR_WANT_CONNECT ||
+	       ssl_err == SSL_ERROR_ZERO_RETURN;
+}
+
 /**
  * Safely print a socket description to the given buffer, with correct overflow
  * checks and all.
@@ -330,6 +341,17 @@ sio_read(int fd, void *buf, size_t count)
 	return n;
 }
 
+int
+ssl_sio_read(SSL *ssl, int fd, void *buf, size_t count)
+{
+	int n = SSL_read(ssl, buf, (int)count);
+	if (n <= 0 && !ssl_is_want_error(ssl, n))
+		diag_set(SocketError, sio_socketname(fd), "ssl_read(%zd)",
+			 count);
+
+	return n;
+}
+
 ssize_t
 sio_write(int fd, const void *buf, size_t count)
 {
@@ -340,6 +362,18 @@ sio_write(int fd, const void *buf, size_t count)
 	return n;
 }
 
+int
+ssl_sio_write(SSL *ssl, int fd, const void *buf, size_t count)
+{
+	assert(count);
+	int n = SSL_write(ssl, buf, (int)count);
+	if (n <= 0 && !ssl_is_want_error(ssl, n))
+		diag_set(SocketError, sio_socketname(fd), "ssl_write(%zd)",
+			 count);
+
+	return n;
+}
+
 ssize_t
 sio_writev(int fd, const struct iovec *iov, int iovcnt)
 {
@@ -350,6 +384,28 @@ sio_writev(int fd, const struct iovec *iov, int iovcnt)
 	return n;
 }
 
+int
+ssl_sio_writev(SSL *ssl, int fd, const struct iovec *iov, int iovcnt)
+{
+	int cnt = iovcnt < IOV_MAX ? iovcnt : IOV_MAX;
+
+	int written = 0;
+	for (int i = 0; i < cnt; ++i) {
+		const void *buffer = iov[i].iov_base;
+		int r = ssl_sio_write(ssl, fd, buffer, iov[i].iov_len);
+		if (r < 0) {
+			if (ssl_is_want_error(ssl, r) || written > 0)
+				return written;
+			else
+				return r;
+		}
+		written += r;
+		if ((size_t)r != iov[i].iov_len)
+			break;
+	}
+	return written;
+}
+
 ssize_t
 sio_sendto(int fd, const void *buf, size_t len, int flags,
 	   const struct sockaddr *dest_addr, socklen_t addrlen)
diff --git a/src/lib/core/sio.h b/src/lib/core/sio.h
index 1687d3403d..501a3b9de1 100644
--- a/src/lib/core/sio.h
+++ b/src/lib/core/sio.h
@@ -47,6 +47,7 @@
 #include <fcntl.h>
 #include <tarantool_ev.h>
 #include <errno.h>
+#include <openssl/ssl.h>
 
 #if defined(__cplusplus)
 extern "C" {
@@ -215,6 +216,27 @@ ssize_t sio_write(int fd, const void *buf, size_t count);
  */
 ssize_t sio_writev(int fd, const struct iovec *iov, int iovcnt);
 
+/**
+ * Read *up to* 'count' bytes from a ssl socket.
+ * The diagnostics is not set for ssl_is_want_error() errors.
+ */
+int
+ssl_sio_read(SSL *ssl, int fd, void *buf, size_t count);
+
+/**
+ * Write up to 'count' bytes to a ssl socket.
+ * The diagnostics is not set in case of ssl_is_want_error() errors.
+ */
+int
+ssl_sio_write(SSL *ssl, int fd, const void *buf, size_t count);
+
+/**
+ * Write to a ssl socket with iovec.
+ * The diagnostics is not set in case of ssl_is_want_error() errors.
+ */
+int
+ssl_sio_writev(SSL *ssl, int fd, const struct iovec *iov, int iovcnt);
+
 /**
  * Send a message on a socket.
  * The diagnostics is not set for sio_wouldblock() errors.
diff --git a/src/lib/core/ssl_error.cc b/src/lib/core/ssl_error.cc
index d7750888e2..3dad98dae1 100644
--- a/src/lib/core/ssl_error.cc
+++ b/src/lib/core/ssl_error.cc
@@ -13,10 +13,6 @@
 #include "trivia/config.h"
 #include "trivia/util.h"
 
-#if defined(ENABLE_SSL)
-# error unimplemented
-#endif
-
 const struct type_info type_SSLError = make_type("SSLError", NULL);
 
 struct error *
diff --git a/src/lib/core/ssl_error.h b/src/lib/core/ssl_error.h
index c6c78df21f..3ab1dc8ab3 100644
--- a/src/lib/core/ssl_error.h
+++ b/src/lib/core/ssl_error.h
@@ -6,11 +6,6 @@
 #pragma once
 
 #include "trivia/config.h"
-
-#if defined(ENABLE_SSL)
-# include "ssl_error_impl.h"
-#else /* !defined(ENABLE_SSL) */
-
 #include <stddef.h>
 
 #include "exception.h"
@@ -38,5 +33,3 @@ class SSLError: public Exception {
 };
 
 #endif /* defined(__cplusplus) */
-
-#endif /* !defined(ENABLE_SSL) */
diff --git a/src/lib/core/ssl_impl.c b/src/lib/core/ssl_impl.c
new file mode 100644
index 0000000000..f1ee092c9f
--- /dev/null
+++ b/src/lib/core/ssl_impl.c
@@ -0,0 +1,560 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include "ssl.h"
+
+#include <stddef.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+
+#include "diag.h"
+#include "iostream.h"
+#include "ssl_init.h"
+#include "sio.h"
+#include "coio.h"
+#include "uri/uri.h"
+#include "tt_static.h"
+#include "ssl_error.h"
+#include "coio_task.h"
+
+void
+ssl_init(void)
+{
+	ssl_init_impl();
+}
+
+void
+ssl_free(void)
+{
+	ssl_free_impl();
+}
+
+/**
+ * Return true if ssl error is fatal (most of operations
+ * must be cancelled in this case).
+ */
+static inline bool
+ssl_is_fatal_error(int ssl_err)
+{
+	return ssl_err == SSL_ERROR_SYSCALL || ssl_err == SSL_ERROR_SSL;
+}
+
+/**
+ * Return string representation of last ssl error.
+ */
+static inline const char*
+ssl_last_error(void)
+{
+	char *buf = tt_static_buf();
+	ERR_error_string_n(ERR_get_error(), buf, TT_STATIC_BUF_LEN);
+	return buf;
+}
+
+/**
+ * Callback, hands back the password to be used during decryption.
+ *
+ * @param buf password buffer, must be filled by this function
+ * @param size password buffer size
+ * @param rwflag indicates for which callback is used for
+ * (decryption or encryption). For our purposes always 0.
+ * @param ud user data, set by application. In our case
+ * contains pointer to a password string.
+ */
+static int
+password_callback(char *buf, int size, int rwflag, void *ud)
+{
+	(void)rwflag;
+	assert(ud != NULL);
+
+	strlcpy(buf, (char *)ud, size);
+	buf[size - 1] = '\0';
+	return (int) strlen(buf);
+}
+
+/**
+ * Applies password for decode private key one by one, return 0 if
+ * key successfully decoded with one of those passwords.
+ *
+ * @param ctx ssl context
+ * @param file private key file
+ * @param key_password_file private key password file
+ */
+static inline int
+try_apply_passwords_from_file(SSL_CTX *ctx, const char *file, const char *key_password_file)
+{
+	int ret = -1;
+
+	FILE *fp = fopen(key_password_file, "r");
+	if (fp == NULL) {
+		const char *msg = "Unable to set private key file, "
+				  "failed to open password file";
+		diag_set(IllegalParams, msg);
+		goto end;
+	}
+
+	char *line = NULL;
+	size_t len = 0;
+
+	while (getline(&line, &len, fp) != -1) {
+		/* trim password variants */
+		if (line[strlen(line) - 1] == '\n') {
+			line[strlen(line) - 1] = '\0';
+		}
+		SSL_CTX_set_default_passwd_cb_userdata(ctx, (void *)line);
+		if (SSL_CTX_use_PrivateKey_file(ctx, file,
+						SSL_FILETYPE_PEM) == 1) {
+			ret = 0;
+			goto end_clear;
+		}
+	}
+
+	diag_set(IllegalParams, "Unable to set private key file: %s",
+		 ssl_last_error());
+
+end_clear:
+	fclose(fp);
+	free(line);
+end:
+	return ret;
+}
+
+/**
+ * Same as `try_apply_passwords_from_file`, using in non-blocking context.
+ */
+static ssize_t
+va_try_apply_passwords_from_file(va_list ap)
+{
+	SSL_CTX *ctx = va_arg(ap, SSL_CTX *);
+	const char *file = va_arg(ap, const char *);
+	const char *key_password_file = va_arg(ap, const char *);
+	return try_apply_passwords_from_file(ctx, file, key_password_file);
+}
+
+/**
+ * Try to set private key to ssl context. If key is encrypted password may be
+ * found in string or in file.
+ *
+ * @param ctx ssl context
+ * @param file private key file path
+ * @param key_password private key password (if needed)
+ * @param key_password_file private key password file (if needed)
+ */
+static int
+ssl_ctx_try_set_key(SSL_CTX *ctx, const char *file, const char *key_password,
+		    const char *key_password_file, enum iostream_mode mode)
+{
+	int ret = -1;
+
+	/*
+	 * if private key file not defined - it is ok,
+	 * possible errors may cauth in other steps
+	 */
+	if (file == NULL) {
+		ret = 0;
+		goto end;
+	}
+
+	SSL_CTX_set_default_passwd_cb(ctx, password_callback);
+
+	/* if password for a key not defined tries to use it as-is */
+	if (key_password_file == NULL && key_password == NULL) {
+		if (SSL_CTX_use_PrivateKey_file(ctx, file,
+						SSL_FILETYPE_PEM) == 1) {
+			ret = 0;
+			goto end;
+		}
+		diag_set(IllegalParams, "Unable to set private key file: %s",
+			 ssl_last_error());
+		goto end;
+	}
+
+	/* step 1 try to encode key with key_password */
+	if (key_password != NULL) {
+		SSL_CTX_set_default_passwd_cb_userdata(ctx,
+						       (void *)key_password);
+		if (SSL_CTX_use_PrivateKey_file(ctx, file,
+						SSL_FILETYPE_PEM) == 1) {
+			ret = 0;
+			goto end;
+		}
+	}
+
+	/* step 2, load password file and try to apply passwords line by line */
+	if (key_password_file == NULL) {
+		diag_set(IllegalParams, "Unable to set private key file: %s",
+			 ssl_last_error());
+		goto end;
+	}
+
+	if (mode == IOSTREAM_CLIENT) {
+		ret = (int)coio_call(va_try_apply_passwords_from_file, ctx,
+				     file, key_password_file);
+	} else {
+		ret = try_apply_passwords_from_file(ctx, file,
+						    key_password_file);
+	}
+
+	ERR_clear_error();
+end:
+	return ret;
+}
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+/**
+ * Same as `X509_STORE_load_file`, using in non-blocking context.
+ */
+static ssize_t
+va_X509_STORE_load_file(va_list ap)
+{
+	X509_STORE *store = va_arg(ap, X509_STORE *);
+	const char *ca_file = va_arg(ap, const char *);
+	return X509_STORE_load_file(store, ca_file);
+}
+#else
+/**
+ * Same as `X509_STORE_load_locations`, using in non-blocking context.
+ */
+static ssize_t
+va_X509_STORE_load_locations(va_list ap)
+{
+	X509_STORE *store = va_arg(ap, X509_STORE *);
+	const char *ca_file = va_arg(ap, const char *);
+	return X509_STORE_load_locations(store, ca_file, NULL);
+}
+#endif
+
+/**
+ * Create a new ssl context from given uri.
+ */
+static inline struct ssl_iostream_ctx *
+ssl_iostream_ctx_new_inner(enum iostream_mode mode, const struct uri *uri)
+{
+	const char *key = uri_param(uri, "ssl_key_file", 0);
+	const char *cert = uri_param(uri, "ssl_cert_file", 0);
+	const char *key_password = uri_param(uri, "ssl_password", 0);
+	const char *key_password_file = uri_param(uri, "ssl_password_file", 0);
+	const char *ca_file = uri_param(uri, "ssl_ca_file", 0);
+	const char *cipher_list = uri_param(uri, "ssl_ciphers", 0);
+
+	const SSL_METHOD *method = NULL;
+	switch (mode) {
+	case IOSTREAM_MODE_UNINITIALIZED:
+		unreachable();
+	case IOSTREAM_CLIENT:
+		method = TLS_client_method();
+		break;
+	case IOSTREAM_SERVER:
+		method = TLS_server_method();
+
+		const char *error = "ssl_key_file and ssl_cert_file"
+			" parameters are mandatory for a server";
+		if (key == NULL || cert == NULL) {
+			diag_set(IllegalParams, error);
+			return NULL;
+		}
+		break;
+	}
+
+	SSL_CTX *ctx = SSL_CTX_new(method);
+	if (ctx == NULL) {
+		diag_set(SSLError, "Unable to create SSL context");
+		return NULL;
+	}
+
+	SSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION);
+
+	if (ca_file != NULL) {
+		X509_STORE *store = SSL_CTX_get_cert_store(ctx);
+
+		int load_ret;
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+		if (mode == IOSTREAM_CLIENT) {
+			load_ret = (int)coio_call(va_X509_STORE_load_file,
+						  store, ca_file);
+		} else {
+			load_ret = X509_STORE_load_file(store, ca_file);
+		}
+#else
+		if (mode == IOSTREAM_CLIENT) {
+			load_ret = (int)coio_call(va_X509_STORE_load_locations,
+						  store, ca_file);
+		} else {
+			load_ret = X509_STORE_load_locations(store, ca_file,
+							     NULL);
+		}
+#endif
+
+		if (load_ret == 0) {
+			diag_set(SSLError, "Unable to load CA file");
+			return NULL;
+		}
+
+		int vmode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT |
+			    SSL_VERIFY_CLIENT_ONCE;
+		SSL_CTX_set_verify(ctx, vmode, NULL);
+	}
+
+	if (cipher_list != NULL) {
+		if (SSL_CTX_set_cipher_list(ctx, cipher_list) == 0) {
+			diag_set(IllegalParams,
+				 "Unable to set cipher list: %s",
+				 cipher_list);
+			goto err;
+		}
+	}
+
+	if (cert != NULL) {
+		int ret = SSL_CTX_use_certificate_file(ctx, cert,
+						       SSL_FILETYPE_PEM);
+		if (ret <= 0) {
+			diag_set(IllegalParams,
+				 "Unable to set certificate file: %s",
+				 ssl_last_error());
+			goto err;
+		}
+	}
+
+	int key_ret = ssl_ctx_try_set_key(
+		ctx, key, key_password, key_password_file, mode);
+	if (key_ret != 0) {
+		goto err;
+	}
+
+	struct ssl_iostream_ctx *io_ctx =
+		xmalloc(sizeof(struct ssl_iostream_ctx));
+	io_ctx->ctx = ctx;
+
+	return io_ctx;
+err:
+	SSL_CTX_free(ctx);
+	return NULL;
+}
+
+/**
+ * Create a new ssl context.
+ */
+struct ssl_iostream_ctx *
+ssl_iostream_ctx_new(enum iostream_mode mode, const struct uri *uri)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+	(void)mode;
+	(void)uri;
+	diag_set(IllegalParams, "SSL is not available in this build");
+	return NULL;
+#else
+	return ssl_iostream_ctx_new_inner(mode, uri);
+#endif
+}
+
+/**
+ * iostream_vtab ssl implementation
+ */
+static const struct iostream_vtab ssl_iostream_vtab;
+
+int
+ssl_iostream_create_supported(struct iostream *io, int fd,
+			      const struct ssl_iostream_ctx *ctx)
+{
+	SSL *ssl = SSL_new(ctx->ctx);
+	if (ssl == NULL) {
+		diag_set(SSLError, "Create ssl object error: %s",
+			 ssl_last_error());
+		return -1;
+	}
+
+	if (SSL_set_fd(ssl, fd) == 0) {
+		diag_set(SSLError, "SSL set fd error: %s",
+			 ssl_last_error());
+		SSL_free(ssl);
+		return -1;
+	}
+
+	io->flags = IOSTREAM_IS_ENCRYPTED;
+	io->fd = fd;
+	io->data = ssl;
+	io->vtab = &ssl_iostream_vtab;
+	return 0;
+}
+
+static void
+ssl_iostream_destroy(struct iostream *io)
+{
+	SSL *ssl = (SSL *)io->data;
+
+	/*
+	 * two checks here:
+	 * 1) check SSL_IOSTREAM_SESSION_READY flag, this will prevent
+	 * from calling coio_* functions from non-coio thread (iproto)
+	 * 2) check !SSL_IOSTREAM_POISON, according too documentation,
+	 * SSL_shutdown function mustn't be called
+	 * if a previous fatal error has occurred on a connection
+	 */
+	bool do_shutdown = (io->flags & SSL_IOSTREAM_SESSION_READY) &&
+			   !(io->flags & SSL_IOSTREAM_POISON);
+	if (do_shutdown) {
+		while (true) {
+			int ret = SSL_shutdown(ssl);
+			if (ret == 1)
+				break;
+			if (ret == 0)
+				continue;
+
+			int ssl_error = SSL_get_error(ssl, ret);
+			switch (ssl_error) {
+			case SSL_ERROR_WANT_READ:
+				coio_wait(io->fd, COIO_READ, 1);
+				break;
+			case SSL_ERROR_WANT_WRITE:
+				coio_wait(io->fd, COIO_WRITE, 1);
+				break;
+			default:
+			{
+				const char *le = ssl_last_error();
+				say_error("SSL_shutdown error: %s", le);
+				goto free;
+			}
+			}
+		}
+	}
+
+free:
+	SSL_free(ssl);
+}
+
+/**
+ * This function implement "lazy session initialization" on ssl connection.
+ * Lazy means that secure session will be initialized (SSL_accept/SSL_connect
+ * called) not when connection is created, but on first io
+ * operation (read/write).
+ */
+static inline ssize_t
+ssl_iostream_init_session(struct iostream *io)
+{
+	if (likely(io->flags & SSL_IOSTREAM_SESSION_READY))
+		return 0;
+
+	SSL *ssl = (SSL *)io->data;
+
+	int r;
+	if (SSL_is_server(ssl)) {
+		r = SSL_accept(ssl);
+	} else {
+		r = SSL_connect(ssl);
+	}
+
+	if (r <= 0) {
+		int ssl_error = SSL_get_error(ssl, r);
+		switch (ssl_error) {
+		case SSL_ERROR_WANT_READ:
+			return IOSTREAM_WANT_READ;
+		case SSL_ERROR_WANT_WRITE:
+			return IOSTREAM_WANT_WRITE;
+		default:
+			diag_set(
+				SSLError,
+				"Init session error: %s",
+				ssl_last_error());
+			return IOSTREAM_ERROR;
+		}
+	}
+
+	io->flags |= SSL_IOSTREAM_SESSION_READY;
+	return 0;
+}
+
+/**
+ * Check stream state and initialize ssl session if needed.
+ */
+static inline ssize_t
+ssl_iostream_io_prolog(struct iostream *io)
+{
+	assert(io->fd >= 0);
+	if (io->flags & SSL_IOSTREAM_POISON)
+		return IOSTREAM_ERROR;
+	return ssl_iostream_init_session(io);
+}
+
+static inline ssize_t
+ssl_err_to_iostream_err(int ssl_error)
+{
+	switch (ssl_error) {
+	case SSL_ERROR_WANT_READ:
+		return IOSTREAM_WANT_READ;
+	case SSL_ERROR_WANT_WRITE:
+		return IOSTREAM_WANT_WRITE;
+	case SSL_ERROR_ZERO_RETURN:
+		return 0;
+	default:
+		return IOSTREAM_ERROR;
+	}
+}
+
+static ssize_t
+ssl_iostream_read(struct iostream *io, void *buf, size_t count)
+{
+	ssize_t r = ssl_iostream_io_prolog(io);
+	if (r != 0)
+		return r;
+
+	SSL *ssl = (SSL *)io->data;
+
+	int ret = ssl_sio_read(ssl, io->fd, buf, count);
+	if (ret > 0)
+		return ret;
+
+	int ssl_error = SSL_get_error(ssl, ret);
+	if (ssl_is_fatal_error(ssl_error)) {
+		io->flags |= SSL_IOSTREAM_POISON;
+	}
+	return ssl_err_to_iostream_err(ssl_error);
+}
+
+static ssize_t
+ssl_iostream_write(struct iostream *io, const void *buf, size_t count)
+{
+	ssize_t r = ssl_iostream_io_prolog(io);
+	if (r != 0)
+		return r;
+
+	SSL *ssl = (SSL *)io->data;
+
+	int ret = ssl_sio_write(ssl, io->fd, buf, count);
+	if (ret > 0)
+		return ret;
+
+	int ssl_error = SSL_get_error(ssl, ret);
+	if (ssl_is_fatal_error(ssl_error)) {
+		io->flags |= SSL_IOSTREAM_POISON;
+	}
+	return ssl_err_to_iostream_err(ssl_error);
+}
+
+static ssize_t
+ssl_iostream_writev(struct iostream *io, const struct iovec *iov, int iovcnt)
+{
+	ssize_t r = ssl_iostream_io_prolog(io);
+	if (r != 0)
+		return r;
+
+	SSL *ssl = (SSL *)io->data;
+
+	int ret = ssl_sio_writev(ssl, io->fd, iov, iovcnt);
+	if (ret > 0)
+		return ret;
+
+	int ssl_error = SSL_get_error(ssl, ret);
+	if (ssl_is_fatal_error(ssl_error)) {
+		io->flags |= SSL_IOSTREAM_POISON;
+	}
+	return ssl_err_to_iostream_err(ssl_error);
+}
+
+static const struct iostream_vtab ssl_iostream_vtab = {
+	/* .destroy = */ ssl_iostream_destroy,
+	/* .read = */ ssl_iostream_read,
+	/* .write = */ ssl_iostream_write,
+	/* .writev = */ ssl_iostream_writev,
+};
diff --git a/src/lib/core/ssl_impl.h b/src/lib/core/ssl_impl.h
new file mode 100644
index 0000000000..7658a95cdc
--- /dev/null
+++ b/src/lib/core/ssl_impl.h
@@ -0,0 +1,66 @@
+#include "iostream.h"
+#include "trivia/util.h"
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct uri;
+
+/**
+ * Secure iostream context.
+ */
+struct ssl_iostream_ctx {
+	/**
+	 * SSL context.
+	 */
+	SSL_CTX *ctx;
+};
+
+void
+ssl_init(void);
+
+void
+ssl_free(void);
+
+struct ssl_iostream_ctx *
+ssl_iostream_ctx_new(enum iostream_mode mode, const struct uri *uri);
+
+static inline void
+ssl_iostream_ctx_delete(struct ssl_iostream_ctx *ctx)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+	(void)ctx;
+	unreachable();
+#else
+	SSL_CTX_free(ctx->ctx);
+	free(ctx);
+#endif
+}
+
+int
+ssl_iostream_create_supported(struct iostream *io, int fd,
+			      const struct ssl_iostream_ctx *ctx);
+
+static inline int
+ssl_iostream_create(struct iostream *io, int fd, enum iostream_mode mode,
+		    const struct ssl_iostream_ctx *ctx)
+{
+	(void)mode;
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+	(void)io;
+	(void)fd;
+	(void)ctx;
+	unreachable();
+	return 0;
+#else
+	return ssl_iostream_create_supported(io, fd, ctx);
+#endif
+}
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/test/box-luatest/transport_test.lua b/test/box-luatest/transport_test.lua
index b1f187b349..e8b4d6982c 100644
--- a/test/box-luatest/transport_test.lua
+++ b/test/box-luatest/transport_test.lua
@@ -44,28 +44,3 @@ g.test_net_box = function()
         'Invalid transport: foo',
         net.connect, {g.server.net_box_uri, params = {transport = 'foo'}})
 end
-
-g.test_listen_ssl = function()
-    t.tarantool.skip_if_enterprise()
-    g.server:exec(function()
-        t.assert_error_msg_equals(
-            'SSL is not available in this build',
-            box.cfg, {listen = 'localhost:0?transport=ssl'})
-    end)
-end
-
-g.test_replication_ssl = function()
-    t.tarantool.skip_if_enterprise()
-    g.server:exec(function()
-        t.assert_error_msg_equals(
-            'SSL is not available in this build',
-            box.cfg, {replication = 'localhost:0?transport=ssl'})
-    end)
-end
-
-g.test_net_box_ssl = function()
-    t.tarantool.skip_if_enterprise()
-    t.assert_error_msg_equals(
-        'SSL is not available in this build',
-        net.connect, {g.server.net_box_uri, params = {transport = 'ssl'}})
-end
diff --git a/test/ssl-luatest/basic_test.lua b/test/ssl-luatest/basic_test.lua
new file mode 100644
index 0000000000..6b5d438862
--- /dev/null
+++ b/test/ssl-luatest/basic_test.lua
@@ -0,0 +1,510 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+local function certs_file(name)
+    return require('fio').abspath('./test/ssl-luatest/certs') .. "/" .. name
+end
+
+g.before_each(function()
+    g.server = server:new({ alias = 'server' })
+    g.server:start()
+    g.client = server:new({ alias = 'client' })
+    g.client:start()
+end)
+
+g.after_each(function()
+    g.server:stop()
+    g.client:stop()
+end)
+
+g.test_ssl_self_signed_works = function()
+    g.server:exec(function()
+        t.assert_error_msg_content_equals(
+                "ssl_key_file and ssl_cert_file parameters are mandatory for a server",
+                function()
+                    box.cfg {
+                        listen = {
+                            uri = 'localhost:0',
+                            params = { transport = 'ssl' }
+                        }
+                    }
+                end)
+    end)
+
+    local listen = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        return box.cfg.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    t.assert_not_equals(listen, nil)
+end
+
+g.test_ssl_password_for_key = function()
+    local listen = g.server:exec(function(key, cert)
+        t.assert_error_msg_matches(
+                "Unable to set private key file: .*bad decrypt",
+                function()
+                    box.cfg {
+                        listen = {
+                            uri = 'localhost:0',
+                            params = {
+                                transport = 'ssl',
+                                ssl_key_file = key,
+                                ssl_cert_file = cert,
+                                ssl_password = 'wrong-password',
+                            }
+                        }
+                    }
+                end
+        )
+
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                    ssl_password = 'foobar',
+                }
+            }
+        }
+
+        return box.cfg.listen
+    end, {
+        certs_file('self-sign-pw-key.key'),
+        certs_file('self-sign-pw-cert.crt'),
+    })
+    t.assert_not_equals(listen, nil)
+end
+
+g.test_ssl_password_file_for_key = function()
+    -- server
+    local srv_uri = g.server:exec(function(key, cert, pw_file, wrong_pw_file)
+        t.assert_error_msg_matches(
+                "Unable to set private key file: .*bad decrypt",
+                function()
+                    box.cfg {
+                        listen = {
+                            uri = 'localhost:0',
+                            params = {
+                                transport = 'ssl',
+                                ssl_key_file = key,
+                                ssl_cert_file = cert,
+                                ssl_password_file = wrong_pw_file,
+                            }
+                        }
+                    }
+                end)
+
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                    ssl_password_file = pw_file,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        return box.info.listen
+    end, {
+        certs_file('self-sign-pw-key.key'),
+        certs_file('self-sign-pw-cert.crt'),
+        certs_file('pw.txt'),
+        certs_file('pw-wrong.txt'),
+    })
+
+    -- check client as well
+    local client_connection = g.client:exec(
+            function(srv_uri, key, cert, pw_file, wrong_pw_file)
+                t.assert_error_msg_matches(
+                        "Unable to set private key file: .*bad decrypt",
+                        function()
+                            require('net.box').connect({
+                                uri = srv_uri,
+                                params = {
+                                    transport = 'ssl',
+                                    ssl_key_file = key,
+                                    ssl_cert_file = cert,
+                                    ssl_password_file = wrong_pw_file,
+                                }
+                            })
+                        end)
+
+                local connection = require('net.box').connect({
+                    uri = srv_uri,
+                    params = {
+                        transport = 'ssl',
+                        ssl_key_file = key,
+                        ssl_cert_file = cert,
+                        ssl_password_file = pw_file,
+                    }
+                })
+                return connection
+            end, {
+                srv_uri,
+                certs_file('self-sign-pw-key.key'),
+                certs_file('self-sign-pw-cert.crt'),
+                certs_file('pw.txt'),
+                certs_file('pw-wrong.txt'),
+            })
+    t.assert_equals(client_connection.error, nil)
+end
+
+g.test_ssl_password_and_ssl_password_file_for_key = function()
+    local srv_uri = g.server:exec(function(key, cert, pw_file, wrong_pw_file)
+        t.assert_error_msg_matches(
+                "Unable to set private key file: .*bad decrypt",
+                function()
+                    box.cfg {
+                        listen = {
+                            uri = 'localhost:0',
+                            params = {
+                                transport = 'ssl',
+                                ssl_key_file = key,
+                                ssl_cert_file = cert,
+                                ssl_password = 'foobar1',
+                                ssl_password_file = wrong_pw_file,
+                            }
+                        }
+                    }
+                end)
+
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                    ssl_password = 'foobar1',
+                    ssl_password_file = pw_file,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        return box.info.listen
+    end, {
+        certs_file('self-sign-pw-key.key'),
+        certs_file('self-sign-pw-cert.crt'),
+        certs_file('pw.txt'),
+        certs_file('pw-wrong.txt'),
+    })
+
+    -- check client as well
+    local client_connection = g.client:exec(
+            function(srv_uri, key, cert, pw_file, wrong_pw_file)
+                t.assert_error_msg_matches(
+                        "Unable to set private key file: .*bad decrypt",
+                        function()
+                            require('net.box').connect({
+                                uri = srv_uri,
+                                params = {
+                                    transport = 'ssl',
+                                    ssl_key_file = key,
+                                    ssl_cert_file = cert,
+                                    ssl_password = 'foobar1',
+                                    ssl_password_file = wrong_pw_file,
+                                }
+                            })
+                        end)
+
+                local connection = require('net.box').connect({
+                    uri = srv_uri,
+                    params = {
+                        transport = 'ssl',
+                        ssl_key_file = key,
+                        ssl_cert_file = cert,
+                        ssl_password = 'foobar1',
+                        ssl_password_file = pw_file,
+                    }
+                })
+                return connection
+            end, {
+                srv_uri,
+                certs_file('self-sign-pw-key.key'),
+                certs_file('self-sign-pw-cert.crt'),
+                certs_file('pw.txt'),
+                certs_file('pw-wrong.txt'),
+            })
+    t.assert_equals(client_connection.error, nil)
+end
+
+g.test_server_verify_client = function()
+    local srv_uri = g.server:exec(function(key, cert, ca)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                    ssl_ca_file = ca,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+        return box.info.listen
+    end, {
+        certs_file('ca-sign-key.key'),
+        certs_file('ca-sign-cert.crt'),
+        certs_file('ca.pem'),
+    })
+
+    local client_connection = g.client:exec(function(srv_uri, key, cert)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+                ssl_key_file = key,
+                ssl_cert_file = cert,
+            }
+        })
+        return connection
+    end, {
+        srv_uri,
+        certs_file('client/ca-sign-key.key'),
+        certs_file('client/ca-sign-cert.crt'),
+    })
+    t.assert_equals(client_connection.error, nil)
+
+    client_connection = g.client:exec(function(srv_uri)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = { transport = 'ssl' },
+        })
+        return connection
+    end, { srv_uri })
+    t.assert_str_matches(client_connection.error,
+        'Init session error: .*sslv3 alert handshake failure')
+
+    client_connection = g.client:exec(function(srv_uri, key, cert)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+                ssl_key_file = key,
+                ssl_cert_file = cert,
+            }
+        })
+        return connection
+    end, {
+        srv_uri,
+        certs_file('client/self-sign-key.pem'),
+        certs_file('client/self-sign-cert.pem'),
+    })
+    t.assert_str_matches(client_connection.error,
+        'Init session error: .*tlsv1 alert unknown ca')
+end
+
+g.test_client_verify_server = function()
+    local srv_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+        return box.info.listen
+    end, { certs_file('ca-sign-key.key'), certs_file('ca-sign-cert.crt') })
+
+    local client_connection = g.client:exec(function(srv_uri, key, cert, ca)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+                ssl_key_file = key,
+                ssl_cert_file = cert,
+                ssl_ca_file = ca,
+            }
+        })
+        return connection
+    end, {
+        srv_uri,
+        certs_file('client/ca-sign-key.key'),
+        certs_file('client/ca-sign-cert.crt'),
+        certs_file('ca.pem'),
+    })
+    t.assert_equals(client_connection.error, nil)
+
+    g.server:stop()
+    g.server:start()
+
+    local srv_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+        return box.info.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    local client_connection = g.client:exec(function(srv_uri, key, cert, ca)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+                ssl_key_file = key,
+                ssl_cert_file = cert,
+                ssl_ca_file = ca,
+            }
+        })
+        return connection
+    end, {
+        srv_uri,
+        certs_file('client/ca-sign-key.key'),
+        certs_file('client/ca-sign-cert.crt'),
+        certs_file('ca.pem'),
+    })
+    t.assert_str_matches(client_connection.error,
+        'Init session error: .*certificate verify failed')
+end
+
+g.test_server_client_verify_both = function()
+    local srv_uri = g.server:exec(function(key, cert, ca)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                    ssl_ca_file = ca,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+        return box.info.listen
+    end, {
+        certs_file('ca-sign-key.key'),
+        certs_file('ca-sign-cert.crt'),
+        certs_file('ca.pem'),
+    })
+
+    local client_connection = g.client:exec(function(srv_uri, key, cert, ca)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+                ssl_key_file = key,
+                ssl_cert_file = cert,
+                ssl_ca_file = ca,
+            }
+        })
+        return connection
+    end, {
+        srv_uri,
+        certs_file('client/ca-sign-key.key'),
+        certs_file('client/ca-sign-cert.crt'),
+        certs_file('ca.pem'),
+    })
+
+    t.assert_equals(client_connection.error, nil)
+end
+
+g.test_set_cipher_list = function()
+    g.server:exec(function(key, cert)
+        t.assert_error_msg_content_equals(
+                "Unable to set cipher list: UNKNOWN-CIPHER-LIST",
+                function()
+                    box.cfg {
+                        listen = {
+                            uri = 'localhost:0',
+                            params = {
+                                transport = 'ssl',
+                                ssl_key_file = key,
+                                ssl_cert_file = cert,
+                                ssl_ciphers = 'UNKNOWN-CIPHER-LIST',
+                            }
+                        }
+                    }
+                end)
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    local listen = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                    ssl_ciphers = 'ECDHE-ECDSA-AES256-GCM-SHA384',
+                }
+            }
+        }
+
+        return box.cfg.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    t.assert_not_equals(listen, nil)
+end
+
+g.test_server_use_wrong_key = function()
+    g.server:exec(function(key, cert)
+        t.assert_error_msg_matches(
+                "Unable to set private key file: .*key values mismatch",
+                function()
+                    box.cfg {
+                        listen = {
+                            uri = 'localhost:0',
+                            params = {
+                                transport = 'ssl',
+                                ssl_key_file = key,
+                                ssl_cert_file = cert,
+                            }
+                        }
+                    }
+                end)
+    end, {
+        certs_file('self-sign-wrong-key.pem'),
+        certs_file('self-sign-cert.pem'),
+    })
+end
+
+g.test_client_use_wrong_key = function()
+    g.client:exec(function(key, cert)
+        t.assert_error_msg_matches(
+                "Unable to set private key file: .*key values mismatch",
+                function()
+                    require('net.box').connect({
+                        uri = 'localhost:0',
+                        params = {
+                            transport = 'ssl',
+                            ssl_key_file = key,
+                            ssl_cert_file = cert,
+                        }
+                    })
+                end)
+    end, {
+        certs_file('self-sign-wrong-key.pem'),
+        certs_file('self-sign-cert.pem'),
+    })
+end
diff --git a/test/ssl-luatest/certs/ca-sign-cert.crt b/test/ssl-luatest/certs/ca-sign-cert.crt
new file mode 100644
index 0000000000..5b6d3d0e50
--- /dev/null
+++ b/test/ssl-luatest/certs/ca-sign-cert.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkDCCAnigAwIBAgIUQ+EXS0A8P0S+4K+W/pO8cGelRXgwDQYJKoZIhvcNAQEL
+BQAwTzELMAkGA1UEBhMCUlUxDDAKBgNVBAgMA2JnZDEMMAoGA1UEBwwDYmdkMREw
+DwYDVQQKDAhwaWNvZGF0YTERMA8GA1UECwwIcGljb2RhdGEwHhcNMjQwMTMxMDgx
+MzUxWhcNMjYxMDI2MDgxMzUxWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29t
+ZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdc4wJKdPMvJhe3H8OXGDShPQn7s
+Z85sb1jOHy4FKdIF7ylDoiuYmI8OoqMEYe9gG/EZJlPeQmkCTa/BV2ya1RMwtJvM
+Yi5pinUM9etOhvwtTGRhwQXNco1pvB9oMLpUAgFtAyQRgMky6w2eTqnmyzvMGpc/
+8eWBfFqClskon+GdcrgWUftq2xf/WM8JBuIO9mPmi+24Fzm2lGFbechnJgKlmCGQ
+FLxPXjoBZy76lE7q5XR1GPg7HwamohG0BnuW8Mh7u9ggiJmNk0lqwgbmiqL7/ew7
+GR1jPoguhi5FzJMcYJqqTNcQ5Wiy5+tgVrbBjpy6GOXYnrB4vQMmsdnrFQIDAQAB
+o24wbDAfBgNVHSMEGDAWgBRd+tplnGSGdMg254qdbD6F7oosCDAJBgNVHRMEAjAA
+MAsGA1UdDwQEAwIE8DASBgNVHREECzAJggd0dC50ZXN0MB0GA1UdDgQWBBTv5FBd
+FrdRaoUDdc7q6EQ2t6cReDANBgkqhkiG9w0BAQsFAAOCAQEAJnVw5YKMlEnsfC+x
+r2p2b3wlIOTVRgQt9o3R4dQIrYM0FOKBL3XuKF1cLRMvlyqxrtfyATtQtMT5VVSa
+rRuTG0/p6r3yDH5nyedrZ09sP6hyv7XWjONivhf4d6bJ65AW1NT4bKyRT/qUJefF
+LVVeqN88qMwLh5E248LbRw0FfCzvfbcZLr80ShAlwIRayHD2z0fikOSXxGK1Wp2l
+/0ojVFHBZczSN5R3eFPjRa9GF86O0z0Mz/QRLodzYEHttY4AHoEmf8zuKwwmBgAZ
+YpHovqXeg+orP6bHnnMlh/oyApGJHgbQcnOubIzZDKuMYbjeRtHZ2B6nGUNFYQXe
+jXsZQg==
+-----END CERTIFICATE-----
diff --git a/test/ssl-luatest/certs/ca-sign-key.key b/test/ssl-luatest/certs/ca-sign-key.key
new file mode 100644
index 0000000000..f7cb5b730f
--- /dev/null
+++ b/test/ssl-luatest/certs/ca-sign-key.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDF1zjAkp08y8mF
+7cfw5cYNKE9CfuxnzmxvWM4fLgUp0gXvKUOiK5iYjw6iowRh72Ab8RkmU95CaQJN
+r8FXbJrVEzC0m8xiLmmKdQz1606G/C1MZGHBBc1yjWm8H2gwulQCAW0DJBGAyTLr
+DZ5OqebLO8walz/x5YF8WoKWySif4Z1yuBZR+2rbF/9YzwkG4g72Y+aL7bgXObaU
+YVt5yGcmAqWYIZAUvE9eOgFnLvqUTurldHUY+DsfBqaiEbQGe5bwyHu72CCImY2T
+SWrCBuaKovv97DsZHWM+iC6GLkXMkxxgmqpM1xDlaLLn62BWtsGOnLoY5diesHi9
+Ayax2esVAgMBAAECggEAPU206LslnOaawfTNzUAtz/rrIHQ65empYRFYAYJdmDlb
+nGGhcvlmVfOSOeQgogYuNkpNnDpFtzdZvt/UnbXp/ldtcrGKMewmyP//LZ6l3R78
+HF1Iiyq0oCfgmSn0O5EpIdv2PxBomgr+S6aTJNXxzYzwcFbouhh/OJzFNVsMywlu
+UaTRTo/YQ/idlOk2m6JMIDjsjTsb4z2n05vuHt/UmG9kMMPbxJ+gXOOtzY2FywNa
+DtOLY/KuZcaQyI50nqQUeoWN/52axeNgGfmAY0O6Cl2pVHRgNkCgjHeqP0dGi2EB
+EQCt0VniiIMqkYuYbefLZ5pbTWG2Gn8w9s/ydFyFOwKBgQD7TZkeXRud7yKnjuQr
+pi7mjjbdF8humi+Tn2O4DcxMevnLh8PztYQz/yjD9QBVHiSwSSIvQ7+nkPwjJukh
+QdVpTAa1VXvyt4uyrF9ozZ7ywDkr5wy978aiX02YQHcRj3fZLqn3monERz6t3fMS
+lP0mdpwhUlzmQ2kXB8sMlCpomwKBgQDJidLfOyU8zC4AMCS+7FIeVZgCb7gHHmkn
+n869kC2AeoN4dWdRMfoXSwXiv3kQA85hIgPSYRkoTVCqjwd0ZHFLOqAFjf4bVGo6
+HLDVg7sHjLjyF9J6LBUvU34ykh13thmZYil9uzMJKDsWYePBiCmR7I7iSRP8m+tr
+imjg1fz+DwKBgGSIlJrrEKNMCYcSyQEVSGn/tr8YRpzIngPmndQB4Rsvj0L3G1Mh
+6LOMflh2aR2kI3VKrJxP9BorDFNbu6QMXD31pjFg82cUOpTOFJUuvUnL0JNZqWVK
+ySIoEji4gWnLBV0jJ+fgMQEOp2qTAi0K4YNDrA5Ajt2nViFagsaTd6vFAoGBAMjq
+zL3dIe9tgkKPvGJWCDvTivqK08TNDXoCOEIFFUerW5vgN0Lb9v33vgNibVeI49sz
+5Ol8AW+LPGr8sirX45Zi+JrxBq3KRyht5+DENdV99fbrxtojTm9i5kGWJyhr8zNV
+iBWfZW6wm89hgYQzsXWXho6S2gkQi+8nCg1zZ4cDAoGBANLjr1+V/lnP8OnvrCKl
+HkYk7k5a+08U8RNNX+xM5CN3dPiGdOTBeYv5/zWSTSySXv7+3ySBwr2/Yhx3xtQA
+PK9+W6gzrt6SR1VFeg4wpNVe3NKDX47QaFLrzWa8y3I2jjXwo9kGKCwo7IbdFSwM
+EKsbT4P7U/B6wTKktUnVoJ1u
+-----END PRIVATE KEY-----
diff --git a/test/ssl-luatest/certs/ca.pem b/test/ssl-luatest/certs/ca.pem
new file mode 100644
index 0000000000..1dcb13a0ba
--- /dev/null
+++ b/test/ssl-luatest/certs/ca.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDfzCCAmegAwIBAgIUTnote9luTPt+9S7U3GErlNfEyAswDQYJKoZIhvcNAQEL
+BQAwTzELMAkGA1UEBhMCUlUxDDAKBgNVBAgMA2JnZDEMMAoGA1UEBwwDYmdkMREw
+DwYDVQQKDAhwaWNvZGF0YTERMA8GA1UECwwIcGljb2RhdGEwHhcNMjQwMTMwMTMz
+MTEyWhcNMjkwMTI4MTMzMTEyWjBPMQswCQYDVQQGEwJSVTEMMAoGA1UECAwDYmdk
+MQwwCgYDVQQHDANiZ2QxETAPBgNVBAoMCHBpY29kYXRhMREwDwYDVQQLDAhwaWNv
+ZGF0YTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIV5Iq53kO3b3HJW
+oszFGSKR4B4J9kMfssaPyKrXJBC2KF//wLvHQ2C9/WoDkpCsRifOc6WFS82V387t
+S8cXLYhQ10LgwFSP9o07GYC51tL/TnsM5e28m1y8YmYBdp3JJOQkaaYb1ACg6pgv
+v+fuM7K+I7yiLczlozH8iE5bXYI0dPa7GQzNrT7w/S1/gmdDz2k4HJTLXVqtE/6D
+6wXASQG8pSvTRxrjrpgz4eXpgZd/Jp+ZtPrGl6fdTy4aINe+z/UVYFdpn1w5wRWx
+ZXHNFyHl1QsX+u/z9MaTxr5rS4HnyOGIjtAfU4zUVxtdd+1vFolWmsUAuYAMoV8S
+4ZDVLLUCAwEAAaNTMFEwHQYDVR0OBBYEFF362mWcZIZ0yDbnip1sPoXuiiwIMB8G
+A1UdIwQYMBaAFF362mWcZIZ0yDbnip1sPoXuiiwIMA8GA1UdEwEB/wQFMAMBAf8w
+DQYJKoZIhvcNAQELBQADggEBAD+F08IVFp0SjRZJJpm9Gx17BdiKVjWOdHZ6UX51
+MKcaSyr3btzbXt+pDDtNgKBb5zWjwkG4Cm5hvi7ELUSZgONetXJwUDR0/cehC+oi
+qX9eFybTcJR/q5BTwvmgiNXQ9ET0Q/rEmFmkgj43pBCElledP7g1MqDccccGB+BV
+1ZQXbhOATCxwUOJ5I/fQX/iEaa6WqK8Hv3oWGxnq3eDtkACtZZ1h2gy2gkO7cf6i
+ZbEBuNpOzSlrPPwFyf8D7NxfXYnYI1qLqxll50khsxtsWpVcD0vNo1Cn5adB+5/j
+8g9DIj67TU/n5cc7ZsawUYPwFGgjmCJrDviWIPU2oGntNzE=
+-----END CERTIFICATE-----
diff --git a/test/ssl-luatest/certs/client/ca-sign-cert.crt b/test/ssl-luatest/certs/client/ca-sign-cert.crt
new file mode 100644
index 0000000000..26e389bfec
--- /dev/null
+++ b/test/ssl-luatest/certs/client/ca-sign-cert.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkDCCAnigAwIBAgIUQrek3Ng5IiSzwpIfAZAZpia4pscwDQYJKoZIhvcNAQEL
+BQAwTzELMAkGA1UEBhMCUlUxDDAKBgNVBAgMA2JnZDEMMAoGA1UEBwwDYmdkMREw
+DwYDVQQKDAhwaWNvZGF0YTERMA8GA1UECwwIcGljb2RhdGEwHhcNMjQwMTMwMTQx
+MTI2WhcNMjYwNzA2MTQxMTI2WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29t
+ZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryTZRvKgmZMlElsyn2Fvj6eeiyTM
+uUoW99zMdLn0R8PMb1EKcTSW4v0XdaGPvkcyMFHUYJoAxX8Xjsg1Au5zePq7DdfL
+PR0lEBFzEfbno1RgWR6IaHQTVI7ZahKNFOCP3VnyNHNL4rms3BIAlTKMiao/a4AO
+hwBi+Z2t4OfRMKovaG4/eZd7W5ddjgu07ywMUY2KbhmfrJU03M51dNzHicpUAvQE
+lR5DQHf7a0XVE9E+gUnaRolhtnNybuORxye3znEJx4DBjCTNj49vwHd1xwugLsCq
+dpknyherptRS4t+EgI3AO4E80xCH1VLiLznMESnE8Odpo+AlmR0F2RQCWQIDAQAB
+o24wbDAfBgNVHSMEGDAWgBRd+tplnGSGdMg254qdbD6F7oosCDAJBgNVHRMEAjAA
+MAsGA1UdDwQEAwIE8DASBgNVHREECzAJggd0dC50ZXN0MB0GA1UdDgQWBBQt7pGN
+QrBwuzXndKD3dZjQXfgLBTANBgkqhkiG9w0BAQsFAAOCAQEAdT39NF/PdM/bEKrd
+VcdLUnkKCUR/Yzm4TzXta3G4TZOF1xENsoeC3gNgb5W00HlX7GF4LBdj1tvLwm/s
+huGiM+o5D3xBAf09hfwmBMC8jDO9Ko1xZGmbu8rCKeJKEW2faETIddzg/zeI+HVg
+RUXSc1YvTbKCGxIT398WEr2hELsZQbnnHqoRp5E51sB1SPdeJeSIiiLozxGTN9Fb
+eGJmwN3b9/VW5Kwp1PIQzKac3KsCxYj03JSqZqZjL93cLhjmqQRAfKfVihysaveP
+4E/DfleAuLq0opnD0By3vm+3r5WOiPpf4XDEhd1SJUWROwypqhjV0rRQheX/NaoJ
+QP0xsQ==
+-----END CERTIFICATE-----
diff --git a/test/ssl-luatest/certs/client/ca-sign-key.key b/test/ssl-luatest/certs/client/ca-sign-key.key
new file mode 100644
index 0000000000..d35a738a40
--- /dev/null
+++ b/test/ssl-luatest/certs/client/ca-sign-key.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCvJNlG8qCZkyUS
+WzKfYW+Pp56LJMy5Shb33Mx0ufRHw8xvUQpxNJbi/Rd1oY++RzIwUdRgmgDFfxeO
+yDUC7nN4+rsN18s9HSUQEXMR9uejVGBZHohodBNUjtlqEo0U4I/dWfI0c0viuazc
+EgCVMoyJqj9rgA6HAGL5na3g59Ewqi9obj95l3tbl12OC7TvLAxRjYpuGZ+slTTc
+znV03MeJylQC9ASVHkNAd/trRdUT0T6BSdpGiWG2c3Ju45HHJ7fOcQnHgMGMJM2P
+j2/Ad3XHC6AuwKp2mSfKF6um1FLi34SAjcA7gTzTEIfVUuIvOcwRKcTw52mj4CWZ
+HQXZFAJZAgMBAAECgf919CaiTCebRkLsW4WiEtwI3PSuf3ycEhvO31s7+WLm8heO
+nuti3DyayfMP1igMWb8xqsnGRVPjyMfgTYXcWUt4fWQND29n+uOLhb7LlYp4WviI
+bwEtfww9fWbHrU2ygBqyaCLd2shuoSD3mLIG3NZZuSDImrDE8quohGGcoEFjFDRr
+1JokVJ/2VPg/mbkmp/OhZSrdxfGWxRoDqyQwlrj8PRLu3bFXPOKxhaOziK74fVWm
+SbpRszSP4kGp3mtdwwnD2kZerj3oiuS2iFCg5aaytsCHWJzhPD0AMk6OPRvJUqld
+GUrPR0REiVEQ3SkeG9NazcpTNMSMfkXV7C/PobECgYEAtkEyGqZ1fbOXw982lZK+
+jA7Vhsk1OFCEB5GLGoKLEZ3w0ORHaQ+AJP+Nr6uyyma4AmEoIjCNSh2P0CCJA7hc
+0IYSZFB5LhPRkXboCSL7ZsSf680oG4fW3yEP6WCughWNyVI5tz379swDRv4Z6/bZ
+N8Mot6oj+J1e0tWR2GxXQRcCgYEA9gMW/Qq0WHGBL6otuKrnOAfDKj3+dgNJyx7e
+f43nWgmQ25y1Hss0BDziL+PnmcPgITEjtalWa84sNAjgJIOieqQFMVFdoCY32a4Q
+yEK/Vh73VWkkCVfWGLgSE59XWg3vXysHcvfM0BE+OPqqnWLimvbzBCNSuR43sHeQ
+s+ltng8CgYA9vhR1RtMixIazipj5Tv9Sf2wC/4MSEuTHk5zmXDc0vqbofY1boC6v
+Wgvr6WTmgKnx1Gw+OOiEpUMlJQCFzB1VXzY32bNT0s76UqgFAF7ez9nFjnj56qj2
+akLzsxcrZpXkEAERfOQxFag+krLBZ5zq8SiIIBGTleybzFJeWUWt/QKBgQCOSoc3
+YMOjDDOecB3129PU0piNjEHr3EcIVxh2SotvxmykuWUBJKM3eEeTT4yboXG3gQaY
+ghNcU2m5xxBtzj1iFhBy0A8JHFHddBJ84i5tD4gbPK79QHKy5XR/KQCLM9gqAWdT
+lgQ+rFqC9mlMku9ilkdFM3EbZWDsjpuBlBg0/QKBgGtgTKUvL9u31zJ+jQ0dvusL
+8lhKpKEwAVdXeSisTHuirvCRna5yx/+LNb1GOK0fXZ4mODSL9jj4kFtcfZ48/Gbx
+x/ZmHiOq8nFbNSsCCmUH33szMwxFxKuN75evZa0aXVi0ctqT9WSY+Ugo2OunYNE+
+C5dBxK8jYFaSsMQyel+4
+-----END PRIVATE KEY-----
diff --git a/test/ssl-luatest/certs/client/self-sign-cert.pem b/test/ssl-luatest/certs/client/self-sign-cert.pem
new file mode 100644
index 0000000000..63b6ecffd7
--- /dev/null
+++ b/test/ssl-luatest/certs/client/self-sign-cert.pem
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF7zCCA9egAwIBAgIUOeCtdod3jvrIkUbDl1OKIN4P+bwwDQYJKoZIhvcNAQEL
+BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM
+CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu
+eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y
+NDAxMjIxMDAzNDhaFw0zNDAxMTkxMDAzNDhaMIGGMQswCQYDVQQGEwJYWDESMBAG
+A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t
+cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU
+Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQDNCYN0Hm6GqTDcojr/yZH6PlE8RpHZiAErefgNmuIt8JiWyuTY6mJkk+nE
+oR+J7yT5lRNj/8Ey1Lf26+xqRA+fXjms3HX7VizY62h4GlaF2MM8QntjGclP+fGG
+H2MP2JJcTYckY7+GBAYMH/yU2SHsEIgRIV9vyXEae4/wDLMNPnrlUPR+na2eOev+
+rZItILNbS6zBlATV89XUdwRcZZTy+9oqVH3wDIgOkXQDdRb9zHSlKvx86wrlNvjB
+KiAoHL9DCuaRGptvw99lVVdSyH7hKtFm1RdiZ2zFP8WGApTaK9f2mH3cyGTYL/6e
+1bdXxv7RlQxIT7AcXOOiSWIFpNX1wmJ44ywzYnDxG8rLaEZdrGGRumt8iEIyfLLA
+K/do5WVpYYcBC1IGNHf+HVmI0Zdny1PH9Xm1ujMQzywI82t2pagX7OOKEC6KUMTT
+42j3/vshgjqtKjz778pz6aqdAg1gB+GLfF3GJZiSSRjiJpQpEErdF2dDN0tQrCiZ
+hE/i3gXgZq+TmPo+TffoesQC/jt/22J7giLAZC4SppdeO7nK5fyARd9qxWDa+iwh
+PQoxfLO5fVcnqUL1cUuohHGyarWiTs8q0iIGgwJRnp+xra8o/4D1cxy3z5tiji7O
+CwJ7R9/TjMZyF8kfQd1qoJ7mydl0LZ+9jWO3UxzevDDdksCMXwIDAQABo1MwUTAd
+BgNVHQ4EFgQUZVHupeRHVxq4GKnzW5FzPu34v28wHwYDVR0jBBgwFoAUZVHupeRH
+Vxq4GKnzW5FzPu34v28wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AgEAcr1btTc2/A+3FBBn4+umNgmpiCBYWEYdTKadqkeBXvnACgAjE5WO2apnjw6o
+lArrO3so5ZDeLAsBZzFzckiItWoId0nRjfKWoSnYqI7jYMR200NeuPWppjT8sUj7
+t+5dd2fPufiL0U2UkxmPvYXL/ZbYoQjF6paJBJdK9eOqpdc1+iAVPS60CZGI3JtV
+lVACu/d5mkigf60kNy6CN3y31vtmSIq1AXxMbQ+yDy4x9qeIbl46esw8x5303SDF
+VqCq9OLHbl28Y+taBsie87UAQRyDk/0l/iElVUeYBJKTP/upSU+kyTDW4NdnmBg0
+kUSn/J3bV27IDKgIj+fbnMzdmohE30cJIpx4eOeAvywMuDd9WQjX8DZUKil+Muy/
+vqs3CkEfjgJElKZTRfx9Al5evUWVBb588mjNvLaJ5OAXgUhBdntPzSNK0id2k33r
+iPRim6tqeBmulXaPV01gz5t3/ITG9bpCvGwxcVZjHD231oOvpQHkxpRo0Qz7EFN9
+LPH43+8Pmn7u5WPozC5JjwhEJyVBvpzU87KO1OuVRer81rhbE1w8j0BQE1noh+ry
+wWEiS2ys67RMLlNU4ba2jZF+PLJ56pPVlkLjb8RFw32sfyRsTEdmW/TnmWLV43qv
+uvYV0T1d4ZU6KnE0bKCBHyLnIAp/kQK8836NSpT3gaOQXr8=
+-----END CERTIFICATE-----
diff --git a/test/ssl-luatest/certs/client/self-sign-key.pem b/test/ssl-luatest/certs/client/self-sign-key.pem
new file mode 100644
index 0000000000..b372856906
--- /dev/null
+++ b/test/ssl-luatest/certs/client/self-sign-key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+3j9bR8O6ooUgJeyhQs86c7T5/zHkiA==
+-----END PRIVATE KEY-----
diff --git a/test/ssl-luatest/certs/pw-wrong.txt b/test/ssl-luatest/certs/pw-wrong.txt
new file mode 100644
index 0000000000..6c897b71f7
--- /dev/null
+++ b/test/ssl-luatest/certs/pw-wrong.txt
@@ -0,0 +1,2 @@
+dasdas11
+foobar11
diff --git a/test/ssl-luatest/certs/pw.txt b/test/ssl-luatest/certs/pw.txt
new file mode 100644
index 0000000000..7f42bd42d5
--- /dev/null
+++ b/test/ssl-luatest/certs/pw.txt
@@ -0,0 +1,2 @@
+dasdas
+foobar
diff --git a/test/ssl-luatest/certs/self-sign-cert.pem b/test/ssl-luatest/certs/self-sign-cert.pem
new file mode 100644
index 0000000000..d3ba65c37f
--- /dev/null
+++ b/test/ssl-luatest/certs/self-sign-cert.pem
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF7zCCA9egAwIBAgIUBRTgnKPOruqAugFby+JMVf7IXPwwDQYJKoZIhvcNAQEL
+BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM
+CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu
+eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y
+NDAxMjIwODMzMThaFw0zNDAxMTkwODMzMThaMIGGMQswCQYDVQQGEwJYWDESMBAG
+A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t
+cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU
+Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQCou5sG5UmvDbkb9l/EXp9ng5bUHAm7yr7ytQfEU4AXizvkYweIDXPEvHQr
+e9wIb09S6qogxkpzF+e6D5lVlTOJ3i1c7b/WWfuLzqfbPWIHjs6mfgD5GFl1tm6h
+zHo12m3o45ETlgosnCDMYKYsr/iHzISktlD/Ht+vZ+278Zs1axyrj8ibPGBK37n9
+kiu/3F8kjBL3/nttD3cbg7ICHaVoWeQyYuQa7nRbAg8ZymJPJEsBLHyvpPDGnfaL
+ZbdwNhw0XQWUcbeqBQeNgSVOHt0mE1T2VSrdghVhytEBCkvtYfjSdAlroWGfuwxD
+WJaagPlim/wZuGmq950vUn0SUHP9gzeNWC1djrG4btMUcln9Dt8l3R2E0XaCLQlE
+qiwMTKORB1NQdVWOaE3W58sd2TB0TBGFwrWUF+2OjQ0UVolhCUyVXOEzTMqgRBEp
+WkXXQJKgyLT6TlDx3R+ma4Y6GjiM8OMM2x0DoCh30b0jiKgTeLBO+Nr/5eoVMtEE
+rCegiCBx7Bxtjmgfc79/3ziMIyHf+xPvwNL0ORIjgLhBHA2UIg5TJRRdckubBAMq
+V1F3wZKlmhhI6/9kpvSo9AA51FyGP8a2u/5TsNRjGn+NK50Wae1iyIIQknYgsy5t
+rbOeqAtAZZDu3IcREoJov+yO50w501XXluBPKSDIUxza3Cv9lwIDAQABo1MwUTAd
+BgNVHQ4EFgQUNhVaWoUqF+75qQ+lcl3PyQCGndEwHwYDVR0jBBgwFoAUNhVaWoUq
+F+75qQ+lcl3PyQCGndEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AgEAbdi/zOX57nvKwuWVF7IM4lRsAOuzEPIas0MU2z66HjMOmLkdAu3jdVGOt78N
+X1rA3lx/xroRm6m1KU0E9YVMflieYSDpE++HyqfwKBbTznKqlU155CzE320l1l6y
+4nL8FUeSK8bmGRn0kpOHN4R8Hc3OheYOOu4u0FauioZgqugXjxoj7uijs8/Pr8i2
+TBzdL9x7p+5/REFJAw3yp0agt+sEQAjFhr7gYmBMy10sTEwK2t+CuJXSfmb4odMX
+3qMxpYiFG4db7HY2q24jg/+IYWqpYMmF5Iyox5czSsFC07+yzTB9IKXF77YhRFYD
+cK6YCyYI6HpcNHIx3y+TjAsDJtxEMEZcgco01+rK9+/Vxmt+czkdxrtSuUnJnaKn
+5Jj4O9WZG9wnP+JZ7kN+8T+321a5fAHXCycDS64mza+r0fJKqoSm+MiSnhUc6RTQ
+2HDjWskCyxaV4yhThKq++XDwtKHN2pOqB0Qp3YXUCOyLgcNYoWZfu11BMHYFO/Ot
+bTMm3CQcGfGSwpwvDxRLxTZzAu87/hX9dbKuQkTi0+YZaR2FSE4+dMWoPPx/hIjr
+534yUHe/fjO3ZLiWw3kmX3zurI5PB+26jrH/Gfxidak1xKCBW05ahdF398vK72yo
+lJME85IdkiQfejVxwoTnmucKpFgcmc9n9S97T36GUYmHbU8=
+-----END CERTIFICATE-----
diff --git a/test/ssl-luatest/certs/self-sign-key.pem b/test/ssl-luatest/certs/self-sign-key.pem
new file mode 100644
index 0000000000..d2aaf1445b
--- /dev/null
+++ b/test/ssl-luatest/certs/self-sign-key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+zQciPkr31A5V0Lb8XBJ+qnHmUs55kXE=
+-----END PRIVATE KEY-----
diff --git a/test/ssl-luatest/certs/self-sign-pw-cert.crt b/test/ssl-luatest/certs/self-sign-pw-cert.crt
new file mode 100644
index 0000000000..d83991554e
--- /dev/null
+++ b/test/ssl-luatest/certs/self-sign-pw-cert.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIUMHVL+ZNDBFFVJ/tqfYVa25kcMCowDQYJKoZIhvcNAQEL
+BQAwTzELMAkGA1UEBhMCUlUxDDAKBgNVBAgMA2JnZDEMMAoGA1UEBwwDYmdkMREw
+DwYDVQQKDAhwaWNvZGF0YTERMA8GA1UECwwIcGljb2RhdGEwHhcNMjQwMTMxMDk1
+MDA5WhcNMjYwNTA1MDk1MDA5WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29t
+ZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAslKRWOF7tdz/nBEnFgCJLaGoJdDY
+ve2iDHHQC7er/84EoDLZxLqTh4NLvmcMps1DkMtmZbRZMKYekyLNEXCdjsUp0T5t
+Lbpmb+E56ERh46EOFs1X/DckeMHLk65oQcydtLIXgj0HCViZ8kzm6jQIZlF2unvh
+99LnIulHurRjakzKLJ8mzM7Dernxr5NBGOt/L49/59LJXfn6/5llJRnVQ+OlZ3XZ
+h0brVFsWSHSaFHBqg9PYuGtjFEBq2nezZTGHw5xVARGwnQUz8IK9fhOKCCia7oBY
+V0wuHBy+Ytxzqq4NY4dASU4+Q7Paws9CuTWS680yGKJfMQHNZoLmgJYz2wIDAQAB
+o3EwbzAfBgNVHSMEGDAWgBRd+tplnGSGdMg254qdbD6F7oosCDAJBgNVHRMEAjAA
+MAsGA1UdDwQEAwIE8DAVBgNVHREEDjAMggp0dC5wdy50ZXN0MB0GA1UdDgQWBBRl
+MXbObvqvNqzaBlFD+DTHtxjYxjANBgkqhkiG9w0BAQsFAAOCAQEALuP/RzFVUMt4
+LSb6yKtmdQTR5eg6/VH+CKTUH+j6B8ZnP4VKqZV9QwqYTWGyyFsUHLqi3QlpbSnd
+pn907HNa3VTqlnnF7G3IBT2+8jQ0nPJPcGSXnhEoC59SAFTT6yBsqT6LvCG4l7Ji
+X8wG8Z3lj3pfvzSln6XwvKxOUnL2mrvFszhMp164HwF6WQGMT1bS2JBGDP1NHfUv
+TujolUsv3ee5Ji9QadtISRhDWkmYdZXOnIfsWxMHDi4QERNYShI5i4IogzNJJJll
+WUM7e5/MhQ5g3Lc78dDUf3pIrb37lLC53sazzisrCruqfRRH3DJYiWJxGP84g7EG
+xARp+uAQ9w==
+-----END CERTIFICATE-----
diff --git a/test/ssl-luatest/certs/self-sign-pw-key.key b/test/ssl-luatest/certs/self-sign-pw-key.key
new file mode 100644
index 0000000000..1d5a03ec0f
--- /dev/null
+++ b/test/ssl-luatest/certs/self-sign-pw-key.key
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIn1/ErmlOKI0CAggA
+MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDd8hdPT58yD88x07lU6KHZBIIE
+0L0FVrX+ZLvbwwjjeliz4HZ5HuoeuBLXSmEnVS99jPV+FP598/BC7Rqwy8eyK/FN
+qlcXvwDH437oG58/lJgWlO7opfGgXVLAyo1dCppv8anC0fjIs9FojY1VF0Itw87/
+t69WdKNzRKkIdn961ZKcREewCfcRRO6Pfrv75zBUDxkL6x4o7u+MvdBHgXikKkEA
+/zEThxxcSJ6z4p/nd6cGuxW6z5XVICXuwMFSmpMhsTNYoZ7rv6aZaFYMDCqEEmUz
+LFj0L4WMQhaAqOXnBxEAVk6pg0XJC3Z/xva5ddxORDBCzSFv+jUJtgbBJkZXeczl
+Ec3WEH4Y3vnSdosYQQQuZN127aK4PJqwZlezBl465ip8RGcTcw53yG4X+PcFE6E6
+o9oCU/5QpCwcYOx2ZuzM6Y02ubYua3y9ceSr6wv1vGqrIQeTPz4JcyERQdfajhXB
+BasJks98M92+MoN/7IuseugBf55xHSrpZj2LyxcQboBnnGbvIFcFOjNEI1R6vbPo
+7uZRMuGKKRiQ2CyOy34rdCBqOlv6r03IZO29Mxxsp+BtV8hmHEAd7SMBgBhZTnbX
+Ru0KuF7wqlbTvCNvP6PExEuAkoA71Gg/0UL8feGrWqvto4RdrvnTfdHIBHSwA9f4
+MZAWsDTezui+bma8Jut1tXr4N0LAu9+hveNxBDkUvQ8Yb2nK3041YfY1j46TuYE7
+CgpEOt7Y8vFqVhGQpBOx10BhR4QFE7wv7rbdldyhbDguxG/r+5konL4dCvYapnyF
+Rr6eNY5flgh1Vyezbihd85BNrYDDMT4gZ0GKEvsXG7+QVAIdZjhKvdCXbT3MVHCd
+5hOKz5BojGL3z1z992k0hBDNEz7g2IxzdiE/fy900avq56vRJZgLLKzYHzX5JPmB
+VHxNhpRllBij4Uw07FknpZWAxsW/rkT/aT3vgAO4g2v8/oO6SSgl2ac/eGrSzWo2
+3eJnDhwQL2S4bslacYvTsRu/A8uo+QDp/YFFvTjty6HeyU1A/I+UQfSgEj7b7Hjr
+M1Bu75XNt0UzMZpT5Zn2eNvqYxWKXXTOrAmRQrQ/PM1ddRj+yS+hwDTtWP3z8GHV
+uKWybOzugLxD+WAfUwusd1IvwxrevfhzaOr10uAmlSf93yy0i8t/g/6QAhk77PdK
+ujIr2M+ORvUf6vsRo4Z/rED/oj1OEbVdv33CkFAE/ZQWrWryFMFZoG2FY6Ggu2iz
+igxjCSgGRtge9E3Yr1UdTT8594TOzAHXKly1U1vwIwCPEpsaHbUrNJ4EaKNLIWfL
+D7tHceujWLlQFL85qGhOQHYp2aYiaoqkomosvXiOrM1+9XMfNkKlmkj9FxNLQhzU
+/noD1u3H6zWXsOdFGNujvOLyCWe2ZEfIkRf1yC51ViqKKhw1eUUeHxQcEYgiCBlp
+Rz5Y2heCAG0rc7vnZhd3v85GGzHh/Miz09QTAIPQKlhkGQuQXJHfdgo2vALS/hww
+6F0hOXDR4GATNZ9/XjCcXu4OKyNJV9lO/L3KwBT6Ab9AOZUyj/sThLncSeBIy5Yc
+Dz7X2ZEjrYHKlnts+Le/kOgHzvMNwgac90iExoawdIeh6sjRKxQl+0AhI6u/0I6g
+wEC1uHmnKNMFujFNt8AIG9GDcKnEA+0qGHypUfM5mFe5
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/test/ssl-luatest/certs/self-sign-wrong-key.pem b/test/ssl-luatest/certs/self-sign-wrong-key.pem
new file mode 100644
index 0000000000..89f5aafddb
--- /dev/null
+++ b/test/ssl-luatest/certs/self-sign-wrong-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC35jiba2tJOIQE
+FlX5hLT3OaV/5CkXqm9+tDVB0WHJ1T/E0cZAcdrBgJHjgQP4gcMGhyfPoUB5GCvy
+/V3uva2dl4ieth+gUCUl8nMdFP82BYFCggERlY5b4BDnbeeexZwrzkKMBZaTc7Kf
+FuNl1E518jZs3GJ3TX1aDKx1q+EYdaeM8B1Ta1fvCZNfZWxTKUE0KBzJ3WFTctYi
+wb0noT4SOjgga4jS410G160ql+7r0rXf3I0/jNK2tzVHygwoCZNNHbEsNyCu2tct
+P7AlmkoMYxKbbM0pDblh6Mljq2shyRU7hjB0xqI57pvqS+A+nk3kigOEc0x1wgJR
+eo3fO86LAgMBAAECggEAEJtKG62GHofA0tZU2jej2JAh7rF4Yn+MxPjaDFZaSc3S
+OIr8UlUrEilIQ1lNtf27J6uXKpafCgW8RkZKBWVR4yF21WTl6JCu4hqJpE6BQ2wY
+1vGCzjHet/49GIu3YazqTuxihwl5e670KjijZ/kq7VdmQctfBZ3/nAej9UEEY98T
+Xvj4vgJQCS4N8vNx/4iVrzxGEBSOZzUZ4m1fIorSzkBd0YB57NFxCkWg0WqD/k1H
+d8cS84SnR/8xnKqKbDm8z3/66ElztIsgu7CYedsXlQ9XIUeqcSHWHJhuLQ39ediw
+NGgCvca6spPbykSeHw5QDWYp7aKirDLLfxkilc2iVQKBgQDGjJd36FoaxtisxJIG
+lmbsmaFeW8n71Ln+LnbMQ+XJOcH6sIOVCZZSOrpSq0lCaRMnB6RyGi0vrn6Pf3lV
+xKOKMpbtLT6nPwPoXdR9TNeIYrlg9IFEoZkGiUBDO0w9/wZgWJKM3VUZE1VPh/kr
+w+ZcSsV70reGRpNFYyu41APfdQKBgQDtHHLBtXsWALrXNIdEltkpGjjEoRPQuVlZ
+vp1VwNyrcH6yVjEQ/lMu/JdC4JO61FGJl/sysCLn7vXiKuesnXw9NAN+MJlpORkr
+HizD0PULhiq8FgpwE4QaEvO2x2BBo6iDMeojR6wdE0DjNRDx/XFlCyB1N8wntNUj
+S45tCwk1/wKBgGN8+Tz9MYEvWE0h5mXkoUg3JxPH/KryigwiriZmc1LkXR3HqvdY
+KXmksc5Qw9HCIvWwr6b5FOFKl2JCJsNiV+wcs2G/BSD6w3OA6MOsaGePyIIbolaI
+fsw2o/vMT8TU8BYA69Yn5cc918aVRLa4X8qpMNF94bYn0Q24xqTDn/ktAoGAXU66
+ohwGqevzmsijCozKPHCDMm2o3JRz4usuAxb4P8bvNMLSYDuVBIKGC91QhU8UHy9d
+vN6vfdH9lNkJflYjE/qp/Timxk+f5eXj+9L4+2X63zVVOjGT5KbcnWrsKCh9IdO8
+rdGAm5h+CmtRlckEnJy6UyZ4ApAY5+DN1X9oSoUCgYBlIRnr+pAdAi9/c1WSyMQM
+HmnemZcLeVDsnzgkrzMKZ/nivGDWGFLgseKlMHZ2cffi6i7gMqE6Ct2qGZS61wy4
+8hy6VgxDfT/DSNN6wSklUT/0oN72vI4ZrJaHaNhN/Un+bl91cK2pWfRpZ/lNy0ar
+sY0tZQA3t9Vp/hfkAvRxBw==
+-----END PRIVATE KEY-----
diff --git a/test/ssl-luatest/net_box_test.lua b/test/ssl-luatest/net_box_test.lua
new file mode 100644
index 0000000000..9854c8df7f
--- /dev/null
+++ b/test/ssl-luatest/net_box_test.lua
@@ -0,0 +1,301 @@
+local server = require('luatest.server')
+local t = require('luatest')
+local fiber = require('fiber')
+local g = t.group()
+
+g.before_each(function()
+    g.server = server:new({ alias = 'server' })
+    g.server:start()
+    g.client = server:new({ alias = 'client' })
+    g.client:start()
+end)
+
+g.after_each(function()
+    g.server:stop()
+    g.client:stop()
+end)
+
+local function certs_file(name)
+    return require('fio').abspath('./test/ssl-luatest/certs') .. "/" .. name
+end
+
+g.test_net_box_works = function()
+    local srv_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        return box.info.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    local eval_result = g.client:exec(function(srv_uri)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+            }
+        })
+        t.assert_equals(connection.error, nil)
+
+        return connection:eval('return 21 * 2')
+    end, { srv_uri })
+
+    t.assert_equals(eval_result, 42)
+end
+
+g.test_client_close_connection = function()
+    local state_and_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        _G['client_connected'] = false
+        _G['client_disconnected'] = false
+        box.session.on_connect(function()
+            _G['client_connected'] = true
+        end)
+        box.session.on_disconnect(function()
+            _G['client_disconnected'] = true
+        end)
+
+        return {
+            box.info.listen,
+            {
+                _G['client_connected'],
+                _G['client_disconnected'],
+            },
+        }
+    end, {
+        certs_file('self-sign-key.pem'),
+        certs_file('self-sign-cert.pem'),
+    })
+
+    local srv_uri = state_and_uri[1]
+    local state = state_and_uri[2]
+
+    t.assert_equals(state, { false, false })
+
+    g.client:exec(function(srv_uri)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+            }
+        })
+        t.assert_equals(connection.error, nil)
+        t.assert_equals(connection:eval('return 21 * 2'), 42)
+
+        connection:close()
+    end, { srv_uri })
+
+    fiber.sleep(1)
+    local state = g.server:exec(function()
+        return { _G['client_connected'], _G['client_disconnected'] }
+    end)
+    t.assert_equals(state, { true, true })
+end
+
+g.test_server_drop_connection = function()
+    local srv_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        return box.info.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    g.client:exec(function(srv_uri)
+        _G['connection'] = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+            }
+        })
+        t.assert_equals(_G['connection'].error, nil)
+        t.assert_equals(_G['connection']:eval('return 21 * 2'), 42)
+    end, { srv_uri })
+
+    g.server:stop()
+
+    g.client:exec(function()
+        t.assert_error_msg_content_equals(
+                "Peer closed",
+                function()
+                    _G['connection']:eval('return 21 * 2')
+                end
+        )
+    end)
+end
+
+g.test_client_reconnect = function()
+    local srv_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        return box.info.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    g.client:exec(function(srv_uri)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+            }
+        })
+        t.assert_equals(connection.error, nil)
+        t.assert_equals(connection:eval('return 21 * 2'), 42)
+    end, { srv_uri })
+
+    g.client:stop()
+    g.client = server:new({ alias = 'client' })
+    g.client:start()
+
+    g.client:exec(function(srv_uri)
+        local connection = require('net.box').connect({
+            uri = srv_uri,
+            params = {
+                transport = 'ssl',
+            }
+        })
+
+        t.assert_equals(connection.error, nil)
+        t.assert_equals(connection:eval('return 21 * 2'), 42)
+    end, { srv_uri })
+end
+
+g.test_ssl_and_plain_transports_in_single_server = function()
+    local srv_uris = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                {
+                    uri = 'localhost:0',
+                    params = {
+                        transport = 'ssl',
+                        ssl_key_file = key,
+                        ssl_cert_file = cert,
+                    }
+                },
+                {
+                    uri = 'localhost:0'
+                }
+            }}
+        t.assert_not_equals(box.cfg.listen, nil)
+
+        return box.info.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    g.client:exec(function(srv_uris)
+        local secure_connection = require('net.box').connect({
+            uri = srv_uris[1],
+            params = {
+                transport = 'ssl',
+            }
+        })
+        t.assert_equals(secure_connection.error, nil)
+        t.assert_equals(secure_connection:eval('return 21 * 2'), 42)
+
+        local connection = require('net.box').connect({
+            uri = srv_uris[2],
+        })
+        t.assert_equals(connection.error, nil)
+        t.assert_equals(connection:eval('return 21 * 2'), 42)
+    end, { srv_uris })
+end
+
+g.test_client_wrong_connection = function()
+    local srv_uri = g.server:exec(function(key, cert)
+        box.cfg {
+            listen = {
+                uri = 'localhost:0',
+                params = {
+                    transport = 'ssl',
+                    ssl_key_file = key,
+                    ssl_cert_file = cert,
+                }
+            }
+        }
+        t.assert_not_equals(box.cfg.listen, nil)
+        return box.info.listen
+    end, { certs_file('self-sign-key.pem'), certs_file('self-sign-cert.pem') })
+
+    g.client:exec(function(srv_uri)
+        t.assert_error_msg_content_equals(
+                "Unable to set cipher list: UNKNOWN-CIPHER-LIST",
+                function()
+                    require('net.box').connect({
+                        uri = srv_uri,
+                        params = {
+                            transport = 'ssl',
+                            ssl_ciphers = 'UNKNOWN-CIPHER-LIST',
+                        }
+                    })
+                end
+        )
+    end, { srv_uri })
+end
+
+g.test_client_reconnect_after = function()
+    fiber.create(function()
+        fiber.sleep(2)
+
+        g.server:exec(function(key, cert)
+            box.cfg {
+                listen = {
+                    uri = 'localhost:3300',
+                    params = {
+                        transport = 'ssl',
+                        ssl_key_file = key,
+                        ssl_cert_file = cert,
+                    }
+                }
+            }
+            t.assert_not_equals(box.cfg.listen, nil)
+        end, {
+            certs_file('self-sign-key.pem'),
+            certs_file('self-sign-cert.pem'),
+        })
+    end)
+
+    g.client:exec(function()
+        local connection = require('net.box').connect({
+            uri = 'localhost:3300',
+            params = {
+                transport = 'ssl',
+            }
+        }, { reconnect_after = 1 })
+        t.assert_equals(connection.error, nil)
+    end)
+end
diff --git a/test/ssl-luatest/replication_test.lua b/test/ssl-luatest/replication_test.lua
new file mode 100644
index 0000000000..b32a35ccef
--- /dev/null
+++ b/test/ssl-luatest/replication_test.lua
@@ -0,0 +1,190 @@
+local t = require('luatest')
+local replica_set = require('luatest.replica_set')
+local fiber = require('fiber')
+
+local g = t.group()
+
+local function certs_file(name)
+    return require('fio').abspath('./test/ssl-luatest/certs') .. "/" .. name
+end
+
+g.before_each(function()
+    g.rs = replica_set:new()
+end)
+
+g.after_each(function()
+    g.rs:stop()
+end)
+
+g.test_replication = function()
+    g.rs:build_and_add_server({ alias = 'master', box_cfg = {
+        replication_timeout = 0.1,
+        replication_connect_timeout = 10,
+        replication = {
+            {
+                uri = 'guest@0.0.0.0:3300',
+                params = {
+                    transport = 'ssl',
+                }
+            },
+            {
+                uri = 'guest@0.0.0.0:3301',
+                params = {
+                    transport = 'ssl',
+                }
+            },
+        },
+        listen = {{
+            uri = '0.0.0.0:3300',
+            params = {
+                transport = 'ssl',
+                ssl_key_file = certs_file('self-sign-key.pem'),
+                ssl_cert_file = certs_file('self-sign-cert.pem'),
+            }
+        }, {uri = '0.0.0.0:3302'}}
+    }, net_box_uri = '0.0.0.0:3302',
+    })
+
+    g.rs:build_and_add_server({ alias = 'replica', box_cfg = {
+        replication_timeout = 0.1,
+        replication_connect_timeout = 10,
+        replication = {
+            {
+                uri = 'guest@0.0.0.0:3300',
+                params = {
+                    transport = 'ssl',
+                }
+            },
+            {
+                uri = 'guest@0.0.0.0:3301',
+                params = {
+                    transport = 'ssl',
+                }
+            },
+        },
+        listen = {{
+            uri = '0.0.0.0:3301',
+            params = {
+                transport = 'ssl',
+                ssl_key_file = certs_file('self-sign-key.pem'),
+                ssl_cert_file = certs_file('self-sign-cert.pem'),
+            }
+        }, {uri = '0.0.0.0:3303'}},
+        read_only = true,
+    }, net_box_uri = '0.0.0.0:3303',
+    })
+    g.rs:start()
+
+    local master_lsn = g.rs:get_server('master'):exec(function()
+        box.schema.space.create('test'):create_index('pk')
+
+        box.space.test:insert({ 1, "1" })
+        box.space.test:insert({ 2, "2" })
+        box.space.test:update(2, { { '=', 2, '3' } })
+        box.space.test:delete(1)
+
+        return box.info.lsn
+    end)
+
+    -- wait until data has been replicated
+    require('fiber').sleep(2)
+
+    local replica_lsn = g.rs:get_server('replica'):exec(function()
+        t.assert_equals(box.space.test:len(), 1)
+        return box.info.replication[1].lsn
+    end)
+
+    t.assert_equals(master_lsn, replica_lsn)
+end
+
+g.test_anon_replication = function()
+    g.rs:build_and_add_server({ alias = 'master', box_cfg = {
+        replication_timeout = 10,
+        replication_connect_timeout = 20,
+        listen = {{
+                      uri = '0.0.0.0:3361',
+                      params = {
+                          transport = 'ssl',
+                          ssl_key_file = certs_file('self-sign-key.pem'),
+                          ssl_cert_file = certs_file('self-sign-cert.pem'),
+                      }
+                  }, {uri = '0.0.0.0:3362'}}
+    }, net_box_uri = '0.0.0.0:3362',
+    })
+
+    g.rs:build_and_add_server({ alias = 'replica', box_cfg = {
+        replication_timeout = 10,
+        replication_connect_timeout = 20,
+        replication = {
+            {
+                uri = 'guest@0.0.0.0:3361',
+                params = {
+                    transport = 'ssl',
+                }
+            },
+        },
+        listen = {uri = '0.0.0.0:3363'},
+        read_only = true,
+        replication_anon=true,
+    }, net_box_uri = '0.0.0.0:3363',
+    })
+    g.rs:start()
+
+    local master_lsn = g.rs:get_server('master'):exec(function()
+        box.schema.space.create('test'):create_index('pk')
+
+        box.space.test:insert({ 1, "1" })
+        box.space.test:insert({ 2, "2" })
+        box.space.test:update(2, { { '=', 2, '3' } })
+        box.space.test:delete(1)
+
+        return box.info.lsn
+    end)
+
+    -- wait until data has been replicated
+    require('fiber').sleep(2)
+
+    local replica_lsn = g.rs:get_server('replica'):exec(function()
+        t.assert_equals(box.space.test:len(), 1)
+        return box.info.replication[1].lsn
+    end)
+
+    t.assert_equals(master_lsn, replica_lsn)
+end
+
+g.test_plain_replication_to_ssl_master = function()
+    g.rs:build_and_add_server({ alias = 'master', box_cfg = {
+        replication_timeout = 0.1,
+        replication_connect_timeout = 10,
+        listen = {{
+            uri = '0.0.0.0:3300',
+            params = {
+                transport = 'ssl',
+                ssl_key_file = certs_file('self-sign-key.pem'),
+                ssl_cert_file = certs_file('self-sign-cert.pem'),
+            }
+        }, {uri = '0.0.0.0:3302'}}
+    }, net_box_uri = '0.0.0.0:3302',
+    })
+
+    g.rs:build_and_add_server({ alias = 'replica', box_cfg = {
+        replication_timeout = 0.1,
+        replication_connect_timeout = 10,
+        replication = 'guest@0.0.0.0:3300',
+        listen = {uri = '0.0.0.0:3303'},
+        read_only = true,
+    }, net_box_uri = '0.0.0.0:3303',
+    })
+    g.rs:start({wait_until_ready = false})
+
+    fiber.sleep(2)
+
+    t.assert_error_msg_content_equals(
+            "net_box is not connected",
+            function()
+                g.rs:get_server('replica'):exec(function()
+                    return box.info.lsn
+                end)
+            end
+    )
+end
diff --git a/test/ssl-luatest/suite.ini b/test/ssl-luatest/suite.ini
new file mode 100644
index 0000000000..a140613fd4
--- /dev/null
+++ b/test/ssl-luatest/suite.ini
@@ -0,0 +1,5 @@
+[default]
+core = luatest
+description = encrypted iproto tests on luatest
+is_parallel = True
+use_unix_sockets_iproto = False
-- 
GitLab