From aa26b2c76819942d893793b48d649177e9e27601 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Sun, 11 Jul 2021 15:08:03 -0400 Subject: [PATCH 01/11] Implement asgiref tls extension --- tests/conftest.py | 14 +++++ tests/test_ssl.py | 52 +++++++++++++++++ uvicorn/config.py | 3 + uvicorn/protocols/http/h11_impl.py | 8 +++ uvicorn/protocols/http/httptools_impl.py | 10 ++++ uvicorn/protocols/utils.py | 74 +++++++++++++++++++++++- 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a5e5a8e12..9ce72c32c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,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: @@ -59,3 +66,10 @@ def tls_ca_ssl_context(tls_certificate: trustme.LeafCert) -> ssl.SSLContext: ssl_ctx = ssl.SSLContext() tls_certificate.configure_cert(ssl_ctx) 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 diff --git a/tests/test_ssl.py b/tests/test_ssl.py index d44e5de49..397a636be 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -1,3 +1,5 @@ +import ssl + import httpx import pytest @@ -29,6 +31,56 @@ async def test_run( assert response.status_code == 204 +@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_pem_path, tls_ca_certificate_pem_path diff --git a/uvicorn/config.py b/uvicorn/config.py index 98430e867..8c3eef637 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -213,6 +213,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 @@ -316,6 +317,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 04b997ffe..5b3d5b1d3 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -18,6 +18,7 @@ get_local_addr, get_path_with_query_string, get_remote_addr, + get_tls_info, is_ssl, ) @@ -69,6 +70,7 @@ def __init__( self.server = None self.client = None self.scheme = None + self.tls = None # Per-request state self.scope = None @@ -85,6 +87,11 @@ def connection_made(self, transport): 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 - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) @@ -171,6 +178,7 @@ def handle_events(self): "raw_path": raw_path, "query_string": query_string, "headers": self.headers, + "extensions": {"tls": self.tls}, } for name, value in self.headers: diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 557bd4f2a..ff4a57b91 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -19,6 +19,7 @@ get_local_addr, get_path_with_query_string, get_remote_addr, + get_tls_info, is_ssl, ) @@ -75,6 +76,7 @@ def __init__( self.client = None self.scheme = None self.pipeline = [] + self.tls = None # Per-request state self.url = None @@ -93,6 +95,11 @@ def connection_made(self, transport): 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 - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) @@ -211,6 +218,9 @@ def on_url(self, url): "raw_path": raw_path, "query_string": parsed_url.query if parsed_url.query else b"", "headers": self.headers, + "extensions": { + "tls": self.tls, + }, } def on_header(self, name: bytes, value: bytes): diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index bbc84f7aa..d0a730a9c 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -1,9 +1,29 @@ import asyncio +import ssl import urllib.parse -from typing import Optional, Tuple +from typing import Any, Dict, Optional, Tuple from asgiref.typing 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, +} + def get_remote_addr(transport: asyncio.Transport) -> Optional[Tuple[str, int]]: socket_info = transport.get_extra_info("socket") @@ -54,3 +74,55 @@ def get_path_with_query_string(scope: WWWScope) -> str: 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 3ae8704476e38d4d9007d42eb345e27f8cd97f16 Mon Sep 17 00:00:00 2001 From: Matt Gilen Date: Sun, 18 Jul 2021 16:10:35 +0000 Subject: [PATCH 02/11] Only add tls extension if connection is over tls --- uvicorn/protocols/http/h11_impl.py | 5 ++++- uvicorn/protocols/http/httptools_impl.py | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index 5b3d5b1d3..35b257e46 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -178,9 +178,12 @@ def handle_events(self): "raw_path": raw_path, "query_string": query_string, "headers": self.headers, - "extensions": {"tls": self.tls}, + "extensions": {}, } + if self.config.is_ssl: + self.scope["extensions"]["tls"] = self.tls + for name, value in self.headers: if name == b"connection": tokens = [token.lower().strip() for token in value.split(b",")] diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index ff4a57b91..e19248eb1 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -218,11 +218,12 @@ def on_url(self, url): "raw_path": raw_path, "query_string": parsed_url.query if parsed_url.query else b"", "headers": self.headers, - "extensions": { - "tls": self.tls, - }, + "extensions": {}, } + if self.config.is_ssl: + self.scope["extensions"]["tls"] = self.tls + def on_header(self, name: bytes, value: bytes): name = name.lower() if name == b"expect" and value.lower() == b"100-continue": From 053dc4f4943a977ec3430d2f0c31b909cc8dd11e Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 6 Dec 2021 08:28:05 -0500 Subject: [PATCH 03/11] Fix formatting issues --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 07f64b12a..f0efcc5b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,8 +85,8 @@ 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 a84dc939eed4678304c751d6537f90d720f55da4 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Sat, 4 Mar 2023 16:43:40 +0000 Subject: [PATCH 04/11] Address linting issues and fix tests --- tests/test_ssl.py | 16 +++++++++------- uvicorn/protocols/http/h11_impl.py | 6 +++--- uvicorn/protocols/http/httptools_impl.py | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index b6ed6d2ae..454264c61 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -39,8 +39,9 @@ async def test_run( @pytest.mark.anyio 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 4d0bd9dd6..3441e0cb6 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -2,7 +2,7 @@ import http import logging import sys -from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union, cast from urllib.parse import unquote import h11 @@ -240,8 +240,8 @@ 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 + upgrade = self._get_upgrade() if upgrade == b"websocket" and self._should_upgrade_to_ws(): self.handle_websocket_upgrade(event) diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 479e1d5db..5055b87f9 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -6,7 +6,17 @@ import urllib from asyncio.events import TimerHandle from collections import deque -from typing import TYPE_CHECKING, Callable, Deque, List, Optional, Tuple, Union, cast +from typing import ( + TYPE_CHECKING, + Callable, + Deque, + Dict, + List, + Optional, + Tuple, + Union, + cast, +) import httptools @@ -248,7 +258,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 5c7b5392561b584400bcb39b2e48c7df2207efab Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Fri, 24 May 2024 22:28:33 -0400 Subject: [PATCH 05/11] 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 | 14 ++++-------- uvicorn/protocols/utils.py | 28 +++++++----------------- 6 files changed, 18 insertions(+), 44 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f1f48b8d9..2a194b767 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,9 +58,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 f6f5adb64..c3b512739 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 32557bf0f..0856cdeae 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -259,7 +259,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 a2868fdea..7ff1a582b 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -3,8 +3,6 @@ import asyncio import http import logging -import sys -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union, cast from typing import Any, Callable, Literal, cast from urllib.parse import unquote @@ -92,7 +90,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 | None = None + self.tls: dict[object, object] = {} # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -229,7 +227,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 upgrade = self._get_upgrade() if upgrade == b"websocket" and self._should_upgrade_to_ws(): diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index fae8fa73b..31575af7f 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -8,17 +8,11 @@ from asyncio.events import TimerHandle from collections import deque from typing import ( - TYPE_CHECKING, + Any, Callable, - Deque, - Dict, - List, - Optional, - Tuple, - Union, + Literal, cast, ) -from typing import Any, Callable, Literal, cast import httptools @@ -102,7 +96,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 | None = None + self.tls: dict[object, object] = {} # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -253,7 +247,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 6ac829a76..061adba1b 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 TYPE_CHECKING, 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 02c53b435b24c1144f3687089b9eccb53e361094 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 27 May 2024 15:49:48 +0200 Subject: [PATCH 06/11] 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 b4da8a2432ed800bb37391484311614a37bd73a7 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 10:35:58 -0400 Subject: [PATCH 07/11] 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 19a5c1ecd08e47b84ae42db3bb63a5229a645212 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 11:14:33 -0400 Subject: [PATCH 08/11] 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 061adba1b..24c3a9ec2 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 eb60eb87812b8f19e45946738ebc06d90272665d Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 11:59:53 -0400 Subject: [PATCH 09/11] Add test for escaping --- tests/conftest.py | 4 ++-- tests/test_ssl.py | 15 +++++++++++++++ uvicorn/protocols/utils.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2a194b767..bb380c6b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,8 +57,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 c3b512739..84e32e858 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -37,13 +37,28 @@ async def test_run( @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"], +) 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_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 24c3a9ec2..7c787bba2 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 9f05147d128ce479d2b6f25b7b546fd23e191280 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Mon, 27 May 2024 12:02:29 -0400 Subject: [PATCH 10/11] 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 bb380c6b6..b795192be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,7 +58,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 050d86288e85b1ea8bf98d8326545395a05c2ca3 Mon Sep 17 00:00:00 2001 From: Matt Gilene Date: Wed, 29 May 2024 10:34:34 -0400 Subject: [PATCH 11/11] 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 7c787bba2..da6eb7681 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