From 70eb04ced62eeadfa2092ff2160f8a2d1645c22e Mon Sep 17 00:00:00 2001 From: Jan Dobes Date: Tue, 8 Jun 2021 16:44:29 +0200 Subject: [PATCH 1/2] Add support for sslcert, sslkey and sslrootcert parameters in DSNs --- asyncpg/connect_utils.py | 50 +++++++++++++++++++++++++++++++++++++--- asyncpg/connection.py | 23 ++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 86259be3..afa19ff0 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -222,6 +222,7 @@ def _parse_hostlist(hostlist, port, *, unquote=False): def _parse_connect_dsn_and_args(*, dsn, host, port, user, password, passfile, database, ssl, + sslcert, sslkey, sslrootcert, sslcrl, connect_timeout, server_settings): # `auth_hosts` is the version of host information for the purposes # of reading the pgpass file. @@ -310,6 +311,26 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if ssl is None: ssl = val + if 'sslcert' in query: + val = query.pop('sslcert') + if sslcert is None: + sslcert = val + + if 'sslkey' in query: + val = query.pop('sslkey') + if sslkey is None: + sslkey = val + + if 'sslrootcert' in query: + val = query.pop('sslrootcert') + if sslrootcert is None: + sslrootcert = val + + if 'sslcrl' in query: + val = query.pop('sslcrl') + if sslcrl is None: + sslcrl = val + if query: if server_settings is None: server_settings = query @@ -427,7 +448,6 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, '`sslmode` parameter must be one of: {}'.format(modes)) # docs at https://www.postgresql.org/docs/10/static/libpq-connect.html - # Not implemented: sslcert & sslkey & sslrootcert & sslcrl params. if sslmode < SSLMode.allow: ssl = False else: @@ -436,6 +456,28 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl.verify_mode = ssl_module.CERT_REQUIRED if sslmode <= SSLMode.require: ssl.verify_mode = ssl_module.CERT_NONE + + if sslcert is None: + sslcert = os.getenv('PGSSLCERT') + + 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 sslcert: + ssl.load_cert_chain(sslcert, keyfile=sslkey) + + if sslrootcert: + ssl.load_verify_locations(cafile=sslrootcert) + + if sslcrl: + ssl.load_verify_locations(cafile=sslcrl) + elif ssl is True: ssl = ssl_module.create_default_context() sslmode = SSLMode.verify_full @@ -463,7 +505,8 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile, statement_cache_size, max_cached_statement_lifetime, max_cacheable_statement_size, - ssl, server_settings): + ssl, sslcert, sslkey, sslrootcert, sslcrl, + server_settings): local_vars = locals() for var_name in {'max_cacheable_statement_size', @@ -491,7 +534,8 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile, addrs, params = _parse_connect_dsn_and_args( dsn=dsn, host=host, port=port, user=user, password=password, passfile=passfile, ssl=ssl, - database=database, connect_timeout=timeout, + sslcert=sslcert, sslkey=sslkey, sslrootcert=sslrootcert, + sslcrl=sslcrl, database=database, connect_timeout=timeout, server_settings=server_settings) config = _ClientConfiguration( diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 4a656124..6b2fc858 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1758,6 +1758,10 @@ async def connect(dsn=None, *, max_cacheable_statement_size=1024 * 15, command_timeout=None, ssl=None, + sslcert=None, + sslkey=None, + sslrootcert=None, + sslcrl=None, connection_class=Connection, record_class=protocol.Record, server_settings=None): @@ -1900,6 +1904,21 @@ async def connect(dsn=None, *, .. note:: *ssl* is ignored for Unix domain socket communication. + + :param sslcert: + This parameter specifies the file name of the client SSL certificate. + + :param sslkey: + This parameter specifies the location for the secret key used for + the client certificate. + + :param sslrootcert: + This parameter specifies the name of a file containing SSL certificate + authority (CA) certificate(s). + + :param sslcrl + This parameter specifies the file name of the SSL certificate + revocation list (CRL). :param dict server_settings: An optional dict of server runtime parameters. Refer to @@ -1993,6 +2012,10 @@ async def connect(dsn=None, *, password=password, passfile=passfile, ssl=ssl, + sslcert=sslcert, + sslkey=sslkey, + sslrootcert=sslrootcert, + sslcrl=sslcrl, database=database, server_settings=server_settings, command_timeout=command_timeout, From 0c857fb85b110cd517d947d7f271d9d98758bae3 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Mon, 9 Aug 2021 15:24:28 -0700 Subject: [PATCH 2/2] Remove redundant connect() arguments, add tests --- asyncpg/_testbase/__init__.py | 4 +- asyncpg/connect_utils.py | 14 +++--- asyncpg/connection.py | 75 +++++++++++++++++++++---------- tests/certs/client.cert.pem | 24 ++++++++++ tests/certs/client.csr.pem | 18 ++++++++ tests/certs/client.key.pem | 27 ++++++++++++ tests/certs/client_ca.cert.pem | 25 +++++++++++ tests/certs/client_ca.cert.srl | 1 + tests/certs/client_ca.key.pem | 27 ++++++++++++ tests/test_connect.py | 81 ++++++++++++++++++++++++++++++++-- 10 files changed, 261 insertions(+), 35 deletions(-) create mode 100644 tests/certs/client.cert.pem create mode 100644 tests/certs/client.csr.pem create mode 100644 tests/certs/client.key.pem create mode 100644 tests/certs/client_ca.cert.pem create mode 100644 tests/certs/client_ca.cert.srl create mode 100644 tests/certs/client_ca.key.pem diff --git a/asyncpg/_testbase/__init__.py b/asyncpg/_testbase/__init__.py index ce7f827f..9944b20f 100644 --- a/asyncpg/_testbase/__init__.py +++ b/asyncpg/_testbase/__init__.py @@ -330,8 +330,10 @@ def tearDownClass(cls): @classmethod def get_connection_spec(cls, kwargs={}): conn_spec = cls.cluster.get_connection_spec() + if kwargs.get('dsn'): + conn_spec.pop('host') conn_spec.update(kwargs) - if not os.environ.get('PGHOST'): + if not os.environ.get('PGHOST') and not kwargs.get('dsn'): if 'database' not in conn_spec: conn_spec['database'] = 'postgres' if 'user' not in conn_spec: diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index afa19ff0..cd94b834 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -222,11 +222,11 @@ def _parse_hostlist(hostlist, port, *, unquote=False): def _parse_connect_dsn_and_args(*, dsn, host, port, user, password, passfile, database, ssl, - sslcert, sslkey, sslrootcert, sslcrl, 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 if dsn: parsed = urllib.parse.urlparse(dsn) @@ -451,12 +451,13 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if sslmode < SSLMode.allow: ssl = False else: - ssl = ssl_module.create_default_context() + ssl = ssl_module.create_default_context( + ssl_module.Purpose.SERVER_AUTH) ssl.check_hostname = sslmode >= SSLMode.verify_full ssl.verify_mode = ssl_module.CERT_REQUIRED if sslmode <= SSLMode.require: ssl.verify_mode = ssl_module.CERT_NONE - + if sslcert is None: sslcert = os.getenv('PGSSLCERT') @@ -477,6 +478,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if sslcrl: ssl.load_verify_locations(cafile=sslcrl) + ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN elif ssl is True: ssl = ssl_module.create_default_context() @@ -505,8 +507,7 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile, statement_cache_size, max_cached_statement_lifetime, max_cacheable_statement_size, - ssl, sslcert, sslkey, sslrootcert, sslcrl, - server_settings): + ssl, server_settings): local_vars = locals() for var_name in {'max_cacheable_statement_size', @@ -534,8 +535,7 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile, addrs, params = _parse_connect_dsn_and_args( dsn=dsn, host=host, port=port, user=user, password=password, passfile=passfile, ssl=ssl, - sslcert=sslcert, sslkey=sslkey, sslrootcert=sslrootcert, - sslcrl=sslcrl, database=database, connect_timeout=timeout, + database=database, connect_timeout=timeout, server_settings=server_settings) config = _ClientConfiguration( diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 6b2fc858..ae46bbc1 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1758,10 +1758,6 @@ async def connect(dsn=None, *, max_cacheable_statement_size=1024 * 15, command_timeout=None, ssl=None, - sslcert=None, - sslkey=None, - sslrootcert=None, - sslcrl=None, connection_class=Connection, record_class=protocol.Record, server_settings=None): @@ -1780,10 +1776,11 @@ async def connect(dsn=None, *, Connection arguments specified using as a single string in the `libpq connection URI format`_: ``postgres://user:password@host:port/database?option=value``. - The following options are recognized by asyncpg: host, port, - user, database (or dbname), password, passfile, sslmode. - Unlike libpq, asyncpg will treat unrecognized options - as `server settings`_ to be used for the connection. + The following options are recognized by asyncpg: ``host``, + ``port``, ``user``, ``database`` (or ``dbname``), ``password``, + ``passfile``, ``sslmode``, ``sslcert``, ``sslkey``, ``sslrootcert``, + and ``sslcrl``. Unlike libpq, asyncpg will treat unrecognized + options as `server settings`_ to be used for the connection. .. note:: @@ -1904,21 +1901,51 @@ async def connect(dsn=None, *, .. note:: *ssl* is ignored for Unix domain socket communication. - - :param sslcert: - This parameter specifies the file name of the client SSL certificate. - :param sslkey: - This parameter specifies the location for the secret key used for - the client certificate. + Example of programmatic SSL context configuration that is equivalent + to ``sslmode=verify-full&sslcert=..&sslkey=..&sslrootcert=..``: - :param sslrootcert: - This parameter specifies the name of a file containing SSL certificate - authority (CA) certificate(s). + .. code-block:: pycon - :param sslcrl - This parameter specifies the file name of the SSL certificate - revocation list (CRL). + >>> import asyncpg + >>> import asyncio + >>> import ssl + >>> async def main(): + ... # Load CA bundle for server certificate verification, + ... # equivalent to sslrootcert= in DSN. + ... sslctx = ssl.create_default_context( + ... ssl.Purpose.SERVER_AUTH, + ... cafile="path/to/ca_bundle.pem") + ... # If True, equivalent to sslmode=verify-full, if False: + ... # sslmode=verify-ca. + ... sslctx.check_hostname = True + ... # Load client certificate and private key for client + ... # authentication, equivalent to sslcert= and sslkey= in + ... # DSN. + ... sslctx.load_cert_chain( + ... "path/to/client.cert", + ... keyfile="path/to/client.key", + ... ) + ... con = await asyncpg.connect(user='postgres', ssl=sslctx) + ... await con.close() + >>> asyncio.run(run()) + + Example of programmatic SSL context configuration that is equivalent + to ``sslmode=require`` (no server certificate or host verification): + + .. code-block:: pycon + + >>> import asyncpg + >>> import asyncio + >>> import ssl + >>> async def main(): + ... sslctx = ssl.create_default_context( + ... ssl.Purpose.SERVER_AUTH) + ... sslctx.check_hostname = False + ... sslctx.verify_mode = ssl.CERT_NONE + ... con = await asyncpg.connect(user='postgres', ssl=sslctx) + ... await con.close() + >>> asyncio.run(run()) :param dict server_settings: An optional dict of server runtime parameters. Refer to @@ -1978,6 +2005,10 @@ async def connect(dsn=None, *, .. versionchanged:: 0.22.0 The *ssl* argument now defaults to ``'prefer'``. + .. versionchanged:: 0.24.0 + The ``sslcert``, ``sslkey``, ``sslrootcert``, and ``sslcrl`` options + are supported in the *dsn* argument. + .. _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 @@ -2012,10 +2043,6 @@ async def connect(dsn=None, *, password=password, passfile=passfile, ssl=ssl, - sslcert=sslcert, - sslkey=sslkey, - sslrootcert=sslrootcert, - sslcrl=sslcrl, database=database, server_settings=server_settings, command_timeout=command_timeout, diff --git a/tests/certs/client.cert.pem b/tests/certs/client.cert.pem new file mode 100644 index 00000000..b6d9a91a --- /dev/null +++ b/tests/certs/client.cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIUPfej8IQ/5bCrihqWImrq2vKPOq0wDQYJKoZIhvcNAQEL +BQAwgaMxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRAwDgYDVQQHDAdU +b3JvbnRvMRgwFgYDVQQKDA9NYWdpY1N0YWNrIEluYy4xFjAUBgNVBAsMDWFzeW5j +cGcgdGVzdHMxHzAdBgNVBAMMFmFzeW5jcGcgdGVzdCBjbGllbnQgQ0ExHTAbBgkq +hkiG9w0BCQEWDmhlbGxvQG1hZ2ljLmlvMB4XDTIxMDgwOTIxNTA1MloXDTMyMDEw +NDIxNTA1MlowgZUxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRAwDgYD +VQQHDAdUb3JvbnRvMRgwFgYDVQQKDA9NYWdpY1N0YWNrIEluYy4xFjAUBgNVBAsM +DWFzeW5jcGcgdGVzdHMxETAPBgNVBAMMCHNzbF91c2VyMR0wGwYJKoZIhvcNAQkB +Fg5oZWxsb0BtYWdpYy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AJjiP9Ik/KRRLK9GMvoH8m1LO+Gyrr8Gz36LpmKJMR/PpwTL+1pOkYSGhOyT3Cw9 +/kWWLJRCvYqKgFtYtbr4S6ReGm3GdSVW+sfVRYDrRQZLPgQSPeq25g2v8UZ63Ota +lPAyUPUZKpxyWz8PL77lV8psb9yv14yBH2kv9BbxKPksWOU8p8OCn1Z3WFFl0ItO +nzMvCp5os+xFrt4SpoRGTx9x4QleY+zrEsYZtmnV4wC+JuJkNw4fuCdrX5k7dghs +uZkcsAZof1nMdYsYiazeDfQKZtJqh5kO7mpwvCudKUWaLJJUwiQA87BwSlnCd/Hh +TZDbC+zeFNjTS49/4Q72xVECAwEAAaM7MDkwHwYDVR0jBBgwFoAUi1jMmAisuOib +mHIE2n0W2WnnaL0wCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwDQYJKoZIhvcNAQEL +BQADggEBACbnp5oOp639ko4jn8axF+so91k0vIcgwDg+NqgtSRsuAENGumHAa8ec +YOks0TCTvNN5E6AfNSxRat5CyguIlJ/Vy3KbkkFNXcCIcI/duAJvNphg7JeqYlQM +VIJhrO/5oNQMzzTw8XzTHnciGbrbiZ04hjwrruEkvmIAwgQPhIgq4H6umTZauTvk +DEo7uLm7RuG9hnDyWCdJxLLljefNL/EAuDYpPzgTeEN6JAnOu0ULIbpxpJKiYEId +8I0U2n0I2NTDOHmsAJiXf8BiHHmpK5SXFyY9s2ZuGkCzvmeZlR81tTXmHZ3v1X2z +8NajoAZfJ+QD50DrbF5E00yovZbyIB4= +-----END CERTIFICATE----- diff --git a/tests/certs/client.csr.pem b/tests/certs/client.csr.pem new file mode 100644 index 00000000..c6a87c65 --- /dev/null +++ b/tests/certs/client.csr.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC2zCCAcMCAQAwgZUxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRAw +DgYDVQQHDAdUb3JvbnRvMRgwFgYDVQQKDA9NYWdpY1N0YWNrIEluYy4xFjAUBgNV +BAsMDWFzeW5jcGcgdGVzdHMxETAPBgNVBAMMCHNzbF91c2VyMR0wGwYJKoZIhvcN +AQkBFg5oZWxsb0BtYWdpYy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJjiP9Ik/KRRLK9GMvoH8m1LO+Gyrr8Gz36LpmKJMR/PpwTL+1pOkYSGhOyT +3Cw9/kWWLJRCvYqKgFtYtbr4S6ReGm3GdSVW+sfVRYDrRQZLPgQSPeq25g2v8UZ6 +3OtalPAyUPUZKpxyWz8PL77lV8psb9yv14yBH2kv9BbxKPksWOU8p8OCn1Z3WFFl +0ItOnzMvCp5os+xFrt4SpoRGTx9x4QleY+zrEsYZtmnV4wC+JuJkNw4fuCdrX5k7 +dghsuZkcsAZof1nMdYsYiazeDfQKZtJqh5kO7mpwvCudKUWaLJJUwiQA87BwSlnC +d/HhTZDbC+zeFNjTS49/4Q72xVECAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQCG +irI2ph09V/4BMe6QMhjBFUatwmTa/05PYGjvT3LAhRzEb3/o/gca0XFSAFrE6zIY +DsgMk1c8aLr9DQsn9cf22oMFImKdnIZ3WLE9MXjN+s1Bjkiqt7uxDpxPo/DdfUTQ +RQC5i/Z2tn29y9K09lEjp35ZhPp3tOA0V4CH0FThAjRR+amwaBjxQ7TTSNfoMUd7 +i/DrylwnNg1iEQmYUwJYopqgxtwseiBUSDXzEvjFPY4AvZKmEQmE5QkybpWIfivt +1kmKhvKKpn5Cb6c0D3XoYqyPN3TxqjH9L8R+tWUCwhYJeDZj5DumFr3Hw/sx8tOL +EctyS6XfO3S2KbmDiyv8 +-----END CERTIFICATE REQUEST----- diff --git a/tests/certs/client.key.pem b/tests/certs/client.key.pem new file mode 100644 index 00000000..90389cab --- /dev/null +++ b/tests/certs/client.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAmOI/0iT8pFEsr0Yy+gfybUs74bKuvwbPfoumYokxH8+nBMv7 +Wk6RhIaE7JPcLD3+RZYslEK9ioqAW1i1uvhLpF4abcZ1JVb6x9VFgOtFBks+BBI9 +6rbmDa/xRnrc61qU8DJQ9RkqnHJbPw8vvuVXymxv3K/XjIEfaS/0FvEo+SxY5Tyn +w4KfVndYUWXQi06fMy8Knmiz7EWu3hKmhEZPH3HhCV5j7OsSxhm2adXjAL4m4mQ3 +Dh+4J2tfmTt2CGy5mRywBmh/Wcx1ixiJrN4N9Apm0mqHmQ7uanC8K50pRZosklTC +JADzsHBKWcJ38eFNkNsL7N4U2NNLj3/hDvbFUQIDAQABAoIBAAIMVeqM0E2rQLwA +ZsJuxNKuBVlauXiZsMHzQQFk8SGJ+KTZzr5A+zYZT0KUIIj/M57fCi3aTwvCG0Ie +CCE/HlRPZm8+D2e2qJlwxAOcI0qYS3ZmgCna1W4tgz/8eWU1y3UEV41RDv8VkR9h +JrSaAfkWRtFgEbUyLaeNGuoLxQ7Bggo9zi1/xDJz/aZ/y4L4y8l1xs2eNVmbRGnj +mPr1daeYhsWgaNiT/Wm3CAxvykptHavyWSsrXzCp0bEw6fAXxBqkeDFGIMVC9q3t +ZRFtqMHi9i7SJtH1XauOC6QxLYgSEmNEie1JYbNx2Zf4h2KvSwDxpTqWhOjJ/m5j +/NSkASECgYEAyHQAqG90yz5QaYnC9lgUhGIMokg9O3LcEbeK7IKIPtC9xINOrnj6 +ecCfhfc1aP3wQI+VKC3kiYerfTJvVsU5CEawBQSRiBY/TZZ7hTR7Rkm3s4xeM+o6 +2zADdVUwmTVYwu0gUKCeDKO4iD8Uhh8J54JrKUejuG50VWZQWGVgqo0CgYEAwz+2 +VdYcfuQykMA3jQBnXmMMK92/Toq6FPDgsa45guEFD6Zfdi9347/0Ipt+cTNg0sUZ +YBLOnNPwLn+yInfFa88Myf0UxCAOoZKfpJg/J27soUJzpd/CGx+vaAHrxMP6t/qo +JAGMBIyOoqquId7jvErlC/sGBk/duya7IdiT1tUCgYBuvM8EPhaKlVE9DJL9Hmmv +PK94E2poZiq3SutffzkfYpgDcPrNnh3ZlxVJn+kMqITKVcfz226On7mYP32MtQWt +0cc57m0rfgbYqRJx4y1bBiyK7ze3fGWpYxv1/OsNKJBxlygsAp9toiC2fAqtkYYa +NE1ZD6+dmr9/0jb+rnq5nQKBgQCtZvwsp4ePOmOeItgzJdSoAxdgLgQlYRd6WaN0 +qeLx1Z6FE6FceTPk1SmhQq+9IYAwMFQk+w78QU3iPg6ahfyTjsMw8M9sj3vvCyU1 +LPGJt/34CehjvKHLLQy/NlWJ3vPgSYDi2Wzc7WgQF72m3ykqpOlfBoWHPY8TE4bG +vG4wMQKBgFSq2GDAJ1ovBl7yWYW7w4SM8X96YPOff+OmI4G/8+U7u3dDM1dYeQxD +7BHLuvr4AXg27LC97u8/eFIBXC1elbco/nAKE1YHj2xcIb/4TsgAqkcysGV08ngi +dULh3q0GpTYyuELZV4bfWE8MjSiGAH+nuMdXYDGuY2QnBq8MdSOH +-----END RSA PRIVATE KEY----- diff --git a/tests/certs/client_ca.cert.pem b/tests/certs/client_ca.cert.pem new file mode 100644 index 00000000..17d3f357 --- /dev/null +++ b/tests/certs/client_ca.cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKTCCAxGgAwIBAgIUKmL8tfNS9LIB6GLB9RpZpTyk3uIwDQYJKoZIhvcNAQEL +BQAwgaMxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRAwDgYDVQQHDAdU +b3JvbnRvMRgwFgYDVQQKDA9NYWdpY1N0YWNrIEluYy4xFjAUBgNVBAsMDWFzeW5j +cGcgdGVzdHMxHzAdBgNVBAMMFmFzeW5jcGcgdGVzdCBjbGllbnQgQ0ExHTAbBgkq +hkiG9w0BCQEWDmhlbGxvQG1hZ2ljLmlvMB4XDTIxMDgwOTIxNDQxM1oXDTQxMDgw +NDIxNDQxM1owgaMxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRAwDgYD +VQQHDAdUb3JvbnRvMRgwFgYDVQQKDA9NYWdpY1N0YWNrIEluYy4xFjAUBgNVBAsM +DWFzeW5jcGcgdGVzdHMxHzAdBgNVBAMMFmFzeW5jcGcgdGVzdCBjbGllbnQgQ0Ex +HTAbBgkqhkiG9w0BCQEWDmhlbGxvQG1hZ2ljLmlvMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAptRYfxKiWExfZguQDva53bIqYa4lJwZA86Qu0peBUcsd +E6zyHNgVv4XSMim1FH12KQ4KPKuQAcVqRMCRAHqB96kUfWQqF//fLajr0umdzcbx ++UTgNux8TkScTl9KNAxhiR/oOGbKFcNSs4raaG8puwwEN66uMhoKk2pN2NwDVfHa +bTekJ3jouTcTCnqCynx4qwI4WStJkuW4IPCmDRVXxOOauT7YalElYLWYtAOqGEvf +noDK2Imhc0h6B5XW8nI54rVCXWwhW1v3RLAJGP+LwSy++bf08xmpHXdKkAj5BmUO +QwJRiJ33Xa17rmi385egx8KpqV04YEAPdV1Z4QM6PQIDAQABo1MwUTAdBgNVHQ4E +FgQUi1jMmAisuOibmHIE2n0W2WnnaL0wHwYDVR0jBBgwFoAUi1jMmAisuOibmHIE +2n0W2WnnaL0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAifNE +ZLZXxECp2Sl6jCViZxgFf2+OHDvRORgI6J0heckYyYF/JHvLaDphh6TkSJAdT6Y3 +hAb7jueTMI+6RIdRzIjTKCGdJqUetiSfAbnQyIp2qmVqdjeFoXTvQL7BdkIE+kOW +0iomMqDB3czTl//LrgVQCYqKM0D/Ytecpg2mbshLfpPxdHyliCJcb4SqfdrDnKoV +HUduBjOVot+6bkB5SEGCrrB4KMFTzbAu+zriKWWz+uycIyeVMLEyhDs59vptOK6e +gWkraG43LZY3cHPiVeN3tA/dWdyJf9rgK21zQDSMB8OSH4yQjdQmkkvRQBjp3Fcy +w2SZIP4o9l1Y7+hMMw== +-----END CERTIFICATE----- diff --git a/tests/certs/client_ca.cert.srl b/tests/certs/client_ca.cert.srl new file mode 100644 index 00000000..0eae4d30 --- /dev/null +++ b/tests/certs/client_ca.cert.srl @@ -0,0 +1 @@ +3DF7A3F0843FE5B0AB8A1A96226AEADAF28F3AAD diff --git a/tests/certs/client_ca.key.pem b/tests/certs/client_ca.key.pem new file mode 100644 index 00000000..519c5a09 --- /dev/null +++ b/tests/certs/client_ca.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAptRYfxKiWExfZguQDva53bIqYa4lJwZA86Qu0peBUcsdE6zy +HNgVv4XSMim1FH12KQ4KPKuQAcVqRMCRAHqB96kUfWQqF//fLajr0umdzcbx+UTg +Nux8TkScTl9KNAxhiR/oOGbKFcNSs4raaG8puwwEN66uMhoKk2pN2NwDVfHabTek +J3jouTcTCnqCynx4qwI4WStJkuW4IPCmDRVXxOOauT7YalElYLWYtAOqGEvfnoDK +2Imhc0h6B5XW8nI54rVCXWwhW1v3RLAJGP+LwSy++bf08xmpHXdKkAj5BmUOQwJR +iJ33Xa17rmi385egx8KpqV04YEAPdV1Z4QM6PQIDAQABAoIBABQrKcO7CftoyEO6 +9CCK/W9q4arLddxg6itKVwrInC66QnqlduO7z+1GjWHZHvYqMMXH17778r30EuPa +7+zB4sKBI2QBXwFlwqJvgIsQCS7edVRwWjbpoiGIM+lZpcvjD0uXmuhurNGyumXQ +TJVBkyb0zfG5YX/XHB40RNMJzjFuiMPDLVQmmDE//FOuWqBG88MgJP9Ghk3J7wA2 +JfDPavb49EzOCSh74zJWP7/QyybzF3ABCMu4OFkaOdqso8FS659XI55QReBbUppu +FRkOgao1BclJhbBdrdtLNjlETM82tfVgW56vaIrrU2z7HskihEyMdB4c+CYbBnPx +QqIhkhUCgYEA0SLVExtNy5Gmi6/ZY9tcd3QIuxcN6Xiup+LgIhWK3+GIoVOPsOjN +27dlVRINPKhrCfVbrLxUtDN5PzphwSA2Qddm4jg3d5FzX+FgKHQpoaU1WjtRPP+w +K+t6W/NbZ8Rn4JyhZQ3Yqj264NA2l3QmuTfZSUQ5m4x7EUakfGU7G1sCgYEAzDaU +jHsovn0FedOUaaYl6pgzjFV8ByPeT9usN54PZyuzyc+WunjJkxCQqD88J9jyG8XB +3V3tQj/CNbMczrS2ZaJ29aI4b/8NwBNR9e6t01bY3B90GJi8S4B4Hf8tYyIlVdeL +tCC4FCZhvl4peaK3AWBj4NhjvdB32ThDXSGxLEcCgYEAiA5tKHz+44ziGMZSW1B+ +m4f1liGtf1Jv7fD/d60kJ/qF9M50ENej9Wkel3Wi/u9ik5v4BCyRvpouKyBEMGxQ +YA1OdaW1ECikMqBg+nB4FR1x1D364ABIEIqlk+SCdsOkANBlf2S+rCJ0zYUnvuhl +uOHIjo3AHJ4MAnU+1V7WUTkCgYBkMedioc7U34x/QJNR3sY9ux2Xnh2zdyLNdc+i +njeafDPDMcoXhcoJERiYpCYEuwnXHIlI7pvJZHUKWe4pcTsI1NSfIk+ki7SYaCJP +kyLQTY0rO3d/1fiU5tyIgzomqIs++fm+kEsg/8/3UkXxOyelUkDPAfy2FgGnn1ZV +7ID8YwKBgQCeZCapdGJ6Iu5oYB17TyE5pLwb+QzaofR5uO8H4pXGVQyilKVCG9Dp +GMnlXD7bwXPVKa8Icow2OIbmgrZ2mzOo9BSY3BlkKbpJDy7UNtAhzsHHN5/AEk8z +YycWQtMiXI+cRsYO0eyHhJeSS2hX+JTe++iZX65twV53agzCHWRIbg== +-----END RSA PRIVATE KEY----- diff --git a/tests/test_connect.py b/tests/test_connect.py index 84eac202..be694d67 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -15,6 +15,7 @@ import tempfile import textwrap import unittest +import urllib.parse import weakref import asyncpg @@ -33,6 +34,9 @@ SSL_CA_CERT_FILE = os.path.join(CERTS, 'ca.cert.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') +CLIENT_SSL_CERT_FILE = os.path.join(CERTS, 'client.cert.pem') +CLIENT_SSL_KEY_FILE = os.path.join(CERTS, 'client.key.pem') class TestSettings(tb.ConnectedTestCase): @@ -1124,6 +1128,8 @@ async def verify_works(sslmode): try: con = await self.connect( dsn='postgresql://foo/?sslmode=' + sslmode, + user='postgres', + database='postgres', host='localhost') self.assertEqual(await con.fetchval('SELECT 42'), 42) self.assertFalse(con._protocol.is_ssl) @@ -1137,6 +1143,8 @@ async def verify_fails(sslmode): with self.assertRaises(ConnectionError): con = await self.connect( dsn='postgresql://foo/?sslmode=' + sslmode, + user='postgres', + database='postgres', host='localhost') await con.fetchval('SELECT 42') finally: @@ -1167,6 +1175,7 @@ def get_server_settings(cls): 'ssl': 'on', 'ssl_cert_file': SSL_CERT_FILE, 'ssl_key_file': SSL_KEY_FILE, + 'ssl_ca_file': CLIENT_CA_CERT_FILE, }) return conf @@ -1245,7 +1254,7 @@ async def verify_works(sslmode, *, host='localhost'): con = None try: con = await self.connect( - dsn='postgresql://foo/?sslmode=' + sslmode, + dsn='postgresql://foo/postgres?sslmode=' + sslmode, host=host, user='ssl_user') self.assertEqual(await con.fetchval('SELECT 42'), 42) @@ -1358,6 +1367,72 @@ async def test_executemany_uvloop_ssl_issue_700(self): await con.close() +@unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster') +class TestClientSSLConnection(BaseTestSSLConnection): + def _add_hba_entry(self): + self.cluster.add_hba_entry( + type='hostssl', address=ipaddress.ip_network('127.0.0.0/24'), + database='postgres', user='ssl_user', + auth_method='cert') + + self.cluster.add_hba_entry( + type='hostssl', address=ipaddress.ip_network('::1/128'), + database='postgres', user='ssl_user', + auth_method='cert') + + async def test_ssl_connection_client_auth_fails_with_wrong_setup(self): + ssl_context = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, + cafile=SSL_CA_CERT_FILE, + ) + + with self.assertRaisesRegex( + exceptions.InvalidAuthorizationSpecificationError, + "requires a valid client certificate", + ): + await self.connect( + host='localhost', + user='ssl_user', + 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, + ) + + try: + self.assertEqual(await con.fetchval('SELECT 42'), 42) + finally: + await con.close() + + async def test_ssl_connection_client_auth_dsn(self): + params = urllib.parse.urlencode({ + '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() + + @unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster') class TestNoSSLConnection(BaseTestSSLConnection): def _add_hba_entry(self): @@ -1376,7 +1451,7 @@ async def verify_works(sslmode, *, host='localhost'): con = None try: con = await self.connect( - dsn='postgresql://foo/?sslmode=' + sslmode, + dsn='postgresql://foo/postgres?sslmode=' + sslmode, host=host, user='ssl_user') self.assertEqual(await con.fetchval('SELECT 42'), 42) @@ -1413,7 +1488,7 @@ async def verify_fails(sslmode, *, host='localhost', async def test_nossl_connection_prefer_cancel(self): con = await self.connect( - dsn='postgresql://foo/?sslmode=prefer', + dsn='postgresql://foo/postgres?sslmode=prefer', host='localhost', user='ssl_user') self.assertFalse(con._protocol.is_ssl)