Skip to content

Commit 2fbb0ab

Browse files
Hugo Osvaldo BarreraWhyNotHugo
authored andcommitted
Clean up some invalid TLS configuration branches
1 parent 60352f8 commit 2fbb0ab

File tree

8 files changed

+88
-86
lines changed

8 files changed

+88
-86
lines changed

CHANGELOG.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ Version 0.19.0
2626
- A new ``asyncio`` backend is now used. So far, this shows substantial speed
2727
improvements in ``discovery`` and ``metasync``, but little change in `sync`.
2828
This will likely continue improving over time. :gh:`906`
29-
- Support for `md5` and `sha1` certificate fingerprints has been dropped. If
30-
you're validating certificate fingerprints, use `sha256` instead.
3129
- The ``google`` storage types no longer require ``requests-oauthlib``, but
3230
require ``python-aiohttp-oauthlib`` instead.
3331
- Vdirsyncer no longer includes experimental support for `EteSync
@@ -41,6 +39,22 @@ Version 0.19.0
4139

4240
.. _etesync-dav: https://github.com/etesync/etesync-dav
4341

42+
Changes to SSL configuration
43+
----------------------------
44+
45+
Support for `md5` and `sha1` certificate fingerprints has been dropped. If
46+
you're validating certificate fingerprints, use `sha256` instead.
47+
48+
# XXX: just make it one arg
49+
50+
When using a custom `verify_fingerprint`, CA validation is always disabled.
51+
52+
If `verify_fingerprint` is unset, CA verification is always active. Disabling
53+
both features is insecure and no longer supported.
54+
55+
The `verify` parameter no longer takes boolean values, it is now optional and
56+
only takes a string to a custom CA for verification.
57+
4458
Version 0.18.0
4559
==============
4660

docs/config.rst

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ CalDAV and CardDAV
175175
url = "..."
176176
#username = ""
177177
#password = ""
178-
#verify = true
178+
#verify = /path/to/custom_ca.pem
179179
#auth = null
180180
#useragent = "vdirsyncer/0.16.4"
181181
#verify_fingerprint = null
@@ -208,12 +208,10 @@ CalDAV and CardDAV
208208
:param url: Base URL or an URL to a calendar.
209209
:param username: Username for authentication.
210210
:param password: Password for authentication.
211-
:param verify: Verify SSL certificate, default True. This can also be a
212-
local path to a self-signed SSL certificate. See :ref:`ssl-tutorial`
213-
for more information.
214-
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the
215-
expected server certificate. See :ref:`ssl-tutorial` for more
216-
information.
211+
:param verify: Optional. Local path to a self-signed SSL certificate.
212+
See :ref:`ssl-tutorial` for more information.
213+
:param verify_fingerprint: Optional. SHA256 fingerprint of the expected
214+
server certificate. See :ref:`ssl-tutorial` for more information.
217215
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
218216
default is preemptive Basic auth, sending credentials even if server
219217
didn't request them. This saves from an additional roundtrip per
@@ -235,7 +233,7 @@ CalDAV and CardDAV
235233
url = "..."
236234
#username = ""
237235
#password = ""
238-
#verify = true
236+
#verify = /path/to/custom_ca.pem
239237
#auth = null
240238
#useragent = "vdirsyncer/0.16.4"
241239
#verify_fingerprint = null
@@ -244,12 +242,10 @@ CalDAV and CardDAV
244242
:param url: Base URL or an URL to an addressbook.
245243
:param username: Username for authentication.
246244
:param password: Password for authentication.
247-
:param verify: Verify SSL certificate, default True. This can also be a
248-
local path to a self-signed SSL certificate. See
249-
:ref:`ssl-tutorial` for more information.
250-
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the expected
251-
server certificate. See :ref:`ssl-tutorial` for
252-
more information.
245+
:param verify: Optional. Local path to a self-signed SSL certificate.
246+
See :ref:`ssl-tutorial` for more information.
247+
:param verify_fingerprint: Optional. SHA256 fingerprint of the expected
248+
server certificate. See :ref:`ssl-tutorial` for more information.
253249
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
254250
default is preemptive Basic auth, sending credentials even if
255251
server didn't request them. This saves from an additional
@@ -478,12 +474,10 @@ leads to an error.
478474
:param url: URL to the ``.ics`` file.
479475
:param username: Username for authentication.
480476
:param password: Password for authentication.
481-
:param verify: Verify SSL certificate, default True. This can also be a
482-
local path to a self-signed SSL certificate. See :ref:`ssl-tutorial`
483-
for more information.
484-
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the
485-
expected server certificate. See :ref:`ssl-tutorial` for more
486-
information.
477+
:param verify: Optional. Local path to a self-signed SSL certificate.
478+
See :ref:`ssl-tutorial` for more information.
479+
:param verify_fingerprint: Optional. SHA256 fingerprint of the expected
480+
server certificate. See :ref:`ssl-tutorial` for more information.
487481
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
488482
default is preemptive Basic auth, sending credentials even if server
489483
didn't request them. This saves from an additional roundtrip per

docs/ssl-tutorial.rst

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,14 @@ To pin the certificate by fingerprint::
1515
type = "caldav"
1616
...
1717
verify_fingerprint = "94:FD:7A:CB:50:75:A4:69:82:0A:F8:23:DF:07:FC:69:3E:CD:90:CA"
18-
#verify = false # Optional: Disable CA validation, useful for self-signed certs
1918

20-
SHA1-, SHA256- or MD5-Fingerprints can be used. They're detected by their
21-
length.
19+
SHA256-Fingerprints can be used. CA validation is disabled when pinning a
20+
fingerprint.
2221

2322
You can use the following command for obtaining a SHA-1 fingerprint::
2423

2524
echo -n | openssl s_client -connect unterwaditzer.net:443 | openssl x509 -noout -fingerprint
2625

27-
Note that ``verify_fingerprint`` doesn't suffice for vdirsyncer to work with
28-
self-signed certificates (or certificates that are not in your trust store). You
29-
most likely need to set ``verify = false`` as well. This disables verification
30-
of the SSL certificate's expiration time and the existence of it in your trust
31-
store, all that's verified now is the fingerprint.
32-
3326
However, please consider using `Let's Encrypt <https://letsencrypt.org/>`_ such
3427
that you can forget about all of that. It is easier to deploy a free
3528
certificate from them than configuring all of your clients to accept the

