Skip to content

api: support SSL key passwords #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,14 @@ jobs:
fail-fast: false
matrix:
tarantool:
- '1.10.11-0-gf0b0e7ecf-r470'
- '2.8.3-21-g7d35cd2be-r470'
- '2.10.0-1-gfa775b383-r486-linux-x86_64'
- bundle: 'bundle-1.10.11-0-gf0b0e7ecf-r470'
path: ''
- bundle: 'bundle-2.8.3-21-g7d35cd2be-r470'
path: ''
- bundle: 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64'
path: ''
- bundle: 'sdk-gc64-2.11.0-entrypoint-107-ga18449d54-r524.linux.x86_64'
path: 'dev/linux/x86_64/master/'
python: ['3.6', '3.7', '3.8', '3.9', '3.10']

steps:
Expand All @@ -131,10 +136,10 @@ jobs:
if: github.event_name != 'pull_request_target'
uses: actions/checkout@v2

- name: Install tarantool ${{ matrix.tarantool }}
- name: Install Tarantool EE SDK
run: |
ARCHIVE_NAME=tarantool-enterprise-bundle-${{ matrix.tarantool }}.tar.gz
curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${ARCHIVE_NAME}
ARCHIVE_NAME=tarantool-enterprise-${{ matrix.tarantool.bundle }}.tar.gz
curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${{ matrix.tarantool.path }}${ARCHIVE_NAME}
tar -xzf ${ARCHIVE_NAME}
rm -f ${ARCHIVE_NAME}

Expand Down Expand Up @@ -163,7 +168,8 @@ jobs:
source tarantool-enterprise/env.sh
make test
env:
TEST_TNT_SSL: ${{ matrix.tarantool == '2.10.0-1-gfa775b383-r486-linux-x86_64' }}
TEST_TNT_SSL: ${{ matrix.tarantool.bundle == 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64' ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to add a key to the matrix:

env:
  TEST_TNT_SSL: ${{ matrix.tarantool.ssl }}

up to you.

matrix.tarantool.bundle == 'sdk-gc64-2.11.0-entrypoint-107-ga18449d54-r524.linux.x86_64'}}

