Skip to content

Commit 9a5b6fd

Browse files
committed
Fix SSL compatibility of libpq
1 parent 36658fa commit 9a5b6fd

File tree

4 files changed

+260
-80
lines changed

4 files changed

+260
-80
lines changed

asyncpg/connect_utils.py

Lines changed: 117 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import ssl as ssl_module
1919
import stat
2020
import struct
21+
import sys
2122
import time
2223
import typing
2324
import urllib.parse
@@ -220,13 +221,27 @@ def _parse_hostlist(hostlist, port, *, unquote=False):
220221
return hosts, port
221222

222223

224+
def _parse_tls_version(tls_version):
225+
if tls_version.startswith('SSL'):
226+
raise ValueError(
227+
f"Unsupported TLS version: {tls_version}"
228+
)
229+
try:
230+
return ssl_module.TLSVersion[tls_version.replace('.', '_')]
231+
except KeyError:
232+
raise ValueError(
233+
f"No such TLS version: {tls_version}"
234+
)
235+
236+
223237
def _parse_connect_dsn_and_args(*, dsn, host, port, user,
224238
password, passfile, database, ssl,
225239
connect_timeout, server_settings):
226240
# `auth_hosts` is the version of host information for the purposes
227241
# of reading the pgpass file.
228242
auth_hosts = None
229-
sslcert = sslkey = sslrootcert = sslcrl = None
243+
sslcert = sslkey = sslrootcert = sslcrl = sslpassword = None
244+
sslcompression = ssl_min_protocol_version = ssl_max_protocol_version = None
230245

231246
if dsn:
232247
parsed = urllib.parse.urlparse(dsn)
@@ -312,24 +327,28 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
312327
ssl = val
313328

314329
if 'sslcert' in query:
315-
val = query.pop('sslcert')
316-
if sslcert is None:
317-
sslcert = val
330+
sslcert = query.pop('sslcert')
318331

319332
if 'sslkey' in query:
320-
val = query.pop('sslkey')
321-
if sslkey is None:
322-
sslkey = val
333+
sslkey = query.pop('sslkey')
323334

324335
if 'sslrootcert' in query:
325-
val = query.pop('sslrootcert')
326-
if sslrootcert is None:
327-
sslrootcert = val
336+
sslrootcert = query.pop('sslrootcert')
328337

329338
if 'sslcrl' in query:
330-
val = query.pop('sslcrl')
331-
if sslcrl is None:
332-
sslcrl = val
339+
sslcrl = query.pop('sslcrl')
340+
341+
if 'sslpassword' in query:
342+
sslpassword = query.pop('sslpassword')
343+
344+
if 'sslcompression' in query:
345+
sslcompression = query.pop('sslcompression')
346+
347+
if 'ssl_min_protocol_version' in query:
348+
ssl_min_protocol_version = query.pop('ssl_min_protocol_version')
349+
350+
if 'ssl_max_protocol_version' in query:
351+
ssl_max_protocol_version = query.pop('ssl_max_protocol_version')
333352

334353
if query:
335354
if server_settings is None:
@@ -451,34 +470,98 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
451470
if sslmode < SSLMode.allow:
452471
ssl = False
453472
else:
454-
ssl = ssl_module.create_default_context(
455-
ssl_module.Purpose.SERVER_AUTH)
473+
ssl = ssl_module.SSLContext(ssl_module.PROTOCOL_TLS_CLIENT)
456474
ssl.check_hostname = sslmode >= SSLMode.verify_full
457-
ssl.verify_mode = ssl_module.CERT_REQUIRED
458-
if sslmode <= SSLMode.require:
475+
if sslmode < SSLMode.require:
459476
ssl.verify_mode = ssl_module.CERT_NONE
477+
else:
478+
if sslrootcert is None:
479+
sslrootcert = os.getenv('PGSSLROOTCERT')
480+
if sslrootcert:
481+
ssl.load_verify_locations(cafile=sslrootcert)
482+
ssl.verify_mode = ssl_module.CERT_REQUIRED
483+
else:
484+
sslrootcert = os.path.expanduser('~/.postgresql/root.crt')
485+
try:
486+
ssl.load_verify_locations(cafile=sslrootcert)
487+
except FileNotFoundError:
488+
if sslmode > SSLMode.require:
489+
raise ValueError(
490+
f'root certificate file "{sslrootcert}" does '
491+
f'not exist\nEither provide the file or '
492+
f'change sslmode to disable server '
493+
f'certificate verification.'
494+
)
495+
elif sslmode == SSLMode.require:
496+
ssl.verify_mode = ssl_module.CERT_NONE
497+
else:
498+
assert False, 'unreachable'
499+
else:
500+
ssl.verify_mode = ssl_module.CERT_REQUIRED
460501

