Skip to content

Commit bb6b84b

Browse files
api: support SSL private key file decryption
Support `ssl_password` and `ssl_password_file` options in Connection, MeshConnection and ConnectionPool to decrypt private SSL key file. Tarantool EE supports SSL passwords and password files only in current master since commit e1f47dd4 (after 2.11.0-entrypoint) [1]. Same as in Tarantool, we try `ssl_password`, then each line in `ssl_password_file` and then try to use key without decryption. If all of the above fail, we re-raise errors. 1. tarantool/tarantool-ee#22 Closes #224.
1 parent 6f0d536 commit bb6b84b

18 files changed

+735
-257
lines changed

Diff for: .github/workflows/testing.yml

+13-7
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,14 @@ jobs:
113113
fail-fast: false
114114
matrix:
115115
tarantool:
116-
- '1.10.11-0-gf0b0e7ecf-r470'
117-
- '2.8.3-21-g7d35cd2be-r470'
118-
- '2.10.0-1-gfa775b383-r486-linux-x86_64'
116+
- bundle: 'bundle-1.10.11-0-gf0b0e7ecf-r470'
117+
path: ''
118+
- bundle: 'bundle-2.8.3-21-g7d35cd2be-r470'
119+
path: ''
120+
- bundle: 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64'
121+
path: ''
122+
- bundle: 'sdk-gc64-2.11.0-entrypoint-107-ga18449d54-r524.linux.x86_64'
123+
path: 'dev/linux/x86_64/master/'
119124
python: ['3.6', '3.7', '3.8', '3.9', '3.10']
120125

121126
steps:
@@ -131,10 +136,10 @@ jobs:
131136
if: github.event_name != 'pull_request_target'
132137
uses: actions/checkout@v2
133138

134-
- name: Install tarantool ${{ matrix.tarantool }}
139+
- name: Install Tarantool EE SDK
135140
run: |
136-
ARCHIVE_NAME=tarantool-enterprise-bundle-${{ matrix.tarantool }}.tar.gz
137-
curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${ARCHIVE_NAME}
141+
ARCHIVE_NAME=tarantool-enterprise-${{ matrix.tarantool.bundle }}.tar.gz
142+
curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${{ matrix.tarantool.path }}${ARCHIVE_NAME}
138143
tar -xzf ${ARCHIVE_NAME}
139144
rm -f ${ARCHIVE_NAME}
140145
@@ -163,7 +168,8 @@ jobs:
163168
source tarantool-enterprise/env.sh
164169
make test
165170
env:
166-
TEST_TNT_SSL: ${{ matrix.tarantool == '2.10.0-1-gfa775b383-r486-linux-x86_64' }}
171+
TEST_TNT_SSL: ${{ matrix.tarantool.bundle == 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64' ||
172+
matrix.tarantool.bundle == 'sdk-gc64-2.11.0-entrypoint-107-ga18449d54-r524.linux.x86_64'}}
167173

168174
run_tests_pip_branch_install_linux:
169175
# We want to run on external PRs, but not on our own internal

Diff for: .gitignore

