Repository: aio-libs/aiohttp

Issue:
Drop md5 and sha1 fingerprints
Should be done in 3.0


Discussion:
I can help with this
To extract signature hash algorithm we need to use either openssl or cryptography. Core library does not expose this information: https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.getpeercert

@alxpy @asvetlov ideas?

@alxpy are you still interested in this issue?
We use `getpeercert(binary_form=True)`, everything works fine for years
Sorry I misunderstood what exactly is needed. I thought you want to disallow client connections to https servers where md5|sha1 was used to sign certificate by CA.

So you want to disallow sha1|md5 while checking fingerprint as described here?

```python
# Attempt to connect to https://www.python.org
# with a pin to a bogus certificate:
bad_fingerprint = b'0'*64
exc = None
try:
    r = await session.get('https://www.python.org',
                          fingerprint=bad_fingerprint)
except aiohttp.FingerprintMismatch as e:
    exc = e
assert exc is not None
assert exc.expected == bad_fingerprint

# www.python.org cert's actual fingerprint
assert exc.got == b'...'
```

Looks like its enough to raise (instead of warning) something from aiohttp/client_exceptions.py in this piece:

https://github.com/aio-libs/aiohttp/blob/20362c52147fdb6de5839f0bb5275c77691e5f62/aiohttp/client_reqrep.py#L348

Should new exception be created? Like InsecureFingerprintHash(ClientSSLError)
I think `ValueError` is enough.
@redixin yes, I'm  interested in this issue, I'll do it on the weekend

Resolving patch:
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -350,11 +350,8 @@ def update_fingerprint(self, fingerprint):
             if not hashfunc:
                 raise ValueError('fingerprint has invalid length')
             elif hashfunc is md5 or hashfunc is sha1:
-                warnings.warn('md5 and sha1 are insecure and deprecated. '
-                              'Use sha256.',
-                              DeprecationWarning, stacklevel=2)
-                client_logger.warn('md5 and sha1 are insecure and deprecated. '
-                                   'Use sha256.')
+                raise ValueError('md5 and sha1 are insecure and '
+                                 'not supported. Use sha256.')
             self._hashfunc = hashfunc
         self._fingerprint = fingerprint
 
diff --git a/aiohttp/connector.py b/aiohttp/connector.py
--- a/aiohttp/connector.py
+++ b/aiohttp/connector.py
@@ -23,7 +23,6 @@
 from .client_reqrep import ClientRequest
 from .helpers import is_ip_address, noop, sentinel
 from .locks import EventResultOrError
-from .log import client_logger
 from .resolver import DefaultResolver
 
 
