Files
baijiahao_data_crawl/venv/Lib/site-packages/mitmproxy/net/tls.py

331 lines
10 KiB
Python
Raw Normal View History

2025-12-25 11:16:59 +08:00
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