run_tests_pip_branch_install_linux:
# We want to run on external PRs, but not on our own internal
Expand Down
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ debian/files
debian/*.substvars

deb_dist

test/data/*.crt
!test/data/ca.crt
!test/data/invalidhost.crt
!test/data/localhost.crt
test/data/*.csr
test/data/*.ext
test/data/*.key
!test/data/localhost.key
!test/data/localhost.enc.key
test/data/*.pem
test/data/*.srl
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Support custom packer and unpacker factories (#191).

- Support [crud module](https://github.com/tarantool/crud) native API (#205).
- Support `ssl_password` and `ssl_password_file` options
to decrypt private SSL key file (#224).

### Changed

Expand Down
85 changes: 70 additions & 15 deletions tarantool/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
DEFAULT_SSL_CERT_FILE,
DEFAULT_SSL_CA_FILE,
DEFAULT_SSL_CIPHERS,
DEFAULT_SSL_PASSWORD,
DEFAULT_SSL_PASSWORD_FILE,
REQUEST_TYPE_OK,
REQUEST_TYPE_ERROR,
IPROTO_GREETING_SIZE,
Expand Down Expand Up @@ -569,6 +571,8 @@ def __init__(self, host, port,
ssl_cert_file=DEFAULT_SSL_CERT_FILE,
ssl_ca_file=DEFAULT_SSL_CA_FILE,
ssl_ciphers=DEFAULT_SSL_CIPHERS,
ssl_password=DEFAULT_SSL_PASSWORD,
ssl_password_file=DEFAULT_SSL_PASSWORD_FILE,
packer_factory=default_packer_factory,
unpacker_factory=default_unpacker_factory):
"""
Expand Down Expand Up @@ -693,6 +697,15 @@ def __init__(self, host, port,
suites the connection can use.
:type ssl_ciphers: :obj:`str` or :obj:`None`, optional

:param ssl_password: Password for decrypting
:paramref:`~tarantool.Connection.ssl_key_file`.
:type ssl_password: :obj:`str` or :obj:`None`, optional

:param ssl_password_file: File with password for decrypting
:paramref:`~tarantool.Connection.ssl_key_file`. Connection
tries every line from the file as a password.
:type ssl_password_file: :obj:`str` or :obj:`None`, optional

:param packer_factory: Request MessagePack packer factory.
Supersedes :paramref:`~tarantool.Connection.encoding`. See
:func:`~tarantool.request.packer_factory` for example of
Expand Down Expand Up @@ -754,6 +767,8 @@ def __init__(self, host, port,
self.ssl_cert_file = ssl_cert_file
self.ssl_ca_file = ssl_ca_file
self.ssl_ciphers = ssl_ciphers
self.ssl_password = ssl_password
self.ssl_password_file = ssl_password_file
self._protocol_version = None
self._features = {
IPROTO_FEATURE_STREAMS: False,
Expand Down Expand Up @@ -884,21 +899,7 @@ def wrap_socket_ssl(self):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)

if self.ssl_cert_file:
# If the password argument is not specified and a password is
# required, OpenSSL’s built-in password prompting mechanism
# will be used to interactively prompt the user for a password.
#
# We should disable this behaviour, because a python
# application that uses the connector unlikely assumes
# interaction with a human + a Tarantool implementation does
# not support this at least for now.
def password_raise_error():
raise SslError("Password for decrypting the private " +
"key is unsupported")
context.load_cert_chain(certfile=self.ssl_cert_file,
keyfile=self.ssl_key_file,
password=password_raise_error)

self._ssl_load_cert_chain(context)
if self.ssl_ca_file:
context.load_verify_locations(cafile=self.ssl_ca_file)
context.verify_mode = ssl.CERT_REQUIRED
Expand All @@ -915,6 +916,60 @@ def password_raise_error():
except Exception as e:
raise SslError(e)

def _ssl_load_cert_chain(self, context):
"""
Decrypt and load SSL certificate and private key files.
Mimic Tarantool EE approach here: see `SSL commit`_.

:param context: SSL context.
:type context: :obj:`ssl.SSLContext`

:raise: :exc:`~tarantool.error.SslError`

:meta private:

.. _SSL commit: https://github.com/tarantool/tarantool-ee/commit/e1f47dd4adbc6657159c611298aad225883a536b
"""

exc_list = []

if self.ssl_password is not None:
try:
context.load_cert_chain(certfile=self.ssl_cert_file,
keyfile=self.ssl_key_file,
password=self.ssl_password)
return
except Exception as e:
exc_list.append(e)


if self.ssl_password_file is not None:
with open(self.ssl_password_file) as file:
for line in file:
try:
context.load_cert_chain(certfile=self.ssl_cert_file,
keyfile=self.ssl_key_file,
password=line.rstrip())
return
except Exception as e:
exc_list.append(e)


try:
def password_raise_error():
raise SslError("Password prompt for decrypting the private " +
"key is unsupported, use ssl_password or " +
"ssl_password_file")
context.load_cert_chain(certfile=self.ssl_cert_file,
keyfile=self.ssl_key_file,
password=password_raise_error)

return
except Exception as e:
exc_list.append(e)

raise SslError(exc_list)

def handshake(self):
"""
Process greeting with Tarantool server.
Expand Down
24 changes: 15 additions & 9 deletions tarantool/connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
POOL_INSTANCE_RECONNECT_DELAY,
POOL_INSTANCE_RECONNECT_MAX_ATTEMPTS,
POOL_REFRESH_DELAY,
SOCKET_TIMEOUT
SOCKET_TIMEOUT,
DEFAULT_SSL_PASSWORD,
DEFAULT_SSL_PASSWORD_FILE,
)
from tarantool.error import (
ClusterConnectWarning,
Expand Down Expand Up @@ -383,13 +385,15 @@ def __init__(self,
.. code-block:: python

{
"host': "str" or None, # mandatory
"port": int or "str", # mandatory
"transport": "str", # optional
"ssl_key_file": "str", # optional
"ssl_cert_file": "str", # optional
"ssl_ca_file": "str", # optional
"ssl_ciphers": "str" # optional
"host': "str" or None, # mandatory
"port": int or "str", # mandatory
"transport": "str", # optional
"ssl_key_file": "str", # optional
"ssl_cert_file": "str", # optional
"ssl_ca_file": "str", # optional
"ssl_ciphers": "str" # optional
"ssl_password": "str", # optional
"ssl_password_file": "str" # optional
}

Refer to corresponding :class:`~tarantool.Connection`
Expand Down Expand Up @@ -492,7 +496,9 @@ def __init__(self,
ssl_key_file=addr['ssl_key_file'],
ssl_cert_file=addr['ssl_cert_file'],
ssl_ca_file=addr['ssl_ca_file'],
ssl_ciphers=addr['ssl_ciphers'])
ssl_ciphers=addr['ssl_ciphers'],
ssl_password=addr['ssl_password'],
ssl_password_file=addr['ssl_password_file'])
)

if connect_now:
Expand Down
4 changes: 4 additions & 0 deletions tarantool/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
DEFAULT_SSL_CA_FILE = None
# Default value for list of SSL ciphers
DEFAULT_SSL_CIPHERS = None
# Default value for SSL key file password
DEFAULT_SSL_PASSWORD = None
# Default value for a path to file with SSL key file password
DEFAULT_SSL_PASSWORD_FILE = None
# Default cluster nodes list refresh interval (seconds)
CLUSTER_DISCOVERY_DELAY = 60
# Default cluster nodes state refresh interval (seconds)
Expand Down
22 changes: 18 additions & 4 deletions tarantool/mesh_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
DEFAULT_SSL_CERT_FILE,
DEFAULT_SSL_CA_FILE,
DEFAULT_SSL_CIPHERS,
DEFAULT_SSL_PASSWORD,
DEFAULT_SSL_PASSWORD_FILE,
CLUSTER_DISCOVERY_DELAY,
)

Expand All @@ -36,7 +38,9 @@
'ssl_key_file': DEFAULT_SSL_KEY_FILE,
'ssl_cert_file': DEFAULT_SSL_CERT_FILE,
'ssl_ca_file': DEFAULT_SSL_CA_FILE,
'ssl_ciphers': DEFAULT_SSL_CIPHERS
'ssl_ciphers': DEFAULT_SSL_CIPHERS,
'ssl_password': DEFAULT_SSL_PASSWORD,
'ssl_password_file': DEFAULT_SSL_PASSWORD_FILE,
}


Expand Down Expand Up @@ -188,6 +192,8 @@ def update_connection(conn, address):
conn.ssl_cert_file = address['ssl_cert_file']
conn.ssl_ca_file = address['ssl_ca_file']
conn.ssl_ciphers = address['ssl_ciphers']
conn.ssl_password = address['ssl_password']
conn.ssl_password_file = address['ssl_password_file']


class RoundRobinStrategy(object):
Expand Down Expand Up @@ -269,6 +275,8 @@ def __init__(self, host=None, port=None,
ssl_cert_file=DEFAULT_SSL_CERT_FILE,
ssl_ca_file=DEFAULT_SSL_CA_FILE,
ssl_ciphers=DEFAULT_SSL_CIPHERS,
ssl_password=DEFAULT_SSL_PASSWORD,
ssl_password_file=DEFAULT_SSL_PASSWORD_FILE,
addrs=None,
strategy_class=RoundRobinStrategy,
cluster_discovery_function=None,
Expand Down Expand Up @@ -427,7 +435,9 @@ def __init__(self, host=None, port=None,
'ssl_key_file': ssl_key_file,
'ssl_cert_file': ssl_cert_file,
'ssl_ca_file': ssl_ca_file,
'ssl_ciphers': ssl_ciphers})
'ssl_ciphers': ssl_ciphers,
'ssl_password': ssl_password,
'ssl_password_file': ssl_password_file})

# Verify that at least one address is provided.
if not addrs:
Expand Down Expand Up @@ -467,7 +477,9 @@ def __init__(self, host=None, port=None,
ssl_key_file=addr['ssl_key_file'],
ssl_cert_file=addr['ssl_cert_file'],
ssl_ca_file=addr['ssl_ca_file'],
ssl_ciphers=addr['ssl_ciphers'])
ssl_ciphers=addr['ssl_ciphers'],
ssl_password=addr['ssl_password'],
ssl_password_file=addr['ssl_password_file'])

def connect(self):
"""
Expand Down Expand Up @@ -574,7 +586,9 @@ def _opt_refresh_instances(self):
'ssl_key_file': self.ssl_key_file,
'ssl_cert_file': self.ssl_cert_file,
'ssl_ca_file': self.ssl_ca_file,
'ssl_ciphers': self.ssl_ciphers}
'ssl_ciphers': self.ssl_ciphers,
'ssl_password': self.ssl_password,
'ssl_password_file': self.ssl_password_file}
if current_addr not in self.strategy.addrs:
self.close()
addr = self.strategy.getnext()
Expand Down
32 changes: 16 additions & 16 deletions test/data/ca.crt
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDLzCCAhegAwIBAgIUMwa7m6dtjVYPK5iZAMX8YUuHtxEwDQYJKoZIhvcNAQEL
MIIDLzCCAhegAwIBAgIUWsIywvK+tkdt1ew8Hyl+q8AimxswDQYJKoZIhvcNAQEL
BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y
MjA2MTYwODQzMThaFw00NDExMTkwODQzMThaMCcxCzAJBgNVBAYTAlVTMRgwFgYD
MjEyMjMxNTM1MjhaFw00NTA1MjgxNTM1MjhaMCcxCzAJBgNVBAYTAlVTMRgwFgYD
VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC923p9pD1ajiAPsM2W6cnjSkexHX2+sJeaLXL6zdFeUjLYRAnfzJ9xVih7
91yWbuJ9OAswWmz83JrtSm1GqZpFucSz5pFqW2AVrhX5TezlxyH9QwPl+Scu1kCd
+wu7Fgkuw7a0SOpYafPQ6smucCWbxkyZTNgysNuWswykal4VCWyekaY/OojEImoG
smGOXe1Pr2x8XsiWVau1UJ0jj/vh5VzF05mletaUOoQ+iorIHAfnOm2K53jAZlNG
X83VJ1ijSDwiKcnFKcQqlq2Zt88UpxMMv0UyFbDCrOj5qfBbAvzZj5IgUi/NvoZz
M+lzwT+/0mADkAHB6EVa4R29zM+fAgMBAAGjUzBRMB0GA1UdDgQWBBSloRx6dBUI
gJb0yzP2c5zQdQQ+2TAfBgNVHSMEGDAWgBSloRx6dBUIgJb0yzP2c5zQdQQ+2TAP
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCCUEnzpu8hZAckICLR
5JRDUiHJ3yJ5iv0b9ChNaz/AQBQGRE8bOPC2M/ZG1RuuQ8IbRbzK0fy1ty9KpG2D
JC9iDL6zPOC3e5x2H8Gxbhvjz4QnHPbYTfdJSmX5tJyNIrJ77g4SW5g8eFApTHyY
5KwRD3IDEu4pZNGsM7l0ODBC/4lvR8u7wPJDGyJBpE3uAKC20XqbG8BWm3kPb9+T
wE4Ak/FEXcwARB0fJ6Jni9iK3TeReyB3rpsYJa4N9iY6f1qNy4qQZ8Va6EWPSNnB
FhvCIYt4LdgM9ffUuHPrCX7qdgSNiL4VijgLaEHjFUUlLb6NHgQfYx/JG7wstiKs
Syzb
AoIBAQDHwdnavXTN4Km9bwnRBV+2GW4Z2eFQ5fw2/Ln0BRUp+Wqzc6Cu9sfxH4DV
6+A0H1swcc0kctdPGZvApk6b4A98/Pry09JEMbCzLlHaVGeNKQr+g7Vrb2/v9337
ofRbhjA+CsD3bBQio5U3ANTvcB2KnUnqA/PmohIpLGyipOpwz2Dr6N4UABZ/wd0o
mYGeRQx7BCgDzK3b+eVmQkWlHuEkAQv7qAVzKGIkw3lUq2Hikq65b+1DXWsENHSC
GRXNRklu/lnsZPfNIcqu0z0OzkFNZ9VWCSQLRmPBzTv5ATSVmZZiiHJSR89q41MK
T2cw9layXRAmtzDX5VUlPvF5GQUtAgMBAAGjUzBRMB0GA1UdDgQWBBSLeibGvWpo
u0/ecImiZxrGfOJgFTAfBgNVHSMEGDAWgBSLeibGvWpou0/ecImiZxrGfOJgFTAP
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC9pXVMMKiaS3LLA+Vl
oNsLKGtkbLbFYJSvjnNVlcVv64jdEQNeBWdYA4V4FDkkOc8PZ5NNqLiMMq6YEcyz
zPJkvtkIVrGh67TKdcbqyoLXyWcnqne0IN+CDhZuspvY5w8BX18q5rM0vtdp/si0
z5BM0o6hzUKU8smCOSjDQr7PtbQaPJT0JFLUmNl64TTymg/Wim7i72E5V7wSX1Zp
VkaWDRjRG6H/lIX/88ppEl6aOAEdezgsu68fNjgJBlUK1qkJVvLg/3ddm/od1kwf
5j3289P1myOvTJoWbaUUVI2GON+kPEAniyi08iJTgHUOHJUEUrbb0STmcGF+rCLT
vPdc
-----END CERTIFICATE-----
14 changes: 14 additions & 0 deletions test/data/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@ openssl x509 -outform pem -in ca.pem -out ca.crt
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"
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_localhost.ext -out localhost.crt
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_invalidhost.ext -out invalidhost.crt

password=mysslpassword

# Tarantool tries every line from the password file.
cat <<EOF > passwords
unusedpassword
$password
EOF

cat <<EOF > invalidpasswords
unusedpassword1
EOF

openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key
Loading