Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SSL compatibility of libpq #827

Merged
merged 6 commits into from Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
167 changes: 133 additions & 34 deletions asyncpg/connect_utils.py
Expand Up @@ -18,6 +18,7 @@
import ssl as ssl_module
import stat
import struct
import sys
import time
import typing
import urllib.parse
Expand Down Expand Up @@ -220,13 +221,35 @@ def _parse_hostlist(hostlist, port, *, unquote=False):
return hosts, port


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}"
)
try:
return ssl_module.TLSVersion[tls_version.replace('.', '_')]
except KeyError:
raise ValueError(
f"No such 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):
# `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)
Expand Down Expand Up @@ -312,24 +335,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:
fantix marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down Expand Up @@ -451,34 +482,102 @@ 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 = _dot_postgresql_path('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 = _dot_postgresql_path('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 = _dot_postgresql_path('postgresql.key')
if not sslkey.exists():
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 = _dot_postgresql_path('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')
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')
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()
Expand Down
14 changes: 14 additions & 0 deletions asyncpg/connection.py
Expand Up @@ -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``,
fantix marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
66 changes: 33 additions & 33 deletions 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-----
19 changes: 19 additions & 0 deletions 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-----
51 changes: 51 additions & 0 deletions tests/certs/ca.key.pem
@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----