Files
baijiahao_data_crawl/venv/Lib/site-packages/mitmproxy/net/tls.py
“shengyudong” 322ac74336 2025-12-25 upload
2025-12-25 11:16:59 +08:00

331 lines
10 KiB
Python

import os
import threading
import typing
from collections.abc import Callable
from collections.abc import Iterable
from enum import Enum
from functools import cache
from functools import lru_cache
from pathlib import Path
from typing import Any
from typing import BinaryIO
import certifi
import OpenSSL
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurveOID
from cryptography.hazmat.primitives.asymmetric.ec import get_curve_for_oid
from cryptography.x509 import ObjectIdentifier
from OpenSSL import SSL
from mitmproxy import certs
# Remove once pyOpenSSL 23.3.0 is released and bump version in pyproject.toml.
try: # pragma: no cover
from OpenSSL.SSL import OP_LEGACY_SERVER_CONNECT # type: ignore
except ImportError:
OP_LEGACY_SERVER_CONNECT = 0x4
# redeclared here for strict type checking
class Method(Enum):
TLS_SERVER_METHOD = SSL.TLS_SERVER_METHOD
TLS_CLIENT_METHOD = SSL.TLS_CLIENT_METHOD
# Type-pyopenssl does not know about these DTLS constants.
DTLS_SERVER_METHOD = SSL.DTLS_SERVER_METHOD # type: ignore
DTLS_CLIENT_METHOD = SSL.DTLS_CLIENT_METHOD # type: ignore
try:
SSL._lib.TLS_server_method # type: ignore
except AttributeError as e: # pragma: no cover
raise RuntimeError(
"Your installation of the cryptography Python package is outdated."
) from e
class Version(Enum):
UNBOUNDED = 0
SSL3 = SSL.SSL3_VERSION
TLS1 = SSL.TLS1_VERSION
TLS1_1 = SSL.TLS1_1_VERSION
TLS1_2 = SSL.TLS1_2_VERSION
TLS1_3 = SSL.TLS1_3_VERSION
INSECURE_TLS_MIN_VERSIONS: tuple[Version, ...] = (
Version.UNBOUNDED,
Version.SSL3,
Version.TLS1,
Version.TLS1_1,
)
class Verify(Enum):
VERIFY_NONE = SSL.VERIFY_NONE
VERIFY_PEER = SSL.VERIFY_PEER
DEFAULT_MIN_VERSION = Version.TLS1_2
DEFAULT_MAX_VERSION = Version.UNBOUNDED
DEFAULT_OPTIONS = SSL.OP_CIPHER_SERVER_PREFERENCE | SSL.OP_NO_COMPRESSION
@cache
def is_supported_version(version: Version):
client_ctx = SSL.Context(SSL.TLS_CLIENT_METHOD)
# Without SECLEVEL, recent OpenSSL versions forbid old TLS versions.
# https://github.com/pyca/cryptography/issues/9523
client_ctx.set_cipher_list(b"@SECLEVEL=0:ALL")
client_ctx.set_min_proto_version(version.value)
client_ctx.set_max_proto_version(version.value)
client_conn = SSL.Connection(client_ctx)
client_conn.set_connect_state()
try:
client_conn.recv(4096)
except SSL.WantReadError:
return True
except SSL.Error:
return False
EC_CURVES: dict[str, EllipticCurve] = {}
for oid in EllipticCurveOID.__dict__.values():
if isinstance(oid, ObjectIdentifier):
curve = get_curve_for_oid(oid)()
EC_CURVES[curve.name] = curve
@typing.overload
def get_curve(name: str) -> EllipticCurve: ...
@typing.overload
def get_curve(name: None) -> None: ...
def get_curve(name: str | None) -> EllipticCurve | None:
if name is None:
return None
return EC_CURVES[name]
class MasterSecretLogger:
def __init__(self, filename: Path):
self.filename = filename.expanduser()
self.f: BinaryIO | None = None
self.lock = threading.Lock()
# required for functools.wraps, which pyOpenSSL uses.
__name__ = "MasterSecretLogger"
def __call__(self, connection: SSL.Connection, keymaterial: bytes) -> None:
with self.lock:
if self.f is None:
self.filename.parent.mkdir(parents=True, exist_ok=True)
self.f = self.filename.open("ab")
self.f.write(b"\n")
self.f.write(keymaterial + b"\n")
self.f.flush()
def close(self):
with self.lock:
if self.f is not None:
self.f.close()
def make_master_secret_logger(filename: str | None) -> MasterSecretLogger | None:
if filename:
return MasterSecretLogger(Path(filename))
return None
log_master_secret = make_master_secret_logger(
os.getenv("MITMPROXY_SSLKEYLOGFILE") or os.getenv("SSLKEYLOGFILE")
)
def _create_ssl_context(
*,
method: Method,
min_version: Version,
max_version: Version,
cipher_list: Iterable[str] | None,
ecdh_curve: EllipticCurve | None,
) -> SSL.Context:
context = SSL.Context(method.value)
ok = SSL._lib.SSL_CTX_set_min_proto_version(context._context, min_version.value) # type: ignore
ok += SSL._lib.SSL_CTX_set_max_proto_version(context._context, max_version.value) # type: ignore
if ok != 2:
raise RuntimeError(
f"Error setting TLS versions ({min_version=}, {max_version=}). "
"The version you specified may be unavailable in your libssl."
)
# Options
context.set_options(DEFAULT_OPTIONS)
# ECDHE for Key exchange
if ecdh_curve is not None:
try:
context.set_tmp_ecdh(ecdh_curve)
except ValueError as e:
raise RuntimeError(f"Elliptic curve specification error: {e}") from e
# Cipher List
if cipher_list is not None:
try:
context.set_cipher_list(b":".join(x.encode() for x in cipher_list))
except SSL.Error as e:
raise RuntimeError(f"SSL cipher specification error: {e}") from e
# SSLKEYLOGFILE
if log_master_secret:
context.set_keylog_callback(log_master_secret)
return context
@lru_cache(256)
def create_proxy_server_context(
*,
method: Method,
min_version: Version,
max_version: Version,
cipher_list: tuple[str, ...] | None,
ecdh_curve: EllipticCurve | None,
verify: Verify,
ca_path: str | None,
ca_pemfile: str | None,
client_cert: str | None,
legacy_server_connect: bool,
) -> SSL.Context:
context: SSL.Context = _create_ssl_context(
method=method,
min_version=min_version,
max_version=max_version,
cipher_list=cipher_list,
ecdh_curve=ecdh_curve,
)
context.set_verify(verify.value, None)
if ca_path is None and ca_pemfile is None:
ca_pemfile = certifi.where()
try:
context.load_verify_locations(ca_pemfile, ca_path)
except SSL.Error as e:
raise RuntimeError(
f"Cannot load trusted certificates ({ca_pemfile=}, {ca_path=})."
) from e
# Client Certs
if client_cert:
try:
context.use_privatekey_file(client_cert)
context.use_certificate_chain_file(client_cert)
except SSL.Error as e:
raise RuntimeError(f"Cannot load TLS client certificate: {e}") from e
# https://github.com/mitmproxy/mitmproxy/discussions/7550
SSL._lib.SSL_CTX_set_post_handshake_auth(context._context, 1) # type: ignore
if legacy_server_connect:
context.set_options(OP_LEGACY_SERVER_CONNECT)
return context
@lru_cache(256)
def create_client_proxy_context(
*,
method: Method,
min_version: Version,
max_version: Version,
cipher_list: tuple[str, ...] | None,
ecdh_curve: EllipticCurve | None,
chain_file: Path | None,
alpn_select_callback: Callable[[SSL.Connection, list[bytes]], Any] | None,
request_client_cert: bool,
extra_chain_certs: tuple[certs.Cert, ...],
dhparams: certs.DHParams,
) -> SSL.Context:
context: SSL.Context = _create_ssl_context(
method=method,
min_version=min_version,
max_version=max_version,
cipher_list=cipher_list,
ecdh_curve=ecdh_curve,
)
if chain_file is not None:
try:
context.load_verify_locations(str(chain_file), None)
except SSL.Error as e:
raise RuntimeError(f"Cannot load certificate chain ({chain_file}).") from e
if alpn_select_callback is not None:
assert callable(alpn_select_callback)
context.set_alpn_select_callback(alpn_select_callback)
if request_client_cert:
# The request_client_cert argument requires some explanation. We're
# supposed to be able to do this with no negative effects - if the
# client has no cert to present, we're notified and proceed as usual.
# Unfortunately, Android seems to have a bug (tested on 4.2.2) - when
# an Android client is asked to present a certificate it does not
# have, it hangs up, which is frankly bogus. Some time down the track
# we may be able to make the proper behaviour the default again, but
# until then we're conservative.
context.set_verify(Verify.VERIFY_PEER.value, accept_all)
else:
context.set_verify(Verify.VERIFY_NONE.value, None)
for i in extra_chain_certs:
context.add_extra_chain_cert(i.to_cryptography())
if dhparams:
res = SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) # type: ignore
SSL._openssl_assert(res == 1) # type: ignore
return context
def accept_all(
conn_: SSL.Connection,
x509: OpenSSL.crypto.X509,
errno: int,
err_depth: int,
is_cert_verified: int,
) -> bool:
# Return true to prevent cert verification error
return True
def starts_like_tls_record(d: bytes) -> bool:
"""
Returns:
True, if the passed bytes could be the start of a TLS record
False, otherwise.
"""
# TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2, and TLSv1.3
# http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello
# https://tls13.ulfheim.net/
# We assume that a client sending less than 3 bytes initially is not a TLS client.
return len(d) > 2 and d[0] == 0x16 and d[1] == 0x03 and 0x00 <= d[2] <= 0x03
def starts_like_dtls_record(d: bytes) -> bool:
"""
Returns:
True, if the passed bytes could be the start of a DTLS record
False, otherwise.
"""
# TLS ClientHello magic, works for DTLS 1.1, DTLS 1.2, and DTLS 1.3.
# https://www.rfc-editor.org/rfc/rfc4347#section-4.1
# https://www.rfc-editor.org/rfc/rfc6347#section-4.1
# https://www.rfc-editor.org/rfc/rfc9147#section-4-6.2
# We assume that a client sending less than 3 bytes initially is not a DTLS client.
return len(d) > 2 and d[0] == 0x16 and d[1] == 0xFE and 0xFD <= d[2] <= 0xFE