Clean up some invalid TLS configuration branches

This commit is contained in:
Hugo Osvaldo Barrera 2022-01-07 17:56:06 +01:00 committed by Hugo Osvaldo Barrera
parent 60352f84fe
commit 2fbb0ab7a5
8 changed files with 88 additions and 86 deletions

View file

@ -26,8 +26,6 @@ Version 0.19.0
- A new ``asyncio`` backend is now used. So far, this shows substantial speed - A new ``asyncio`` backend is now used. So far, this shows substantial speed
improvements in ``discovery`` and ``metasync``, but little change in `sync`. improvements in ``discovery`` and ``metasync``, but little change in `sync`.
This will likely continue improving over time. :gh:`906` This will likely continue improving over time. :gh:`906`
- Support for `md5` and `sha1` certificate fingerprints has been dropped. If
you're validating certificate fingerprints, use `sha256` instead.
- The ``google`` storage types no longer require ``requests-oauthlib``, but - The ``google`` storage types no longer require ``requests-oauthlib``, but
require ``python-aiohttp-oauthlib`` instead. require ``python-aiohttp-oauthlib`` instead.
- Vdirsyncer no longer includes experimental support for `EteSync - Vdirsyncer no longer includes experimental support for `EteSync
@ -41,6 +39,22 @@ Version 0.19.0
.. _etesync-dav: https://github.com/etesync/etesync-dav .. _etesync-dav: https://github.com/etesync/etesync-dav
Changes to SSL configuration
----------------------------
Support for `md5` and `sha1` certificate fingerprints has been dropped. If
you're validating certificate fingerprints, use `sha256` instead.
# XXX: just make it one arg
When using a custom `verify_fingerprint`, CA validation is always disabled.
If `verify_fingerprint` is unset, CA verification is always active. Disabling
both features is insecure and no longer supported.
The `verify` parameter no longer takes boolean values, it is now optional and
only takes a string to a custom CA for verification.
Version 0.18.0 Version 0.18.0
============== ==============

View file

@ -175,7 +175,7 @@ CalDAV and CardDAV
url = "..." url = "..."
#username = "" #username = ""
#password = "" #password = ""
#verify = true #verify = /path/to/custom_ca.pem
#auth = null #auth = null
#useragent = "vdirsyncer/0.16.4" #useragent = "vdirsyncer/0.16.4"
#verify_fingerprint = null #verify_fingerprint = null
@ -208,12 +208,10 @@ CalDAV and CardDAV
:param url: Base URL or an URL to a calendar. :param url: Base URL or an URL to a calendar.
:param username: Username for authentication. :param username: Username for authentication.
:param password: Password for authentication. :param password: Password for authentication.
:param verify: Verify SSL certificate, default True. This can also be a :param verify: Optional. Local path to a self-signed SSL certificate.
local path to a self-signed SSL certificate. See :ref:`ssl-tutorial` See :ref:`ssl-tutorial` for more information.
for more information. :param verify_fingerprint: Optional. SHA256 fingerprint of the expected
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the server certificate. See :ref:`ssl-tutorial` for more information.
expected server certificate. See :ref:`ssl-tutorial` for more
information.
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The :param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
default is preemptive Basic auth, sending credentials even if server default is preemptive Basic auth, sending credentials even if server
didn't request them. This saves from an additional roundtrip per didn't request them. This saves from an additional roundtrip per
@ -235,7 +233,7 @@ CalDAV and CardDAV
url = "..." url = "..."
#username = "" #username = ""
#password = "" #password = ""
#verify = true #verify = /path/to/custom_ca.pem
#auth = null #auth = null
#useragent = "vdirsyncer/0.16.4" #useragent = "vdirsyncer/0.16.4"
#verify_fingerprint = null #verify_fingerprint = null
@ -244,12 +242,10 @@ CalDAV and CardDAV
:param url: Base URL or an URL to an addressbook. :param url: Base URL or an URL to an addressbook.
:param username: Username for authentication. :param username: Username for authentication.
:param password: Password for authentication. :param password: Password for authentication.
:param verify: Verify SSL certificate, default True. This can also be a :param verify: Optional. Local path to a self-signed SSL certificate.
local path to a self-signed SSL certificate. See See :ref:`ssl-tutorial` for more information.
:ref:`ssl-tutorial` for more information. :param verify_fingerprint: Optional. SHA256 fingerprint of the expected
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the expected server certificate. See :ref:`ssl-tutorial` for more information.
server certificate. See :ref:`ssl-tutorial` for
more information.
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The :param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
default is preemptive Basic auth, sending credentials even if default is preemptive Basic auth, sending credentials even if
server didn't request them. This saves from an additional server didn't request them. This saves from an additional
@ -478,12 +474,10 @@ leads to an error.
:param url: URL to the ``.ics`` file. :param url: URL to the ``.ics`` file.
:param username: Username for authentication. :param username: Username for authentication.
:param password: Password for authentication. :param password: Password for authentication.
:param verify: Verify SSL certificate, default True. This can also be a :param verify: Optional. Local path to a self-signed SSL certificate.
local path to a self-signed SSL certificate. See :ref:`ssl-tutorial` See :ref:`ssl-tutorial` for more information.
for more information. :param verify_fingerprint: Optional. SHA256 fingerprint of the expected
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the server certificate. See :ref:`ssl-tutorial` for more information.
expected server certificate. See :ref:`ssl-tutorial` for more
information.
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The :param auth: Optional. Either ``basic``, ``digest`` or ``guess``. The
default is preemptive Basic auth, sending credentials even if server default is preemptive Basic auth, sending credentials even if server
didn't request them. This saves from an additional roundtrip per didn't request them. This saves from an additional roundtrip per

