Skip to content

Commit 2dbe12c

Browse files
Support SSL encrypted connection to Tarantool EE
This patch adds support for using SSL to encrypt the client-server communications [1]. The patch is based on a similar patch in tarantool/tarantool-python connector [2]. To use SSL encrypted connection, use Connection parameters: conn = asynctnt.Connection(host='127.0.0.1', port=3301, transport=asynctnt.Transport.SSL, ssl_key_file='./ssl/host.key', ssl_cert_file='./ssl/host.crt', ssl_ca_file='./ssl/ca.crt', ssl_ciphers='ECDHE-RSA-AES256-GCM-SHA384') If Tarantool server uses "ssl" transport, client connection also need to use asynctnt.Transport.SSL transport. If server side had ssl_ca_file set, ssl_key_file and ssl_cert_file are mandatory from the client side, otherwise optional. CA file and ciphers are optional. See available ciphers in Tarantool EE documentation [3]. 1. https://www.tarantool.io/en/enterprise_doc/security/#enterprise-iproto-encryption 2. tarantool/tarantool-python#220 3. https://www.tarantool.io/en/enterprise_doc/security/#supported-ciphers Closes igorcoding#22
1 parent 108c6f9 commit 2dbe12c

File tree

6 files changed

+151
-9
lines changed

6 files changed

