diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 61eae0c0..528739d0 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -35,6 +35,9 @@ _SSL_REQUEST_CODE = 80877103 +# Required by PostgreSQL 17+ for direct TLS connections. +_ALPN_PROTOCOLS = ['postgresql'] + class SSLMode(enum.IntEnum): disable = 0 @@ -811,8 +814,13 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl_max_protocol_version ) + if ssl_module.HAS_ALPN: + ssl.set_alpn_protocols(_ALPN_PROTOCOLS) + elif ssl is True: ssl = ssl_module.create_default_context() + if ssl_module.HAS_ALPN: + ssl.set_alpn_protocols(_ALPN_PROTOCOLS) sslmode = SSLMode.verify_full elif isinstance(ssl, ssl_module.SSLContext): sslmode = SSLMode.require diff --git a/asyncpg/connection.py b/asyncpg/connection.py index d28d95c4..e3186bc3 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -2337,6 +2337,10 @@ async def connect(dsn=None, *, Pass ``True`` to skip PostgreSQL STARTTLS mode and perform a direct SSL connection. Requires ``ssl='require'``, ``'verify-ca'``, ``'verify-full'``, ``True``, or an explicit ``SSLContext``. + PostgreSQL 17+ requires the ``postgresql`` ALPN protocol for direct + SSL connections: asyncpg sets it on the contexts it creates, but an + explicit ``SSLContext`` must set it with + ``ctx.set_alpn_protocols(['postgresql'])``. :param dict server_settings: An optional dict of server runtime parameters. Refer to @@ -2456,6 +2460,8 @@ async def connect(dsn=None, *, ``ssl=True``, or an explicit ``SSLContext``. Other values (``'disable'``, ``'allow'``, and ``'prefer'``) will raise a ``ClientConfigurationError``. + SSL contexts created by asyncpg now set the ``postgresql`` ALPN + protocol, which PostgreSQL 17+ requires for direct SSL connections. .. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext .. _create_default_context: diff --git a/tests/test_connect.py b/tests/test_connect.py index e00c7b6c..66341f40 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -2331,6 +2331,20 @@ async def test_direct_tls_connection(self): self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) self.assertFalse(ctx.check_hostname) + async def test_direct_tls_native(self): + if self.cluster.get_pg_version() < (17, 0): + self.skipTest('native direct TLS requires PostgreSQL 17+') + + for mode in ('require', 'verify-ca', 'verify-full', True): + with self.subTest(mode=mode): + with unittest.mock.patch.dict(os.environ, { + 'SSL_CERT_FILE': SSL_CA_CERT_FILE, + }): + await self._test_works( + dsn='postgresql://ssl_user@localhost/postgres' + '?sslrootcert=' + SSL_CA_CERT_FILE, + ssl=mode, direct_tls=True, expected_ssl=True) + async def test_direct_tls_configuration_sources(self): base_dsn = 'postgresql://ssl_user@localhost/postgres'