tests/storage/test_http.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,4 @@ def test_verify_false_disallowed(aio_connector):
126126
with pytest.raises(ValueError) as excinfo:
127127
HttpStorage(url="http://example.com", verify=False, connector=aio_connector)
128128

129-
assert "forbidden" in str(excinfo.value).lower()
130-
assert "consider setting verify_fingerprint" in str(excinfo.value).lower()
129+
assert "must be a path to a pem-file." in str(excinfo.value).lower()

tests/system/utils/test_main.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ async def test_request_ssl():
3535
)
3636
assert "certificate verify failed" in str(excinfo.value)
3737

38-
await http.request(
39-
"GET",
40-
"https://self-signed.badssl.com/",
41-
verify=False,
42-
session=session,
43-
)
38+
# XXX FIXME
39+
40+
with pytest.raises(Exception):
41+
await http.request(
42+
"GET",
43+
"https://self-signed.badssl.com/",
44+
verify=False,
45+
session=session,
46+
)
4447

4548

4649
def fingerprint_of_cert(cert, hash=hashes.SHA256) -> str:
@@ -62,10 +65,12 @@ async def test_request_ssl_leaf_fingerprint(
6265
httpserver.expect_request("/").respond_with_data("OK")
6366
url = f"https://127.0.0.1:{httpserver.port}/"
6467

65-
await http.request("GET", url, verify_fingerprint=fingerprint, session=aio_session)
68+
ssl = http.prepare_verify(None, fingerprint)
69+
await http.request("GET", url, ssl=ssl, session=aio_session)
6670

71+
ssl = http.prepare_verify(None, bogus)
6772
with pytest.raises(aiohttp.ServerFingerprintMismatch):
68-
await http.request("GET", url, verify_fingerprint=bogus, session=aio_session)
73+
await http.request("GET", url, ssl=ssl, session=aio_session)
6974

7075

7176
@pytest.mark.xfail(reason="Not implemented")

vdirsyncer/http.py

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from ssl import create_default_context
23

34
import aiohttp
45

@@ -64,30 +65,23 @@ def prepare_auth(auth, username, password):
6465

6566

6667
def prepare_verify(verify, verify_fingerprint):
67-
if isinstance(verify, (str, bytes)):
68-
verify = expand_path(verify)
69-
elif not isinstance(verify, bool):
68+
if isinstance(verify, str):
69+
return create_default_context(cafile=expand_path(verify))
70+
elif verify is not None:
7071
raise exceptions.UserError(
71-
"Invalid value for verify ({}), "
72-
"must be a path to a PEM-file or boolean.".format(verify)
72+
f"Invalid value for verify ({verify}), must be a path to a PEM-file."
7373
)
7474

7575
if verify_fingerprint is not None:
76-
if not isinstance(verify_fingerprint, (bytes, str)):
76+
if not isinstance(verify_fingerprint, str):
7777
raise exceptions.UserError(
7878
"Invalid value for verify_fingerprint "
79-
"({}), must be a string or null.".format(verify_fingerprint)
79+
f"({verify_fingerprint}), must be a string."
8080
)
81-
elif not verify:
82-
raise exceptions.UserError(
83-
"Disabling all SSL validation is forbidden. Consider setting "
84-
"verify_fingerprint if you have a broken or self-signed cert."
85-
)
8681

87-
return {
88-
"verify": verify,
89-
"verify_fingerprint": verify_fingerprint,
90-
}
82+
return aiohttp.Fingerprint(bytes.fromhex(verify_fingerprint.replace(":", "")))
83+
84+
return None
9185

9286

9387
def prepare_client_cert(cert):
@@ -99,15 +93,18 @@ def prepare_client_cert(cert):
9993

10094

10195
async def request(
102-
method, url, session, latin1_fallback=True, verify_fingerprint=None, **kwargs
96+
method,
97+
url,
98+
session,
99+
latin1_fallback=True,
100+
**kwargs,
103101
):
104-
"""
105-
Wrapper method for requests, to ease logging and mocking. Parameters should
106-
be the same as for ``requests.request``, except:
102+
"""Wrapper method for requests, to ease logging and mocking.
103+
104+
Parameters should be the same as for ``aiohttp.request``, as well as:
107105
108106
:param session: A requests session object to use.
109-
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the
110-
expected server certificate.
107+
:param verify_fingerprint: Optional. SHA256 of the expected server certificate.
111108
:param latin1_fallback: RFC-2616 specifies the default Content-Type of
112109
text/* to be latin1, which is not always correct, but exactly what
113110
requests is doing. Setting this parameter to False will use charset
@@ -116,13 +113,7 @@ async def request(
116113
https://github.com/kennethreitz/requests/issues/2042
117114
"""
118115

119-
if verify_fingerprint is not None:
120-
ssl = aiohttp.Fingerprint(bytes.fromhex(verify_fingerprint.replace(":", "")))
121-
kwargs.pop("verify", None)
122-
elif kwargs.pop("verify", None) is False:
123-
ssl = False
124-
else:
125-
ssl = None # TODO XXX: Check all possible values for this
116+
# TODO: Support for client-side certifications.
126117

127118
session.hooks = {"response": _fix_redirects}
128119

@@ -138,29 +129,29 @@ async def request(
138129

139130
kwargs.pop("cert", None) # TODO XXX FIXME!
140131

141-
r = await session.request(method, url, ssl=ssl, **kwargs)
132+
response = await session.request(method, url, **kwargs)
142133

143134
# See https://github.com/kennethreitz/requests/issues/2042
144-
content_type = r.headers.get("Content-Type", "")
135+
content_type = response.headers.get("Content-Type", "")
145136
if (
146137
not latin1_fallback
147138
and "charset" not in content_type
148139
and content_type.startswith("text/")
149140
):
150141
logger.debug("Removing latin1 fallback")
151-
r.encoding = None
142+
response.encoding = None
152143

153-
logger.debug(r.status)
154-
logger.debug(r.headers)
155-
logger.debug(r.content)
144+
logger.debug(response.status)
145+
logger.debug(response.headers)
146+
logger.debug(response.content)
156147

157-
if r.status == 412:
158-
raise exceptions.PreconditionFailed(r.reason)
159-
if r.status in (404, 410):
160-
raise exceptions.NotFoundError(r.reason)
148+
if response.status == 412:
149+
raise exceptions.PreconditionFailed(response.reason)
150+
if response.status in (404, 410):
151+
raise exceptions.NotFoundError(response.reason)
161152

162-
r.raise_for_status()
163-
return r
153+
response.raise_for_status()
154+
return response
164155

165156

166157
def _fix_redirects(r, *args, **kwargs):

vdirsyncer/storage/dav.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ def __init__(
375375
url,
376376
username="",
377377
password="",
378-
verify=True,
378+
verify=None,
379379
auth=None,
380380
useragent=USERAGENT,
381381
verify_fingerprint=None,
@@ -389,7 +389,10 @@ def __init__(
389389
auth = prepare_auth(auth, username, password)
390390
if auth:
391391
self._settings["auth"] = auth
392-
self._settings.update(prepare_verify(verify, verify_fingerprint))
392+
393+
ssl = prepare_verify(verify, verify_fingerprint)
394+
if ssl:
395+
self._settings["ssl"] = ssl
393396

394397
self.useragent = useragent
395398
self.url = url.rstrip("/") + "/"

vdirsyncer/storage/http.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(
2727
url,
2828
username="",
2929
password="",
30-
verify=True,
30+
verify=None,
3131
auth=None,
3232
useragent=USERAGENT,
3333
verify_fingerprint=None,
@@ -45,7 +45,10 @@ def __init__(
4545
auth = prepare_auth(auth, username, password)
4646
if auth:
4747
self._settings["auth"] = auth
48-
self._settings.update(prepare_verify(verify, verify_fingerprint))
48+
49+
ssl = prepare_verify(verify, verify_fingerprint)
50+
if ssl:
51+
self._settings["ssl"] = ssl
4952

5053
self.username, self.password = username, password
5154
self.useragent = useragent

0 commit comments

Comments
 (0)