Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
tls.py693 linesDownload Raw Back to layers
1import struct2import time3import typing4from collections.abc import Iterator5from dataclasses import dataclass6from logging import DEBUG7from logging import ERROR8from logging import INFO9from logging import WARNING10 11from OpenSSL import SSL12 13from mitmproxy import certs14from mitmproxy import connection15from mitmproxy.connection import TlsVersion16from mitmproxy.net.tls import starts_like_dtls_record17from mitmproxy.net.tls import starts_like_tls_record18from mitmproxy.proxy import commands19from mitmproxy.proxy import context20from mitmproxy.proxy import events21from mitmproxy.proxy import layer22from mitmproxy.proxy import tunnel23from mitmproxy.proxy.commands import StartHook24from mitmproxy.proxy.layers import tcp25from mitmproxy.proxy.layers import udp26from mitmproxy.tls import ClientHello27from mitmproxy.tls import ClientHelloData28from mitmproxy.tls import TlsData29from mitmproxy.utils import human30 31 32def handshake_record_contents(data: bytes) -> Iterator[bytes]:33    """34    Returns a generator that yields the bytes contained in each handshake record.35    This will raise an error on the first non-handshake record, so fully exhausting this36    generator is a bad idea.37    """38    offset = 039    while True:40        if len(data) < offset + 5:41            return42        record_header = data[offset : offset + 5]43        if not starts_like_tls_record(record_header):44            raise ValueError(f"Expected TLS record, got {record_header!r} instead.")45        record_size = struct.unpack("!H", record_header[3:])[0]46        if record_size == 0:47            raise ValueError("Record must not be empty.")48        offset += 549 50        if len(data) < offset + record_size:51            return52        record_body = data[offset : offset + record_size]53        yield record_body54        offset += record_size55 56 57def get_client_hello(data: bytes) -> bytes | None:58    """59    Read all TLS records that contain the initial ClientHello.60    Returns the raw handshake packet bytes, without TLS record headers.61    """62    client_hello = b""63    for d in handshake_record_contents(data):64        client_hello += d65        if len(client_hello) >= 4:66            client_hello_size = struct.unpack("!I", b"\x00" + client_hello[1:4])[0] + 467            if len(client_hello) >= client_hello_size:68                return client_hello[:client_hello_size]69    return None70 71 72def parse_client_hello(data: bytes) -> ClientHello | None:73    """74    Check if the supplied bytes contain a full ClientHello message,75    and if so, parse it.76 77    Returns:78        - A ClientHello object on success79        - None, if the TLS record is not complete80 81    Raises:82        - A ValueError, if the passed ClientHello is invalid83    """84    # Check if ClientHello is complete85    client_hello = get_client_hello(data)86    if client_hello:87        try:88            return ClientHello(client_hello[4:])89        except EOFError as e:90            raise ValueError("Invalid ClientHello") from e91    return None92 93 94def dtls_handshake_record_contents(data: bytes) -> Iterator[bytes]:95    """96    Returns a generator that yields the bytes contained in each handshake record.97    This will raise an error on the first non-handshake record, so fully exhausting this98    generator is a bad idea.99    """100    offset = 0101    while True:102        # DTLS includes two new fields, totaling 8 bytes, between Version and Length103        if len(data) < offset + 13:104            return105        record_header = data[offset : offset + 13]106        if not starts_like_dtls_record(record_header):107            raise ValueError(f"Expected DTLS record, got {record_header!r} instead.")108        # Length fields starts at 11109        record_size = struct.unpack("!H", record_header[11:])[0]110        if record_size == 0:111            raise ValueError("Record must not be empty.")112        offset += 13113 114        if len(data) < offset + record_size:115            return116        record_body = data[offset : offset + record_size]117        yield record_body118        offset += record_size119 120 121def get_dtls_client_hello(data: bytes) -> bytes | None:122    """123    Read all DTLS records that contain the initial ClientHello.124    Returns the raw handshake packet bytes, without TLS record headers.125    """126    client_hello = b""127    for d in dtls_handshake_record_contents(data):128        client_hello += d129        if len(client_hello) >= 13:130            # comment about slicing: we skip the epoch and sequence number131            client_hello_size = (132                struct.unpack("!I", b"\x00" + client_hello[9:12])[0] + 12133            )134            if len(client_hello) >= client_hello_size:135                return client_hello[:client_hello_size]136    return None137 138 139def dtls_parse_client_hello(data: bytes) -> ClientHello | None:140    """141    Check if the supplied bytes contain a full ClientHello message,142    and if so, parse it.143 144    Returns:145        - A ClientHello object on success146        - None, if the TLS record is not complete147 148    Raises:149        - A ValueError, if the passed ClientHello is invalid150    """151    # Check if ClientHello is complete152    client_hello = get_dtls_client_hello(data)153    if client_hello:154        try:155            return ClientHello(client_hello[12:], dtls=True)156        except EOFError as e:157            raise ValueError("Invalid ClientHello") from e158    return None159 160 161HTTP1_ALPNS = (b"http/1.1", b"http/1.0", b"http/0.9")162HTTP2_ALPN = b"h2"163HTTP3_ALPN = b"h3"164HTTP_ALPNS = (HTTP3_ALPN, HTTP2_ALPN, *HTTP1_ALPNS)165 166 167# We need these classes as hooks can only have one argument at the moment.168 169 170@dataclass171class TlsClienthelloHook(StartHook):172    """173    Mitmproxy has received a TLS ClientHello message.174 175    This hook decides whether a server connection is needed176    to negotiate TLS with the client (data.establish_server_tls_first)177    """178 179    data: ClientHelloData180 181 182@dataclass183class TlsStartClientHook(StartHook):184    """185    TLS negotation between mitmproxy and a client is about to start.186 187    An addon is expected to initialize data.ssl_conn.188    (by default, this is done by `mitmproxy.addons.tlsconfig`)189    """190 191    data: TlsData192 193 194@dataclass195class TlsStartServerHook(StartHook):196    """197    TLS negotation between mitmproxy and a server is about to start.198 199    An addon is expected to initialize data.ssl_conn.200    (by default, this is done by `mitmproxy.addons.tlsconfig`)201    """202 203    data: TlsData204 205 206@dataclass207class TlsEstablishedClientHook(StartHook):208    """209    The TLS handshake with the client has been completed successfully.210    """211 212    data: TlsData213 214 215@dataclass216class TlsEstablishedServerHook(StartHook):217    """218    The TLS handshake with the server has been completed successfully.219    """220 221    data: TlsData222 223 224@dataclass225class TlsFailedClientHook(StartHook):226    """227    The TLS handshake with the client has failed.228    """229 230    data: TlsData231 232 233@dataclass234class TlsFailedServerHook(StartHook):235    """236    The TLS handshake with the server has failed.237    """238 239    data: TlsData240 241 242class TLSLayer(tunnel.TunnelLayer):243    tls: SSL.Connection = None  # type: ignore244    """The OpenSSL connection object"""245 246    def __init__(self, context: context.Context, conn: connection.Connection):247        super().__init__(248            context,249            tunnel_connection=conn,250            conn=conn,251        )252 253        conn.tls = True254 255    def __repr__(self):256        return (257            super().__repr__().replace(")", f" {self.conn.sni!r} {self.conn.alpn!r})")258        )259 260    @property261    def is_dtls(self):262        return self.conn.transport_protocol == "udp"263 264    @property265    def proto_name(self):266        return "DTLS" if self.is_dtls else "TLS"267 268    def start_tls(self) -> layer.CommandGenerator[None]:269        assert not self.tls270 271        tls_start = TlsData(self.conn, self.context, is_dtls=self.is_dtls)272        if self.conn == self.context.client:273            yield TlsStartClientHook(tls_start)274        else:275            yield TlsStartServerHook(tls_start)276        if not tls_start.ssl_conn:277            yield commands.Log(278                f"No {self.proto_name} context was provided, failing connection.", ERROR279            )280            yield commands.CloseConnection(self.conn)281            return282        assert tls_start.ssl_conn283        self.tls = tls_start.ssl_conn284 285    def tls_interact(self) -> layer.CommandGenerator[None]:286        while True:287            try:288                data = self.tls.bio_read(65535)289            except SSL.WantReadError:290                return  # Okay, nothing more waiting to be sent.291            else:292                yield commands.SendData(self.conn, data)293 294    def receive_handshake_data(295        self, data: bytes296    ) -> layer.CommandGenerator[tuple[bool, str | None]]:297        # bio_write errors for b"", so we need to check first if we actually received something.298        if data:299            self.tls.bio_write(data)300        try:301            self.tls.do_handshake()302        except SSL.WantReadError:303            yield from self.tls_interact()304            return False, None305        except SSL.Error as e:306            # provide more detailed information for some errors.307            last_err = (308                e.args and isinstance(e.args[0], list) and e.args[0] and e.args[0][-1]309            )310            if last_err in [311                (312                    "SSL routines",313                    "tls_process_server_certificate",314                    "certificate verify failed",315                ),316                ("SSL routines", "", "certificate verify failed"),  # OpenSSL 3+317            ]:318                verify_result = SSL._lib.SSL_get_verify_result(self.tls._ssl)  # type: ignore319                error = SSL._ffi.string(  # type: ignore320                    SSL._lib.X509_verify_cert_error_string(verify_result)  # type: ignore321                ).decode()322                err = f"Certificate verify failed: {error}"323            elif last_err in [324                ("SSL routines", "ssl3_read_bytes", "tlsv1 alert unknown ca"),325                ("SSL routines", "ssl3_read_bytes", "sslv3 alert bad certificate"),326                ("SSL routines", "ssl3_read_bytes", "ssl/tls alert bad certificate"),327                ("SSL routines", "", "tlsv1 alert unknown ca"),  # OpenSSL 3+328                ("SSL routines", "", "sslv3 alert bad certificate"),  # OpenSSL 3+329                ("SSL routines", "", "ssl/tls alert bad certificate"),  # OpenSSL 3.2+330            ]:331                assert isinstance(last_err, tuple)332                err = last_err[2]333            elif (334                last_err335                in [336                    ("SSL routines", "ssl3_get_record", "wrong version number"),337                    ("SSL routines", "", "wrong version number"),  # OpenSSL 3+338                    ("SSL routines", "", "packet length too long"),  # OpenSSL 3+339                    ("SSL routines", "", "record layer failure"),  # OpenSSL 3+340                ]341                and data[:4].isascii()342            ):343                err = f"The remote server does not speak TLS."344            elif last_err in [345                ("SSL routines", "ssl3_read_bytes", "tlsv1 alert protocol version"),346                ("SSL routines", "", "tlsv1 alert protocol version"),  # OpenSSL 3+347            ]:348                err = (349                    f"The remote server and mitmproxy cannot agree on a TLS version to use. "350                    f"You may need to adjust mitmproxy's tls_version_server_min option."351                )352            else:353                err = f"OpenSSL {e!r}"354            return False, err355        else:356            # Here we set all attributes that are only known *after* the handshake.357 358            # Get all peer certificates.359            # https://www.openssl.org/docs/man1.1.1/man3/SSL_get_peer_cert_chain.html360            # If called on the client side, the stack also contains the peer's certificate; if called on the server361            # side, the peer's certificate must be obtained separately using SSL_get_peer_certificate(3).362            all_certs = self.tls.get_peer_cert_chain() or []363            if self.conn == self.context.client:364                cert = self.tls.get_peer_certificate()365                if cert:366                    all_certs.insert(0, cert)367            self.conn.certificate_list = []368            for cert in all_certs:369                try:370                    # This may fail for weird certs, https://github.com/mitmproxy/mitmproxy/issues/6968.371                    parsed_cert = certs.Cert.from_pyopenssl(cert)372                except ValueError as e:373                    yield commands.Log(374                        f"{self.debug}[tls] failed to parse certificate: {e}", WARNING375                    )376                else:377                    self.conn.certificate_list.append(parsed_cert)378 379            self.conn.timestamp_tls_setup = time.time()380            self.conn.alpn = self.tls.get_alpn_proto_negotiated()381            self.conn.cipher = self.tls.get_cipher_name()382            self.conn.tls_version = typing.cast(383                TlsVersion, self.tls.get_protocol_version_name()384            )385            if self.debug:386                yield commands.Log(387                    f"{self.debug}[tls] tls established: {self.conn}", DEBUG388                )389            if self.conn == self.context.client:390                yield TlsEstablishedClientHook(391                    TlsData(self.conn, self.context, self.tls)392                )393            else:394                yield TlsEstablishedServerHook(395                    TlsData(self.conn, self.context, self.tls)396                )397            yield from self.receive_data(b"")398            return True, None399 400    def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]:401        self.conn.error = err402        if self.conn == self.context.client:403            yield TlsFailedClientHook(TlsData(self.conn, self.context, self.tls))404        else:405            yield TlsFailedServerHook(TlsData(self.conn, self.context, self.tls))406        yield from super().on_handshake_error(err)407 408    def receive_data(self, data: bytes) -> layer.CommandGenerator[None]:409        if data:410            self.tls.bio_write(data)411 412        plaintext = bytearray()413        close = False414        while True:415            try:416                plaintext.extend(self.tls.recv(65535))417            except SSL.WantReadError:418                break419            except SSL.ZeroReturnError:420                close = True421                break422            except SSL.Error as e:423                # This may be happening because the other side send an alert.424                # There's somewhat ugly behavior with Firefox on Android here,425                # which upon mistrusting a certificate still completes the handshake426                # and then sends an alert in the next packet. At this point we have unfortunately427                # already fired out `tls_established_client` hook.428                yield commands.Log(f"TLS Error: {e}", WARNING)429                break430 431        # Can we send something?432        # Note that this must happen after `recv()`, which may have advanced the state machine.433        # https://github.com/mitmproxy/mitmproxy/discussions/7550434        yield from self.tls_interact()435 436        if plaintext:437            yield from self.event_to_child(438                events.DataReceived(self.conn, bytes(plaintext))439            )440        if close:441            self.conn.state &= ~connection.ConnectionState.CAN_READ442            if self.debug:443                yield commands.Log(f"{self.debug}[tls] close_notify {self.conn}", DEBUG)444            yield from self.event_to_child(events.ConnectionClosed(self.conn))445 446    def receive_close(self) -> layer.CommandGenerator[None]:447        if self.tls.get_shutdown() & SSL.RECEIVED_SHUTDOWN:448            pass  # We have already dispatched a ConnectionClosed to the child layer.449        else:450            yield from super().receive_close()451 452    def send_data(self, data: bytes) -> layer.CommandGenerator[None]:453        try:454            self.tls.sendall(data)455        except (SSL.ZeroReturnError, SSL.SysCallError):456            # The other peer may still be trying to send data over, which we discard here.457            pass458        yield from self.tls_interact()459 460    def send_close(461        self, command: commands.CloseConnection462    ) -> layer.CommandGenerator[None]:463        # We should probably shutdown the TLS connection properly here.464        yield from super().send_close(command)465 466 467class ServerTLSLayer(TLSLayer):468    """469    This layer establishes TLS for a single server connection.470    """471 472    wait_for_clienthello: bool = False473 474    def __init__(self, context: context.Context, conn: connection.Server | None = None):475        super().__init__(context, conn or context.server)476 477    def start_handshake(self) -> layer.CommandGenerator[None]:478        wait_for_clienthello = (479            # if command_to_reply_to is set, we've been instructed to open the connection from the child layer.480            # in that case any potential ClientHello is already parsed (by the ClientTLS child layer).481            not self.command_to_reply_to482            # if command_to_reply_to is not set, the connection was already open when this layer received its Start483            # event (eager connection strategy). We now want to establish TLS right away, _unless_ we already know484            # that there's TLS on the client side as well (we check if our immediate child layer is set to be ClientTLS)485            # In this case want to wait for ClientHello to be parsed, so that we can incorporate SNI/ALPN from there.486            and isinstance(self.child_layer, ClientTLSLayer)487        )488        if wait_for_clienthello:489            self.wait_for_clienthello = True490            self.tunnel_state = tunnel.TunnelState.CLOSED491        else:492            yield from self.start_tls()493            if self.tls:494                yield from self.receive_handshake_data(b"")495 496    def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]:497        if self.wait_for_clienthello:498            for command in super().event_to_child(event):499                if (500                    isinstance(command, commands.OpenConnection)501                    and command.connection == self.conn502                ):503                    self.wait_for_clienthello = False504                    # swallow OpenConnection here by not re-yielding it.505                else:506                    yield command507        else:508            yield from super().event_to_child(event)509 510    def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]:511        yield commands.Log(f"Server TLS handshake failed. {err}", level=WARNING)512        yield from super().on_handshake_error(err)513 514 515class ClientTLSLayer(TLSLayer):516    """517    This layer establishes TLS on a single client connection.518 519    ┌─────┐520    │Start│521    └┬────┘522     ↓523    ┌────────────────────┐524    │Wait for ClientHello│525    └┬───────────────────┘526     ↓527    ┌────────────────┐528    │Process messages│529    └────────────────┘530 531    """532 533    recv_buffer: bytearray534    server_tls_available: bool535    client_hello_parsed: bool = False536 537    def __init__(self, context: context.Context):538        if context.client.tls:539            # In the case of TLS-over-TLS, we already have client TLS. As the outer TLS connection between client540            # and proxy isn't that interesting to us, we just unset the attributes here and keep the inner TLS541            # session's attributes.542            # Alternatively we could create a new Client instance,543            # but for now we keep it simple. There is a proof-of-concept at544            # https://github.com/mitmproxy/mitmproxy/commit/9b6e2a716888b7787514733b76a5936afa485352.545            context.client.alpn = None546            context.client.cipher = None547            context.client.sni = None548            context.client.timestamp_tls_setup = None549            context.client.tls_version = None550            context.client.certificate_list = []551            context.client.mitmcert = None552            context.client.alpn_offers = []553            context.client.cipher_list = []554 555        super().__init__(context, context.client)556        self.server_tls_available = isinstance(self.context.layers[-2], ServerTLSLayer)557        self.recv_buffer = bytearray()558 559    def start_handshake(self) -> layer.CommandGenerator[None]:560        yield from ()561 562    def receive_handshake_data(563        self, data: bytes564    ) -> layer.CommandGenerator[tuple[bool, str | None]]:565        if self.client_hello_parsed:566            return (yield from super().receive_handshake_data(data))567        self.recv_buffer.extend(data)568        try:569            if self.is_dtls:570                client_hello = dtls_parse_client_hello(self.recv_buffer)571            else:572                client_hello = parse_client_hello(self.recv_buffer)573        except ValueError:574            return False, f"Cannot parse ClientHello: {self.recv_buffer.hex()}"575 576        if client_hello:577            self.client_hello_parsed = True578        else:579            return False, None580 581        self.conn.sni = client_hello.sni582        self.conn.alpn_offers = client_hello.alpn_protocols583        tls_clienthello = ClientHelloData(self.context, client_hello)584        yield TlsClienthelloHook(tls_clienthello)585 586        if tls_clienthello.ignore_connection:587            # we've figured out that we don't want to intercept this connection, so we assign fake connection objects588            # to all TLS layers. This makes the real connection contents just go through.589            self.conn = self.tunnel_connection = connection.Client(590                peername=("ignore-conn", 0), sockname=("ignore-conn", 0)591            )592            parent_layer = self.context.layers[self.context.layers.index(self) - 1]593            if isinstance(parent_layer, ServerTLSLayer):594                parent_layer.conn = parent_layer.tunnel_connection = connection.Server(595                    address=None596                )597            if self.is_dtls:598                self.child_layer = udp.UDPLayer(self.context, ignore=True)599            else:600                self.child_layer = tcp.TCPLayer(self.context, ignore=True)601            yield from self.event_to_child(602                events.DataReceived(self.context.client, bytes(self.recv_buffer))603            )604            self.recv_buffer.clear()605            return True, None606        if (607            tls_clienthello.establish_server_tls_first608            and not self.context.server.tls_established609        ):610            err = yield from self.start_server_tls()611            if err:612                yield commands.Log(613                    f"Unable to establish {self.proto_name} connection with server ({err}). "614                    f"Trying to establish {self.proto_name} with client anyway. "615                    f"If you plan to redirect requests away from this server, "616                    f"consider setting `connection_strategy` to `lazy` to suppress early connections."617                )618 619        yield from self.start_tls()620        if not self.conn.connected:621            return False, "connection closed early"622 623        ret = yield from super().receive_handshake_data(bytes(self.recv_buffer))624        self.recv_buffer.clear()625        return ret626 627    def start_server_tls(self) -> layer.CommandGenerator[str | None]:628        """629        We often need information from the upstream connection to establish TLS with the client.630        For example, we need to check if the client does ALPN or not.631        """632        if not self.server_tls_available:633            return f"No server {self.proto_name} available."634        err = yield commands.OpenConnection(self.context.server)635        return err636 637    def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]:638        if self.conn.sni:639            dest = self.conn.sni640        else:641            dest = human.format_address(self.context.server.address)642        level: int = WARNING643        if err.startswith("Cannot parse ClientHello"):644            pass645        elif (646            "('SSL routines', 'tls_early_post_process_client_hello', 'unsupported protocol')"647            in err648            or "('SSL routines', '', 'unsupported protocol')" in err  # OpenSSL 3+649        ):650            err = (651                f"Client and mitmproxy cannot agree on a TLS version to use. "652                f"You may need to adjust mitmproxy's tls_version_client_min option."653            )654        elif (655            "unknown ca" in err656            or "bad certificate" in err657            or "certificate unknown" in err658        ):659            err = (660                f"The client does not trust the proxy's certificate for {dest} ({err})"661            )662        elif err == "connection closed":663            err = (664                f"The client disconnected during the handshake. If this happens consistently for {dest}, "665                f"this may indicate that the client does not trust the proxy's certificate."666            )667            level = INFO668        elif err == "connection closed early":669            pass670        else:671            err = f"The client may not trust the proxy's certificate for {dest} ({err})"672        if err != "connection closed early":673            yield commands.Log(f"Client TLS handshake failed. {err}", level=level)674        yield from super().on_handshake_error(err)675        self.event_to_child = self.errored  # type: ignore676 677    def errored(self, event: events.Event) -> layer.CommandGenerator[None]:678        if self.debug is not None:679            yield commands.Log(680                f"{self.debug}[tls] Swallowing {event} as handshake failed.", DEBUG681            )682 683 684class MockTLSLayer(TLSLayer):685    """Mock layer to disable actual TLS and use cleartext in tests.686 687    Use like so:688        monkeypatch.setattr(tls, "ServerTLSLayer", tls.MockTLSLayer)689    """690 691    def __init__(self, ctx: context.Context):692        super().__init__(ctx, connection.Server(address=None))693 
codekingpro/portable-devtools · Team Ai