Skip to main content

Command Palette

Search for a command to run...

How TLS Works in .NET

Updated
5 min read
How TLS Works in .NET

TLS sits underneath almost every network call you make in .NET, whether it goes through HttpClient, SslStream, Kestrel, gRPC or QUIC. Most people treat TLS as a black box. Once you understand how the handshake works, how .NET validates certificates, how ALPN decides the protocol, and how session resumption speeds up repeat connections, you stop guessing and start diagnosing the right problems.

In .NET the core TLS behaviour does not come from managed code. The runtime wraps the operating system’s native crypto stack. Windows relies on SChannel, Linux typically uses OpenSSL and macOS uses SecureTransport. .NET orchestrates the handshake and encryption process but it delegates the actual cryptography to the platform. For TCP connections this all flows through SslStream; for QUIC and HTTP/3 it goes through MsQuic. The abstractions look high level but underneath them is a strict record protocol and a carefully structured handshake. A TLS 1.3 handshake always begins with the client. The first message it sends, ClientHello, carries the supported TLS versions, the cipher suites it is willing to use, various random values, the key share needed for establishing the shared secret, and two pieces of metadata that matter deeply in .NET systems, ALPN and SNI. ALPN tells the server which application protocols the client is willing to speak. SNI tells the server which hostname the client is targeting. Without SNI the server would not know which certificate to present. This matters when you host multiple domains behind one IP address, without SNI the handshake cannot pick the right certificate.

The server responds with ServerHello and includes its own key share, the chosen cipher suite, the certificate chain, evidence that it owns the private key, and the final messages needed to prove the handshake is authentic. The client verifies the certificate chain against the trusted roots on the local machine. It also verifies that the server certificate matches the hostname inside the SAN fields. .NET is strict here and will fail immediately if the certificate chain is incomplete, the dates do not align, the clock on the machine is wrong or the hostname does not appear in the SAN list. After validation succeeds, the client confirms the handshake by sending its Finished message and both sides switch to encrypted traffic.

ALPN has a direct impact on real application behaviour. When a client offers a list of protocols such as HTTP/3, HTTP/2 and HTTP/1.1, the server selects one based on what it supports. If HTTP/3 is enabled and QUIC is available on the host, the connection will run over QUIC. If not, the selection drops down to HTTP/2. If the server only supports HTTP/1.1, that becomes the negotiated protocol regardless of your expectations. Many engineers assume their connections are using HTTP/2 when they are not, and ALPN is usually the reason. The choice is made during the handshake and it dictates how the rest of the connection behaves.

Once the handshake is complete, the cost of establishing a secure connection becomes more noticeable in systems that open many short lived connections. This is where TLS session resumption matters. Under TLS 1.3 the server can send session tickets after the handshake. The client stores these tickets and presents them the next time it connects. If the server recognises the ticket, the handshake collapses into a much shorter exchange. The shared secret can be re-established without sending the full certificate chain. In practical terms this means the first HTTPS request to an endpoint is always slower than subsequent ones. HttpClient automatically benefits from resumption through its connection pooling. SslStream can benefit too if the server issues tickets and the client reconnects soon enough.

Certificate handling is one of the most common sources of TLS failures in .NET systems. Validation is strict by design. The chain must build to a trusted root. The SAN list must contain the exact hostname the client used. The validity period must be correct and the system clock must be in sync. If the server forgets to include intermediate certificates, the client fails even if the leaf certificate is valid. In internal environments developers sometimes try to bypass validation using custom callbacks, but this simply moves the risk instead of removing it. It is always better to fix the certificate chain than to disable validation.

On the server side, Kestrel uses SNI to select the correct certificate. When the ClientHello indicates the hostname, Kestrel matches it against the registered certificates and picks the right one. If the hostname does not match any certificate, the handshake fails. This behaviour is essential for multi tenant environments and for modern hosting setups where multiple domains share the same IP and port. SNI is the only signal the server receives to choose the certificate; without it only a single certificate would be possible.

Once the handshake is complete, the TLS record layer takes over. TLS is not a raw stream protocol. It breaks data into encrypted records. Each record contains a header and encrypted payload. SslStream hides this complexity and gives you a continuous logical stream, but internally it deals with partial records, fragmentation, buffering, and decryption boundaries. If you build a custom protocol on top of SslStream, the record layer is invisible but always present. QUIC changes this by integrating TLS into its own frame system rather than layering it over a separate transport. TLS in .NET behaves differently when the underlying transport is QUIC. With HTTP/3, TLS 1.3 is embedded directly in the QUIC handshake. After the initial hello messages, everything is encrypted and sent as QUIC frames. There is no TLS record layer on top of TCP because there is no TCP. QUIC handles reliability, congestion control and multiplexing itself. The TLS handshake simply provides the cryptographic foundation. This shifts the performance profile of secure connections significantly because QUIC avoids head-of-line blocking and supports independent bidirectional streams, even when packets are lost.

When you understand how TLS works inside .NET, connection problems become diagnosable instead of mysterious. Mis-negotiated ALPN explains unexpected HTTP versions. Missing SAN entries explain handshake failures. Repeated handshakes explain poor performance. SNI logic explains why a certificate mismatch happened. Session resumption explains why repeat requests become faster. None of this requires memorising every message in the wire protocol. You just need a clear model of what .NET is doing and why.