461-
if sslcert is None:
462-
sslcert = os.getenv('PGSSLCERT')
502+
if sslcrl is None:
503+
sslcrl = os.getenv('PGSSLCRL')
504+
if sslcrl:
505+
ssl.load_verify_locations(cafile=sslcrl)
506+
ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN
507+
else:
508+
sslcrl = os.path.expanduser('~/.postgresql/root.crl')
509+
try:
510+
ssl.load_verify_locations(cafile=sslcrl)
511+
except FileNotFoundError:
512+
pass
513+
else:
514+
ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN
463515

464516
if sslkey is None:
465517
sslkey = os.getenv('PGSSLKEY')
466-
467-
if sslrootcert is None:
468-
sslrootcert = os.getenv('PGSSLROOTCERT')
469-
470-
if sslcrl is None:
471-
sslcrl = os.getenv('PGSSLCRL')
472-
518+
if not sslkey:
519+
sslkey = os.path.expanduser('~/.postgresql/postgresql.key')
520+
if not os.path.exists(sslkey):
521+
sslkey = None
522+
if not sslpassword:
523+
sslpassword = ''
524+
if sslcert is None:
525+
sslcert = os.getenv('PGSSLCERT')
473526
if sslcert:
474-
ssl.load_cert_chain(sslcert, keyfile=sslkey)
475-
476-
if sslrootcert:
477-
ssl.load_verify_locations(cafile=sslrootcert)
478-
479-
if sslcrl:
480-
ssl.load_verify_locations(cafile=sslcrl)
481-
ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN
527+
ssl.load_cert_chain(
528+
sslcert, keyfile=sslkey, password=lambda: sslpassword
529+
)
530+
else:
531+
sslcert = os.path.expanduser('~/.postgresql/postgresql.crt')
532+
try:
533+
ssl.load_cert_chain(
534+
sslcert, keyfile=sslkey, password=lambda: sslpassword
535+
)
536+
except FileNotFoundError:
537+
pass
538+
539+
# OpenSSL 1.1.1 keylog file, copied from create_default_context()
540+
if hasattr(ssl, 'keylog_filename'):
541+
keylogfile = os.environ.get('SSLKEYLOGFILE')
542+
if keylogfile and not sys.flags.ignore_environment:
543+
ssl.keylog_filename = keylogfile
544+
545+
if sslcompression is None:
546+
sslcompression = os.getenv('PGSSLCOMPRESSION')
547+
if sslcompression == '1':
548+
ssl.verify_flags ^= ssl_module.OP_NO_COMPRESSION
549+
550+
if ssl_min_protocol_version is None:
551+
ssl_min_protocol_version = os.getenv(
552+
'PGSSLMINPROTOCOLVERSION', 'TLSv1.2'
553+
)
554+
if ssl_min_protocol_version:
555+
ssl.minimum_version = _parse_tls_version(
556+
ssl_min_protocol_version
557+
)
558+
559+
if ssl_max_protocol_version is None:
560+
ssl_max_protocol_version = os.getenv('PGSSLMAXPROTOCOLVERSION')
561+
if ssl_max_protocol_version:
562+
ssl.maximum_version = _parse_tls_version(
563+
ssl_max_protocol_version
564+
)
482565

483566
elif ssl is True:
484567
ssl = ssl_module.create_default_context()