+12
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,15 @@ debian/files
4141
debian/*.substvars
4242

4343
deb_dist
44+
45+
test/data/*.crt
46+
!test/data/ca.crt
47+
!test/data/invalidhost.crt
48+
!test/data/localhost.crt
49+
test/data/*.csr
50+
test/data/*.ext
51+
test/data/*.key
52+
!test/data/localhost.key
53+
!test/data/localhost.enc.key
54+
test/data/*.pem
55+
test/data/*.srl

Diff for: CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- Support custom packer and unpacker factories (#191).
11-
1211
- Support [crud module](https://github.com/tarantool/crud) native API (#205).
12+
- Support `ssl_password` and `ssl_password_file` options
13+
to decrypt private SSL key file (#224).
1314

1415
### Changed
1516

Diff for: tarantool/connection.py

+70-15
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
DEFAULT_SSL_CERT_FILE,
5757
DEFAULT_SSL_CA_FILE,
5858
DEFAULT_SSL_CIPHERS,
59+
DEFAULT_SSL_PASSWORD,
60+
DEFAULT_SSL_PASSWORD_FILE,
5961
REQUEST_TYPE_OK,
6062
REQUEST_TYPE_ERROR,
6163
IPROTO_GREETING_SIZE,
@@ -569,6 +571,8 @@ def __init__(self, host, port,
569571
ssl_cert_file=DEFAULT_SSL_CERT_FILE,
570572
ssl_ca_file=DEFAULT_SSL_CA_FILE,
571573
ssl_ciphers=DEFAULT_SSL_CIPHERS,
574+
ssl_password=DEFAULT_SSL_PASSWORD,
575+
ssl_password_file=DEFAULT_SSL_PASSWORD_FILE,
572576
packer_factory=default_packer_factory,
573577
unpacker_factory=default_unpacker_factory):
574578
"""
@@ -693,6 +697,15 @@ def __init__(self, host, port,
693697
suites the connection can use.
694698
:type ssl_ciphers: :obj:`str` or :obj:`None`, optional
695699
700+
:param ssl_password: Password for decrypting
701+
:paramref:`~tarantool.Connection.ssl_key_file`.
702+
:type ssl_password: :obj:`str` or :obj:`None`, optional
703+
704+
:param ssl_password_file: File with password for decrypting
705+
:paramref:`~tarantool.Connection.ssl_key_file`. Connection
706+
tries every line from the file as a password.
707+
:type ssl_password_file: :obj:`str` or :obj:`None`, optional
708+
696709
:param packer_factory: Request MessagePack packer factory.
697710
Supersedes :paramref:`~tarantool.Connection.encoding`. See
698711
:func:`~tarantool.request.packer_factory` for example of
@@ -754,6 +767,8 @@ def __init__(self, host, port,
754767
self.ssl_cert_file = ssl_cert_file
755768
self.ssl_ca_file = ssl_ca_file
756769
self.ssl_ciphers = ssl_ciphers
770+
self.ssl_password = ssl_password
771+
self.ssl_password_file = ssl_password_file
757772
self._protocol_version = None
758773
self._features = {
759774
IPROTO_FEATURE_STREAMS: False,
@@ -884,21 +899,7 @@ def wrap_socket_ssl(self):
884899
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
885900

886901
if self.ssl_cert_file:
887-
# If the password argument is not specified and a password is
888-
# required, OpenSSL’s built-in password prompting mechanism
889-
# will be used to interactively prompt the user for a password.
890-
#
891-
# We should disable this behaviour, because a python
892-
# application that uses the connector unlikely assumes
893-
# interaction with a human + a Tarantool implementation does
894-
# not support this at least for now.
895-
def password_raise_error():
896-
raise SslError("Password for decrypting the private " +
897-
"key is unsupported")
898-
context.load_cert_chain(certfile=self.ssl_cert_file,
899-
keyfile=self.ssl_key_file,
900-
password=password_raise_error)
901-
902+
self._ssl_load_cert_chain(context)
902903
if self.ssl_ca_file:
903904
context.load_verify_locations(cafile=self.ssl_ca_file)
904905
context.verify_mode = ssl.CERT_REQUIRED
@@ -915,6 +916,60 @@ def password_raise_error():
915916
except Exception as e:
916917
raise SslError(e)
917918

919+
def _ssl_load_cert_chain(self, context):
920+
"""
921+
Decrypt and load SSL certificate and private key files.
922+
Mimic Tarantool EE approach here: see `SSL commit`_.
923+
924+
:param context: SSL context.
925+
:type context: :obj:`ssl.SSLContext`
926+
927+
:raise: :exc:`~tarantool.error.SslError`
928+
929+
:meta private:
930+
931+
.. _SSL commit: https://github.com/tarantool/tarantool-ee/commit/e1f47dd4adbc6657159c611298aad225883a536b
932+
"""
933+
934+
exc_list = []
935+
936+
if self.ssl_password is not None:
937+
try:
938+
context.load_cert_chain(certfile=self.ssl_cert_file,
939+
keyfile=self.ssl_key_file,
940+
password=self.ssl_password)
941+
return
942+
except Exception as e:
943+
exc_list.append(e)
944+
945+
946+
if self.ssl_password_file is not None:
947+
with open(self.ssl_password_file) as file:
948+
for line in file:
949+
try:
950+
context.load_cert_chain(certfile=self.ssl_cert_file,
951+
keyfile=self.ssl_key_file,
952+
password=line.rstrip())
953+
return
954+
except Exception as e:
955+
exc_list.append(e)
956+
957+
958+
try:
959+
def password_raise_error():
960+
raise SslError("Password prompt for decrypting the private " +
961+
"key is unsupported, use ssl_password or " +
962+
"ssl_password_file")
963+
context.load_cert_chain(certfile=self.ssl_cert_file,
964+
keyfile=self.ssl_key_file,
965+
password=password_raise_error)
966+
967+
return
968+
except Exception as e:
969+
exc_list.append(e)
970+
971+
raise SslError(exc_list)
972+
918973
def handshake(self):
919974
"""
920975
Process greeting with Tarantool server.

Diff for: tarantool/connection_pool.py

+15-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
POOL_INSTANCE_RECONNECT_DELAY,
1818
POOL_INSTANCE_RECONNECT_MAX_ATTEMPTS,
1919
POOL_REFRESH_DELAY,
20-
SOCKET_TIMEOUT
20+
SOCKET_TIMEOUT,
21+
DEFAULT_SSL_PASSWORD,
22+
DEFAULT_SSL_PASSWORD_FILE,
2123
)
2224
from tarantool.error import (
2325
ClusterConnectWarning,
@@ -383,13 +385,15 @@ def __init__(self,
383385
.. code-block:: python
384386
385387
{
386-
"host': "str" or None, # mandatory
387-
"port": int or "str", # mandatory
388-
"transport": "str", # optional
389-
"ssl_key_file": "str", # optional
390-
"ssl_cert_file": "str", # optional
391-
"ssl_ca_file": "str", # optional
392-
"ssl_ciphers": "str" # optional
388+
"host': "str" or None, # mandatory
389+
"port": int or "str", # mandatory
390+
"transport": "str", # optional
391+
"ssl_key_file": "str", # optional
392+
"ssl_cert_file": "str", # optional
393+
"ssl_ca_file": "str", # optional
394+
"ssl_ciphers": "str" # optional
395+
"ssl_password": "str", # optional
396+
"ssl_password_file": "str" # optional
393397
}
394398
395399
Refer to corresponding :class:`~tarantool.Connection`
@@ -492,7 +496,9 @@ def __init__(self,
492496
ssl_key_file=addr['ssl_key_file'],
493497
ssl_cert_file=addr['ssl_cert_file'],
494498
ssl_ca_file=addr['ssl_ca_file'],
495-
ssl_ciphers=addr['ssl_ciphers'])
499+
ssl_ciphers=addr['ssl_ciphers'],
500+
ssl_password=addr['ssl_password'],
501+
ssl_password_file=addr['ssl_password_file'])
496502
)
497503

498504
if connect_now:

Diff for: tarantool/const.py

+4
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@
117117
DEFAULT_SSL_CA_FILE = None
118118
# Default value for list of SSL ciphers
119119
DEFAULT_SSL_CIPHERS = None
120+
# Default value for SSL key file password
121+
DEFAULT_SSL_PASSWORD = None
122+
# Default value for a path to file with SSL key file password
123+
DEFAULT_SSL_PASSWORD_FILE = None
120124
# Default cluster nodes list refresh interval (seconds)
121125
CLUSTER_DISCOVERY_DELAY = 60
122126
# Default cluster nodes state refresh interval (seconds)

Diff for: tarantool/mesh_connection.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
DEFAULT_SSL_CERT_FILE,
2525
DEFAULT_SSL_CA_FILE,
2626
DEFAULT_SSL_CIPHERS,
27+
DEFAULT_SSL_PASSWORD,
28+
DEFAULT_SSL_PASSWORD_FILE,
2729
CLUSTER_DISCOVERY_DELAY,
2830
)
2931

@@ -36,7 +38,9 @@
3638
'ssl_key_file': DEFAULT_SSL_KEY_FILE,
3739
'ssl_cert_file': DEFAULT_SSL_CERT_FILE,
3840
'ssl_ca_file': DEFAULT_SSL_CA_FILE,
39-
'ssl_ciphers': DEFAULT_SSL_CIPHERS
41+
'ssl_ciphers': DEFAULT_SSL_CIPHERS,
42+
'ssl_password': DEFAULT_SSL_PASSWORD,
43+
'ssl_password_file': DEFAULT_SSL_PASSWORD_FILE,
4044
}
4145

4246

@@ -188,6 +192,8 @@ def update_connection(conn, address):
188192
conn.ssl_cert_file = address['ssl_cert_file']
189193
conn.ssl_ca_file = address['ssl_ca_file']
190194
conn.ssl_ciphers = address['ssl_ciphers']
195+
conn.ssl_password = address['ssl_password']
196+
conn.ssl_password_file = address['ssl_password_file']
191197

192198

193199
class RoundRobinStrategy(object):
@@ -269,6 +275,8 @@ def __init__(self, host=None, port=None,
269275
ssl_cert_file=DEFAULT_SSL_CERT_FILE,
270276
ssl_ca_file=DEFAULT_SSL_CA_FILE,
271277
ssl_ciphers=DEFAULT_SSL_CIPHERS,
278+
ssl_password=DEFAULT_SSL_PASSWORD,
279+
ssl_password_file=DEFAULT_SSL_PASSWORD_FILE,
272280
addrs=None,
273281
strategy_class=RoundRobinStrategy,
274282
cluster_discovery_function=None,
@@ -427,7 +435,9 @@ def __init__(self, host=None, port=None,
427435
'ssl_key_file': ssl_key_file,
428436
'ssl_cert_file': ssl_cert_file,
429437
'ssl_ca_file': ssl_ca_file,
430-
'ssl_ciphers': ssl_ciphers})
438+
'ssl_ciphers': ssl_ciphers,
439+
'ssl_password': ssl_password,
440+
'ssl_password_file': ssl_password_file})
431441

432442
# Verify that at least one address is provided.
433443
if not addrs:
@@ -467,7 +477,9 @@ def __init__(self, host=None, port=None,
467477
ssl_key_file=addr['ssl_key_file'],
468478
ssl_cert_file=addr['ssl_cert_file'],
469479
ssl_ca_file=addr['ssl_ca_file'],
470-
ssl_ciphers=addr['ssl_ciphers'])
480+
ssl_ciphers=addr['ssl_ciphers'],
481+
ssl_password=addr['ssl_password'],
482+
ssl_password_file=addr['ssl_password_file'])
471483

472484
def connect(self):
473485
"""
@@ -574,7 +586,9 @@ def _opt_refresh_instances(self):
574586
'ssl_key_file': self.ssl_key_file,
575587
'ssl_cert_file': self.ssl_cert_file,
576588
'ssl_ca_file': self.ssl_ca_file,
577-
'ssl_ciphers': self.ssl_ciphers}
589+
'ssl_ciphers': self.ssl_ciphers,
590+
'ssl_password': self.ssl_password,
591+
'ssl_password_file': self.ssl_password_file}
578592
if current_addr not in self.strategy.addrs:
579593
self.close()
580594
addr = self.strategy.getnext()

Diff for: test/data/ca.crt

+16-16
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
-----BEGIN CERTIFICATE-----
2-
MIIDLzCCAhegAwIBAgIUMwa7m6dtjVYPK5iZAMX8YUuHtxEwDQYJKoZIhvcNAQEL
2+
MIIDLzCCAhegAwIBAgIUWsIywvK+tkdt1ew8Hyl+q8AimxswDQYJKoZIhvcNAQEL
33
BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y
4-
MjA2MTYwODQzMThaFw00NDExMTkwODQzMThaMCcxCzAJBgNVBAYTAlVTMRgwFgYD
4+
MjEyMjMxNTM1MjhaFw00NTA1MjgxNTM1MjhaMCcxCzAJBgNVBAYTAlVTMRgwFgYD
55
VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
6-
AoIBAQC923p9pD1ajiAPsM2W6cnjSkexHX2+sJeaLXL6zdFeUjLYRAnfzJ9xVih7
7-
91yWbuJ9OAswWmz83JrtSm1GqZpFucSz5pFqW2AVrhX5TezlxyH9QwPl+Scu1kCd
8-
+wu7Fgkuw7a0SOpYafPQ6smucCWbxkyZTNgysNuWswykal4VCWyekaY/OojEImoG
9-
smGOXe1Pr2x8XsiWVau1UJ0jj/vh5VzF05mletaUOoQ+iorIHAfnOm2K53jAZlNG
10-
X83VJ1ijSDwiKcnFKcQqlq2Zt88UpxMMv0UyFbDCrOj5qfBbAvzZj5IgUi/NvoZz
11-
M+lzwT+/0mADkAHB6EVa4R29zM+fAgMBAAGjUzBRMB0GA1UdDgQWBBSloRx6dBUI
12-
gJb0yzP2c5zQdQQ+2TAfBgNVHSMEGDAWgBSloRx6dBUIgJb0yzP2c5zQdQQ+2TAP
13-
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCCUEnzpu8hZAckICLR
14-
5JRDUiHJ3yJ5iv0b9ChNaz/AQBQGRE8bOPC2M/ZG1RuuQ8IbRbzK0fy1ty9KpG2D
15-
JC9iDL6zPOC3e5x2H8Gxbhvjz4QnHPbYTfdJSmX5tJyNIrJ77g4SW5g8eFApTHyY
16-
5KwRD3IDEu4pZNGsM7l0ODBC/4lvR8u7wPJDGyJBpE3uAKC20XqbG8BWm3kPb9+T
17-
wE4Ak/FEXcwARB0fJ6Jni9iK3TeReyB3rpsYJa4N9iY6f1qNy4qQZ8Va6EWPSNnB
18-
FhvCIYt4LdgM9ffUuHPrCX7qdgSNiL4VijgLaEHjFUUlLb6NHgQfYx/JG7wstiKs
19-
Syzb
6+
AoIBAQDHwdnavXTN4Km9bwnRBV+2GW4Z2eFQ5fw2/Ln0BRUp+Wqzc6Cu9sfxH4DV
7+
6+A0H1swcc0kctdPGZvApk6b4A98/Pry09JEMbCzLlHaVGeNKQr+g7Vrb2/v9337
8+
ofRbhjA+CsD3bBQio5U3ANTvcB2KnUnqA/PmohIpLGyipOpwz2Dr6N4UABZ/wd0o
9+
mYGeRQx7BCgDzK3b+eVmQkWlHuEkAQv7qAVzKGIkw3lUq2Hikq65b+1DXWsENHSC
10+
GRXNRklu/lnsZPfNIcqu0z0OzkFNZ9VWCSQLRmPBzTv5ATSVmZZiiHJSR89q41MK
11+
T2cw9layXRAmtzDX5VUlPvF5GQUtAgMBAAGjUzBRMB0GA1UdDgQWBBSLeibGvWpo
12+
u0/ecImiZxrGfOJgFTAfBgNVHSMEGDAWgBSLeibGvWpou0/ecImiZxrGfOJgFTAP
13+
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC9pXVMMKiaS3LLA+Vl
14+
oNsLKGtkbLbFYJSvjnNVlcVv64jdEQNeBWdYA4V4FDkkOc8PZ5NNqLiMMq6YEcyz
15+
zPJkvtkIVrGh67TKdcbqyoLXyWcnqne0IN+CDhZuspvY5w8BX18q5rM0vtdp/si0
16+
z5BM0o6hzUKU8smCOSjDQr7PtbQaPJT0JFLUmNl64TTymg/Wim7i72E5V7wSX1Zp
17+
VkaWDRjRG6H/lIX/88ppEl6aOAEdezgsu68fNjgJBlUK1qkJVvLg/3ddm/od1kwf
18+
5j3289P1myOvTJoWbaUUVI2GON+kPEAniyi08iJTgHUOHJUEUrbb0STmcGF+rCLT
19+
vPdc
2020
-----END CERTIFICATE-----

Diff for: test/data/generate.sh

+14
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,17 @@ openssl x509 -outform pem -in ca.pem -out ca.crt
3333
openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost"
3434
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_localhost.ext -out localhost.crt
3535
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_invalidhost.ext -out invalidhost.crt
36+
37+
password=mysslpassword
38+
39+
# Tarantool tries every line from the password file.
40+
cat <<EOF > passwords
41+
unusedpassword
42+
$password
43+
EOF
44+
45+
cat <<EOF > invalidpasswords
46+
unusedpassword1
47+
EOF
48+
49+
openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key

0 commit comments

Comments
 (0)