View file

@ -15,21 +15,14 @@ To pin the certificate by fingerprint::
type = "caldav" type = "caldav"
... ...
verify_fingerprint = "94:FD:7A:CB:50:75:A4:69:82:0A:F8:23:DF:07:FC:69:3E:CD:90:CA" verify_fingerprint = "94:FD:7A:CB:50:75:A4:69:82:0A:F8:23:DF:07:FC:69:3E:CD:90:CA"
#verify = false # Optional: Disable CA validation, useful for self-signed certs
SHA1-, SHA256- or MD5-Fingerprints can be used. They're detected by their SHA256-Fingerprints can be used. CA validation is disabled when pinning a
length. fingerprint.
You can use the following command for obtaining a SHA-1 fingerprint:: You can use the following command for obtaining a SHA-1 fingerprint::
echo -n | openssl s_client -connect unterwaditzer.net:443 | openssl x509 -noout -fingerprint echo -n | openssl s_client -connect unterwaditzer.net:443 | openssl x509 -noout -fingerprint
Note that ``verify_fingerprint`` doesn't suffice for vdirsyncer to work with
self-signed certificates (or certificates that are not in your trust store). You
most likely need to set ``verify = false`` as well. This disables verification
of the SSL certificate's expiration time and the existence of it in your trust
store, all that's verified now is the fingerprint.
However, please consider using `Let's Encrypt <https://letsencrypt.org/>`_ such However, please consider using `Let's Encrypt <https://letsencrypt.org/>`_ such
that you can forget about all of that. It is easier to deploy a free that you can forget about all of that. It is easier to deploy a free
certificate from them than configuring all of your clients to accept the certificate from them than configuring all of your clients to accept the

View file

@ -126,5 +126,4 @@ def test_verify_false_disallowed(aio_connector):
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
HttpStorage(url="http://example.com", verify=False, connector=aio_connector) HttpStorage(url="http://example.com", verify=False, connector=aio_connector)
assert "forbidden" in str(excinfo.value).lower() assert "must be a path to a pem-file." in str(excinfo.value).lower()
assert "consider setting verify_fingerprint" in str(excinfo.value).lower()

