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/tests/conftest.py b/tests/conftest.py index b1214061a..b795192be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,13 @@ 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") + ) + + @pytest.fixture def tls_ca_certificate_pem_path(tls_certificate_authority: trustme.CA): with tls_certificate_authority.cert_pem.tempfile() as ca_cert_pem: @@ -109,6 +116,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): """ diff --git a/tests/test_ssl.py b/tests/test_ssl.py index da60bb8dd..84e32e858 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -1,3 +1,5 @@ +import ssl + import httpx import pytest @@ -34,6 +36,69 @@ 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"], +) +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", + http="httptools", + limit_max_requests=1, + 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, + ) + 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.anyio +async def test_run_h11_client_cert( + tls_ca_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, + loop="asyncio", + http="h11", + limit_max_requests=1, + 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, + ) + 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.anyio async def test_run_chain( tls_ca_ssl_context, diff --git a/tools/generate_tls_const.py b/tools/generate_tls_const.py new file mode 100644 index 000000000..eb704fcd9 --- /dev/null +++ b/tools/generate_tls_const.py @@ -0,0 +1,49 @@ +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/config.py b/uvicorn/config.py index fca8d5fd2..0856cdeae 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -259,6 +259,7 @@ def __init__( self.callback_notify = callback_notify self.ssl_keyfile = ssl_keyfile self.ssl_certfile = ssl_certfile + 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 @@ -406,6 +407,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 a2735fc78..7ff1a582b 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -20,8 +20,20 @@ ) 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 ( + 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,6 +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[object, object] = {} # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -96,6 +109,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) @@ -205,8 +223,12 @@ def handle_events(self) -> None: "query_string": query_string, "headers": self.headers, "state": self.app_state.copy(), + "extensions": {}, } + if self.config.is_ssl: + self.scope["extensions"]["tls"] = self.tls + 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 6dff0d631..5d0d94749 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 @@ -21,8 +26,20 @@ ) 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 ( + 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\\"]') @@ -79,6 +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[object, object] = {} # Per-request state self.scope: HTTPScope = None # type: ignore[assignment] @@ -98,6 +116,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) @@ -220,8 +243,12 @@ def on_message_begin(self) -> None: "root_path": self.root_path, "headers": self.headers, "state": self.app_state.copy(), + "extensions": {}, } + if self.config.is_ssl: + self.scope["extensions"]["tls"] = self.tls + # Parser callbacks def on_url(self, url: bytes) -> None: self.url += url diff --git a/uvicorn/protocols/http/tls_const.py b/uvicorn/protocols/http/tls_const.py new file mode 100644 index 000000000..472b80c20 --- /dev/null +++ b/uvicorn/protocols/http/tls_const.py @@ -0,0 +1,360 @@ +# 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 4845e1f7b..da6eb7681 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -1,9 +1,30 @@ from __future__ import annotations import asyncio +import ssl 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", + "countryName": "C", + "userId": "UID", +} + +TLS_VERSION_MAP: dict[str, int] = { + "TLSv1": 0x0301, + "TLSv1.1": 0x0302, + "TLSv1.2": 0x0303, + "TLSv1.3": 0x0304, +} class ClientDisconnected(IOError): ... @@ -54,3 +75,74 @@ 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 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 + # 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: + ### + + 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) + + return ssl_info