Skip to content

Commit 96281b0

Browse files
committed
Fix SSL compatibility of libpq
1 parent 36658fa commit 96281b0

File tree

4 files changed

+264
-80
lines changed

4 files changed

+264
-80
lines changed

asyncpg/connect_utils.py

Lines changed: 121 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,32 @@ 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(
349+
'ssl_min_protocol_version'
350+
)
351+
352+
if 'ssl_max_protocol_version' in query:
353+
ssl_max_protocol_version = query.pop(
354+
'ssl_max_protocol_version'
355+
)
333356

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

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

464520
if sslkey is None:
465521
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-
522+
if not sslkey:
523+
sslkey = os.path.expanduser('~/.postgresql/postgresql.key')
524+
if not os.path.exists(sslkey):
525+
sslkey = None
526+
if not sslpassword:
527+
sslpassword = ''
528+
if sslcert is None:
529+
sslcert = os.getenv('PGSSLCERT')
473530
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
531+
ssl.load_cert_chain(
532+
sslcert, keyfile=sslkey, password=lambda: sslpassword
533+
)
534+
else:
535+
sslcert = os.path.expanduser('~/.postgresql/postgresql.crt')
536+
try:
537+
ssl.load_cert_chain(
538+
sslcert, keyfile=sslkey, password=lambda: sslpassword
539+
)
540+
except FileNotFoundError:
541+
pass
542+
543+
# OpenSSL 1.1.1 keylog file, copied from create_default_context()
544+
if hasattr(ssl, 'keylog_filename'):
545+
keylogfile = os.environ.get('SSLKEYLOGFILE')
546+
if keylogfile and not sys.flags.ignore_environment:
547+
ssl.keylog_filename = keylogfile
548+
549+
if sslcompression is None:
550+
sslcompression = os.getenv('PGSSLCOMPRESSION')
551+
if sslcompression == '1':
552+
ssl.options &= ~ssl_module.OP_NO_COMPRESSION
553+
554+
if ssl_min_protocol_version is None:
555+
ssl_min_protocol_version = os.getenv(
556+
'PGSSLMINPROTOCOLVERSION', 'TLSv1.2'
557+
)
558+
if ssl_min_protocol_version:
559+
ssl.minimum_version = _parse_tls_version(
560+
ssl_min_protocol_version
561+
)
562+
563+
if ssl_max_protocol_version is None:
564+
ssl_max_protocol_version = os.getenv('PGSSLMAXPROTOCOLVERSION')
565+
if ssl_max_protocol_version:
566+
ssl.maximum_version = _parse_tls_version(
567+
ssl_max_protocol_version
568+
)
482569

483570
elif ssl is True:
484571
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)