@@ -579,7 +578,7 @@ class TCPConnector(BaseConnector):
     """TCP connector.
 
     verify_ssl - Set to True to check ssl certifications.
-    fingerprint - Pass the binary md5, sha1, or sha256
+    fingerprint - Pass the binary sha256
         digest of the expected certificate in DER format to verify
         that the certificate the server presents matches. See also
         https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning
@@ -625,11 +624,8 @@ def __init__(self, *, verify_ssl=True, fingerprint=None,
             if not hashfunc:
                 raise ValueError('fingerprint has invalid length')
             elif hashfunc is md5 or hashfunc is sha1:
-                warnings.warn('md5 and sha1 are insecure and deprecated. '
-                              'Use sha256.',
-                              DeprecationWarning, stacklevel=2)
-                client_logger.warn('md5 and sha1 are insecure and deprecated. '
-                                   'Use sha256.')
+                raise ValueError('md5 and sha1 are insecure and '
+                                 'not supported. Use sha256.')
             self._hashfunc = hashfunc
         self._fingerprint = fingerprint
 


Test patch:
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -288,27 +288,18 @@ async def handler(request):
     assert txt == 'Test message'
 
 
-@pytest.mark.parametrize('fingerprint', [
-    b'\xa2\x06G\xad\xaa\xf5\xd8\\J\x99^by;\x06=',
-    b's\x93\xfd:\xed\x08\x1do\xa9\xaeq9\x1a\xe3\xc5\x7f\x89\xe7l\xf9',
-    b'0\x9a\xc9D\x83\xdc\x91\'\x88\x91\x11\xa1d\x97\xfd\xcb~7U\x14D@L'
-    b'\x11\xab\x99\xa8\xae\xb7\x14\xee\x8b'],
-    ids=['md5', 'sha1', 'sha256'])
 async def test_tcp_connector_fingerprint_ok(test_server, test_client,
-                                            loop, ssl_ctx, fingerprint):
+                                            loop, ssl_ctx):
+
+    fingerprint = (b'0\x9a\xc9D\x83\xdc\x91\'\x88\x91\x11\xa1d\x97\xfd'
+                   b'\xcb~7U\x14D@L'
+                   b'\x11\xab\x99\xa8\xae\xb7\x14\xee\x8b')
 
     async def handler(request):
         return web.HTTPOk(text='Test message')
 
-    # Test for deprecation warning on md5 and sha1 len digests.
-    if len(fingerprint) == 16 or len(fingerprint) == 20:
-        with pytest.warns(DeprecationWarning) as cm:
-            connector = aiohttp.TCPConnector(loop=loop, verify_ssl=False,
-                                             fingerprint=fingerprint)
-        assert 'Use sha256.' in str(cm[0].message)
-    else:
-        connector = aiohttp.TCPConnector(loop=loop, verify_ssl=False,
-                                         fingerprint=fingerprint)
+    connector = aiohttp.TCPConnector(loop=loop, verify_ssl=False,
+                                     fingerprint=fingerprint)
     app = web.Application()
     app.router.add_route('GET', '/', handler)
     server = await test_server(app, ssl=ssl_ctx)
@@ -319,14 +310,12 @@ async def handler(request):
     resp.close()
 
 
-@pytest.mark.parametrize('fingerprint', [
-    b'\xa2\x06G\xad\xaa\xf5\xd8\\J\x99^by;\x06=',
-    b's\x93\xfd:\xed\x08\x1do\xa9\xaeq9\x1a\xe3\xc5\x7f\x89\xe7l\xf9',
-    b'0\x9a\xc9D\x83\xdc\x91\'\x88\x91\x11\xa1d\x97\xfd\xcb~7U\x14D@L'
-    b'\x11\xab\x99\xa8\xae\xb7\x14\xee\x8b'],
-    ids=['md5', 'sha1', 'sha256'])
 async def test_tcp_connector_fingerprint_fail(test_server, test_client,
-                                              loop, ssl_ctx, fingerprint):
+                                              loop, ssl_ctx):
+
+    fingerprint = (b'0\x9a\xc9D\x83\xdc\x91\'\x88\x91\x11\xa1d\x97\xfd'
+                   b'\xcb~7U\x14D@L'
+                   b'\x11\xab\x99\xa8\xae\xb7\x14\xee\x8b')
 
     async def handler(request):
         return web.HTTPOk(text='Test message')
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -1107,12 +1107,15 @@ def test_bad_fingerprint(loop):
                       fingerprint=b'invalid', loop=loop)
 
 
-def test_insecure_fingerprint(loop):
-    with pytest.warns(DeprecationWarning):
+def test_insecure_fingerprint_md5(loop):
+    with pytest.raises(ValueError):
         ClientRequest('get', URL('http://python.org'),
                       fingerprint=hashlib.md5(b"foo").digest(),
                       loop=loop)
-    with pytest.warns(DeprecationWarning):
+
+
+def test_insecure_fingerprint_sha1(loop):
+    with pytest.raises(ValueError):
         ClientRequest('get', URL('http://python.org'),
                       fingerprint=hashlib.sha1(b"foo").digest(),
                       loop=loop)
diff --git a/tests/test_connector.py b/tests/test_connector.py
--- a/tests/test_connector.py
+++ b/tests/test_connector.py
@@ -1077,13 +1077,23 @@ def test_tcp_connector_ctor(loop):
 
 
 def test_tcp_connector_ctor_fingerprint_valid(loop):
-    valid = b'\xa2\x06G\xad\xaa\xf5\xd8\\J\x99^by;\x06='
-    # md5 and sha1 are deprecated
-    with pytest.warns(DeprecationWarning):
-        conn = aiohttp.TCPConnector(loop=loop, fingerprint=valid)
+    valid = hashlib.sha256(b"foo").digest()
+    conn = aiohttp.TCPConnector(fingerprint=valid, loop=loop)
     assert conn.fingerprint == valid
 
 
+def test_insecure_fingerprint_md5(loop):
+    with pytest.raises(ValueError):
+        aiohttp.TCPConnector(fingerprint=hashlib.md5(b"foo").digest(),
+                             loop=loop)
+
+
+def test_insecure_fingerprint_sha1(loop):
+    with pytest.raises(ValueError):
+        aiohttp.TCPConnector(fingerprint=hashlib.sha1(b"foo").digest(),
+                             loop=loop)
+
+
 def test_tcp_connector_fingerprint_invalid(loop):
     invalid = b'\x00'
     with pytest.raises(ValueError):