From 96281b0c423518463d7d5fb21f141f46af34bdc6 Mon Sep 17 00:00:00 2001 From: Fantix King Date: Mon, 13 Sep 2021 12:20:21 -0400 Subject: [PATCH 1/6] Fix SSL compatibility of libpq --- asyncpg/connect_utils.py | 155 +++++++++++++++++++++------ asyncpg/connection.py | 14 +++ tests/certs/client.key.protected.pem | 30 ++++++ tests/test_connect.py | 145 +++++++++++++++++-------- 4 files changed, 264 insertions(+), 80 deletions(-) create mode 100644 tests/certs/client.key.protected.pem diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index cd94b834..7ebcc0cd 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -18,6 +18,7 @@ import ssl as ssl_module import stat import struct +import sys import time import typing import urllib.parse @@ -220,13 +221,27 @@ def _parse_hostlist(hostlist, port, *, unquote=False): return hosts, port +def _parse_tls_version(tls_version): + if tls_version.startswith('SSL'): + raise ValueError( + f"Unsupported TLS version: {tls_version}" + ) + try: + return ssl_module.TLSVersion[tls_version.replace('.', '_')] + except KeyError: + raise ValueError( + f"No such TLS version: {tls_version}" + ) + + def _parse_connect_dsn_and_args(*, dsn, host, port, user, password, passfile, database, ssl, connect_timeout, server_settings): # `auth_hosts` is the version of host information for the purposes # of reading the pgpass file. auth_hosts = None - sslcert = sslkey = sslrootcert = sslcrl = None + sslcert = sslkey = sslrootcert = sslcrl = sslpassword = None + sslcompression = ssl_min_protocol_version = ssl_max_protocol_version = None if dsn: parsed = urllib.parse.urlparse(dsn) @@ -312,24 +327,32 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl = val if 'sslcert' in query: - val = query.pop('sslcert') - if sslcert is None: - sslcert = val + sslcert = query.pop('sslcert') if 'sslkey' in query: - val = query.pop('sslkey') - if sslkey is None: - sslkey = val + sslkey = query.pop('sslkey') if 'sslrootcert' in query: - val = query.pop('sslrootcert') - if sslrootcert is None: - sslrootcert = val + sslrootcert = query.pop('sslrootcert') if 'sslcrl' in query: - val = query.pop('sslcrl') - if sslcrl is None: - sslcrl = val + sslcrl = query.pop('sslcrl') + + if 'sslpassword' in query: + sslpassword = query.pop('sslpassword') + + if 'sslcompression' in query: + sslcompression = query.pop('sslcompression') + + if 'ssl_min_protocol_version' in query: + ssl_min_protocol_version = query.pop( + 'ssl_min_protocol_version' + ) + + if 'ssl_max_protocol_version' in query: + ssl_max_protocol_version = query.pop( + 'ssl_max_protocol_version' + ) if query: if server_settings is None: @@ -451,34 +474,98 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if sslmode < SSLMode.allow: ssl = False else: - ssl = ssl_module.create_default_context( - ssl_module.Purpose.SERVER_AUTH) + ssl = ssl_module.SSLContext(ssl_module.PROTOCOL_TLS_CLIENT) ssl.check_hostname = sslmode >= SSLMode.verify_full - ssl.verify_mode = ssl_module.CERT_REQUIRED - if sslmode <= SSLMode.require: + if sslmode < SSLMode.require: ssl.verify_mode = ssl_module.CERT_NONE + else: + if sslrootcert is None: + sslrootcert = os.getenv('PGSSLROOTCERT') + if sslrootcert: + ssl.load_verify_locations(cafile=sslrootcert) + ssl.verify_mode = ssl_module.CERT_REQUIRED + else: + sslrootcert = os.path.expanduser('~/.postgresql/root.crt') + try: + ssl.load_verify_locations(cafile=sslrootcert) + except FileNotFoundError: + if sslmode > SSLMode.require: + raise ValueError( + f'root certificate file "{sslrootcert}" does ' + f'not exist\nEither provide the file or ' + f'change sslmode to disable server ' + f'certificate verification.' + ) + elif sslmode == SSLMode.require: + ssl.verify_mode = ssl_module.CERT_NONE + else: + assert False, 'unreachable' + else: + ssl.verify_mode = ssl_module.CERT_REQUIRED - if sslcert is None: - sslcert = os.getenv('PGSSLCERT') + if sslcrl is None: + sslcrl = os.getenv('PGSSLCRL') + if sslcrl: + ssl.load_verify_locations(cafile=sslcrl) + ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN + else: + sslcrl = os.path.expanduser('~/.postgresql/root.crl') + try: + ssl.load_verify_locations(cafile=sslcrl) + except FileNotFoundError: + pass + else: + ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN if sslkey is None: sslkey = os.getenv('PGSSLKEY') - - if sslrootcert is None: - sslrootcert = os.getenv('PGSSLROOTCERT') - - if sslcrl is None: - sslcrl = os.getenv('PGSSLCRL') - + if not sslkey: + sslkey = os.path.expanduser('~/.postgresql/postgresql.key') + if not os.path.exists(sslkey): + sslkey = None + if not sslpassword: + sslpassword = '' + if sslcert is None: + sslcert = os.getenv('PGSSLCERT') if sslcert: - ssl.load_cert_chain(sslcert, keyfile=sslkey) - - if sslrootcert: - ssl.load_verify_locations(cafile=sslrootcert) - - if sslcrl: - ssl.load_verify_locations(cafile=sslcrl) - ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN + ssl.load_cert_chain( + sslcert, keyfile=sslkey, password=lambda: sslpassword + ) + else: + sslcert = os.path.expanduser('~/.postgresql/postgresql.crt') + try: + ssl.load_cert_chain( + sslcert, keyfile=sslkey, password=lambda: sslpassword + ) + except FileNotFoundError: + pass + + # OpenSSL 1.1.1 keylog file, copied from create_default_context() + if hasattr(ssl, 'keylog_filename'): + keylogfile = os.environ.get('SSLKEYLOGFILE') + if keylogfile and not sys.flags.ignore_environment: + ssl.keylog_filename = keylogfile + + if sslcompression is None: + sslcompression = os.getenv('PGSSLCOMPRESSION') + if sslcompression == '1': + ssl.options &= ~ssl_module.OP_NO_COMPRESSION + + if ssl_min_protocol_version is None: + ssl_min_protocol_version = os.getenv( + 'PGSSLMINPROTOCOLVERSION', 'TLSv1.2' + ) + if ssl_min_protocol_version: + ssl.minimum_version = _parse_tls_version( + ssl_min_protocol_version + ) + + if ssl_max_protocol_version is None: + ssl_max_protocol_version = os.getenv('PGSSLMAXPROTOCOLVERSION') + if ssl_max_protocol_version: + ssl.maximum_version = _parse_tls_version( + ssl_max_protocol_version + ) elif ssl is True: ssl = ssl_module.create_default_context() diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 26249679..1c7ca801 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -2020,6 +2020,20 @@ async def connect(dsn=None, *, The ``sslcert``, ``sslkey``, ``sslrootcert``, and ``sslcrl`` options are supported in the *dsn* argument. + .. versionchanged:: 0.25.0 + The ``sslpassword``, ``sslcompression``, ``ssl_min_protocol_version``, + and ``ssl_max_protocol_version`` options are supported in the *dsn* + argument. + + .. versionchanged:: 0.25.0 + Default system root CA certificates won't be loaded when specifying a + particular sslmode, following the same behavior in libpq. + + .. versionchanged:: 0.25.0 + The ``sslcert``, ``sslkey``, ``sslrootcert``, and ``sslcrl`` options + in the *dsn* argument now have consistent default values of files under + ``~/.postgresql/`` as libpq. + .. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext .. _create_default_context: https://docs.python.org/3/library/ssl.html#ssl.create_default_context diff --git a/tests/certs/client.key.protected.pem b/tests/certs/client.key.protected.pem new file mode 100644 index 00000000..0c5a9795 --- /dev/null +++ b/tests/certs/client.key.protected.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,B222CD7D00828606A07DBC489D400921 + +LRHsNGUsD5bG9+x/1UlzImN0rqEF10sFPBmxKeQpXQ/hy4iR+X/Gagoyagi23wOn +EZf0sCLJx95ixG+4fXJDX0jgBtqeziVNS4FLWHIuf3+blja8nf4tkmmH9pF8jFQ0 +i1an3TP6KRyDKa17gioOdtSsS51BZmPkp3MByJQsrMhyB0txEUsGtUMaBTYmVN/5 +uYHf9MsmfcfQy30nt2t6St6W82QupHHMOx5xyhPJo8cqQncZC7Dwo4hyDV3h3vWn +UjaRZiEMmQ3IgCwfJd1VmMECvrwXd/sTOXNhofWwDQIqmQ3GGWdrRnmgD863BQT3 +V8RVyPLkutOnrZ/kiMSAuiXGsSYK0TV8F9TaP/abLob4P8jbKYLcuR7ws3cu1xBl +XWt9RALxGPUyHIy+BWLXJTYL8T+TVJpiKsAGCQB54j8VQBSArwFL4LnzdUu1txe2 +qa6ZEwt4q6SEwOTJpJWz3oJ1j+OTsRCN+4dlyo7sEZMeyTRp9nUzwulhd+fOdIhY +2UllMG71opKfNxZzEW7lq6E/waf0MmxwjUJmgwVO218yag9oknHnoFwewF42DGY7 +072h23EJeKla7sI+MAB18z01z6C/yHWXLybOlXaGqk6zOm3OvTUFnUXtKzlBO2v3 +FQwrOE5U/VEyQkNWzHzh4j4LxYEL9/B08PxaveUwvNVGn9I3YknE6uMfcU7VuxDq ++6bgM6r+ez+9QLFSjH/gQuPs2DKX0h3b9ppQNx+MANX0DEGbGabJiBp887f8pG6Q +tW0i0+rfzYz3JwnwIuMZjYz6qUlP4bJMEmmDfod3fbnvg3MoCSMTUvi1Tq3Iiv4L +GM5/YNkL0V3PhOI686aBfU7GLGXQFhdbQ9xrSoQRBmmNBqTCSf+iIEoTxlBac8GQ +vSzDO+A+ovBP36K13Yn7gzuN/3PLZXH2TZ8t2b/OkEXOciH5KbycGHQA7gqxX1P4 +J55gpqPAWe8e7wKheWj3BMfmbWuH4rpiEkrLpqbTSfTwIKqplk253chmJj5I82XI +ioFLS5vCi9JJsTrQ720O+VQPVB5xeA80WL8NxamWQb/KkvVnb4dTmaV30RCgLLZC +tuMx8YSW71ALLT15qFB2zlMDKZO1jjunNE71BUFBPIkTKEOCyMAiF60fFeIWezxy +kvBBOg7+MTcZNeW110FqRWNGr2A5KYFN15g+YVpfEoF26slHisSjVW5ndzGh0kaQ +sIOjQitA9JYoLua7sHvsr6H5KdCGjNxv7O7y8wLGBVApRhU0wxZtbClqqEUvCLLP +UiLDp9L34wDL7sGrfNgWA4UuN29XQzTxI5kbv/EPKhyt2oVHLqUiE+eGyvnuYm+X +KqFi016nQaxTU5Kr8Pl0pSHbJMLFDWLSpsbbTB6YJpdEGxJoj3JB3VncOpwcuK+G +xZ1tV2orPt1s/6m+/ihzRgoEkyLwcLRPN7ojgD/sqS679ZGf1IkDMgFCQe4g0UWm +Fw7v816MNCgypUM5hQaU+Jp8vSlEc29RbrdSHbcxrKj/xPCLWrAbvmI5tgonKmuJ +J1LW8AXyh/EUp/uUh++jqVGx+8pFfcmJw6V6JrJzQ7HMlakkry7N1eAGrIJGtYCW +-----END RSA PRIVATE KEY----- diff --git a/tests/test_connect.py b/tests/test_connect.py index be694d67..b942aa33 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -10,11 +10,13 @@ import ipaddress import os import platform +import shutil import ssl import stat import tempfile import textwrap import unittest +import unittest.mock import urllib.parse import weakref @@ -37,6 +39,34 @@ CLIENT_CA_CERT_FILE = os.path.join(CERTS, 'client_ca.cert.pem') CLIENT_SSL_CERT_FILE = os.path.join(CERTS, 'client.cert.pem') CLIENT_SSL_KEY_FILE = os.path.join(CERTS, 'client.key.pem') +CLIENT_SSL_PROTECTED_KEY_FILE = os.path.join(CERTS, 'client.key.protected.pem') + + +@contextlib.contextmanager +def mock_dot_postgresql(*, ca=True, client=False, protected=False): + with tempfile.TemporaryDirectory() as temp_dir: + pg_home = os.path.join(temp_dir, '.postgresql') + os.mkdir(pg_home) + if ca: + shutil.copyfile( + SSL_CA_CERT_FILE, os.path.join(pg_home, 'root.crt') + ) + if client: + shutil.copyfile( + CLIENT_SSL_CERT_FILE, os.path.join(pg_home, 'postgresql.crt') + ) + if protected: + shutil.copyfile( + CLIENT_SSL_PROTECTED_KEY_FILE, + os.path.join(pg_home, 'postgresql.key'), + ) + else: + shutil.copyfile( + CLIENT_SSL_KEY_FILE, + os.path.join(pg_home, 'postgresql.key'), + ) + with unittest.mock.patch.dict('os.environ', {'HOME': temp_dir}): + yield class TestSettings(tb.ConnectedTestCase): @@ -1155,8 +1185,10 @@ async def verify_fails(sslmode): await verify_works('allow') await verify_works('prefer') await verify_fails('require') - await verify_fails('verify-ca') - await verify_fails('verify-full') + with mock_dot_postgresql(): + await verify_fails('require') + await verify_fails('verify-ca') + await verify_fails('verify-full') async def test_connection_implicit_host(self): conn_spec = self.get_connection_spec() @@ -1263,8 +1295,7 @@ async def verify_works(sslmode, *, host='localhost'): if con: await con.close() - async def verify_fails(sslmode, *, host='localhost', - exn_type=ssl.SSLError): + async def verify_fails(sslmode, *, host='localhost', exn_type): # XXX: uvloop artifact old_handler = self.loop.get_exception_handler() con = None @@ -1286,23 +1317,16 @@ async def verify_fails(sslmode, *, host='localhost', await verify_works('allow') await verify_works('prefer') await verify_works('require') - await verify_fails('verify-ca') - await verify_fails('verify-full') + await verify_fails('verify-ca', exn_type=ValueError) + await verify_fails('verify-full', exn_type=ValueError) - orig_create_default_context = ssl.create_default_context - try: - def custom_create_default_context(*args, **kwargs): - ctx = orig_create_default_context(*args, **kwargs) - ctx.load_verify_locations(cafile=SSL_CA_CERT_FILE) - return ctx - ssl.create_default_context = custom_create_default_context + with mock_dot_postgresql(): + await verify_works('require') await verify_works('verify-ca') await verify_works('verify-ca', host='127.0.0.1') await verify_works('verify-full') await verify_fails('verify-full', host='127.0.0.1', exn_type=ssl.CertificateError) - finally: - ssl.create_default_context = orig_create_default_context async def test_ssl_connection_default_context(self): # XXX: uvloop artifact @@ -1396,41 +1420,68 @@ async def test_ssl_connection_client_auth_fails_with_wrong_setup(self): ssl=ssl_context, ) - async def test_ssl_connection_client_auth_custom_context(self): - ssl_context = ssl.create_default_context( - ssl.Purpose.SERVER_AUTH, - cafile=SSL_CA_CERT_FILE, - ) - ssl_context.load_cert_chain( - CLIENT_SSL_CERT_FILE, - keyfile=CLIENT_SSL_KEY_FILE, - ) - - con = await self.connect( - host='localhost', - user='ssl_user', - ssl=ssl_context, - ) + async def _test_works(self, **conn_args): + con = await self.connect(**conn_args) try: self.assertEqual(await con.fetchval('SELECT 42'), 42) finally: await con.close() + async def test_ssl_connection_client_auth_custom_context(self): + for key_file in (CLIENT_SSL_KEY_FILE, CLIENT_SSL_PROTECTED_KEY_FILE): + ssl_context = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, + cafile=SSL_CA_CERT_FILE, + ) + ssl_context.load_cert_chain( + CLIENT_SSL_CERT_FILE, + keyfile=key_file, + password='secRet', + ) + await self._test_works( + host='localhost', + user='ssl_user', + ssl=ssl_context, + ) + async def test_ssl_connection_client_auth_dsn(self): - params = urllib.parse.urlencode({ + params = { 'sslrootcert': SSL_CA_CERT_FILE, 'sslcert': CLIENT_SSL_CERT_FILE, 'sslkey': CLIENT_SSL_KEY_FILE, 'sslmode': 'verify-full', - }) - dsn = 'postgres://ssl_user@localhost/postgres?' + params - con = await self.connect(dsn=dsn) - - try: - self.assertEqual(await con.fetchval('SELECT 42'), 42) - finally: - await con.close() + } + params_str = urllib.parse.urlencode(params) + dsn = 'postgres://ssl_user@localhost/postgres?' + params_str + await self._test_works(dsn=dsn) + + params['sslkey'] = CLIENT_SSL_PROTECTED_KEY_FILE + params['sslpassword'] = 'secRet' + params_str = urllib.parse.urlencode(params) + dsn = 'postgres://ssl_user@localhost/postgres?' + params_str + await self._test_works(dsn=dsn) + + async def test_ssl_connection_client_auth_env(self): + env = { + 'PGSSLROOTCERT': SSL_CA_CERT_FILE, + 'PGSSLCERT': CLIENT_SSL_CERT_FILE, + 'PGSSLKEY': CLIENT_SSL_KEY_FILE, + } + dsn = 'postgres://ssl_user@localhost/postgres?sslmode=verify-full' + with unittest.mock.patch.dict('os.environ', env): + await self._test_works(dsn=dsn) + + env['PGSSLKEY'] = CLIENT_SSL_PROTECTED_KEY_FILE + with unittest.mock.patch.dict('os.environ', env): + await self._test_works(dsn=dsn + '&sslpassword=secRet') + + async def test_ssl_connection_client_auth_dot_postgresql(self): + dsn = 'postgres://ssl_user@localhost/postgres?sslmode=verify-full' + with mock_dot_postgresql(client=True): + await self._test_works(dsn=dsn) + with mock_dot_postgresql(client=True, protected=True): + await self._test_works(dsn=dsn + '&sslpassword=secRet') @unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster') @@ -1460,14 +1511,15 @@ async def verify_works(sslmode, *, host='localhost'): if con: await con.close() - async def verify_fails(sslmode, *, host='localhost', - exn_type=ssl.SSLError): + async def verify_fails(sslmode, *, host='localhost'): # XXX: uvloop artifact old_handler = self.loop.get_exception_handler() con = None try: self.loop.set_exception_handler(lambda *args: None) - with self.assertRaises(exn_type): + with self.assertRaises( + asyncpg.InvalidAuthorizationSpecificationError + ): con = await self.connect( dsn='postgresql://foo/?sslmode=' + sslmode, host=host, @@ -1478,13 +1530,14 @@ async def verify_fails(sslmode, *, host='localhost', await con.close() self.loop.set_exception_handler(old_handler) - invalid_auth_err = asyncpg.InvalidAuthorizationSpecificationError await verify_works('disable') await verify_works('allow') await verify_works('prefer') - await verify_fails('require', exn_type=invalid_auth_err) - await verify_fails('verify-ca') - await verify_fails('verify-full') + await verify_fails('require') + with mock_dot_postgresql(): + await verify_fails('require') + await verify_fails('verify-ca') + await verify_fails('verify-full') async def test_nossl_connection_prefer_cancel(self): con = await self.connect( From 6e912fd66a1c3246eb648bfa01e1afa815a5ed1f Mon Sep 17 00:00:00 2001 From: Fantix King Date: Tue, 14 Sep 2021 16:22:19 -0400 Subject: [PATCH 2/6] Add CRL test --- tests/certs/ca.cert.pem | 66 ++++++------ tests/certs/ca.crl.pem | 19 ++++ tests/certs/ca.key.pem | 51 +++++++++ tests/certs/gen.py | 202 ++++++++++++++++++++++++++++++++++++ tests/certs/server.cert.pem | 63 ++++++----- tests/certs/server.key.pem | 98 ++++++++--------- tests/test_connect.py | 20 +++- 7 files changed, 404 insertions(+), 115 deletions(-) create mode 100644 tests/certs/ca.crl.pem create mode 100644 tests/certs/ca.key.pem create mode 100644 tests/certs/gen.py diff --git a/tests/certs/ca.cert.pem b/tests/certs/ca.cert.pem index 0329883d..4a8a7016 100644 --- a/tests/certs/ca.cert.pem +++ b/tests/certs/ca.cert.pem @@ -1,35 +1,35 @@ -----BEGIN CERTIFICATE----- -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 -ztcZLWC9GiyC/TFSJ1hDylkvvwcCX6PD7fLu +MIIGFjCCA/6gAwIBAgIIDAM+rFY5KqgwDQYJKoZIhvcNAQELBQAwgaExCzAJBgNV +BAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRAwDgYDVQQHDAdUb3JvbnRvMRgwFgYD +VQQKDA9NYWdpY1N0YWNrIEluYy4xFjAUBgNVBAsMDWFzeW5jcGcgdGVzdHMxHTAb +BgNVBAMMFGFzeW5jcGcgdGVzdCByb290IGNhMR0wGwYJKoZIhvcNAQkBFg5oZWxs +b0BtYWdpYy5pbzAeFw0yMTA5MTMxNjA2MDFaFw00MDExMTMxNjA2MDFaMIGhMQsw +CQYDVQQGEwJDQTEQMA4GA1UECAwHT250YXJpbzEQMA4GA1UEBwwHVG9yb250bzEY +MBYGA1UECgwPTWFnaWNTdGFjayBJbmMuMRYwFAYDVQQLDA1hc3luY3BnIHRlc3Rz +MR0wGwYDVQQDDBRhc3luY3BnIHRlc3Qgcm9vdCBjYTEdMBsGCSqGSIb3DQEJARYO +aGVsbG9AbWFnaWMuaW8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDK +mu24288Os23VtRf8kp57sj7+s+PSD/8+KiZiJ4sy5KrUUVijVQgfCpxPzpWWtQ/7 +JbjQMt+kZqJwKqdzXAY8osnljpYYvbNWnc0GZY09F6z95GqVgX/81Fe8W3Jz6I9w +S2CXVneKGtux+6fztKbrA2b1kn69b3xClEHRLFZl9hKG8ck2H+gI5AEDgQmhTIXa +pl85bPuh54uKiUGnedPk07biCw3ZE5GTGWzEq5qMqFEfb19/L1vOvgx/Q4aqmjJw +lONB9DzMftetdKaR5SS+vH0QUhiWXwy7j1TjYtJP4M6fLinwguMYG8Qbg7NkL4QC +9T7zR5CZPJ0Q/Npiwv7qdMzyL7QklZ9y3YeA5wceyc2/zh0INN5bf4J1mDZjhYH9 +CIgVHSj6z44rWq9L+OzYT0EMDhZO0OeakTWgqXNICfeEXZ5hy3QVCUvKrgmnqs0f +imdH6dZQIGQIQ8Vcg/psk2hEP1hRWROn/cgCdadcEqbMdbtOUuMcnr0K6B/bVbXx +jAV4eVcCcS3w3wIG4Ki2aIXnXrHyEJmZJb03Ko7VXP0NTGuGfPYQj2ox4a4wViOG +pxxbnGGAFqV+BIVlhUMfL9PlatqsI6kUzJIsJUiyk6oPb3KeNQ5+MtS0S1DV0jA5 +wxDQZyEFiUsl6GLYSm4RajxoHdLR7Xqj3D7EWKGt/wIDAQABo1AwTjAMBgNVHRME +BTADAQH/MB0GA1UdDgQWBBRvLFXv6sI+ePP5aegYUWoVHAfRzTAfBgNVHSMEGDAW +gBRvLFXv6sI+ePP5aegYUWoVHAfRzTANBgkqhkiG9w0BAQsFAAOCAgEAK+QAtzhk +ih8Tng9cOheswrbWf9pclMyfl38+NsJxsZnpa2SlBp3qJl0fymyNLLBfyeRUFr++ +x1cRAEwVv6R6Iepj252+U+Cmz48xIthF29JxoC+x2P2YDGyqVBm4uuw54EIF0r0H +AvjTPSNa54gA3+KiK64ypFdlHZrwx3W9b5tUsfycpj2Jrn2HgTbWQD2gaYeIIdq6 +DNmPCJg6NQE9jlvNmVqlBavjc7MJqqd+0+XtCIWhaoqeu/T6g2Epth25cuqPKc0E +rltKiXNiZHcDfFnu7B6kw2LVA6EQdf5GO9JtAaiwhRugp1dJ5rdQqdaYpJngZtvd +8+PSdDZrXow0a1jW2w+3lM5XW3qtzIKJz4Q8CXL540s+SeRjLRwY02OZCvG4fC8c +D57MIFKoReYy5LgBHdPGmx8Kexo7vk2ib9taQCSd6fh0Ol070pNiOnLP9lE9iEqq +EvU1A+0dtPHbfyXqw9tdY18nxXbooypQZSqfxPSq3Bpv8KTsr9SSG+DV2LcJRfvi +OfVTPeIWW8C8SkbEXaTCUVgaNeYqvFsfsvkTmfhO8GHglDgnsveXHfnAwlC2Uxdq +T64oKToV7N1L2RA0JR9gJ4RQwPfyaFOHOPjd+3t4DFVl54GNbNfvELHRReoyJPse +SZeL4h6T3L17FWzugHMjxFi4f1/nPNk7d5Y= -----END CERTIFICATE----- diff --git a/tests/certs/ca.crl.pem b/tests/certs/ca.crl.pem new file mode 100644 index 00000000..b5eb1d2e --- /dev/null +++ b/tests/certs/ca.crl.pem @@ -0,0 +1,19 @@ +-----BEGIN X509 CRL----- +MIIDAjCB6wIBATANBgkqhkiG9w0BAQsFADCBoTELMAkGA1UEBhMCQ0ExEDAOBgNV +BAgMB09udGFyaW8xEDAOBgNVBAcMB1Rvcm9udG8xGDAWBgNVBAoMD01hZ2ljU3Rh +Y2sgSW5jLjEWMBQGA1UECwwNYXN5bmNwZyB0ZXN0czEdMBsGA1UEAwwUYXN5bmNw +ZyB0ZXN0IHJvb3QgY2ExHTAbBgkqhkiG9w0BCQEWDmhlbGxvQG1hZ2ljLmlvFw0y +MTA5MTQxNjA2MDFaFw0yMTA5MTUxNjA2MDFaMBUwEwICEAAXDTIxMDkxNDE2MDYw +MVowDQYJKoZIhvcNAQELBQADggIBAL4yfNmvGS8SkIVbRzdAC9+XJPw/dBJOUJwr +EgERICAz7OTqG1PkmMhPL00Dm9fe52+KnSwHgL749W0S/X5rTNMSwLyGiiJ5HYbH +GFRKQ/cvXLi4jYpSI1Ac94kk0japf3SfwEw3+122oba8SiAVP0nY3bHpHvNfOaDV +fhbFTwb5bFm6ThqlKLZxGCKP0fGeQ4homuwgRiLE/UOiue5ted1ph0PkKVui208k +FnhNYXSllakTGT8ZZZZVid/4tSHqJEY9vbdMXNv1GX8mhjoU1Gv9dOuyFGgUc9Vx +e7gzf/Wf36vKI29o8QGkkTslRZpMG59z3sG4Y0vJEoqXMB6eQLOr5iUCyj2CyDha +66pwrdc1fRt3EvNXUWkdHfY3EHb7DxueedDEgtmfSNbEaZTXa5RaZRavNGNTaPDf +UcrDU4w1N0wkYLQxPqd+VPcf1iKyfkAydpeOq9CChqRD0Tx58eTn6N/lLGFPPRfs +x47BA4FmefBeXZzd5HiXCUouk3qHIHs2yCzFs+TEBkx5eV42cP++HxjirPydLf6Y +G/o/TKRnc/2Lw+dCzvUV/p3geuw4+vq1BIFanwB9jp4tGaBrffIAyle8vPQLw6bp +1o1O39pdxniz+c9r0Kw/ETxTqRLbasSib5FHq5G/G9a+QxPsLAzKgwLWhR4fXvbu +YPbhYhRP +-----END X509 CRL----- diff --git a/tests/certs/ca.key.pem b/tests/certs/ca.key.pem new file mode 100644 index 00000000..2d73448f --- /dev/null +++ b/tests/certs/ca.key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/tests/certs/gen.py b/tests/certs/gen.py new file mode 100644 index 00000000..c08f3061 --- /dev/null +++ b/tests/certs/gen.py @@ -0,0 +1,202 @@ +import datetime +import os + +from cryptography import x509 +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509 import oid + + +def _new_cert(issuer=None, is_issuer=False, serial_number=None, **subject): + backend = backends.default_backend() + private_key = rsa.generate_private_key( + public_exponent=65537, key_size=4096, backend=backend + ) + public_key = private_key.public_key() + subject = x509.Name( + [ + x509.NameAttribute(getattr(oid.NameOID, key.upper()), value) + for key, value in subject.items() + ] + ) + builder = ( + x509.CertificateBuilder() + .subject_name(subject) + .public_key(public_key) + .serial_number(serial_number or int.from_bytes(os.urandom(8), "big")) + ) + if issuer: + issuer_cert, signing_key = issuer + builder = ( + builder.issuer_name(issuer_cert.subject) + .not_valid_before(issuer_cert.not_valid_before) + .not_valid_after(issuer_cert.not_valid_after) + ) + aki_ext = x509.AuthorityKeyIdentifier( + key_identifier=issuer_cert.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier + ).value.digest, + authority_cert_issuer=[x509.DirectoryName(issuer_cert.subject)], + authority_cert_serial_number=issuer_cert.serial_number, + ) + else: + signing_key = private_key + builder = ( + builder.issuer_name(subject) + .not_valid_before( + datetime.datetime.today() - datetime.timedelta(days=1) + ) + .not_valid_after( + datetime.datetime.today() + datetime.timedelta(weeks=1000) + ) + ) + aki_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key( + public_key + ) + if is_issuer: + builder = ( + builder.add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=False, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(public_key), + critical=False, + ) + .add_extension( + aki_ext, + critical=False, + ) + ) + else: + builder = ( + builder.add_extension( + x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=True, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=False, + ) + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=False, + ) + .add_extension( + x509.ExtendedKeyUsage([oid.ExtendedKeyUsageOID.SERVER_AUTH]), + critical=False, + ) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("localhost")]), + critical=False, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(public_key), + critical=False, + ) + .add_extension( + aki_ext, + critical=False, + ) + ) + certificate = builder.sign( + private_key=signing_key, + algorithm=hashes.SHA256(), + backend=backend, + ) + return certificate, private_key + + +def _write_cert(path, cert_key_pair, password=None): + certificate, private_key = cert_key_pair + if password: + encryption = serialization.BestAvailableEncryption(password) + else: + encryption = serialization.NoEncryption() + with open(path + ".key.pem", "wb") as f: + f.write( + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=encryption, + ) + ) + with open(path + ".cert.pem", "wb") as f: + f.write( + certificate.public_bytes( + encoding=serialization.Encoding.PEM, + ) + ) + + +def new_ca(path, **subject): + cert_key_pair = _new_cert(is_issuer=True, **subject) + _write_cert(path, cert_key_pair) + return cert_key_pair + + +def new_cert( + path, ca_cert_key_pair, password=None, is_issuer=False, **subject +): + cert_key_pair = _new_cert( + issuer=ca_cert_key_pair, is_issuer=is_issuer, **subject + ) + _write_cert(path, cert_key_pair, password) + return cert_key_pair + + +def new_crl(path, issuer, cert): + issuer_cert, signing_key = issuer + revoked_cert = ( + x509.RevokedCertificateBuilder() + .serial_number(cert[0].serial_number) + .revocation_date(datetime.datetime.today()) + .build() + ) + builder = ( + x509.CertificateRevocationListBuilder() + .issuer_name(issuer_cert.subject) + .last_update(datetime.datetime.today()) + .next_update(datetime.datetime.today() + datetime.timedelta(days=1)) + .add_revoked_certificate(revoked_cert) + ) + crl = builder.sign(private_key=signing_key, algorithm=hashes.SHA256()) + with open(path + ".crl.pem", "wb") as f: + f.write(crl.public_bytes(encoding=serialization.Encoding.PEM)) + + +def main(): + ca = new_ca( + "ca", + country_name="CA", + state_or_province_name="Ontario", + locality_name="Toronto", + organization_name="MagicStack Inc.", + organizational_unit_name="asyncpg tests", + common_name="asyncpg test root ca", + email_address="hello@magic.io", + ) + server = new_cert( + "server", + ca, + country_name="CA", + state_or_province_name="Ontario", + organization_name="MagicStack Inc.", + organizational_unit_name="asyncpg tests", + common_name="localhost", + email_address="hello@magic.io", + serial_number=4096, + ) + new_crl('server', ca, server) + + +if __name__ == "__main__": + main() diff --git a/tests/certs/server.cert.pem b/tests/certs/server.cert.pem index ce8bf0f4..a4678151 100644 --- a/tests/certs/server.cert.pem +++ b/tests/certs/server.cert.pem @@ -1,40 +1,39 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/tests/certs/server.key.pem b/tests/certs/server.key.pem index d802fb3b..9c69c46c 100644 --- a/tests/certs/server.key.pem +++ b/tests/certs/server.key.pem @@ -1,51 +1,51 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/tests/test_connect.py b/tests/test_connect.py index b942aa33..4052e6ef 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -34,6 +34,7 @@ CERTS = os.path.join(os.path.dirname(__file__), 'certs') SSL_CA_CERT_FILE = os.path.join(CERTS, 'ca.cert.pem') +SSL_CA_CRL_FILE = os.path.join(CERTS, 'ca.crl.pem') SSL_CERT_FILE = os.path.join(CERTS, 'server.cert.pem') SSL_KEY_FILE = os.path.join(CERTS, 'server.key.pem') CLIENT_CA_CERT_FILE = os.path.join(CERTS, 'client_ca.cert.pem') @@ -43,7 +44,7 @@ @contextlib.contextmanager -def mock_dot_postgresql(*, ca=True, client=False, protected=False): +def mock_dot_postgresql(*, ca=True, crl=False, client=False, protected=False): with tempfile.TemporaryDirectory() as temp_dir: pg_home = os.path.join(temp_dir, '.postgresql') os.mkdir(pg_home) @@ -51,6 +52,10 @@ def mock_dot_postgresql(*, ca=True, client=False, protected=False): shutil.copyfile( SSL_CA_CERT_FILE, os.path.join(pg_home, 'root.crt') ) + if crl: + shutil.copyfile( + SSL_CA_CRL_FILE, os.path.join(pg_home, 'root.crl') + ) if client: shutil.copyfile( CLIENT_SSL_CERT_FILE, os.path.join(pg_home, 'postgresql.crt') @@ -1328,6 +1333,19 @@ async def verify_fails(sslmode, *, host='localhost', exn_type): await verify_fails('verify-full', host='127.0.0.1', exn_type=ssl.CertificateError) + with mock_dot_postgresql(crl=True): + await verify_fails('disable', exn_type=invalid_auth_err) + await verify_works('allow') + await verify_works('prefer') + await verify_fails('require', + exn_type=ssl.CertificateError) + await verify_fails('verify-ca', + exn_type=ssl.CertificateError) + await verify_fails('verify-ca', host='127.0.0.1', + exn_type=ssl.CertificateError) + await verify_fails('verify-full', + exn_type=ssl.CertificateError) + async def test_ssl_connection_default_context(self): # XXX: uvloop artifact old_handler = self.loop.get_exception_handler() From 3ffdde5d9318992cc6b2bf6e9c09329a51f4234f Mon Sep 17 00:00:00 2001 From: Fantix King Date: Tue, 14 Sep 2021 16:56:22 -0400 Subject: [PATCH 3/6] Add TLS version test --- tests/test_connect.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_connect.py b/tests/test_connect.py index 4052e6ef..d69fdcaf 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1213,6 +1213,8 @@ def get_server_settings(cls): 'ssl_cert_file': SSL_CERT_FILE, 'ssl_key_file': SSL_KEY_FILE, 'ssl_ca_file': CLIENT_CA_CERT_FILE, + 'ssl_min_protocol_version': 'TLSv1.2', + 'ssl_max_protocol_version': 'TLSv1.2', }) return conf @@ -1408,6 +1410,42 @@ async def test_executemany_uvloop_ssl_issue_700(self): finally: await con.close() + async def test_tls_version(self): + # XXX: uvloop artifact + old_handler = self.loop.get_exception_handler() + try: + self.loop.set_exception_handler(lambda *args: None) + with self.assertRaisesRegex(ssl.SSLError, 'protocol version'): + await self.connect( + dsn='postgresql://ssl_user@localhost/postgres' + '?sslmode=require&ssl_min_protocol_version=TLSv1.3' + ) + with self.assertRaisesRegex(ssl.SSLError, 'protocol version'): + await self.connect( + dsn='postgresql://ssl_user@localhost/postgres' + '?sslmode=require' + '&ssl_min_protocol_version=TLSv1.1' + '&ssl_max_protocol_version=TLSv1.1' + ) + with self.assertRaisesRegex(ssl.SSLError, 'no protocols'): + await self.connect( + dsn='postgresql://ssl_user@localhost/postgres' + '?sslmode=require' + '&ssl_min_protocol_version=TLSv1.2' + '&ssl_max_protocol_version=TLSv1.1' + ) + con = await self.connect( + dsn='postgresql://ssl_user@localhost/postgres?sslmode=require' + '&ssl_min_protocol_version=TLSv1.2' + '&ssl_max_protocol_version=TLSv1.2' + ) + try: + self.assertEqual(await con.fetchval('SELECT 42'), 42) + finally: + await con.close() + finally: + self.loop.set_exception_handler(old_handler) + @unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster') class TestClientSSLConnection(BaseTestSSLConnection): From 3badfce26e17c8ddde3594f2c5a187703b5f2f37 Mon Sep 17 00:00:00 2001 From: Fantix King Date: Tue, 14 Sep 2021 18:26:24 -0400 Subject: [PATCH 4/6] Fix for Python 3.6, win32, pg11 and lack of TLSv1.1 --- asyncpg/connect_utils.py | 28 +++++++++++++++------- tests/test_connect.py | 50 ++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 7ebcc0cd..6f7dd44c 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -222,6 +222,10 @@ def _parse_hostlist(hostlist, port, *, unquote=False): def _parse_tls_version(tls_version): + if not hasattr(ssl_module, 'TLSVersion'): + raise ValueError( + "TLSVersion is not supported in this version of Python" + ) if tls_version.startswith('SSL'): raise ValueError( f"Unsupported TLS version: {tls_version}" @@ -234,6 +238,10 @@ def _parse_tls_version(tls_version): ) +def _dot_postgresql_path(filename) -> pathlib.Path: + return (pathlib.Path.home() / '.postgresql' / filename).resolve() + + def _parse_connect_dsn_and_args(*, dsn, host, port, user, password, passfile, database, ssl, connect_timeout, server_settings): @@ -485,7 +493,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl.load_verify_locations(cafile=sslrootcert) ssl.verify_mode = ssl_module.CERT_REQUIRED else: - sslrootcert = os.path.expanduser('~/.postgresql/root.crt') + sslrootcert = _dot_postgresql_path('root.crt') try: ssl.load_verify_locations(cafile=sslrootcert) except FileNotFoundError: @@ -509,7 +517,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl.load_verify_locations(cafile=sslcrl) ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN else: - sslcrl = os.path.expanduser('~/.postgresql/root.crl') + sslcrl = _dot_postgresql_path('root.crl') try: ssl.load_verify_locations(cafile=sslcrl) except FileNotFoundError: @@ -520,8 +528,8 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if sslkey is None: sslkey = os.getenv('PGSSLKEY') if not sslkey: - sslkey = os.path.expanduser('~/.postgresql/postgresql.key') - if not os.path.exists(sslkey): + sslkey = _dot_postgresql_path('postgresql.key') + if not sslkey.exists(): sslkey = None if not sslpassword: sslpassword = '' @@ -532,7 +540,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, sslcert, keyfile=sslkey, password=lambda: sslpassword ) else: - sslcert = os.path.expanduser('~/.postgresql/postgresql.crt') + sslcert = _dot_postgresql_path('postgresql.crt') try: ssl.load_cert_chain( sslcert, keyfile=sslkey, password=lambda: sslpassword @@ -552,13 +560,17 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl.options &= ~ssl_module.OP_NO_COMPRESSION if ssl_min_protocol_version is None: - ssl_min_protocol_version = os.getenv( - 'PGSSLMINPROTOCOLVERSION', 'TLSv1.2' - ) + ssl_min_protocol_version = os.getenv('PGSSLMINPROTOCOLVERSION') if ssl_min_protocol_version: ssl.minimum_version = _parse_tls_version( ssl_min_protocol_version ) + else: + try: + ssl.minimum_version = _parse_tls_version('TLSv1.2') + except ValueError: + # Python 3.6 does not have ssl.TLSVersion + pass if ssl_max_protocol_version is None: ssl_max_protocol_version = os.getenv('PGSSLMAXPROTOCOLVERSION') diff --git a/tests/test_connect.py b/tests/test_connect.py index d69fdcaf..ab19e19c 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -9,10 +9,12 @@ import contextlib import ipaddress import os +import pathlib import platform import shutil import ssl import stat +import sys import tempfile import textwrap import unittest @@ -46,31 +48,26 @@ @contextlib.contextmanager def mock_dot_postgresql(*, ca=True, crl=False, client=False, protected=False): with tempfile.TemporaryDirectory() as temp_dir: - pg_home = os.path.join(temp_dir, '.postgresql') - os.mkdir(pg_home) + home = pathlib.Path(temp_dir) + pg_home = home / '.postgresql' + pg_home.mkdir() if ca: - shutil.copyfile( - SSL_CA_CERT_FILE, os.path.join(pg_home, 'root.crt') - ) + shutil.copyfile(SSL_CA_CERT_FILE, pg_home / 'root.crt') if crl: - shutil.copyfile( - SSL_CA_CRL_FILE, os.path.join(pg_home, 'root.crl') - ) + shutil.copyfile(SSL_CA_CRL_FILE, pg_home / 'root.crl') if client: - shutil.copyfile( - CLIENT_SSL_CERT_FILE, os.path.join(pg_home, 'postgresql.crt') - ) + shutil.copyfile(CLIENT_SSL_CERT_FILE, pg_home / 'postgresql.crt') if protected: shutil.copyfile( - CLIENT_SSL_PROTECTED_KEY_FILE, - os.path.join(pg_home, 'postgresql.key'), + CLIENT_SSL_PROTECTED_KEY_FILE, pg_home / 'postgresql.key' ) else: shutil.copyfile( - CLIENT_SSL_KEY_FILE, - os.path.join(pg_home, 'postgresql.key'), + CLIENT_SSL_KEY_FILE, pg_home / 'postgresql.key' ) - with unittest.mock.patch.dict('os.environ', {'HOME': temp_dir}): + with unittest.mock.patch( + 'pathlib.Path.home', unittest.mock.Mock(return_value=home) + ): yield @@ -1213,9 +1210,10 @@ def get_server_settings(cls): 'ssl_cert_file': SSL_CERT_FILE, 'ssl_key_file': SSL_KEY_FILE, 'ssl_ca_file': CLIENT_CA_CERT_FILE, - 'ssl_min_protocol_version': 'TLSv1.2', - 'ssl_max_protocol_version': 'TLSv1.2', }) + if cls.cluster.get_pg_version() >= (12, 0): + conf['ssl_min_protocol_version'] = 'TLSv1.2' + conf['ssl_max_protocol_version'] = 'TLSv1.2' return conf @@ -1340,13 +1338,13 @@ async def verify_fails(sslmode, *, host='localhost', exn_type): await verify_works('allow') await verify_works('prefer') await verify_fails('require', - exn_type=ssl.CertificateError) + exn_type=ssl.SSLError) await verify_fails('verify-ca', - exn_type=ssl.CertificateError) + exn_type=ssl.SSLError) await verify_fails('verify-ca', host='127.0.0.1', - exn_type=ssl.CertificateError) + exn_type=ssl.SSLError) await verify_fails('verify-full', - exn_type=ssl.CertificateError) + exn_type=ssl.SSLError) async def test_ssl_connection_default_context(self): # XXX: uvloop artifact @@ -1410,7 +1408,13 @@ async def test_executemany_uvloop_ssl_issue_700(self): finally: await con.close() + @unittest.skipIf( + sys.version_info < (3, 7), "Python < 3.7 doesn't have ssl.TLSVersion" + ) async def test_tls_version(self): + if self.cluster.get_pg_version() < (12, 0): + self.skipTest("PostgreSQL < 12 cannot set ssl protocol version") + # XXX: uvloop artifact old_handler = self.loop.get_exception_handler() try: @@ -1420,7 +1424,7 @@ async def test_tls_version(self): dsn='postgresql://ssl_user@localhost/postgres' '?sslmode=require&ssl_min_protocol_version=TLSv1.3' ) - with self.assertRaisesRegex(ssl.SSLError, 'protocol version'): + with self.assertRaises(ssl.SSLError): await self.connect( dsn='postgresql://ssl_user@localhost/postgres' '?sslmode=require' From 9e7385209f3ca86c8c23934bd4ce8de2aefaa1eb Mon Sep 17 00:00:00 2001 From: Fantix King Date: Wed, 15 Sep 2021 10:41:27 -0400 Subject: [PATCH 5/6] CRF: remove sslcompression --- asyncpg/connect_utils.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 6f7dd44c..f6f9d651 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -249,7 +249,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, # of reading the pgpass file. auth_hosts = None sslcert = sslkey = sslrootcert = sslcrl = sslpassword = None - sslcompression = ssl_min_protocol_version = ssl_max_protocol_version = None + ssl_min_protocol_version = ssl_max_protocol_version = None if dsn: parsed = urllib.parse.urlparse(dsn) @@ -349,9 +349,6 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if 'sslpassword' in query: sslpassword = query.pop('sslpassword') - if 'sslcompression' in query: - sslcompression = query.pop('sslcompression') - if 'ssl_min_protocol_version' in query: ssl_min_protocol_version = query.pop( 'ssl_min_protocol_version' @@ -554,11 +551,6 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if keylogfile and not sys.flags.ignore_environment: ssl.keylog_filename = keylogfile - if sslcompression is None: - sslcompression = os.getenv('PGSSLCOMPRESSION') - if sslcompression == '1': - ssl.options &= ~ssl_module.OP_NO_COMPRESSION - if ssl_min_protocol_version is None: ssl_min_protocol_version = os.getenv('PGSSLMINPROTOCOLVERSION') if ssl_min_protocol_version: From f510d1f898e0475e45fc6984160222db58a01f31 Mon Sep 17 00:00:00 2001 From: Fantix King Date: Wed, 15 Sep 2021 14:14:18 -0400 Subject: [PATCH 6/6] Update asyncpg/connection.py Co-authored-by: Elvis Pranskevichus --- asyncpg/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 1c7ca801..a7ec7719 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -2021,7 +2021,7 @@ async def connect(dsn=None, *, are supported in the *dsn* argument. .. versionchanged:: 0.25.0 - The ``sslpassword``, ``sslcompression``, ``ssl_min_protocol_version``, + The ``sslpassword``, ``ssl_min_protocol_version``, and ``ssl_max_protocol_version`` options are supported in the *dsn* argument.