From 016c2300d7d7572f73de7cfad3b75e49cc98a754 Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Tue, 11 Mar 2025 20:06:08 +0000 Subject: [PATCH 01/18] Implement asgiref tls extension --- tests/conftest.py | 12 ++++ tests/test_ssl.py | 54 +++++++++++++++++- uvicorn/config.py | 3 + uvicorn/protocols/http/h11_impl.py | 7 +++ uvicorn/protocols/http/httptools_impl.py | 9 +++ uvicorn/protocols/utils.py | 73 ++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1b0c0e84e..dc5630441 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,13 @@ def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: ) +@pytest.fixture +def tls_client_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: + return tls_certificate_authority.issue_cert( + "client@example.com", common_name="uvicorn client" + ) + + @pytest.fixture def tls_ca_certificate_pem_path(tls_certificate_authority: trustme.CA): with tls_certificate_authority.cert_pem.tempfile() as ca_cert_pem: @@ -258,3 +265,8 @@ def ws_protocol_cls(request: pytest.FixtureRequest): ) def http_protocol_cls(request: pytest.FixtureRequest): return import_from_string(request.param) +@pytest.fixture +def tls_client_certificate_pem_path(tls_client_certificate: trustme.LeafCert): + private_key_and_cert_chain = tls_client_certificate.private_key_and_cert_chain_pem + with private_key_and_cert_chain.tempfile() as client_cert_pem: + yield client_cert_pem diff --git a/tests/test_ssl.py b/tests/test_ssl.py index da60bb8dd..c6b275ab1 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -1,3 +1,5 @@ +import ssl + import httpx import pytest @@ -34,7 +36,57 @@ async def test_run( assert response.status_code == 204 -@pytest.mark.anyio +@pytest.mark.asyncio +async def test_run_httptools_client_cert( + tls_ca_ssl_context, + tls_ca_certificate_pem_path, + tls_ca_certificate_private_key_path, + tls_client_certificate_pem_path, +): + config = Config( + app=app, + loop="asyncio", + http="httptools", + limit_max_requests=1, + ssl_keyfile=tls_ca_certificate_private_key_path, + ssl_certfile=tls_ca_certificate_pem_path, + ssl_ca_certs=tls_ca_certificate_pem_path, + ssl_cert_reqs=ssl.CERT_REQUIRED, + ) + async with run_server(config): + async with httpx.AsyncClient( + verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path + ) as client: + response = await client.get("https://127.0.0.1:8000") + assert response.status_code == 204 + + +@pytest.mark.asyncio +async def test_run_h11_client_cert( + tls_ca_ssl_context, + tls_ca_certificate_pem_path, + tls_ca_certificate_private_key_path, + tls_client_certificate_pem_path, +): + config = Config( + app=app, + loop="asyncio", + http="h11", + limit_max_requests=1, + ssl_keyfile=tls_ca_certificate_private_key_path, + ssl_certfile=tls_ca_certificate_pem_path, + ssl_ca_certs=tls_ca_certificate_pem_path, + ssl_cert_reqs=ssl.CERT_REQUIRED, + ) + async with run_server(config): + async with httpx.AsyncClient( + verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path + ) as client: + response = await client.get("https://127.0.0.1:8000") + assert response.status_code == 204 + + +@pytest.mark.asyncio async def test_run_chain( tls_ca_ssl_context, tls_certificate_key_and_chain_path, diff --git a/uvicorn/config.py b/uvicorn/config.py index ae996c1cb..d0b4ebcd9 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -260,6 +260,7 @@ def __init__( self.callback_notify = callback_notify self.ssl_keyfile = ssl_keyfile self.ssl_certfile = ssl_certfile + self.ssl_cert_pem: Optional[str] = None self.ssl_keyfile_password = ssl_keyfile_password self.ssl_version = ssl_version self.ssl_cert_reqs = ssl_cert_reqs @@ -407,6 +408,8 @@ def load(self) -> None: ca_certs=self.ssl_ca_certs, ciphers=self.ssl_ciphers, ) + with open(self.ssl_certfile) as file: + self.ssl_cert_pem = file.read() else: self.ssl = None diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index b8cdde3ab..2b5d3d8dd 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -78,6 +78,7 @@ def __init__( self.server: tuple[str, int] | None = None self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None + self.tls = None # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -96,6 +97,11 @@ def connection_made( # type: ignore[override] self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" + if self.config.is_ssl: + self.tls = get_tls_info(transport) + if self.tls: + self.tls["server_cert"] = self.config.ssl_cert_pem + if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) @@ -212,6 +218,7 @@ def handle_events(self) -> None: "query_string": query_string, "headers": self.headers, "state": self.app_state.copy(), + "extensions": {"tls": self.tls}, } if self._should_upgrade(): self.handle_websocket_upgrade(event) diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index e8795ed35..6e8bf1528 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -87,6 +87,7 @@ def __init__( self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque() + self.tls = None # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -106,6 +107,11 @@ def connection_made( # type: ignore[override] self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" + if self.config.is_ssl: + self.tls = get_tls_info(transport) + if self.tls: + self.tls["server_cert"] = self.config.ssl_cert_pem + if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) @@ -230,6 +236,9 @@ def on_message_begin(self) -> None: "root_path": self.root_path, "headers": self.headers, "state": self.app_state.copy(), + "extensions": { + "tls": self.tls, + }, } # Parser callbacks diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index e1d6f01d5..12286a6d6 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -1,10 +1,31 @@ from __future__ import annotations import asyncio +import ssl import urllib.parse +from typing import Any, Dict, Optional, Tuple from uvicorn._types import WWWScope +RDNS_MAPPING: Dict[str, str] = { + "commonName": "CN", + "localityName": "L", + "stateOrProvinceName": "ST", + "organizationName": "O", + "organizationalUnitName": "OU", + "countryName": "C", + "streetAddress": "STREET", + "domainComponent": "DC", + "userId": "UID", +} + +TLS_VERSION_MAP: Dict[str, int] = { + "TLSv1": 0x0301, + "TLSv1.1": 0x0302, + "TLSv1.2": 0x0303, + "TLSv1.3": 0x0304, +} + class ClientDisconnected(OSError): ... @@ -54,3 +75,55 @@ def get_path_with_query_string(scope: WWWScope) -> str: if scope["query_string"]: path_with_query_string = "{}?{}".format(path_with_query_string, scope["query_string"].decode("ascii")) return path_with_query_string + + +def get_tls_info(transport: asyncio.Transport) -> Optional[Dict]: + + ### + # server_cert: Unable to set from transport information + # client_cert_chain: Just the peercert, currently no access to the full cert chain + # client_cert_name: + # client_cert_error: No access to this + # tls_version: + # cipher_suite: Too hard to convert without direct access to openssl + ### + + ssl_info: Dict[str, Any] = { + "server_cert": None, + "client_cert_chain": [], + "client_cert_name": None, + "client_cert_error": None, + "tls_version": None, + "cipher_suite": None, + } + + ssl_object = transport.get_extra_info("ssl_object", default=None) + peercert = ssl_object.getpeercert() + + if peercert: + rdn_strings = [] + for rdn in peercert["subject"]: + rdn_strings.append( + "+".join( + [ + "%s = %s" % (RDNS_MAPPING[entry[0]], entry[1]) + for entry in reversed(rdn) + if entry[0] in RDNS_MAPPING + ] + ) + ) + + ssl_info["client_cert_chain"] = [ + ssl.DER_cert_to_PEM_cert(ssl_object.getpeercert(binary_form=True)) + ] + ssl_info["client_cert_name"] = ", ".join(rdn_strings) if rdn_strings else "" + ssl_info["tls_version"] = ( + TLS_VERSION_MAP[ssl_object.version()] + if ssl_object.version() in TLS_VERSION_MAP + else None + ) + ssl_info["cipher_suite"] = list(ssl_object.cipher()) + + return ssl_info + + return None From b3f1f6f8e0af5a2d48737f96f5a9b49e03e2d0ac Mon Sep 17 00:00:00 2001 From: Matt Gilen Date: Sun, 18 Jul 2021 16:10:35 +0000 Subject: [PATCH 02/18] Only add tls extension if connection is over tls --- uvicorn/protocols/http/h11_impl.py | 6 +++++- uvicorn/protocols/http/httptools_impl.py | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 2b5d3d8dd..266a40d25 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -218,8 +218,12 @@ def handle_events(self) -> None: "query_string": query_string, "headers": self.headers, "state": self.app_state.copy(), - "extensions": {"tls": self.tls}, + "extensions": {"tls": self.tls},"extensions": {}, } + + if self.config.is_ssl: + self.scope["extensions"]["tls"] = self.tls + if self._should_upgrade(): self.handle_websocket_upgrade(event) return diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 6e8bf1528..3a1ff1a72 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -236,11 +236,12 @@ def on_message_begin(self) -> None: "root_path": self.root_path, "headers": self.headers, "state": self.app_state.copy(), - "extensions": { - "tls": self.tls, - }, + "extensions": {}, } + if self.config.is_ssl: + self.scope["extensions"]["tls"] = self.tls + # Parser callbacks def on_url(self, url: bytes) -> None: self.url += url From 1944afc24d07f65a1a73ef04e20f6e9210be12ae Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 6 Dec 2021 08:28:05 -0500 Subject: [PATCH 03/18] Fix formatting issues --- tests/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index dc5630441..63b7cd72d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,6 +114,13 @@ def tls_ca_ssl_context(tls_certificate_authority: trustme.CA) -> ssl.SSLContext: return ssl_ctx +@pytest.fixture +def tls_client_certificate_pem_path(tls_client_certificate: trustme.LeafCert): + private_key_and_cert_chain = tls_client_certificate.private_key_and_cert_chain_pem + with private_key_and_cert_chain.tempfile() as client_cert_pem: + yield client_cert_pem + + @pytest.fixture(scope="package") def reload_directory_structure(tmp_path_factory: pytest.TempPathFactory): """ From fc065bf81b44b570082061fb91aca55cac5e127f Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Sat, 4 Mar 2023 16:43:40 +0000 Subject: [PATCH 04/18] Address linting issues and fix tests --- tests/test_ssl.py | 16 +++++++++------- uvicorn/protocols/http/h11_impl.py | 4 ++-- uvicorn/protocols/http/httptools_impl.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index c6b275ab1..d9312ff5f 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -39,8 +39,9 @@ async def test_run( @pytest.mark.asyncio async def test_run_httptools_client_cert( tls_ca_ssl_context, + tls_certificate_server_cert_path, + tls_certificate_private_key_path, tls_ca_certificate_pem_path, - tls_ca_certificate_private_key_path, tls_client_certificate_pem_path, ): config = Config( @@ -48,8 +49,8 @@ async def test_run_httptools_client_cert( loop="asyncio", http="httptools", limit_max_requests=1, - ssl_keyfile=tls_ca_certificate_private_key_path, - ssl_certfile=tls_ca_certificate_pem_path, + ssl_keyfile=tls_certificate_private_key_path, + ssl_certfile=tls_certificate_server_cert_path, ssl_ca_certs=tls_ca_certificate_pem_path, ssl_cert_reqs=ssl.CERT_REQUIRED, ) @@ -65,7 +66,8 @@ async def test_run_httptools_client_cert( async def test_run_h11_client_cert( tls_ca_ssl_context, tls_ca_certificate_pem_path, - tls_ca_certificate_private_key_path, + tls_certificate_server_cert_path, + tls_certificate_private_key_path, tls_client_certificate_pem_path, ): config = Config( @@ -73,8 +75,8 @@ async def test_run_h11_client_cert( loop="asyncio", http="h11", limit_max_requests=1, - ssl_keyfile=tls_ca_certificate_private_key_path, - ssl_certfile=tls_ca_certificate_pem_path, + ssl_keyfile=tls_certificate_private_key_path, + ssl_certfile=tls_certificate_server_cert_path, ssl_ca_certs=tls_ca_certificate_pem_path, ssl_cert_reqs=ssl.CERT_REQUIRED, ) @@ -86,7 +88,7 @@ async def test_run_h11_client_cert( assert response.status_code == 204 -@pytest.mark.asyncio +@pytest.mark.anyio async def test_run_chain( tls_ca_ssl_context, tls_certificate_key_and_chain_path, diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 266a40d25..580042893 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -3,7 +3,7 @@ import asyncio import http import logging -from typing import Any, Callable, Literal, cast +from typing import Callable, cast from urllib.parse import unquote import h11 @@ -222,7 +222,7 @@ def handle_events(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = self.tls + self.scope["extensions"]["tls"] = self.tls # type: ignore[index, assignment] # noqa: E501 if self._should_upgrade(): self.handle_websocket_upgrade(event) diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 3a1ff1a72..817450da4 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -240,7 +240,7 @@ def on_message_begin(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = self.tls + self.scope["extensions"]["tls"] = self.tls # type: ignore[index, assignment] # noqa: E501 # Parser callbacks def on_url(self, url: bytes) -> None: From 090d1cdaff2cc1049bbfbb0a65dbf89a5d19e445 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Fri, 24 May 2024 22:28:33 -0400 Subject: [PATCH 05/18] Fix issues found by check script --- tests/conftest.py | 4 +--- tests/test_ssl.py | 8 ++----- uvicorn/config.py | 2 +- uvicorn/protocols/http/h11_impl.py | 6 ++--- uvicorn/protocols/http/httptools_impl.py | 11 +++++++--- uvicorn/protocols/utils.py | 28 +++++++----------------- 6 files changed, 23 insertions(+), 36 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 63b7cd72d..238a94baf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,9 +56,7 @@ def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: @pytest.fixture def tls_client_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: - return tls_certificate_authority.issue_cert( - "client@example.com", common_name="uvicorn client" - ) + return tls_certificate_authority.issue_cert("client@example.com", common_name="uvicorn client") @pytest.fixture diff --git a/tests/test_ssl.py b/tests/test_ssl.py index d9312ff5f..51d1a6d48 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -55,9 +55,7 @@ async def test_run_httptools_client_cert( ssl_cert_reqs=ssl.CERT_REQUIRED, ) async with run_server(config): - async with httpx.AsyncClient( - verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path - ) as client: + async with httpx.AsyncClient(verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path) as client: response = await client.get("https://127.0.0.1:8000") assert response.status_code == 204 @@ -81,9 +79,7 @@ async def test_run_h11_client_cert( ssl_cert_reqs=ssl.CERT_REQUIRED, ) async with run_server(config): - async with httpx.AsyncClient( - verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path - ) as client: + async with httpx.AsyncClient(verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path) as client: response = await client.get("https://127.0.0.1:8000") assert response.status_code == 204 diff --git a/uvicorn/config.py b/uvicorn/config.py index d0b4ebcd9..594d1707e 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -260,7 +260,7 @@ def __init__( self.callback_notify = callback_notify self.ssl_keyfile = ssl_keyfile self.ssl_certfile = ssl_certfile - self.ssl_cert_pem: Optional[str] = None + self.ssl_cert_pem: str | None = None self.ssl_keyfile_password = ssl_keyfile_password self.ssl_version = ssl_version self.ssl_cert_reqs = ssl_cert_reqs diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 580042893..f87ea58a2 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -3,7 +3,7 @@ import asyncio import http import logging -from typing import Callable, cast +from typing import Any, Callable, Literal, cast from urllib.parse import unquote import h11 @@ -78,7 +78,7 @@ def __init__( self.server: tuple[str, int] | None = None self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None - self.tls = None + self.tls: dict[object, object] = {} # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -222,7 +222,7 @@ def handle_events(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = self.tls # type: ignore[index, assignment] # noqa: E501 + self.scope["extensions"]["tls"] = self.tls if self._should_upgrade(): self.handle_websocket_upgrade(event) diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 817450da4..7cb8c0d45 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -7,7 +7,12 @@ import urllib from asyncio.events import TimerHandle from collections import deque -from typing import Any, Callable, Literal, cast +from typing import ( + Any, + Callable, + Literal, + cast, +) import httptools @@ -87,7 +92,7 @@ def __init__( self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque() - self.tls = None + self.tls: dict[object, object] = {} # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -240,7 +245,7 @@ def on_message_begin(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = self.tls # type: ignore[index, assignment] # noqa: E501 + self.scope["extensions"]["tls"] = self.tls # Parser callbacks def on_url(self, url: bytes) -> None: diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 12286a6d6..d4f4e7c7b 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -3,11 +3,10 @@ import asyncio import ssl import urllib.parse -from typing import Any, Dict, Optional, Tuple from uvicorn._types import WWWScope -RDNS_MAPPING: Dict[str, str] = { +RDNS_MAPPING: dict[str, str] = { "commonName": "CN", "localityName": "L", "stateOrProvinceName": "ST", @@ -19,7 +18,7 @@ "userId": "UID", } -TLS_VERSION_MAP: Dict[str, int] = { +TLS_VERSION_MAP: dict[str, int] = { "TLSv1": 0x0301, "TLSv1.1": 0x0302, "TLSv1.2": 0x0303, @@ -77,8 +76,7 @@ def get_path_with_query_string(scope: WWWScope) -> str: return path_with_query_string -def get_tls_info(transport: asyncio.Transport) -> Optional[Dict]: - +def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: ### # server_cert: Unable to set from transport information # client_cert_chain: Just the peercert, currently no access to the full cert chain @@ -88,7 +86,7 @@ def get_tls_info(transport: asyncio.Transport) -> Optional[Dict]: # cipher_suite: Too hard to convert without direct access to openssl ### - ssl_info: Dict[str, Any] = { + ssl_info: dict[object, object] = { "server_cert": None, "client_cert_chain": [], "client_cert_name": None, @@ -105,25 +103,15 @@ def get_tls_info(transport: asyncio.Transport) -> Optional[Dict]: for rdn in peercert["subject"]: rdn_strings.append( "+".join( - [ - "%s = %s" % (RDNS_MAPPING[entry[0]], entry[1]) - for entry in reversed(rdn) - if entry[0] in RDNS_MAPPING - ] + [f"{RDNS_MAPPING[entry[0]]} = {entry[1]}" for entry in reversed(rdn) if entry[0] in RDNS_MAPPING] ) ) - ssl_info["client_cert_chain"] = [ - ssl.DER_cert_to_PEM_cert(ssl_object.getpeercert(binary_form=True)) - ] + ssl_info["client_cert_chain"] = [ssl.DER_cert_to_PEM_cert(ssl_object.getpeercert(binary_form=True))] ssl_info["client_cert_name"] = ", ".join(rdn_strings) if rdn_strings else "" ssl_info["tls_version"] = ( - TLS_VERSION_MAP[ssl_object.version()] - if ssl_object.version() in TLS_VERSION_MAP - else None + TLS_VERSION_MAP[ssl_object.version()] if ssl_object.version() in TLS_VERSION_MAP else None ) ssl_info["cipher_suite"] = list(ssl_object.cipher()) - return ssl_info - - return None + return ssl_info From adb8535e1372889732d4f7a4caa73908c1cb9b0c Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 27 May 2024 15:49:48 +0200 Subject: [PATCH 06/18] add generated TLS constants --- tools/generate_tls_const.py | 43 ++++ uvicorn/protocols/http/tls_const.py | 358 ++++++++++++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 tools/generate_tls_const.py create mode 100644 uvicorn/protocols/http/tls_const.py diff --git a/tools/generate_tls_const.py b/tools/generate_tls_const.py new file mode 100644 index 000000000..e8a1b0ae5 --- /dev/null +++ b/tools/generate_tls_const.py @@ -0,0 +1,43 @@ +import pprint +import xml.etree.ElementTree as ET + +import httpx + +GENERATED_FILENAME = "uvicorn/protocols/http/tls_const.py" + +TLS_PARAMETERS_URL = "https://www.iana.org/assignments/tls-parameters/tls-parameters.xml" +NAMESPACES = {"iana": "http://www.iana.org/assignments"} +TLS_CIPHER_SUITES_XPATH = './/iana:registry[@id="tls-parameters-4"]/iana:record' + +content = httpx.get(TLS_PARAMETERS_URL).content +root = ET.fromstring(content) + +tls_cipher_suites = {} + +for record in root.findall(TLS_CIPHER_SUITES_XPATH, NAMESPACES): + cipher = record.find("iana:description", NAMESPACES).text + if cipher == "Unassigned": + continue + if cipher == "Reserved": + continue + + value = record.find("iana:value", NAMESPACES).text + if "-" in value: + continue + + vs = [int(v, 16) for v in value.split(",")] + code = (vs[0] << 8) + vs[1] + tls_cipher_suites[cipher] = code + + +GENERATED_SOURCE = f""" +# generated by tools/generate_tls_const.py + +from typing import Final + +TLS_CIPHER_SUITES: Final[dict[str, int]] = {pprint.pformat(tls_cipher_suites)} +""" + + +with open(GENERATED_FILENAME, "wt") as fp: + fp.write(GENERATED_SOURCE) diff --git a/uvicorn/protocols/http/tls_const.py b/uvicorn/protocols/http/tls_const.py new file mode 100644 index 000000000..6d82f2430 --- /dev/null +++ b/uvicorn/protocols/http/tls_const.py @@ -0,0 +1,358 @@ +# generated by tools/generate_tls_const.py.py + +from typing import Final + +TLS_CIPHER_SUITES: Final[dict[str, int]] = { + "TLS_AEGIS_128L_SHA256": 4871, + "TLS_AEGIS_256_SHA512": 4870, + "TLS_AES_128_CCM_8_SHA256": 4869, + "TLS_AES_128_CCM_SHA256": 4868, + "TLS_AES_128_GCM_SHA256": 4865, + "TLS_AES_256_GCM_SHA384": 4866, + "TLS_CHACHA20_POLY1305_SHA256": 4867, + "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA": 17, + "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA": 19, + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA": 50, + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256": 64, + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256": 162, + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA": 56, + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256": 106, + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384": 163, + "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256": 49218, + "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256": 49238, + "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384": 49219, + "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384": 49239, + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA": 68, + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256": 189, + "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256": 49280, + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA": 135, + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256": 195, + "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384": 49281, + "TLS_DHE_DSS_WITH_DES_CBC_SHA": 18, + "TLS_DHE_DSS_WITH_SEED_CBC_SHA": 153, + "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA": 143, + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA": 144, + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256": 178, + "TLS_DHE_PSK_WITH_AES_128_CCM": 49318, + "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256": 170, + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA": 145, + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384": 179, + "TLS_DHE_PSK_WITH_AES_256_CCM": 49319, + "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384": 171, + "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256": 49254, + "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256": 49260, + "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384": 49255, + "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384": 49261, + "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49302, + "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256": 49296, + "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49303, + "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384": 49297, + "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256": 52397, + "TLS_DHE_PSK_WITH_NULL_SHA": 45, + "TLS_DHE_PSK_WITH_NULL_SHA256": 180, + "TLS_DHE_PSK_WITH_NULL_SHA384": 181, + "TLS_DHE_PSK_WITH_RC4_128_SHA": 142, + "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA": 20, + "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA": 22, + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA": 51, + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256": 103, + "TLS_DHE_RSA_WITH_AES_128_CCM": 49310, + "TLS_DHE_RSA_WITH_AES_128_CCM_8": 49314, + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256": 158, + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA": 57, + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256": 107, + "TLS_DHE_RSA_WITH_AES_256_CCM": 49311, + "TLS_DHE_RSA_WITH_AES_256_CCM_8": 49315, + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384": 159, + "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256": 49220, + "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256": 49234, + "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384": 49221, + "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384": 49235, + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA": 69, + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256": 190, + "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49276, + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA": 136, + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256": 196, + "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49277, + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256": 52394, + "TLS_DHE_RSA_WITH_DES_CBC_SHA": 21, + "TLS_DHE_RSA_WITH_SEED_CBC_SHA": 154, + "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA": 11, + "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA": 13, + "TLS_DH_DSS_WITH_AES_128_CBC_SHA": 48, + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256": 62, + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256": 164, + "TLS_DH_DSS_WITH_AES_256_CBC_SHA": 54, + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256": 104, + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384": 165, + "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256": 49214, + "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256": 49240, + "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384": 49215, + "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384": 49241, + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA": 66, + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256": 187, + "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256": 49282, + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA": 133, + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256": 193, + "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384": 49283, + "TLS_DH_DSS_WITH_DES_CBC_SHA": 12, + "TLS_DH_DSS_WITH_SEED_CBC_SHA": 151, + "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA": 14, + "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA": 16, + "TLS_DH_RSA_WITH_AES_128_CBC_SHA": 49, + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256": 63, + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256": 160, + "TLS_DH_RSA_WITH_AES_256_CBC_SHA": 55, + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256": 105, + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384": 161, + "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256": 49216, + "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256": 49236, + "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384": 49217, + "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384": 49237, + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA": 67, + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256": 188, + "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49278, + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA": 134, + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256": 194, + "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49279, + "TLS_DH_RSA_WITH_DES_CBC_SHA": 15, + "TLS_DH_RSA_WITH_SEED_CBC_SHA": 152, + "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA": 25, + "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5": 23, + "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA": 27, + "TLS_DH_anon_WITH_AES_128_CBC_SHA": 52, + "TLS_DH_anon_WITH_AES_128_CBC_SHA256": 108, + "TLS_DH_anon_WITH_AES_128_GCM_SHA256": 166, + "TLS_DH_anon_WITH_AES_256_CBC_SHA": 58, + "TLS_DH_anon_WITH_AES_256_CBC_SHA256": 109, + "TLS_DH_anon_WITH_AES_256_GCM_SHA384": 167, + "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256": 49222, + "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256": 49242, + "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384": 49223, + "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384": 49243, + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA": 70, + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256": 191, + "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256": 49284, + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA": 137, + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256": 197, + "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384": 49285, + "TLS_DH_anon_WITH_DES_CBC_SHA": 26, + "TLS_DH_anon_WITH_RC4_128_MD5": 24, + "TLS_DH_anon_WITH_SEED_CBC_SHA": 155, + "TLS_ECCPWD_WITH_AES_128_CCM_SHA256": 49330, + "TLS_ECCPWD_WITH_AES_128_GCM_SHA256": 49328, + "TLS_ECCPWD_WITH_AES_256_CCM_SHA384": 49331, + "TLS_ECCPWD_WITH_AES_256_GCM_SHA384": 49329, + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA": 49160, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": 49161, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": 49187, + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM": 49324, + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8": 49326, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": 49195, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": 49162, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384": 49188, + "TLS_ECDHE_ECDSA_WITH_AES_256_CCM": 49325, + "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8": 49327, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": 49196, + "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256": 49224, + "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256": 49244, + "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384": 49225, + "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384": 49245, + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256": 49266, + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256": 49286, + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384": 49267, + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384": 49287, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": 52393, + "TLS_ECDHE_ECDSA_WITH_NULL_SHA": 49158, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": 49159, + "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA": 49204, + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA": 49205, + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256": 49207, + "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256": 53251, + "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256": 53253, + "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256": 53249, + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA": 49206, + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384": 49208, + "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384": 53250, + "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256": 49264, + "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384": 49265, + "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49306, + "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49307, + "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256": 52396, + "TLS_ECDHE_PSK_WITH_NULL_SHA": 49209, + "TLS_ECDHE_PSK_WITH_NULL_SHA256": 49210, + "TLS_ECDHE_PSK_WITH_NULL_SHA384": 49211, + "TLS_ECDHE_PSK_WITH_RC4_128_SHA": 49203, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": 49170, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": 49171, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": 49191, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": 49199, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": 49172, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384": 49192, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": 49200, + "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256": 49228, + "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256": 49248, + "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384": 49229, + "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384": 49249, + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256": 49270, + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49290, + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384": 49271, + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49291, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": 52392, + "TLS_ECDHE_RSA_WITH_NULL_SHA": 49168, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": 49169, + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA": 49155, + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA": 49156, + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256": 49189, + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256": 49197, + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA": 49157, + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384": 49190, + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384": 49198, + "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256": 49226, + "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256": 49246, + "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384": 49227, + "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384": 49247, + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256": 49268, + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256": 49288, + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384": 49269, + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384": 49289, + "TLS_ECDH_ECDSA_WITH_NULL_SHA": 49153, + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA": 49154, + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA": 49165, + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA": 49166, + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256": 49193, + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256": 49201, + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA": 49167, + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384": 49194, + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384": 49202, + "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256": 49230, + "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256": 49250, + "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384": 49231, + "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384": 49251, + "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256": 49272, + "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49292, + "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384": 49273, + "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49293, + "TLS_ECDH_RSA_WITH_NULL_SHA": 49163, + "TLS_ECDH_RSA_WITH_RC4_128_SHA": 49164, + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA": 49175, + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA": 49176, + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA": 49177, + "TLS_ECDH_anon_WITH_NULL_SHA": 49173, + "TLS_ECDH_anon_WITH_RC4_128_SHA": 49174, + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV": 255, + "TLS_FALLBACK_SCSV": 22016, + "TLS_GOSTR341112_256_WITH_28147_CNT_IMIT": 49410, + "TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC": 49408, + "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_L": 49411, + "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_S": 49413, + "TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC": 49409, + "TLS_GOSTR341112_256_WITH_MAGMA_MGM_L": 49412, + "TLS_GOSTR341112_256_WITH_MAGMA_MGM_S": 49414, + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5": 41, + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA": 38, + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5": 42, + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA": 39, + "TLS_KRB5_EXPORT_WITH_RC4_40_MD5": 43, + "TLS_KRB5_EXPORT_WITH_RC4_40_SHA": 40, + "TLS_KRB5_WITH_3DES_EDE_CBC_MD5": 35, + "TLS_KRB5_WITH_3DES_EDE_CBC_SHA": 31, + "TLS_KRB5_WITH_DES_CBC_MD5": 34, + "TLS_KRB5_WITH_DES_CBC_SHA": 30, + "TLS_KRB5_WITH_IDEA_CBC_MD5": 37, + "TLS_KRB5_WITH_IDEA_CBC_SHA": 33, + "TLS_KRB5_WITH_RC4_128_MD5": 36, + "TLS_KRB5_WITH_RC4_128_SHA": 32, + "TLS_NULL_WITH_NULL_NULL": 0, + "TLS_PSK_DHE_WITH_AES_128_CCM_8": 49322, + "TLS_PSK_DHE_WITH_AES_256_CCM_8": 49323, + "TLS_PSK_WITH_3DES_EDE_CBC_SHA": 139, + "TLS_PSK_WITH_AES_128_CBC_SHA": 140, + "TLS_PSK_WITH_AES_128_CBC_SHA256": 174, + "TLS_PSK_WITH_AES_128_CCM": 49316, + "TLS_PSK_WITH_AES_128_CCM_8": 49320, + "TLS_PSK_WITH_AES_128_GCM_SHA256": 168, + "TLS_PSK_WITH_AES_256_CBC_SHA": 141, + "TLS_PSK_WITH_AES_256_CBC_SHA384": 175, + "TLS_PSK_WITH_AES_256_CCM": 49317, + "TLS_PSK_WITH_AES_256_CCM_8": 49321, + "TLS_PSK_WITH_AES_256_GCM_SHA384": 169, + "TLS_PSK_WITH_ARIA_128_CBC_SHA256": 49252, + "TLS_PSK_WITH_ARIA_128_GCM_SHA256": 49258, + "TLS_PSK_WITH_ARIA_256_CBC_SHA384": 49253, + "TLS_PSK_WITH_ARIA_256_GCM_SHA384": 49259, + "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49300, + "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256": 49294, + "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49301, + "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384": 49295, + "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256": 52395, + "TLS_PSK_WITH_NULL_SHA": 44, + "TLS_PSK_WITH_NULL_SHA256": 176, + "TLS_PSK_WITH_NULL_SHA384": 177, + "TLS_PSK_WITH_RC4_128_SHA": 138, + "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA": 8, + "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5": 6, + "TLS_RSA_EXPORT_WITH_RC4_40_MD5": 3, + "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA": 147, + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA": 148, + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256": 182, + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256": 172, + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA": 149, + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384": 183, + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384": 173, + "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256": 49256, + "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256": 49262, + "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384": 49257, + "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384": 49263, + "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49304, + "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256": 49298, + "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49305, + "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384": 49299, + "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256": 52398, + "TLS_RSA_PSK_WITH_NULL_SHA": 46, + "TLS_RSA_PSK_WITH_NULL_SHA256": 184, + "TLS_RSA_PSK_WITH_NULL_SHA384": 185, + "TLS_RSA_PSK_WITH_RC4_128_SHA": 146, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": 10, + "TLS_RSA_WITH_AES_128_CBC_SHA": 47, + "TLS_RSA_WITH_AES_128_CBC_SHA256": 60, + "TLS_RSA_WITH_AES_128_CCM": 49308, + "TLS_RSA_WITH_AES_128_CCM_8": 49312, + "TLS_RSA_WITH_AES_128_GCM_SHA256": 156, + "TLS_RSA_WITH_AES_256_CBC_SHA": 53, + "TLS_RSA_WITH_AES_256_CBC_SHA256": 61, + "TLS_RSA_WITH_AES_256_CCM": 49309, + "TLS_RSA_WITH_AES_256_CCM_8": 49313, + "TLS_RSA_WITH_AES_256_GCM_SHA384": 157, + "TLS_RSA_WITH_ARIA_128_CBC_SHA256": 49212, + "TLS_RSA_WITH_ARIA_128_GCM_SHA256": 49232, + "TLS_RSA_WITH_ARIA_256_CBC_SHA384": 49213, + "TLS_RSA_WITH_ARIA_256_GCM_SHA384": 49233, + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA": 65, + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256": 186, + "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49274, + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA": 132, + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256": 192, + "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49275, + "TLS_RSA_WITH_DES_CBC_SHA": 9, + "TLS_RSA_WITH_IDEA_CBC_SHA": 7, + "TLS_RSA_WITH_NULL_MD5": 1, + "TLS_RSA_WITH_NULL_SHA": 2, + "TLS_RSA_WITH_NULL_SHA256": 59, + "TLS_RSA_WITH_RC4_128_MD5": 4, + "TLS_RSA_WITH_RC4_128_SHA": 5, + "TLS_RSA_WITH_SEED_CBC_SHA": 150, + "TLS_SHA256_SHA256": 49332, + "TLS_SHA384_SHA384": 49333, + "TLS_SM4_CCM_SM3": 199, + "TLS_SM4_GCM_SM3": 198, + "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA": 49180, + "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA": 49183, + "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA": 49186, + "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA": 49179, + "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA": 49182, + "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA": 49185, + "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA": 49178, + "TLS_SRP_SHA_WITH_AES_128_CBC_SHA": 49181, + "TLS_SRP_SHA_WITH_AES_256_CBC_SHA": 49184, +} From 4efb397071c36f4be6cffbc936ef9e4227365cee Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 10:35:58 -0400 Subject: [PATCH 07/18] Add generate script and update generation to run formatting automatically --- scripts/generate | 11 +++++++++++ tools/generate_tls_const.py | 6 ++++++ uvicorn/protocols/http/tls_const.py | 4 +++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 scripts/generate diff --git a/scripts/generate b/scripts/generate new file mode 100644 index 000000000..d9a515ee6 --- /dev/null +++ b/scripts/generate @@ -0,0 +1,11 @@ +#!/bin/sh -e + +if [ -d 'venv' ] ; then + PREFIX="venv/bin/" +else + PREFIX="" +fi + +set -x + +${PREFIX}python -m tools.generate_tls_const \ No newline at end of file diff --git a/tools/generate_tls_const.py b/tools/generate_tls_const.py index e8a1b0ae5..eb704fcd9 100644 --- a/tools/generate_tls_const.py +++ b/tools/generate_tls_const.py @@ -1,5 +1,6 @@ import pprint import xml.etree.ElementTree as ET +import subprocess import httpx @@ -33,6 +34,8 @@ GENERATED_SOURCE = f""" # generated by tools/generate_tls_const.py +from __future__ import annotations + from typing import Final TLS_CIPHER_SUITES: Final[dict[str, int]] = {pprint.pformat(tls_cipher_suites)} @@ -41,3 +44,6 @@ with open(GENERATED_FILENAME, "wt") as fp: fp.write(GENERATED_SOURCE) + +subprocess.run(["ruff", "format", GENERATED_FILENAME]) +subprocess.run(["ruff", "check", "--fix", GENERATED_FILENAME]) diff --git a/uvicorn/protocols/http/tls_const.py b/uvicorn/protocols/http/tls_const.py index 6d82f2430..472b80c20 100644 --- a/uvicorn/protocols/http/tls_const.py +++ b/uvicorn/protocols/http/tls_const.py @@ -1,4 +1,6 @@ -# generated by tools/generate_tls_const.py.py +# generated by tools/generate_tls_const.py + +from __future__ import annotations from typing import Final From 626a2564ce07f3979c457264c89e7d08c88c36d4 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 11:14:33 -0400 Subject: [PATCH 08/18] Added DN escaping and use new generated cipher_suite lookup table --- uvicorn/protocols/utils.py | 47 +++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index d4f4e7c7b..f5aab5f5d 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -5,16 +5,17 @@ import urllib.parse from uvicorn._types import WWWScope +from uvicorn.protocols.http.tls_const import TLS_CIPHER_SUITES RDNS_MAPPING: dict[str, str] = { + "domainComponent": "DC", "commonName": "CN", + "organizationalUnitName": "OU", + "organizationName": "O", + "streetAddress": "STREET", "localityName": "L", "stateOrProvinceName": "ST", - "organizationName": "O", - "organizationalUnitName": "OU", "countryName": "C", - "streetAddress": "STREET", - "domainComponent": "DC", "userId": "UID", } @@ -76,6 +77,32 @@ def get_path_with_query_string(scope: WWWScope) -> str: return path_with_query_string +def escape_dn_chars(s: str) -> str: + """ + Escape all DN special characters found in s + with a back-slash (see RFC 4514, section 2.4) + + Based upon the implementation here - https://github.com/python-ldap/python-ldap/blob/e885b621562a3c987934be3fba3873d21026bf5c/Lib/ldap/dn.py#L17 + """ + if s: + s = s.replace("\\", "\\\\") + s = s.replace(",", "\\,") + s = s.replace("+", "\\+") + s = s.replace('"', '\\"') + s = s.replace("<", "\\<") + s = s.replace(">", "\\>") + s = s.replace(";", "\\;") + s = s.replace("=", "\\=") + s = s.replace("\000", "\\\000") + s = s.replace("\n", "\\0a") + s = s.replace("\r", "\\0d") + if s[-1] == " ": + s = "".join((s[:-1], "\\ ")) + if s[0] == "#" or s[0] == " ": + s = "".join(("\\", s)) + return s + + def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: ### # server_cert: Unable to set from transport information @@ -83,7 +110,7 @@ def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: # client_cert_name: # client_cert_error: No access to this # tls_version: - # cipher_suite: Too hard to convert without direct access to openssl + # cipher_suite: ### ssl_info: dict[object, object] = { @@ -103,15 +130,19 @@ def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: for rdn in peercert["subject"]: rdn_strings.append( "+".join( - [f"{RDNS_MAPPING[entry[0]]} = {entry[1]}" for entry in reversed(rdn) if entry[0] in RDNS_MAPPING] + [ + f"{RDNS_MAPPING[entry[0]]}={escape_dn_chars(entry[1])}" + for entry in reversed(rdn) + if entry[0] in RDNS_MAPPING + ] ) ) ssl_info["client_cert_chain"] = [ssl.DER_cert_to_PEM_cert(ssl_object.getpeercert(binary_form=True))] - ssl_info["client_cert_name"] = ", ".join(rdn_strings) if rdn_strings else "" + ssl_info["client_cert_name"] = ",".join(rdn_strings) if rdn_strings else "" ssl_info["tls_version"] = ( TLS_VERSION_MAP[ssl_object.version()] if ssl_object.version() in TLS_VERSION_MAP else None ) - ssl_info["cipher_suite"] = list(ssl_object.cipher()) + ssl_info["cipher_suite"] = TLS_CIPHER_SUITES[ssl_object.cipher()[0]] return ssl_info From 01215dd339a3d016a420c85f3bce19df4d8a482f Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 11:59:53 -0400 Subject: [PATCH 09/18] Add test for escaping --- tests/conftest.py | 4 ++-- tests/test_ssl.py | 18 ++++++++++++++++++ uvicorn/protocols/utils.py | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 238a94baf..cae7041dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,8 +55,8 @@ def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: @pytest.fixture -def tls_client_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: - return tls_certificate_authority.issue_cert("client@example.com", common_name="uvicorn client") +def tls_client_certificate(request, tls_certificate_authority: trustme.CA) -> trustme.LeafCert: + return tls_certificate_authority.issue_cert("client@example.com", common_name=getattr(request, "param", "uvicorn client")) @pytest.fixture diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 51d1a6d48..b7eda120d 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -36,6 +36,17 @@ async def test_run( assert response.status_code == 204 +@pytest.mark.anyio +@pytest.mark.parametrize( + "tls_client_certificate, expected_common_name", + [ + ("test common name", "test common name"), + (' \\,+"<>;=\000\n\r ', 'CN=\\ \\\\\\,\\+\\"\\<\\>\\;\\=\\\x00\\0a\\0d\\ '), + ], + indirect=["tls_client_certificate"], +) + + @pytest.mark.asyncio async def test_run_httptools_client_cert( tls_ca_ssl_context, @@ -43,7 +54,14 @@ async def test_run_httptools_client_cert( tls_certificate_private_key_path, tls_ca_certificate_pem_path, tls_client_certificate_pem_path, + expected_common_name, ): + async def app(scope, receive, send): + assert scope["type"] == "http" + assert expected_common_name in scope["extensions"]["tls"]["client_cert_name"] + await send({"type": "http.response.start", "status": 204, "headers": []}) + await send({"type": "http.response.body", "body": b"", "more_body": False}) + config = Config( app=app, loop="asyncio", diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index f5aab5f5d..7951eaf9a 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -143,6 +143,6 @@ def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: ssl_info["tls_version"] = ( TLS_VERSION_MAP[ssl_object.version()] if ssl_object.version() in TLS_VERSION_MAP else None ) - ssl_info["cipher_suite"] = TLS_CIPHER_SUITES[ssl_object.cipher()[0]] + ssl_info["cipher_suite"] = getattr(TLS_CIPHER_SUITES, ssl_object.cipher()[0], None) return ssl_info From ac16263946c2a929adec2112307ed73696a245ca Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 12:02:29 -0400 Subject: [PATCH 10/18] Run formatting on tests --- tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index cae7041dd..07bddc94b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,9 @@ def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: @pytest.fixture def tls_client_certificate(request, tls_certificate_authority: trustme.CA) -> trustme.LeafCert: - return tls_certificate_authority.issue_cert("client@example.com", common_name=getattr(request, "param", "uvicorn client")) + return tls_certificate_authority.issue_cert( + "client@example.com", common_name=getattr(request, "param", "uvicorn client") + ) @pytest.fixture From 4c851d6fe4d01b46d953b82ee82acb0f736c91a1 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Wed, 29 May 2024 10:34:34 -0400 Subject: [PATCH 11/18] fix incorrect dictionary access --- uvicorn/protocols/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 7951eaf9a..49cda4fb8 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -143,6 +143,6 @@ def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: ssl_info["tls_version"] = ( TLS_VERSION_MAP[ssl_object.version()] if ssl_object.version() in TLS_VERSION_MAP else None ) - ssl_info["cipher_suite"] = getattr(TLS_CIPHER_SUITES, ssl_object.cipher()[0], None) + ssl_info["cipher_suite"] = TLS_CIPHER_SUITES.get(ssl_object.cipher()[0], None) return ssl_info From b5c8690018b62a599a47ed1e75f740f6d5cf90ad Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Thu, 20 Feb 2025 13:32:01 +0000 Subject: [PATCH 12/18] Simplify and remove code --- scripts/generate | 11 - tests/conftest.py | 11 + tests/test_ssl.py | 25 +- tools/generate_tls_const.py | 49 --- uvicorn/protocols/http/h11_impl.py | 6 +- uvicorn/protocols/http/httptools_impl.py | 4 +- uvicorn/protocols/http/tls_const.py | 360 ----------------------- uvicorn/protocols/utils.py | 98 ++---- 8 files changed, 52 insertions(+), 512 deletions(-) delete mode 100644 scripts/generate delete mode 100644 tools/generate_tls_const.py delete mode 100644 uvicorn/protocols/http/tls_const.py diff --git a/scripts/generate b/scripts/generate deleted file mode 100644 index d9a515ee6..000000000 --- a/scripts/generate +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -e - -if [ -d 'venv' ] ; then - PREFIX="venv/bin/" -else - PREFIX="" -fi - -set -x - -${PREFIX}python -m tools.generate_tls_const \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 07bddc94b..d52fc23cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,6 +113,17 @@ def tls_ca_ssl_context(tls_certificate_authority: trustme.CA) -> ssl.SSLContext: tls_certificate_authority.configure_trust(ssl_ctx) return ssl_ctx +@pytest.fixture +def tls_client_ssl_context(tls_certificate_authority: trustme.CA, tls_client_certificate: trustme.LeafCert) -> ssl.SSLContext: + ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + tls_certificate_authority.configure_trust(ssl_ctx) + + # Load the client certificate chain into the SSL context + with tls_client_certificate.private_key_and_cert_chain_pem.tempfile() as client_cert_pem: + ssl_ctx.load_cert_chain(certfile=client_cert_pem) + + + return ssl_ctx @pytest.fixture def tls_client_certificate_pem_path(tls_client_certificate: trustme.LeafCert): diff --git a/tests/test_ssl.py b/tests/test_ssl.py index b7eda120d..79de96546 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3,6 +3,8 @@ import httpx import pytest +from cryptography import x509 + from tests.utils import run_server from uvicorn.config import Config @@ -41,24 +43,28 @@ async def test_run( "tls_client_certificate, expected_common_name", [ ("test common name", "test common name"), - (' \\,+"<>;=\000\n\r ', 'CN=\\ \\\\\\,\\+\\"\\<\\>\\;\\=\\\x00\\0a\\0d\\ '), ], indirect=["tls_client_certificate"], ) -@pytest.mark.asyncio +@pytest.mark.anyio async def test_run_httptools_client_cert( - tls_ca_ssl_context, + tls_client_ssl_context, tls_certificate_server_cert_path, tls_certificate_private_key_path, tls_ca_certificate_pem_path, - tls_client_certificate_pem_path, expected_common_name, ): async def app(scope, receive, send): assert scope["type"] == "http" - assert expected_common_name in scope["extensions"]["tls"]["client_cert_name"] + assert len(scope["extensions"]["tls"]["client_cert_chain"]) >= 1 + cert = x509.load_pem_x509_certificate(scope["extensions"]["tls"]["client_cert_chain"][0].encode('utf-8')) + assert cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value == expected_common_name + cipher_suites = [cipher['name'] for cipher in ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).get_ciphers()] + assert scope["extensions"]["tls"]["cipher_suite"] in cipher_suites + assert (scope["extensions"]["tls"]["tls_version"].startswith("TLSv") or scope["extensions"]["tls"]["tls_version"].startswith("SSLv")) + await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) @@ -73,18 +79,17 @@ async def app(scope, receive, send): ssl_cert_reqs=ssl.CERT_REQUIRED, ) async with run_server(config): - async with httpx.AsyncClient(verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path) as client: + async with httpx.AsyncClient(verify=tls_client_ssl_context) as client: response = await client.get("https://127.0.0.1:8000") assert response.status_code == 204 -@pytest.mark.asyncio +@pytest.mark.anyio async def test_run_h11_client_cert( - tls_ca_ssl_context, + tls_client_ssl_context, tls_ca_certificate_pem_path, tls_certificate_server_cert_path, tls_certificate_private_key_path, - tls_client_certificate_pem_path, ): config = Config( app=app, @@ -97,7 +102,7 @@ async def test_run_h11_client_cert( ssl_cert_reqs=ssl.CERT_REQUIRED, ) async with run_server(config): - async with httpx.AsyncClient(verify=tls_ca_ssl_context, cert=tls_client_certificate_pem_path) as client: + async with httpx.AsyncClient(verify=tls_client_ssl_context) as client: response = await client.get("https://127.0.0.1:8000") assert response.status_code == 204 diff --git a/tools/generate_tls_const.py b/tools/generate_tls_const.py deleted file mode 100644 index eb704fcd9..000000000 --- a/tools/generate_tls_const.py +++ /dev/null @@ -1,49 +0,0 @@ -import pprint -import xml.etree.ElementTree as ET -import subprocess - -import httpx - -GENERATED_FILENAME = "uvicorn/protocols/http/tls_const.py" - -TLS_PARAMETERS_URL = "https://www.iana.org/assignments/tls-parameters/tls-parameters.xml" -NAMESPACES = {"iana": "http://www.iana.org/assignments"} -TLS_CIPHER_SUITES_XPATH = './/iana:registry[@id="tls-parameters-4"]/iana:record' - -content = httpx.get(TLS_PARAMETERS_URL).content -root = ET.fromstring(content) - -tls_cipher_suites = {} - -for record in root.findall(TLS_CIPHER_SUITES_XPATH, NAMESPACES): - cipher = record.find("iana:description", NAMESPACES).text - if cipher == "Unassigned": - continue - if cipher == "Reserved": - continue - - value = record.find("iana:value", NAMESPACES).text - if "-" in value: - continue - - vs = [int(v, 16) for v in value.split(",")] - code = (vs[0] << 8) + vs[1] - tls_cipher_suites[cipher] = code - - -GENERATED_SOURCE = f""" -# generated by tools/generate_tls_const.py - -from __future__ import annotations - -from typing import Final - -TLS_CIPHER_SUITES: Final[dict[str, int]] = {pprint.pformat(tls_cipher_suites)} -""" - - -with open(GENERATED_FILENAME, "wt") as fp: - fp.write(GENERATED_SOURCE) - -subprocess.run(["ruff", "format", GENERATED_FILENAME]) -subprocess.run(["ruff", "check", "--fix", GENERATED_FILENAME]) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index f87ea58a2..fe0bfe9a3 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -98,9 +98,7 @@ def connection_made( # type: ignore[override] self.scheme = "https" if is_ssl(transport) else "http" if self.config.is_ssl: - self.tls = get_tls_info(transport) - if self.tls: - self.tls["server_cert"] = self.config.ssl_cert_pem + self.tls = get_tls_info(transport, self.config) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" @@ -218,7 +216,7 @@ def handle_events(self) -> None: "query_string": query_string, "headers": self.headers, "state": self.app_state.copy(), - "extensions": {"tls": self.tls},"extensions": {}, + "extensions": {}, } if self.config.is_ssl: diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 7cb8c0d45..8668ef0ba 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -113,9 +113,7 @@ def connection_made( # type: ignore[override] self.scheme = "https" if is_ssl(transport) else "http" if self.config.is_ssl: - self.tls = get_tls_info(transport) - if self.tls: - self.tls["server_cert"] = self.config.ssl_cert_pem + self.tls = get_tls_info(transport,self.config) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" diff --git a/uvicorn/protocols/http/tls_const.py b/uvicorn/protocols/http/tls_const.py deleted file mode 100644 index 472b80c20..000000000 --- a/uvicorn/protocols/http/tls_const.py +++ /dev/null @@ -1,360 +0,0 @@ -# generated by tools/generate_tls_const.py - -from __future__ import annotations - -from typing import Final - -TLS_CIPHER_SUITES: Final[dict[str, int]] = { - "TLS_AEGIS_128L_SHA256": 4871, - "TLS_AEGIS_256_SHA512": 4870, - "TLS_AES_128_CCM_8_SHA256": 4869, - "TLS_AES_128_CCM_SHA256": 4868, - "TLS_AES_128_GCM_SHA256": 4865, - "TLS_AES_256_GCM_SHA384": 4866, - "TLS_CHACHA20_POLY1305_SHA256": 4867, - "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA": 17, - "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA": 19, - "TLS_DHE_DSS_WITH_AES_128_CBC_SHA": 50, - "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256": 64, - "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256": 162, - "TLS_DHE_DSS_WITH_AES_256_CBC_SHA": 56, - "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256": 106, - "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384": 163, - "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256": 49218, - "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256": 49238, - "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384": 49219, - "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384": 49239, - "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA": 68, - "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256": 189, - "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256": 49280, - "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA": 135, - "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256": 195, - "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384": 49281, - "TLS_DHE_DSS_WITH_DES_CBC_SHA": 18, - "TLS_DHE_DSS_WITH_SEED_CBC_SHA": 153, - "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA": 143, - "TLS_DHE_PSK_WITH_AES_128_CBC_SHA": 144, - "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256": 178, - "TLS_DHE_PSK_WITH_AES_128_CCM": 49318, - "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256": 170, - "TLS_DHE_PSK_WITH_AES_256_CBC_SHA": 145, - "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384": 179, - "TLS_DHE_PSK_WITH_AES_256_CCM": 49319, - "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384": 171, - "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256": 49254, - "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256": 49260, - "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384": 49255, - "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384": 49261, - "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49302, - "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256": 49296, - "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49303, - "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384": 49297, - "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256": 52397, - "TLS_DHE_PSK_WITH_NULL_SHA": 45, - "TLS_DHE_PSK_WITH_NULL_SHA256": 180, - "TLS_DHE_PSK_WITH_NULL_SHA384": 181, - "TLS_DHE_PSK_WITH_RC4_128_SHA": 142, - "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA": 20, - "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA": 22, - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA": 51, - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256": 103, - "TLS_DHE_RSA_WITH_AES_128_CCM": 49310, - "TLS_DHE_RSA_WITH_AES_128_CCM_8": 49314, - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256": 158, - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA": 57, - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256": 107, - "TLS_DHE_RSA_WITH_AES_256_CCM": 49311, - "TLS_DHE_RSA_WITH_AES_256_CCM_8": 49315, - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384": 159, - "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256": 49220, - "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256": 49234, - "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384": 49221, - "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384": 49235, - "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA": 69, - "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256": 190, - "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49276, - "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA": 136, - "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256": 196, - "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49277, - "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256": 52394, - "TLS_DHE_RSA_WITH_DES_CBC_SHA": 21, - "TLS_DHE_RSA_WITH_SEED_CBC_SHA": 154, - "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA": 11, - "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA": 13, - "TLS_DH_DSS_WITH_AES_128_CBC_SHA": 48, - "TLS_DH_DSS_WITH_AES_128_CBC_SHA256": 62, - "TLS_DH_DSS_WITH_AES_128_GCM_SHA256": 164, - "TLS_DH_DSS_WITH_AES_256_CBC_SHA": 54, - "TLS_DH_DSS_WITH_AES_256_CBC_SHA256": 104, - "TLS_DH_DSS_WITH_AES_256_GCM_SHA384": 165, - "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256": 49214, - "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256": 49240, - "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384": 49215, - "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384": 49241, - "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA": 66, - "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256": 187, - "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256": 49282, - "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA": 133, - "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256": 193, - "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384": 49283, - "TLS_DH_DSS_WITH_DES_CBC_SHA": 12, - "TLS_DH_DSS_WITH_SEED_CBC_SHA": 151, - "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA": 14, - "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA": 16, - "TLS_DH_RSA_WITH_AES_128_CBC_SHA": 49, - "TLS_DH_RSA_WITH_AES_128_CBC_SHA256": 63, - "TLS_DH_RSA_WITH_AES_128_GCM_SHA256": 160, - "TLS_DH_RSA_WITH_AES_256_CBC_SHA": 55, - "TLS_DH_RSA_WITH_AES_256_CBC_SHA256": 105, - "TLS_DH_RSA_WITH_AES_256_GCM_SHA384": 161, - "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256": 49216, - "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256": 49236, - "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384": 49217, - "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384": 49237, - "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA": 67, - "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256": 188, - "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49278, - "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA": 134, - "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256": 194, - "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49279, - "TLS_DH_RSA_WITH_DES_CBC_SHA": 15, - "TLS_DH_RSA_WITH_SEED_CBC_SHA": 152, - "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA": 25, - "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5": 23, - "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA": 27, - "TLS_DH_anon_WITH_AES_128_CBC_SHA": 52, - "TLS_DH_anon_WITH_AES_128_CBC_SHA256": 108, - "TLS_DH_anon_WITH_AES_128_GCM_SHA256": 166, - "TLS_DH_anon_WITH_AES_256_CBC_SHA": 58, - "TLS_DH_anon_WITH_AES_256_CBC_SHA256": 109, - "TLS_DH_anon_WITH_AES_256_GCM_SHA384": 167, - "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256": 49222, - "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256": 49242, - "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384": 49223, - "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384": 49243, - "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA": 70, - "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256": 191, - "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256": 49284, - "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA": 137, - "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256": 197, - "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384": 49285, - "TLS_DH_anon_WITH_DES_CBC_SHA": 26, - "TLS_DH_anon_WITH_RC4_128_MD5": 24, - "TLS_DH_anon_WITH_SEED_CBC_SHA": 155, - "TLS_ECCPWD_WITH_AES_128_CCM_SHA256": 49330, - "TLS_ECCPWD_WITH_AES_128_GCM_SHA256": 49328, - "TLS_ECCPWD_WITH_AES_256_CCM_SHA384": 49331, - "TLS_ECCPWD_WITH_AES_256_GCM_SHA384": 49329, - "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA": 49160, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": 49161, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": 49187, - "TLS_ECDHE_ECDSA_WITH_AES_128_CCM": 49324, - "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8": 49326, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": 49195, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": 49162, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384": 49188, - "TLS_ECDHE_ECDSA_WITH_AES_256_CCM": 49325, - "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8": 49327, - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": 49196, - "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256": 49224, - "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256": 49244, - "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384": 49225, - "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384": 49245, - "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256": 49266, - "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256": 49286, - "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384": 49267, - "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384": 49287, - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": 52393, - "TLS_ECDHE_ECDSA_WITH_NULL_SHA": 49158, - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": 49159, - "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA": 49204, - "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA": 49205, - "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256": 49207, - "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256": 53251, - "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256": 53253, - "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256": 53249, - "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA": 49206, - "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384": 49208, - "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384": 53250, - "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256": 49264, - "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384": 49265, - "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49306, - "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49307, - "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256": 52396, - "TLS_ECDHE_PSK_WITH_NULL_SHA": 49209, - "TLS_ECDHE_PSK_WITH_NULL_SHA256": 49210, - "TLS_ECDHE_PSK_WITH_NULL_SHA384": 49211, - "TLS_ECDHE_PSK_WITH_RC4_128_SHA": 49203, - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": 49170, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": 49171, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": 49191, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": 49199, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": 49172, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384": 49192, - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": 49200, - "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256": 49228, - "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256": 49248, - "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384": 49229, - "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384": 49249, - "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256": 49270, - "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49290, - "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384": 49271, - "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49291, - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": 52392, - "TLS_ECDHE_RSA_WITH_NULL_SHA": 49168, - "TLS_ECDHE_RSA_WITH_RC4_128_SHA": 49169, - "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA": 49155, - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA": 49156, - "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256": 49189, - "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256": 49197, - "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA": 49157, - "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384": 49190, - "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384": 49198, - "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256": 49226, - "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256": 49246, - "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384": 49227, - "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384": 49247, - "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256": 49268, - "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256": 49288, - "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384": 49269, - "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384": 49289, - "TLS_ECDH_ECDSA_WITH_NULL_SHA": 49153, - "TLS_ECDH_ECDSA_WITH_RC4_128_SHA": 49154, - "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA": 49165, - "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA": 49166, - "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256": 49193, - "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256": 49201, - "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA": 49167, - "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384": 49194, - "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384": 49202, - "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256": 49230, - "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256": 49250, - "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384": 49231, - "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384": 49251, - "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256": 49272, - "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49292, - "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384": 49273, - "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49293, - "TLS_ECDH_RSA_WITH_NULL_SHA": 49163, - "TLS_ECDH_RSA_WITH_RC4_128_SHA": 49164, - "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA": 49175, - "TLS_ECDH_anon_WITH_AES_128_CBC_SHA": 49176, - "TLS_ECDH_anon_WITH_AES_256_CBC_SHA": 49177, - "TLS_ECDH_anon_WITH_NULL_SHA": 49173, - "TLS_ECDH_anon_WITH_RC4_128_SHA": 49174, - "TLS_EMPTY_RENEGOTIATION_INFO_SCSV": 255, - "TLS_FALLBACK_SCSV": 22016, - "TLS_GOSTR341112_256_WITH_28147_CNT_IMIT": 49410, - "TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC": 49408, - "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_L": 49411, - "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_S": 49413, - "TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC": 49409, - "TLS_GOSTR341112_256_WITH_MAGMA_MGM_L": 49412, - "TLS_GOSTR341112_256_WITH_MAGMA_MGM_S": 49414, - "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5": 41, - "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA": 38, - "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5": 42, - "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA": 39, - "TLS_KRB5_EXPORT_WITH_RC4_40_MD5": 43, - "TLS_KRB5_EXPORT_WITH_RC4_40_SHA": 40, - "TLS_KRB5_WITH_3DES_EDE_CBC_MD5": 35, - "TLS_KRB5_WITH_3DES_EDE_CBC_SHA": 31, - "TLS_KRB5_WITH_DES_CBC_MD5": 34, - "TLS_KRB5_WITH_DES_CBC_SHA": 30, - "TLS_KRB5_WITH_IDEA_CBC_MD5": 37, - "TLS_KRB5_WITH_IDEA_CBC_SHA": 33, - "TLS_KRB5_WITH_RC4_128_MD5": 36, - "TLS_KRB5_WITH_RC4_128_SHA": 32, - "TLS_NULL_WITH_NULL_NULL": 0, - "TLS_PSK_DHE_WITH_AES_128_CCM_8": 49322, - "TLS_PSK_DHE_WITH_AES_256_CCM_8": 49323, - "TLS_PSK_WITH_3DES_EDE_CBC_SHA": 139, - "TLS_PSK_WITH_AES_128_CBC_SHA": 140, - "TLS_PSK_WITH_AES_128_CBC_SHA256": 174, - "TLS_PSK_WITH_AES_128_CCM": 49316, - "TLS_PSK_WITH_AES_128_CCM_8": 49320, - "TLS_PSK_WITH_AES_128_GCM_SHA256": 168, - "TLS_PSK_WITH_AES_256_CBC_SHA": 141, - "TLS_PSK_WITH_AES_256_CBC_SHA384": 175, - "TLS_PSK_WITH_AES_256_CCM": 49317, - "TLS_PSK_WITH_AES_256_CCM_8": 49321, - "TLS_PSK_WITH_AES_256_GCM_SHA384": 169, - "TLS_PSK_WITH_ARIA_128_CBC_SHA256": 49252, - "TLS_PSK_WITH_ARIA_128_GCM_SHA256": 49258, - "TLS_PSK_WITH_ARIA_256_CBC_SHA384": 49253, - "TLS_PSK_WITH_ARIA_256_GCM_SHA384": 49259, - "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49300, - "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256": 49294, - "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49301, - "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384": 49295, - "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256": 52395, - "TLS_PSK_WITH_NULL_SHA": 44, - "TLS_PSK_WITH_NULL_SHA256": 176, - "TLS_PSK_WITH_NULL_SHA384": 177, - "TLS_PSK_WITH_RC4_128_SHA": 138, - "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA": 8, - "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5": 6, - "TLS_RSA_EXPORT_WITH_RC4_40_MD5": 3, - "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA": 147, - "TLS_RSA_PSK_WITH_AES_128_CBC_SHA": 148, - "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256": 182, - "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256": 172, - "TLS_RSA_PSK_WITH_AES_256_CBC_SHA": 149, - "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384": 183, - "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384": 173, - "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256": 49256, - "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256": 49262, - "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384": 49257, - "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384": 49263, - "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256": 49304, - "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256": 49298, - "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384": 49305, - "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384": 49299, - "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256": 52398, - "TLS_RSA_PSK_WITH_NULL_SHA": 46, - "TLS_RSA_PSK_WITH_NULL_SHA256": 184, - "TLS_RSA_PSK_WITH_NULL_SHA384": 185, - "TLS_RSA_PSK_WITH_RC4_128_SHA": 146, - "TLS_RSA_WITH_3DES_EDE_CBC_SHA": 10, - "TLS_RSA_WITH_AES_128_CBC_SHA": 47, - "TLS_RSA_WITH_AES_128_CBC_SHA256": 60, - "TLS_RSA_WITH_AES_128_CCM": 49308, - "TLS_RSA_WITH_AES_128_CCM_8": 49312, - "TLS_RSA_WITH_AES_128_GCM_SHA256": 156, - "TLS_RSA_WITH_AES_256_CBC_SHA": 53, - "TLS_RSA_WITH_AES_256_CBC_SHA256": 61, - "TLS_RSA_WITH_AES_256_CCM": 49309, - "TLS_RSA_WITH_AES_256_CCM_8": 49313, - "TLS_RSA_WITH_AES_256_GCM_SHA384": 157, - "TLS_RSA_WITH_ARIA_128_CBC_SHA256": 49212, - "TLS_RSA_WITH_ARIA_128_GCM_SHA256": 49232, - "TLS_RSA_WITH_ARIA_256_CBC_SHA384": 49213, - "TLS_RSA_WITH_ARIA_256_GCM_SHA384": 49233, - "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA": 65, - "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256": 186, - "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256": 49274, - "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA": 132, - "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256": 192, - "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384": 49275, - "TLS_RSA_WITH_DES_CBC_SHA": 9, - "TLS_RSA_WITH_IDEA_CBC_SHA": 7, - "TLS_RSA_WITH_NULL_MD5": 1, - "TLS_RSA_WITH_NULL_SHA": 2, - "TLS_RSA_WITH_NULL_SHA256": 59, - "TLS_RSA_WITH_RC4_128_MD5": 4, - "TLS_RSA_WITH_RC4_128_SHA": 5, - "TLS_RSA_WITH_SEED_CBC_SHA": 150, - "TLS_SHA256_SHA256": 49332, - "TLS_SHA384_SHA384": 49333, - "TLS_SM4_CCM_SM3": 199, - "TLS_SM4_GCM_SM3": 198, - "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA": 49180, - "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA": 49183, - "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA": 49186, - "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA": 49179, - "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA": 49182, - "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA": 49185, - "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA": 49178, - "TLS_SRP_SHA_WITH_AES_128_CBC_SHA": 49181, - "TLS_SRP_SHA_WITH_AES_256_CBC_SHA": 49184, -} diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 49cda4fb8..6b9766ce7 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -3,28 +3,10 @@ import asyncio import ssl import urllib.parse +import sys from uvicorn._types import WWWScope -from uvicorn.protocols.http.tls_const import TLS_CIPHER_SUITES - -RDNS_MAPPING: dict[str, str] = { - "domainComponent": "DC", - "commonName": "CN", - "organizationalUnitName": "OU", - "organizationName": "O", - "streetAddress": "STREET", - "localityName": "L", - "stateOrProvinceName": "ST", - "countryName": "C", - "userId": "UID", -} - -TLS_VERSION_MAP: dict[str, int] = { - "TLSv1": 0x0301, - "TLSv1.1": 0x0302, - "TLSv1.2": 0x0303, - "TLSv1.3": 0x0304, -} +from uvicorn.config import Config class ClientDisconnected(OSError): ... @@ -77,38 +59,11 @@ def get_path_with_query_string(scope: WWWScope) -> str: return path_with_query_string -def escape_dn_chars(s: str) -> str: - """ - Escape all DN special characters found in s - with a back-slash (see RFC 4514, section 2.4) - - Based upon the implementation here - https://github.com/python-ldap/python-ldap/blob/e885b621562a3c987934be3fba3873d21026bf5c/Lib/ldap/dn.py#L17 - """ - if s: - s = s.replace("\\", "\\\\") - s = s.replace(",", "\\,") - s = s.replace("+", "\\+") - s = s.replace('"', '\\"') - s = s.replace("<", "\\<") - s = s.replace(">", "\\>") - s = s.replace(";", "\\;") - s = s.replace("=", "\\=") - s = s.replace("\000", "\\\000") - s = s.replace("\n", "\\0a") - s = s.replace("\r", "\\0d") - if s[-1] == " ": - s = "".join((s[:-1], "\\ ")) - if s[0] == "#" or s[0] == " ": - s = "".join(("\\", s)) - return s - - -def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: + +def get_tls_info(transport: asyncio.Transport, server_config: Config) -> dict[object, object]: ### - # server_cert: Unable to set from transport information - # client_cert_chain: Just the peercert, currently no access to the full cert chain - # client_cert_name: - # client_cert_error: No access to this + # server_cert: Unable to set from transport information, need to set from server_config + # client_cert_chain: # tls_version: # cipher_suite: ### @@ -116,33 +71,26 @@ def get_tls_info(transport: asyncio.Transport) -> dict[object, object]: ssl_info: dict[object, object] = { "server_cert": None, "client_cert_chain": [], - "client_cert_name": None, - "client_cert_error": None, "tls_version": None, "cipher_suite": None, } - ssl_object = transport.get_extra_info("ssl_object", default=None) - peercert = ssl_object.getpeercert() - - if peercert: - rdn_strings = [] - for rdn in peercert["subject"]: - rdn_strings.append( - "+".join( - [ - f"{RDNS_MAPPING[entry[0]]}={escape_dn_chars(entry[1])}" - for entry in reversed(rdn) - if entry[0] in RDNS_MAPPING - ] - ) - ) - - ssl_info["client_cert_chain"] = [ssl.DER_cert_to_PEM_cert(ssl_object.getpeercert(binary_form=True))] - ssl_info["client_cert_name"] = ",".join(rdn_strings) if rdn_strings else "" - ssl_info["tls_version"] = ( - TLS_VERSION_MAP[ssl_object.version()] if ssl_object.version() in TLS_VERSION_MAP else None - ) - ssl_info["cipher_suite"] = TLS_CIPHER_SUITES.get(ssl_object.cipher()[0], None) + ssl_info["server_cert"] = server_config.ssl_cert_pem + + ssl_object = transport.get_extra_info("ssl_object") + if not ssl_object: + return ssl_info + + if sys.version_info < (3, 13): + peer_cert = ssl_object.getpeercert(binary_form=True) + if peer_cert: + ssl_info["client_cert_chain"].append(ssl.DER_cert_to_PEM_cert(peer_cert)) + else: + client_chain = ssl_object.get_verified_chain() + for cert in client_chain: + ssl_info["client_cert_chain"].append(ssl.DER_cert_to_PEM_cert(cert)) + + ssl_info["tls_version"] = ssl_object.version() + ssl_info["cipher_suite"] = ssl_object.cipher()[0] if ssl_object.cipher() else None return ssl_info From 1dfa4356c6741bf7aa7bebe8442fd1b3bb7cacb8 Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Sat, 22 Feb 2025 22:03:15 +0000 Subject: [PATCH 13/18] fix coverage --- uvicorn/protocols/utils.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 6b9766ce7..0a0fdd7f5 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -78,19 +78,13 @@ def get_tls_info(transport: asyncio.Transport, server_config: Config) -> dict[ob ssl_info["server_cert"] = server_config.ssl_cert_pem ssl_object = transport.get_extra_info("ssl_object") - if not ssl_object: - return ssl_info - - if sys.version_info < (3, 13): - peer_cert = ssl_object.getpeercert(binary_form=True) - if peer_cert: - ssl_info["client_cert_chain"].append(ssl.DER_cert_to_PEM_cert(peer_cert)) - else: - client_chain = ssl_object.get_verified_chain() + if ssl_object is not None: + client_chain = ssl_object.get_verified_chain() if hasattr(ssl_object, "get_verified_chain") else [ssl_object.getpeercert(binary_form=True)] for cert in client_chain: - ssl_info["client_cert_chain"].append(ssl.DER_cert_to_PEM_cert(cert)) - - ssl_info["tls_version"] = ssl_object.version() - ssl_info["cipher_suite"] = ssl_object.cipher()[0] if ssl_object.cipher() else None + if cert is not None: + ssl_info["client_cert_chain"].append(ssl.DER_cert_to_PEM_cert(cert)) + + ssl_info["tls_version"] = ssl_object.version() + ssl_info["cipher_suite"] = ssl_object.cipher()[0] if ssl_object.cipher() else None - return ssl_info + return ssl_info \ No newline at end of file From 534e8d1d9f529915e935a93e70a90327f645dbf6 Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Sat, 22 Feb 2025 22:13:01 +0000 Subject: [PATCH 14/18] run linting --- tests/conftest.py | 17 ++++------------- tests/test_ssl.py | 11 +++++------ uvicorn/protocols/http/h11_impl.py | 10 +++++----- uvicorn/protocols/http/httptools_impl.py | 17 ++++++----------- uvicorn/protocols/utils.py | 24 +++++++++++++++++------- 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d52fc23cd..4d2c46a22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,24 +113,20 @@ def tls_ca_ssl_context(tls_certificate_authority: trustme.CA) -> ssl.SSLContext: tls_certificate_authority.configure_trust(ssl_ctx) return ssl_ctx + @pytest.fixture -def tls_client_ssl_context(tls_certificate_authority: trustme.CA, tls_client_certificate: trustme.LeafCert) -> ssl.SSLContext: +def tls_client_ssl_context( + tls_certificate_authority: trustme.CA, tls_client_certificate: trustme.LeafCert +) -> ssl.SSLContext: ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) tls_certificate_authority.configure_trust(ssl_ctx) # Load the client certificate chain into the SSL context with tls_client_certificate.private_key_and_cert_chain_pem.tempfile() as client_cert_pem: ssl_ctx.load_cert_chain(certfile=client_cert_pem) - return ssl_ctx -@pytest.fixture -def tls_client_certificate_pem_path(tls_client_certificate: trustme.LeafCert): - private_key_and_cert_chain = tls_client_certificate.private_key_and_cert_chain_pem - with private_key_and_cert_chain.tempfile() as client_cert_pem: - yield client_cert_pem - @pytest.fixture(scope="package") def reload_directory_structure(tmp_path_factory: pytest.TempPathFactory): @@ -283,8 +279,3 @@ def ws_protocol_cls(request: pytest.FixtureRequest): ) def http_protocol_cls(request: pytest.FixtureRequest): return import_from_string(request.param) -@pytest.fixture -def tls_client_certificate_pem_path(tls_client_certificate: trustme.LeafCert): - private_key_and_cert_chain = tls_client_certificate.private_key_and_cert_chain_pem - with private_key_and_cert_chain.tempfile() as client_cert_pem: - yield client_cert_pem diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 79de96546..ddb5dcc14 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -2,7 +2,6 @@ import httpx import pytest - from cryptography import x509 from tests.utils import run_server @@ -46,8 +45,6 @@ async def test_run( ], indirect=["tls_client_certificate"], ) - - @pytest.mark.anyio async def test_run_httptools_client_cert( tls_client_ssl_context, @@ -59,11 +56,13 @@ async def test_run_httptools_client_cert( async def app(scope, receive, send): assert scope["type"] == "http" assert len(scope["extensions"]["tls"]["client_cert_chain"]) >= 1 - cert = x509.load_pem_x509_certificate(scope["extensions"]["tls"]["client_cert_chain"][0].encode('utf-8')) + cert = x509.load_pem_x509_certificate(scope["extensions"]["tls"]["client_cert_chain"][0].encode("utf-8")) assert cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value == expected_common_name - cipher_suites = [cipher['name'] for cipher in ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).get_ciphers()] + cipher_suites = [cipher["name"] for cipher in ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).get_ciphers()] assert scope["extensions"]["tls"]["cipher_suite"] in cipher_suites - assert (scope["extensions"]["tls"]["tls_version"].startswith("TLSv") or scope["extensions"]["tls"]["tls_version"].startswith("SSLv")) + assert scope["extensions"]["tls"]["tls_version"].startswith("TLSv") or scope["extensions"]["tls"][ + "tls_version" + ].startswith("SSLv") await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index fe0bfe9a3..9da876575 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -20,8 +20,8 @@ ) from uvicorn.config import Config from uvicorn.logging import TRACE_LOG_LEVEL -from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable -from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.protocols.http.flow_control import CLOSE_HEADER,HIGH_WATER_LIMIT,FlowControl, service_unavailable +from uvicorn.protocols.utils import TLSInfo, get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, get_tls_info, is_ssl from uvicorn.server import ServerState @@ -78,7 +78,7 @@ def __init__( self.server: tuple[str, int] | None = None self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None - self.tls: dict[object, object] = {} + self.tls: TLSInfo = TLSInfo() # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -220,8 +220,8 @@ def handle_events(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = self.tls - + self.scope["extensions"]["tls"] = cast(dict[object, object], self.tls) + if self._should_upgrade(): self.handle_websocket_upgrade(event) return diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 8668ef0ba..5dfef5a7e 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -7,12 +7,7 @@ import urllib from asyncio.events import TimerHandle from collections import deque -from typing import ( - Any, - Callable, - Literal, - cast, -) +from typing import Any, Callable, Literal, cast import httptools @@ -27,7 +22,7 @@ from uvicorn.config import Config from uvicorn.logging import TRACE_LOG_LEVEL from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable -from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, get_tls_info, is_ssl from uvicorn.server import ServerState HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]') @@ -92,7 +87,7 @@ def __init__( self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque() - self.tls: dict[object, object] = {} + self.tls: TLSInfo = TLSInfo() # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -113,7 +108,7 @@ def connection_made( # type: ignore[override] self.scheme = "https" if is_ssl(transport) else "http" if self.config.is_ssl: - self.tls = get_tls_info(transport,self.config) + self.tls = get_tls_info(transport, self.config) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" @@ -239,11 +234,11 @@ def on_message_begin(self) -> None: "root_path": self.root_path, "headers": self.headers, "state": self.app_state.copy(), - "extensions": {}, + "extensions": {}, } if self.config.is_ssl: - self.scope["extensions"]["tls"] = self.tls + self.scope["extensions"]["tls"] = cast(dict[object, object], self.tls) # Parser callbacks def on_url(self, url: bytes) -> None: diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 0a0fdd7f5..058db245f 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -3,7 +3,7 @@ import asyncio import ssl import urllib.parse -import sys +from typing import TypedDict from uvicorn._types import WWWScope from uvicorn.config import Config @@ -59,16 +59,22 @@ def get_path_with_query_string(scope: WWWScope) -> str: return path_with_query_string +class TLSInfo(TypedDict, total=False): + server_cert: str | None + client_cert_chain: list[str] + tls_version: str | None + cipher_suite: str | None -def get_tls_info(transport: asyncio.Transport, server_config: Config) -> dict[object, object]: + +def get_tls_info(transport: asyncio.Transport, server_config: Config) -> TLSInfo: ### # server_cert: Unable to set from transport information, need to set from server_config - # client_cert_chain: + # client_cert_chain: # tls_version: # cipher_suite: ### - ssl_info: dict[object, object] = { + ssl_info: TLSInfo = { "server_cert": None, "client_cert_chain": [], "tls_version": None, @@ -79,12 +85,16 @@ def get_tls_info(transport: asyncio.Transport, server_config: Config) -> dict[ob ssl_object = transport.get_extra_info("ssl_object") if ssl_object is not None: - client_chain = ssl_object.get_verified_chain() if hasattr(ssl_object, "get_verified_chain") else [ssl_object.getpeercert(binary_form=True)] + client_chain = ( + ssl_object.get_verified_chain() + if hasattr(ssl_object, "get_verified_chain") + else [ssl_object.getpeercert(binary_form=True)] + ) for cert in client_chain: if cert is not None: ssl_info["client_cert_chain"].append(ssl.DER_cert_to_PEM_cert(cert)) - + ssl_info["tls_version"] = ssl_object.version() ssl_info["cipher_suite"] = ssl_object.cipher()[0] if ssl_object.cipher() else None - return ssl_info \ No newline at end of file + return ssl_info From 635f6ffb1224eb3ea6acd2450b86e877e97ae520 Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Tue, 11 Mar 2025 19:29:36 +0000 Subject: [PATCH 15/18] Move tlsInfo directly to the scope, drop the attribute --- uvicorn/protocols/http/h11_impl.py | 6 +----- uvicorn/protocols/http/httptools_impl.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 9da876575..01d361c89 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -78,7 +78,6 @@ def __init__( self.server: tuple[str, int] | None = None self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None - self.tls: TLSInfo = TLSInfo() # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -97,9 +96,6 @@ def connection_made( # type: ignore[override] self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" - if self.config.is_ssl: - self.tls = get_tls_info(transport, self.config) - if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) @@ -220,7 +216,7 @@ def handle_events(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = cast(dict[object, object], self.tls) + self.scope["extensions"]["tls"] = get_tls_info(self.transport, self.config) if self._should_upgrade(): self.handle_websocket_upgrade(event) diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 5dfef5a7e..79251f72a 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -87,7 +87,6 @@ def __init__( self.client: tuple[str, int] | None = None self.scheme: Literal["http", "https"] | None = None self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque() - self.tls: TLSInfo = TLSInfo() # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -107,9 +106,6 @@ def connection_made( # type: ignore[override] self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" - if self.config.is_ssl: - self.tls = get_tls_info(transport, self.config) - if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % self.client if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) @@ -238,7 +234,7 @@ def on_message_begin(self) -> None: } if self.config.is_ssl: - self.scope["extensions"]["tls"] = cast(dict[object, object], self.tls) + self.scope["extensions"]["tls"] = get_tls_info(self.transport, self.config) # Parser callbacks def on_url(self, url: bytes) -> None: From 3186a0dbeee35909df81ced9d44b27ab961c8c7f Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Tue, 11 Mar 2025 19:39:37 +0000 Subject: [PATCH 16/18] move TLSInfo to _types --- uvicorn/_types.py | 13 ++++++++++++- uvicorn/protocols/utils.py | 14 +++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/uvicorn/_types.py b/uvicorn/_types.py index c927cc11d..ad144b4a3 100644 --- a/uvicorn/_types.py +++ b/uvicorn/_types.py @@ -53,6 +53,17 @@ class ASGIVersions(TypedDict): version: Literal["2.0"] | Literal["3.0"] +class TLSExtensionInfo(TypedDict, total=False): + server_cert: str | None + client_cert_chain: list[str] + tls_version: str | None + cipher_suite: str | None + + +class Extensions(TypedDict, total=False): + tls: TLSExtensionInfo + + class HTTPScope(TypedDict): type: Literal["http"] asgi: ASGIVersions @@ -67,7 +78,7 @@ class HTTPScope(TypedDict): client: tuple[str, int] | None server: tuple[str, int | None] | None state: NotRequired[dict[str, Any]] - extensions: NotRequired[dict[str, dict[object, object]]] + extensions: NotRequired[Extensions] class WebSocketScope(TypedDict): diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 058db245f..581f046fd 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -3,9 +3,8 @@ import asyncio import ssl import urllib.parse -from typing import TypedDict -from uvicorn._types import WWWScope +from uvicorn._types import TLSExtensionInfo, WWWScope from uvicorn.config import Config @@ -59,14 +58,7 @@ def get_path_with_query_string(scope: WWWScope) -> str: return path_with_query_string -class TLSInfo(TypedDict, total=False): - server_cert: str | None - client_cert_chain: list[str] - tls_version: str | None - cipher_suite: str | None - - -def get_tls_info(transport: asyncio.Transport, server_config: Config) -> TLSInfo: +def get_tls_info(transport: asyncio.Transport, server_config: Config) -> TLSExtensionInfo: ### # server_cert: Unable to set from transport information, need to set from server_config # client_cert_chain: @@ -74,7 +66,7 @@ def get_tls_info(transport: asyncio.Transport, server_config: Config) -> TLSInfo # cipher_suite: ### - ssl_info: TLSInfo = { + ssl_info: TLSExtensionInfo = { "server_cert": None, "client_cert_chain": [], "tls_version": None, From 1fc2bebb7c75468686049a9c69bb97f5bce29ea8 Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Tue, 11 Mar 2025 22:02:12 +0000 Subject: [PATCH 17/18] cosmetic change --- uvicorn/protocols/http/h11_impl.py | 11 +++++++++-- uvicorn/protocols/http/httptools_impl.py | 9 ++++++++- uvicorn/protocols/utils.py | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 01d361c89..2794110d9 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -20,8 +20,15 @@ ) from uvicorn.config import Config from uvicorn.logging import TRACE_LOG_LEVEL -from uvicorn.protocols.http.flow_control import CLOSE_HEADER,HIGH_WATER_LIMIT,FlowControl, service_unavailable -from uvicorn.protocols.utils import TLSInfo, get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, get_tls_info, is_ssl +from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import ( + get_client_addr, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + get_tls_info, + is_ssl, +) from uvicorn.server import ServerState diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 79251f72a..f00bd14da 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -22,7 +22,14 @@ from uvicorn.config import Config from uvicorn.logging import TRACE_LOG_LEVEL from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable -from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, get_tls_info, is_ssl +from uvicorn.protocols.utils import ( + get_client_addr, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + get_tls_info, + is_ssl, +) from uvicorn.server import ServerState HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]') diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 581f046fd..b71c803b8 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -58,7 +58,7 @@ def get_path_with_query_string(scope: WWWScope) -> str: return path_with_query_string -def get_tls_info(transport: asyncio.Transport, server_config: Config) -> TLSExtensionInfo: +def get_tls_info(transport: asyncio.Transport, config: Config) -> TLSExtensionInfo: ### # server_cert: Unable to set from transport information, need to set from server_config # client_cert_chain: @@ -73,7 +73,7 @@ def get_tls_info(transport: asyncio.Transport, server_config: Config) -> TLSExte "cipher_suite": None, } - ssl_info["server_cert"] = server_config.ssl_cert_pem + ssl_info["server_cert"] = config.ssl_cert_pem ssl_object = transport.get_extra_info("ssl_object") if ssl_object is not None: From d37661981f8200be3563ddb181448ee1b79889a4 Mon Sep 17 00:00:00 2001 From: Eirik Hexeberg Henriksen Date: Tue, 11 Mar 2025 22:37:25 +0000 Subject: [PATCH 18/18] use unused_tcp_port in tests --- tests/test_ssl.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index ddb5dcc14..0ea37e5c1 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -52,6 +52,7 @@ async def test_run_httptools_client_cert( tls_certificate_private_key_path, tls_ca_certificate_pem_path, expected_common_name, + unused_tcp_port: int, ): async def app(scope, receive, send): assert scope["type"] == "http" @@ -76,10 +77,11 @@ async def app(scope, receive, send): ssl_certfile=tls_certificate_server_cert_path, ssl_ca_certs=tls_ca_certificate_pem_path, ssl_cert_reqs=ssl.CERT_REQUIRED, + port=unused_tcp_port, ) async with run_server(config): async with httpx.AsyncClient(verify=tls_client_ssl_context) as client: - response = await client.get("https://127.0.0.1:8000") + response = await client.get(f"https://127.0.0.1:{unused_tcp_port}") assert response.status_code == 204 @@ -89,6 +91,7 @@ async def test_run_h11_client_cert( tls_ca_certificate_pem_path, tls_certificate_server_cert_path, tls_certificate_private_key_path, + unused_tcp_port: int, ): config = Config( app=app, @@ -99,10 +102,11 @@ async def test_run_h11_client_cert( ssl_certfile=tls_certificate_server_cert_path, ssl_ca_certs=tls_ca_certificate_pem_path, ssl_cert_reqs=ssl.CERT_REQUIRED, + port=unused_tcp_port, ) async with run_server(config): async with httpx.AsyncClient(verify=tls_client_ssl_context) as client: - response = await client.get("https://127.0.0.1:8000") + response = await client.get(f"https://127.0.0.1:{unused_tcp_port}") assert response.status_code == 204