asyncpg/connection.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,20 @@ async def connect(dsn=None, *,
20202020
The ``sslcert``, ``sslkey``, ``sslrootcert``, and ``sslcrl`` options
20212021
are supported in the *dsn* argument.
20222022
2023+
.. versionchanged:: 0.25.0
2024+
The ``sslpassword``, ``sslcompression``, ``ssl_min_protocol_version``,
2025+
and ``ssl_max_protocol_version`` options are supported in the *dsn*
2026+
argument.
2027+
2028+
.. versionchanged:: 0.25.0
2029+
Default system root CA certificates won't be loaded when specifying a
2030+
particular sslmode, following the same behavior in libpq.
2031+
2032+
.. versionchanged:: 0.25.0
2033+
The ``sslcert``, ``sslkey``, ``sslrootcert``, and ``sslcrl`` options
2034+
in the *dsn* argument now have consistent default values of files under
2035+
``~/.postgresql/`` as libpq.
2036+
20232037
.. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
20242038
.. _create_default_context:
20252039
https://docs.python.org/3/library/ssl.html#ssl.create_default_context

tests/certs/client.key.protected.pem

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
Proc-Type: 4,ENCRYPTED
3+
DEK-Info: AES-256-CBC,B222CD7D00828606A07DBC489D400921
4+
5+
LRHsNGUsD5bG9+x/1UlzImN0rqEF10sFPBmxKeQpXQ/hy4iR+X/Gagoyagi23wOn
6+
EZf0sCLJx95ixG+4fXJDX0jgBtqeziVNS4FLWHIuf3+blja8nf4tkmmH9pF8jFQ0
7+
i1an3TP6KRyDKa17gioOdtSsS51BZmPkp3MByJQsrMhyB0txEUsGtUMaBTYmVN/5
8+
uYHf9MsmfcfQy30nt2t6St6W82QupHHMOx5xyhPJo8cqQncZC7Dwo4hyDV3h3vWn
9+
UjaRZiEMmQ3IgCwfJd1VmMECvrwXd/sTOXNhofWwDQIqmQ3GGWdrRnmgD863BQT3
10+
V8RVyPLkutOnrZ/kiMSAuiXGsSYK0TV8F9TaP/abLob4P8jbKYLcuR7ws3cu1xBl
11+
XWt9RALxGPUyHIy+BWLXJTYL8T+TVJpiKsAGCQB54j8VQBSArwFL4LnzdUu1txe2
12+
qa6ZEwt4q6SEwOTJpJWz3oJ1j+OTsRCN+4dlyo7sEZMeyTRp9nUzwulhd+fOdIhY
13+
2UllMG71opKfNxZzEW7lq6E/waf0MmxwjUJmgwVO218yag9oknHnoFwewF42DGY7
14+
072h23EJeKla7sI+MAB18z01z6C/yHWXLybOlXaGqk6zOm3OvTUFnUXtKzlBO2v3
15+
FQwrOE5U/VEyQkNWzHzh4j4LxYEL9/B08PxaveUwvNVGn9I3YknE6uMfcU7VuxDq
16+
+6bgM6r+ez+9QLFSjH/gQuPs2DKX0h3b9ppQNx+MANX0DEGbGabJiBp887f8pG6Q
17+
tW0i0+rfzYz3JwnwIuMZjYz6qUlP4bJMEmmDfod3fbnvg3MoCSMTUvi1Tq3Iiv4L
18+
GM5/YNkL0V3PhOI686aBfU7GLGXQFhdbQ9xrSoQRBmmNBqTCSf+iIEoTxlBac8GQ
19+
vSzDO+A+ovBP36K13Yn7gzuN/3PLZXH2TZ8t2b/OkEXOciH5KbycGHQA7gqxX1P4
20+
J55gpqPAWe8e7wKheWj3BMfmbWuH4rpiEkrLpqbTSfTwIKqplk253chmJj5I82XI
21+
ioFLS5vCi9JJsTrQ720O+VQPVB5xeA80WL8NxamWQb/KkvVnb4dTmaV30RCgLLZC
22+
tuMx8YSW71ALLT15qFB2zlMDKZO1jjunNE71BUFBPIkTKEOCyMAiF60fFeIWezxy
23+
kvBBOg7+MTcZNeW110FqRWNGr2A5KYFN15g+YVpfEoF26slHisSjVW5ndzGh0kaQ
24+
sIOjQitA9JYoLua7sHvsr6H5KdCGjNxv7O7y8wLGBVApRhU0wxZtbClqqEUvCLLP
25+
UiLDp9L34wDL7sGrfNgWA4UuN29XQzTxI5kbv/EPKhyt2oVHLqUiE+eGyvnuYm+X
26+
KqFi016nQaxTU5Kr8Pl0pSHbJMLFDWLSpsbbTB6YJpdEGxJoj3JB3VncOpwcuK+G
27+
xZ1tV2orPt1s/6m+/ihzRgoEkyLwcLRPN7ojgD/sqS679ZGf1IkDMgFCQe4g0UWm
28+
Fw7v816MNCgypUM5hQaU+Jp8vSlEc29RbrdSHbcxrKj/xPCLWrAbvmI5tgonKmuJ
29+
J1LW8AXyh/EUp/uUh++jqVGx+8pFfcmJw6V6JrJzQ7HMlakkry7N1eAGrIJGtYCW
30+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)