View file

@ -35,6 +35,9 @@ async def test_request_ssl():
) )
assert "certificate verify failed" in str(excinfo.value) assert "certificate verify failed" in str(excinfo.value)
# XXX FIXME
with pytest.raises(Exception):
await http.request( await http.request(
"GET", "GET",
"https://self-signed.badssl.com/", "https://self-signed.badssl.com/",
@ -62,10 +65,12 @@ async def test_request_ssl_leaf_fingerprint(
httpserver.expect_request("/").respond_with_data("OK") httpserver.expect_request("/").respond_with_data("OK")
url = f"https://127.0.0.1:{httpserver.port}/" url = f"https://127.0.0.1:{httpserver.port}/"
await http.request("GET", url, verify_fingerprint=fingerprint, session=aio_session) ssl = http.prepare_verify(None, fingerprint)
await http.request("GET", url, ssl=ssl, session=aio_session)
ssl = http.prepare_verify(None, bogus)
with pytest.raises(aiohttp.ServerFingerprintMismatch): with pytest.raises(aiohttp.ServerFingerprintMismatch):
await http.request("GET", url, verify_fingerprint=bogus, session=aio_session) await http.request("GET", url, ssl=ssl, session=aio_session)
@pytest.mark.xfail(reason="Not implemented") @pytest.mark.xfail(reason="Not implemented")

View file

@ -1,4 +1,5 @@
import logging import logging
from ssl import create_default_context
import aiohttp import aiohttp
@ -64,30 +65,23 @@ def prepare_auth(auth, username, password):
def prepare_verify(verify, verify_fingerprint): def prepare_verify(verify, verify_fingerprint):
if isinstance(verify, (str, bytes)): if isinstance(verify, str):
verify = expand_path(verify) return create_default_context(cafile=expand_path(verify))
elif not isinstance(verify, bool): elif verify is not None:
raise exceptions.UserError( raise exceptions.UserError(
"Invalid value for verify ({}), " f"Invalid value for verify ({verify}), must be a path to a PEM-file."
"must be a path to a PEM-file or boolean.".format(verify)
) )
if verify_fingerprint is not None: if verify_fingerprint is not None:
if not isinstance(verify_fingerprint, (bytes, str)): if not isinstance(verify_fingerprint, str):
raise exceptions.UserError( raise exceptions.UserError(
"Invalid value for verify_fingerprint " "Invalid value for verify_fingerprint "
"({}), must be a string or null.".format(verify_fingerprint) f"({verify_fingerprint}), must be a string."
)
elif not verify:
raise exceptions.UserError(
"Disabling all SSL validation is forbidden. Consider setting "
"verify_fingerprint if you have a broken or self-signed cert."
) )
return { return aiohttp.Fingerprint(bytes.fromhex(verify_fingerprint.replace(":", "")))
"verify": verify,
"verify_fingerprint": verify_fingerprint, return None
}
def prepare_client_cert(cert): def prepare_client_cert(cert):
@ -99,15 +93,18 @@ def prepare_client_cert(cert):
async def request( async def request(
method, url, session, latin1_fallback=True, verify_fingerprint=None, **kwargs method,
url,
session,
latin1_fallback=True,
**kwargs,
): ):
""" """Wrapper method for requests, to ease logging and mocking.
Wrapper method for requests, to ease logging and mocking. Parameters should
be the same as for ``requests.request``, except: Parameters should be the same as for ``aiohttp.request``, as well as:
:param session: A requests session object to use. :param session: A requests session object to use.
:param verify_fingerprint: Optional. SHA1 or MD5 fingerprint of the :param verify_fingerprint: Optional. SHA256 of the expected server certificate.
expected server certificate.
:param latin1_fallback: RFC-2616 specifies the default Content-Type of :param latin1_fallback: RFC-2616 specifies the default Content-Type of
text/* to be latin1, which is not always correct, but exactly what text/* to be latin1, which is not always correct, but exactly what
requests is doing. Setting this parameter to False will use charset requests is doing. Setting this parameter to False will use charset
@ -116,13 +113,7 @@ async def request(
https://github.com/kennethreitz/requests/issues/2042 https://github.com/kennethreitz/requests/issues/2042
""" """
if verify_fingerprint is not None: # TODO: Support for client-side certifications.
ssl = aiohttp.Fingerprint(bytes.fromhex(verify_fingerprint.replace(":", "")))
kwargs.pop("verify", None)
elif kwargs.pop("verify", None) is False:
ssl = False
else:
ssl = None # TODO XXX: Check all possible values for this
session.hooks = {"response": _fix_redirects} session.hooks = {"response": _fix_redirects}
@ -138,29 +129,29 @@ async def request(
kwargs.pop("cert", None) # TODO XXX FIXME! kwargs.pop("cert", None) # TODO XXX FIXME!
r = await session.request(method, url, ssl=ssl, **kwargs) response = await session.request(method, url, **kwargs)
# See https://github.com/kennethreitz/requests/issues/2042 # See https://github.com/kennethreitz/requests/issues/2042
content_type = r.headers.get("Content-Type", "") content_type = response.headers.get("Content-Type", "")
if ( if (
not latin1_fallback not latin1_fallback
and "charset" not in content_type and "charset" not in content_type
and content_type.startswith("text/") and content_type.startswith("text/")
): ):
logger.debug("Removing latin1 fallback") logger.debug("Removing latin1 fallback")
r.encoding = None response.encoding = None
logger.debug(r.status) logger.debug(response.status)
logger.debug(r.headers) logger.debug(response.headers)
logger.debug(r.content) logger.debug(response.content)
if r.status == 412: if response.status == 412:
raise exceptions.PreconditionFailed(r.reason) raise exceptions.PreconditionFailed(response.reason)
if r.status in (404, 410): if response.status in (404, 410):
raise exceptions.NotFoundError(r.reason) raise exceptions.NotFoundError(response.reason)
r.raise_for_status() response.raise_for_status()
return r return response
def _fix_redirects(r, *args, **kwargs): def _fix_redirects(r, *args, **kwargs):

View file

@ -375,7 +375,7 @@ class DAVSession:
url, url,
username="", username="",
password="", password="",
verify=True, verify=None,
auth=None, auth=None,
useragent=USERAGENT, useragent=USERAGENT,
verify_fingerprint=None, verify_fingerprint=None,
@ -389,7 +389,10 @@ class DAVSession:
auth = prepare_auth(auth, username, password) auth = prepare_auth(auth, username, password)
if auth: if auth:
self._settings["auth"] = auth self._settings["auth"] = auth
self._settings.update(prepare_verify(verify, verify_fingerprint))
ssl = prepare_verify(verify, verify_fingerprint)
if ssl:
self._settings["ssl"] = ssl
self.useragent = useragent self.useragent = useragent
self.url = url.rstrip("/") + "/" self.url = url.rstrip("/") + "/"

View file

@ -27,7 +27,7 @@ class HttpStorage(Storage):
url, url,
username="", username="",
password="", password="",
verify=True, verify=None,
auth=None, auth=None,
useragent=USERAGENT, useragent=USERAGENT,
verify_fingerprint=None, verify_fingerprint=None,
@ -45,7 +45,10 @@ class HttpStorage(Storage):
auth = prepare_auth(auth, username, password) auth = prepare_auth(auth, username, password)
if auth: if auth:
self._settings["auth"] = auth self._settings["auth"] = auth
self._settings.update(prepare_verify(verify, verify_fingerprint))
ssl = prepare_verify(verify, verify_fingerprint)
if ssl:
self._settings["ssl"] = ssl
self.username, self.password = username, password self.username, self.password = username, password
self.useragent = useragent self.useragent = useragent