diff --git a/CHANGES/3487.misc b/CHANGES/3487.misc new file mode 100644 index 00000000000..19d2173b26b --- /dev/null +++ b/CHANGES/3487.misc @@ -0,0 +1 @@ +Integrate [`trustme`](https://trustme.readthedocs.io/en/latest/) to better test TLS support. diff --git a/requirements/ci-wheel.txt b/requirements/ci-wheel.txt index 46aa4daaa5a..c2f2ddea225 100644 --- a/requirements/ci-wheel.txt +++ b/requirements/ci-wheel.txt @@ -13,6 +13,7 @@ pytest==4.0.2 pytest-cov==2.6.0 pytest-mock==1.10.0 tox==3.6.1 +trustme==0.4.0 twine==1.12.1 yarl==1.3.0 diff --git a/tests/conftest.py b/tests/conftest.py index 3485f77d917..f0058649e59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,11 @@ +import hashlib import pathlib import shutil +import ssl import tempfile import pytest +import trustme pytest_plugins = ['aiohttp.pytest_plugin', 'pytester'] @@ -15,3 +18,54 @@ def shorttmpdir(): tmpdir = pathlib.Path(tempfile.mkdtemp()) yield tmpdir shutil.rmtree(tmpdir, ignore_errors=True) + + +@pytest.fixture +def tls_certificate_authority(): + return trustme.CA() + + +@pytest.fixture +def tls_certificate(tls_certificate_authority): + return tls_certificate_authority.issue_server_cert( + 'localhost', + '127.0.0.1', + '::1', + ) + + +@pytest.fixture +def ssl_ctx(tls_certificate): + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + tls_certificate.configure_cert(ssl_ctx) + return ssl_ctx + + +@pytest.fixture +def client_ssl_ctx(tls_certificate_authority): + ssl_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) + tls_certificate_authority.configure_trust(ssl_ctx) + return ssl_ctx + + +@pytest.fixture +def tls_ca_certificate_pem_path(tls_certificate_authority): + with tls_certificate_authority.cert_pem.tempfile() as ca_cert_pem: + yield ca_cert_pem + + +@pytest.fixture +def tls_certificate_pem_path(tls_certificate): + with tls_certificate.private_key_and_cert_chain_pem.tempfile() as cert_pem: + yield cert_pem + + +@pytest.fixture +def tls_certificate_pem_bytes(tls_certificate): + return tls_certificate.cert_chain_pems[0].bytes() + + +@pytest.fixture +def tls_certificate_fingerprint_sha256(tls_certificate_pem_bytes): + tls_cert_der = ssl.PEM_cert_to_DER_cert(tls_certificate_pem_bytes.decode()) + return hashlib.sha256(tls_cert_der).digest() diff --git a/tests/sample.crt b/tests/sample.crt deleted file mode 100644 index 6a1e3f3c2e7..00000000000 --- a/tests/sample.crt +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICMzCCAZwCCQDFl4ys0fU7iTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQGEwJV -UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuLUZyYW5jaXNjbzEi -MCAGA1UECgwZUHl0aG9uIFNvZnR3YXJlIEZvbmRhdGlvbjAeFw0xMzAzMTgyMDA3 -MjhaFw0yMzAzMTYyMDA3MjhaMF4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp -Zm9ybmlhMRYwFAYDVQQHDA1TYW4tRnJhbmNpc2NvMSIwIAYDVQQKDBlQeXRob24g -U29mdHdhcmUgRm9uZGF0aW9uMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCn -t3s+J7L0xP/YdAQOacpPi9phlrzKZhcXL3XMu2LCUg2fNJpx/47Vc5TZSaO11uO7 -gdwVz3Z7Q2epAgwo59JLffLt5fia8+a/SlPweI/j4+wcIIIiqusnLfpqR8cIAavg -Z06cLYCDvb9wMlheIvSJY12skc1nnphWS2YJ0Xm6uQIDAQABMA0GCSqGSIb3DQEB -BQUAA4GBAE9PknG6pv72+5z/gsDGYy8sK5UNkbWSNr4i4e5lxVsF03+/M71H+3AB -MxVX4+A+Vlk2fmU+BrdHIIUE0r1dDcO3josQ9hc9OJpp5VLSQFP8VeuJCmzYPp9I -I8WbW93cnXnChTrYQVdgVoFdv7GE9YgU7NYkrGIM0nZl1/f/bHPB ------END CERTIFICATE----- diff --git a/tests/sample.crt.der b/tests/sample.crt.der deleted file mode 100644 index ce22b75b9e0..00000000000 Binary files a/tests/sample.crt.der and /dev/null differ diff --git a/tests/sample.key b/tests/sample.key deleted file mode 100644 index edfea8dcab3..00000000000 --- a/tests/sample.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQCnt3s+J7L0xP/YdAQOacpPi9phlrzKZhcXL3XMu2LCUg2fNJpx -/47Vc5TZSaO11uO7gdwVz3Z7Q2epAgwo59JLffLt5fia8+a/SlPweI/j4+wcIIIi -qusnLfpqR8cIAavgZ06cLYCDvb9wMlheIvSJY12skc1nnphWS2YJ0Xm6uQIDAQAB -AoGABfm8k19Yue3W68BecKEGS0VBV57GRTPT+MiBGvVGNIQ15gk6w3sGfMZsdD1y -bsUkQgcDb2d/4i5poBTpl/+Cd41V+c20IC/sSl5X1IEreHMKSLhy/uyjyiyfXlP1 -iXhToFCgLWwENWc8LzfUV8vuAV5WG6oL9bnudWzZxeqx8V0CQQDR7xwVj6LN70Eb -DUhSKLkusmFw5Gk9NJ/7wZ4eHg4B8c9KNVvSlLCLhcsVTQXuqYeFpOqytI45SneP -lr0vrvsDAkEAzITYiXu6ox5huDCG7imX2W9CAYuX638urLxBqBXMS7GqBzojD6RL -21Q8oPwJWJquERa3HDScq1deiQbM9uKIkwJBAIa1PLslGN216Xv3UPHPScyKD/aF -ynXIv+OnANPoiyp6RH4ksQ/18zcEGiVH8EeNpvV9tlAHhb+DZibQHgNr74sCQQC0 -zhToplu/bVKSlUQUNO0rqrI9z30FErDewKeCw5KSsIRSU1E/uM3fHr9iyq4wiL6u -GNjUtKZ0y46lsT9uW6LFAkB5eqeEQnshAdr3X5GykWHJ8DDGBXPPn6Rce1NX4RSq -V9khG2z1bFyfo+hMqpYnF2k32hVq3E54RS8YYnwBsVof ------END RSA PRIVATE KEY----- diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 74f17af6ec9..0d490ac627f 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -6,7 +6,6 @@ import json import pathlib import socket -import ssl from unittest import mock import pytest @@ -25,18 +24,9 @@ def here(): return pathlib.Path(__file__).parent -@pytest.fixture -def ssl_ctx(here): - ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - ssl_ctx.load_cert_chain( - str(here / 'sample.crt'), - str(here / 'sample.key')) - return ssl_ctx - - @pytest.fixture def fname(here): - return here / 'sample.key' + return here / 'conftest.py' def ceil(val): @@ -274,8 +264,11 @@ async def handler(request): assert 200 == resp.status -async def test_ssl_client(ssl_ctx, aiohttp_server, aiohttp_client) -> None: - connector = aiohttp.TCPConnector(ssl=False) +async def test_ssl_client( + aiohttp_server, ssl_ctx, + aiohttp_client, client_ssl_ctx, +) -> None: + connector = aiohttp.TCPConnector(ssl=client_ssl_ctx) async def handler(request): return web.Response(text='Test message') @@ -291,17 +284,16 @@ async def handler(request): assert txt == 'Test message' -async def test_tcp_connector_fingerprint_ok(aiohttp_server, aiohttp_client, - 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 test_tcp_connector_fingerprint_ok( + aiohttp_server, aiohttp_client, + ssl_ctx, tls_certificate_fingerprint_sha256, +): + tls_fingerprint = Fingerprint(tls_certificate_fingerprint_sha256) async def handler(request): return web.Response(text='Test message') - connector = aiohttp.TCPConnector(ssl=Fingerprint(fingerprint)) + connector = aiohttp.TCPConnector(ssl=tls_fingerprint) app = web.Application() app.router.add_route('GET', '/', handler) server = await aiohttp_server(app, ssl=ssl_ctx) @@ -312,17 +304,14 @@ async def handler(request): resp.close() -async def test_tcp_connector_fingerprint_fail(aiohttp_server, aiohttp_client, - 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 test_tcp_connector_fingerprint_fail( + aiohttp_server, aiohttp_client, + ssl_ctx, tls_certificate_fingerprint_sha256, +): async def handler(request): return web.Response(text='Test message') - bad_fingerprint = b'\x00' * len(fingerprint) + bad_fingerprint = b'\x00' * len(tls_certificate_fingerprint_sha256) connector = aiohttp.TCPConnector(ssl=Fingerprint(bad_fingerprint)) @@ -335,7 +324,7 @@ async def handler(request): await client.get('/') exc = cm.value assert exc.expected == bad_fingerprint - assert exc.got == fingerprint + assert exc.got == tls_certificate_fingerprint_sha256 async def test_format_task_get(aiohttp_server) -> None: @@ -1402,7 +1391,7 @@ async def handler(request): 'text/plain', 'application/octet-stream'] assert request.headers['content-disposition'] == ( - "inline; filename=\"sample.key\"; filename*=utf-8''sample.key") + "inline; filename=\"conftest.py\"; filename*=utf-8''conftest.py") return web.Response() @@ -1428,6 +1417,7 @@ async def handler(request): # then use 'application/octet-stream' default assert request.content_type in ['application/pgp-keys', 'text/plain', + 'text/x-python', 'application/octet-stream'] return web.Response() diff --git a/tests/test_client_request.py b/tests/test_client_request.py index bffe7acb19f..4388d9ce60b 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -801,7 +801,7 @@ async def test_chunked_transfer_encoding(loop, conn) -> None: async def test_file_upload_not_chunked(loop) -> None: here = os.path.dirname(__file__) - fname = os.path.join(here, 'sample.key') + fname = os.path.join(here, 'aiohttp.png') with open(fname, 'rb') as f: req = ClientRequest( 'post', URL('http://python.org/'), @@ -828,7 +828,7 @@ async def test_precompressed_data_stays_intact(loop) -> None: async def test_file_upload_not_chunked_seek(loop) -> None: here = os.path.dirname(__file__) - fname = os.path.join(here, 'sample.key') + fname = os.path.join(here, 'aiohttp.png') with open(fname, 'rb') as f: f.seek(100) req = ClientRequest( @@ -842,7 +842,7 @@ async def test_file_upload_not_chunked_seek(loop) -> None: async def test_file_upload_force_chunked(loop) -> None: here = os.path.dirname(__file__) - fname = os.path.join(here, 'sample.key') + fname = os.path.join(here, 'aiohttp.png') with open(fname, 'rb') as f: req = ClientRequest( 'post', URL('http://python.org/'), @@ -1270,11 +1270,11 @@ async def create_connection(req, traces, timeout): conn.close() -def test_verify_ssl_false_with_ssl_context(loop) -> None: +def test_verify_ssl_false_with_ssl_context(loop, ssl_ctx) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(ValueError): _merge_ssl_params(None, verify_ssl=False, - ssl_context=mock.Mock(), fingerprint=None) + ssl_context=ssl_ctx, fingerprint=None) def test_bad_fingerprint(loop) -> None: diff --git a/tests/test_connector.py b/tests/test_connector.py index 88db9797f85..0e80efa0515 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -3,7 +3,6 @@ import asyncio import gc import hashlib -import os.path import platform import socket import ssl @@ -1999,20 +1998,16 @@ async def test_resolver_not_called_with_address_is_ip(loop) -> None: resolver.resolve.assert_not_called() -async def test_tcp_connector_raise_connector_ssl_error(aiohttp_server) -> None: +async def test_tcp_connector_raise_connector_ssl_error( + aiohttp_server, ssl_ctx, +) -> None: async def handler(request): return web.Response() app = web.Application() app.router.add_get('/', handler) - here = os.path.join(os.path.dirname(__file__), '..', 'tests') - keyfile = os.path.join(here, 'sample.key') - certfile = os.path.join(here, 'sample.crt') - sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - sslcontext.load_cert_chain(certfile, keyfile) - - srv = await aiohttp_server(app, ssl=sslcontext) + srv = await aiohttp_server(app, ssl=ssl_ctx) port = unused_port() conn = aiohttp.TCPConnector(local_addr=('127.0.0.1', port)) @@ -2038,27 +2033,22 @@ async def handler(request): async def test_tcp_connector_do_not_raise_connector_ssl_error( - aiohttp_server) -> None: + aiohttp_server, ssl_ctx, client_ssl_ctx, +) -> None: async def handler(request): return web.Response() app = web.Application() app.router.add_get('/', handler) - here = os.path.join(os.path.dirname(__file__), '..', 'tests') - keyfile = os.path.join(here, 'sample.key') - certfile = os.path.join(here, 'sample.crt') - sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - sslcontext.load_cert_chain(certfile, keyfile) - - srv = await aiohttp_server(app, ssl=sslcontext) + srv = await aiohttp_server(app, ssl=ssl_ctx) port = unused_port() conn = aiohttp.TCPConnector(local_addr=('127.0.0.1', port)) session = aiohttp.ClientSession(connector=conn) url = srv.make_url('/') - r = await session.get(url, ssl=sslcontext) + r = await session.get(url, ssl=client_ssl_ctx) r.release() first_conn = next(iter(conn._conns.values()))[0][0] @@ -2068,7 +2058,7 @@ async def handler(request): except AttributeError: _sslcontext = first_conn.transport._sslcontext - assert _sslcontext is sslcontext + assert _sslcontext is client_ssl_ctx r.close() await session.close() diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 412ae328300..1a14a6bf5e7 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -128,8 +128,8 @@ def test_static(router) -> None: info = resource.get_info() assert info['prefix'] == '/prefix' assert info['directory'] == folder - url = resource.url_for(filename='sample.key') - assert url == URL('/prefix/sample.key') + url = resource.url_for(filename='aiohttp.png') + assert url == URL('/prefix/aiohttp.png') def test_head_deco(router) -> None: diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 19c89132cdb..384536c5285 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -34,7 +34,7 @@ def here(): @pytest.fixture def fname(here): - return here / 'sample.key' + return here / 'conftest.py' async def test_simple_get(aiohttp_client) -> None: @@ -298,7 +298,7 @@ def check_file(fs): async def handler(request): data = await request.post() - assert ['sample.crt'] == list(data.keys()) + assert ['data.unknown_mime_type'] == list(data.keys()) for fs in data.values(): check_file(fs) fs.file.close() @@ -309,7 +309,7 @@ async def handler(request): app.router.add_post('/', handler) client = await aiohttp_client(app) - fname = here / 'sample.crt' + fname = here / 'data.unknown_mime_type' resp = await client.post('/', data=[fname.open()]) assert 200 == resp.status @@ -361,7 +361,7 @@ def check_file(fs): async def handler(request): data = await request.post() - assert ['sample.crt', 'sample.key'] == list(data.keys()) + assert ['data.unknown_mime_type', 'conftest.py'] == list(data.keys()) for fs in data.values(): check_file(fs) fs.file.close() @@ -372,8 +372,8 @@ async def handler(request): app.router.add_post('/', handler) client = await aiohttp_client(app) - with (here / 'sample.crt').open() as f1: - with (here / 'sample.key').open() as f2: + with (here / 'data.unknown_mime_type').open() as f1: + with (here / 'conftest.py').open() as f2: resp = await client.post('/', data=[f1, f2]) assert 200 == resp.status @@ -899,12 +899,17 @@ async def handler(request): resp = await client.get('/') assert 200 == resp.status resp_data = await resp.read() + expected_content_disposition = ( + 'attachment; filename="conftest.py"; filename*=utf-8\'\'conftest.py' + ) assert resp_data == data assert resp.headers.get('Content-Type') in ( - 'application/octet-stream', 'application/pgp-keys') + 'application/octet-stream', 'text/x-python', 'text/plain', + ) assert resp.headers.get('Content-Length') == str(len(resp_data)) - assert (resp.headers.get('Content-Disposition') == - 'attachment; filename="sample.key"; filename*=utf-8\'\'sample.key') + assert ( + resp.headers.get('Content-Disposition') == expected_content_disposition + ) async def test_response_with_file_ctype(aiohttp_client, fname) -> None: @@ -923,11 +928,15 @@ async def handler(request): resp = await client.get('/') assert 200 == resp.status resp_data = await resp.read() + expected_content_disposition = ( + 'attachment; filename="conftest.py"; filename*=utf-8\'\'conftest.py' + ) assert resp_data == data assert resp.headers.get('Content-Type') == 'text/binary' assert resp.headers.get('Content-Length') == str(len(resp_data)) - assert (resp.headers.get('Content-Disposition') == - 'attachment; filename="sample.key"; filename*=utf-8\'\'sample.key') + assert ( + resp.headers.get('Content-Disposition') == expected_content_disposition + ) async def test_response_with_payload_disp(aiohttp_client, fname) -> None: diff --git a/tests/test_web_sendfile_functional.py b/tests/test_web_sendfile_functional.py index 792c47566bd..fe48d7f2c69 100644 --- a/tests/test_web_sendfile_functional.py +++ b/tests/test_web_sendfile_functional.py @@ -267,18 +267,16 @@ async def handler(request): @pytest.mark.skipif(not ssl, reason="ssl not supported") -async def test_static_file_ssl(aiohttp_server, aiohttp_client) -> None: +async def test_static_file_ssl( + aiohttp_server, ssl_ctx, + aiohttp_client, client_ssl_ctx, +) -> None: dirname = os.path.dirname(__file__) filename = 'data.unknown_mime_type' - ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - ssl_ctx.load_cert_chain( - os.path.join(dirname, 'sample.crt'), - os.path.join(dirname, 'sample.key') - ) app = web.Application() app.router.add_static('/static', dirname) server = await aiohttp_server(app, ssl=ssl_ctx) - conn = aiohttp.TCPConnector(ssl=False) + conn = aiohttp.TCPConnector(ssl=client_ssl_ctx) client = await aiohttp_client(server, connector=conn) resp = await client.get('/static/'+filename) diff --git a/tests/test_worker.py b/tests/test_worker.py index 27c75248774..675b37968a8 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -1,7 +1,6 @@ """Tests for aiohttp/worker.py""" import asyncio import os -import pathlib import socket import ssl from unittest import mock @@ -248,37 +247,43 @@ def raiser(): worker.notify.assert_called_with() -def test__create_ssl_context_without_certs_and_ciphers(worker) -> None: - here = pathlib.Path(__file__).parent +def test__create_ssl_context_without_certs_and_ciphers( + worker, + tls_certificate_pem_path, +) -> None: worker.cfg.ssl_version = ssl.PROTOCOL_SSLv23 worker.cfg.cert_reqs = ssl.CERT_OPTIONAL - worker.cfg.certfile = str(here / 'sample.crt') - worker.cfg.keyfile = str(here / 'sample.key') + worker.cfg.certfile = tls_certificate_pem_path + worker.cfg.keyfile = tls_certificate_pem_path worker.cfg.ca_certs = None worker.cfg.ciphers = None - crt = worker._create_ssl_context(worker.cfg) - assert isinstance(crt, ssl.SSLContext) + ctx = worker._create_ssl_context(worker.cfg) + assert isinstance(ctx, ssl.SSLContext) -def test__create_ssl_context_with_ciphers(worker) -> None: - here = pathlib.Path(__file__).parent +def test__create_ssl_context_with_ciphers( + worker, + tls_certificate_pem_path, +) -> None: worker.cfg.ssl_version = ssl.PROTOCOL_SSLv23 worker.cfg.cert_reqs = ssl.CERT_OPTIONAL - worker.cfg.certfile = str(here / 'sample.crt') - worker.cfg.keyfile = str(here / 'sample.key') + worker.cfg.certfile = tls_certificate_pem_path + worker.cfg.keyfile = tls_certificate_pem_path worker.cfg.ca_certs = None worker.cfg.ciphers = 'PSK' ctx = worker._create_ssl_context(worker.cfg) assert isinstance(ctx, ssl.SSLContext) -def test__create_ssl_context_with_ca_certs(worker) -> None: - here = pathlib.Path(__file__).parent +def test__create_ssl_context_with_ca_certs( + worker, + tls_ca_certificate_pem_path, tls_certificate_pem_path, +) -> None: worker.cfg.ssl_version = ssl.PROTOCOL_SSLv23 worker.cfg.cert_reqs = ssl.CERT_OPTIONAL - worker.cfg.certfile = str(here / 'sample.crt') - worker.cfg.keyfile = str(here / 'sample.key') - worker.cfg.ca_certs = str(here / 'sample.crt') + worker.cfg.certfile = tls_certificate_pem_path + worker.cfg.keyfile = tls_certificate_pem_path + worker.cfg.ca_certs = tls_ca_certificate_pem_path worker.cfg.ciphers = None ctx = worker._create_ssl_context(worker.cfg) assert isinstance(ctx, ssl.SSLContext)