+151
-9
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
**New features:**
5+
* Support SSL encrypted connection to Tarantool EE (closes [#22](https://github.com/igorcoding/asynctnt/issues/22))
6+
37
## v2.0.1
48
* Fixed an issue with encoding datetimes less than 01-01-1970 (fixes [#29](https://github.com/igorcoding/asynctnt/issues/29))
59
* Fixed "Edit on Github" links in docs (fixes [#26](https://github.com/igorcoding/asynctnt/issues/26))

asynctnt/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .const import Transport
12
from .connection import Connection, connect
23
from .iproto.protocol import (
34
Iterator, Response, TarantoolTuple, PushIterator,

asynctnt/connection.py

+106-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import asyncio
22
import enum
33
import functools
4+
import ssl
45
import os
56
from typing import Optional, Union
67

78
from .api import Api
9+
from .const import Transport
810
from .exceptions import TarantoolDatabaseError, \
9-
ErrorCode, TarantoolError
11+
ErrorCode, TarantoolError, SSLError
1012
from .iproto import protocol
1113
from .log import logger
1214
from .stream import Stream
@@ -27,11 +29,13 @@ class ConnectionState(enum.IntEnum):
2729

2830
class Connection(Api):
2931
__slots__ = (
30-
'_host', '_port', '_username', '_password',
31-
'_fetch_schema', '_auto_refetch_schema', '_initial_read_buffer_size',
32-
'_encoding', '_connect_timeout', '_reconnect_timeout',
33-
'_request_timeout', '_ping_timeout', '_loop', '_state', '_state_prev',
34-
'_transport', '_protocol',
32+
'_host', '_port', '_parameter_transport', '_ssl_key_file',
33+
'_ssl_cert_file', '_ssl_ca_file', '_ssl_ciphers',
34+
'_username', '_password', '_fetch_schema',
35+
'_auto_refetch_schema', '_initial_read_buffer_size',
36+
'_encoding', '_connect_timeout', '_ssl_handshake_timeout',
37+
'_reconnect_timeout', '_request_timeout', '_ping_timeout',
38+
'_loop', '_state', '_state_prev', '_transport', '_protocol',
3539
'_disconnect_waiter', '_reconnect_task',
3640
'_connect_lock', '_disconnect_lock',
3741
'_ping_task', '__create_task'
@@ -40,11 +44,17 @@ class Connection(Api):
4044
def __init__(self, *,
4145
host: str = '127.0.0.1',
4246
port: Union[int, str] = 3301,
47+
transport: Optional[Transport] = Transport.DEFAULT,
48+
ssl_key_file: Optional[str] = None,
49+
ssl_cert_file: Optional[str] = None,
50+
ssl_ca_file: Optional[str] = None,
51+
ssl_ciphers: Optional[str] = None,
4352
username: Optional[str] = None,
4453
password: Optional[str] = None,
4554
fetch_schema: bool = True,
4655
auto_refetch_schema: bool = True,
4756
connect_timeout: float = 3.,
57+
ssl_handshake_timeout: float = 3.,
4858
request_timeout: float = -1.,
4959
reconnect_timeout: float = 1. / 3.,
5060
ping_timeout: float = 5.,
@@ -78,6 +88,22 @@ def __init__(self, *,
7888
:param port:
7989
Tarantool port
8090
(pass ``/path/to/sockfile`` to connect ot unix socket)
91+
:param transport:
92+
This parameter can be used to configure traffic encryption.
93+
Pass ``asynctnt.Transport.SSL`` value to enable SSL
94+
encryption (by default there is no encryption)
95+
:param ssl_key_file:
96+
A path to a private SSL key file.
97+
Optional, mandatory if server uses CA file
98+
:param ssl_cert_file:
99+
A path to an SSL certificate file.
100+
Optional, mandatory if server uses CA file
101+
:param ssl_ca_file:
102+
A path to a trusted certificate authorities (CA) file.
103+
Optional
104+
:param ssl_ciphers:
105+
A colon-separated (:) list of SSL cipher suites
106+
the connection can use. Optional
81107
:param username:
82108
Username to use for auth
83109
(if ``None`` you are connected as a guest)
@@ -93,6 +119,10 @@ def __init__(self, *,
93119
be checked by Tarantool, so no errors will occur
94120
:param connect_timeout:
95121
Time in seconds how long to wait for connecting to socket
122+
:param ssl_handshake_timeout:
123+
Time in seconds to wait for the TLS handshake to complete
124+
before aborting the connection (used only for a TLS
125+
connection)
96126
:param request_timeout:
97127
Request timeout (in seconds) for all requests
98128
(by default there is no timeout)
@@ -116,6 +146,13 @@ def __init__(self, *,
116146
super().__init__()
117147
self._host = host
118148
self._port = port
149+
150+
self._parameter_transport = transport
151+
self._ssl_key_file = ssl_key_file
152+
self._ssl_cert_file = ssl_cert_file
153+
self._ssl_ca_file = ssl_ca_file
154+
self._ssl_ciphers = ssl_ciphers
155+
119156
self._username = username
120157
self._password = password
121158
self._fetch_schema = False if fetch_schema is None else fetch_schema
@@ -131,6 +168,7 @@ def __init__(self, *,
131168
self._encoding = encoding or 'utf-8'
132169

133170
self._connect_timeout = connect_timeout
171+
self._ssl_handshake_timeout = ssl_handshake_timeout
134172
self._reconnect_timeout = reconnect_timeout or 0
135173
self._request_timeout = request_timeout
136174
self._ping_timeout = ping_timeout or 0
@@ -220,6 +258,54 @@ def protocol_factory(self,
220258
on_connection_lost=self.connection_lost,
221259
loop=self._loop)
222260

261+
def _create_ssl_context(self):
262+
try:
263+
if hasattr(ssl, 'TLSVersion'):
264+
# Since python 3.7
265+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
266+
# Reset to default OpenSSL values.
267+
context.check_hostname = False
268+
context.verify_mode = ssl.CERT_NONE
269+
# Require TLSv1.2, because other protocol versions don't seem
270+
# to support the GOST cipher.
271+
context.minimum_version = ssl.TLSVersion.TLSv1_2
272+
context.maximum_version = ssl.TLSVersion.TLSv1_2
273+
else:
274+
# Deprecated, but it works for python < 3.7
275+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
276+
277+
if self._ssl_cert_file:
278+
# If the password argument is not specified and a password is
279+
# required, OpenSSL’s built-in password prompting mechanism
280+
# will be used to interactively prompt the user for a password.
281+
#
282+
# We should disable this behaviour, because a python
283+
# application that uses the connector unlikely assumes
284+
# interaction with a human + a Tarantool implementation does
285+
# not support this at least for now.
286+
def password_raise_error():
287+
raise SSLError("a password for decrypting the private " +
288+
"key is unsupported")
289+
context.load_cert_chain(certfile=self._ssl_cert_file,
290+
keyfile=self._ssl_key_file,
291+
password=password_raise_error)
292+
293+
if self._ssl_ca_file:
294+
context.load_verify_locations(cafile=self._ssl_ca_file)
295+
context.verify_mode = ssl.CERT_REQUIRED
296+
# A Tarantool implementation does not check hostname. We don't
297+
# do that too. As a result we don't set here:
298+
# context.check_hostname = True
299+
300+
if self._ssl_ciphers:
301+
context.set_ciphers(self._ssl_ciphers)
302+
303+
return context
304+
except SSLError as e:
305+
raise
306+
except Exception as e:
307+
raise SSLError(e)
308+
223309
async def _connect(self, return_exceptions: bool = True):
224310
if self._loop is None:
225311
self._loop = get_running_loop()
@@ -246,6 +332,12 @@ async def full_connect():
246332
while True:
247333
connected_fut = _create_future(self._loop)
248334

335+
ssl_context = None
336+
ssl_handshake_timeout = None
337+
if self._parameter_transport == Transport.SSL:
338+
ssl_context = self._create_ssl_context()
339+
ssl_handshake_timeout = self._ssl_handshake_timeout
340+
249341
if self._host.startswith('unix/'):
250342
unix_path = self._port
251343
assert isinstance(unix_path, str), \
@@ -260,13 +352,16 @@ async def full_connect():
260352
conn = self._loop.create_unix_connection(
261353
functools.partial(self.protocol_factory,
262354
connected_fut),
263-
unix_path
264-
)
355+
unix_path,
356+
ssl=ssl_context,
357+
ssl_handshake_timeout=ssl_handshake_timeout)
265358
else:
266359
conn = self._loop.create_connection(
267360
functools.partial(self.protocol_factory,
268361
connected_fut),
269-
self._host, self._port)
362+
self._host, self._port,
363+
ssl=ssl_context,
364+
ssl_handshake_timeout=ssl_handshake_timeout)
270365

271366
tr, pr = await conn
272367

@@ -337,6 +432,8 @@ async def full_connect():
337432

338433
if return_exceptions:
339434
self._reconnect_task = None
435+
if isinstance(e, ssl.SSLError):
436+
e = SSLError(e)
340437
raise e
341438

342439
logger.exception(e)

asynctnt/const.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import enum
2+
3+
class Transport(enum.IntEnum):
4+
DEFAULT = 1
5+
SSL = 2

asynctnt/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class TarantoolNotConnectedError(TarantoolNetworkError):
4242
"""
4343
pass
4444

45+
class SSLError(TarantoolError):
46+
"""
47+
Raised when something is wrong with encrypted connection
48+
"""
49+
pass
50+
4551

4652
class ErrorCode(enum.IntEnum):
4753
"""

docs/examples.md

+29
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,32 @@ async def main():
6565

6666
asyncio.run(main())
6767
```
68+
69+
## Connect with SSL encryption
70+
```python
71+
import asyncio
72+
import asynctnt
73+
74+
75+
async def main():
76+
conn = asynctnt.Connection(host='127.0.0.1',
77+
port=3301,
78+
transport=asynctnt.Transport.SSL,
79+
ssl_key_file='./ssl/host.key',
80+
ssl_cert_file='./ssl/host.crt',
81+
ssl_ca_file='./ssl/ca.crt',
82+
ssl_ciphers='ECDHE-RSA-AES256-GCM-SHA384')
83+
await conn.connect()
84+
85+
resp = await conn.ping()
86+
print(resp)
87+
88+
await conn.disconnect()
89+
90+
asyncio.run(main())
91+
```
92+
93+
Stdout:
94+
```
95+
<Response sync=4 rowcount=0 data=None>
96+
```

0 commit comments

Comments
 (0)