ZetCode

C# SslStream

last modified October 17, 2026

In this article we show how to secure TCP connections with TLS in C# using the SslStream class.

TLS (Transport Layer Security) encrypts the data sent over a connection and lets the client verify the identity of the server with a certificate. It is the security layer of HTTPS, but it can protect any TCP protocol. The name SslStream comes from SSL, the predecessor of TLS; modern .NET negotiates TLS 1.2 or TLS 1.3 and does not use SSL at all.

The SslStream class from the System.Net.Security namespace wraps another stream, usually a NetworkStream. After the TLS handshake, we read and write the SslStream as an ordinary stream; the encryption is transparent. HttpClient uses SslStream internally for HTTPS; we use it directly for our own protocols.

C# SslStream client

In the first example, we connect to a web server over TLS, print the details of the connection and the server certificate, and send an HTTP request.

Program.cs
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;

var host = "example.com";

using var client = new TcpClient();
await client.ConnectAsync(host, 443);

await using var ssl = new SslStream(client.GetStream());
await ssl.AuthenticateAsClientAsync(host);

Console.WriteLine($"Protocol: {ssl.SslProtocol}");
Console.WriteLine($"Cipher suite: {ssl.NegotiatedCipherSuite}");

var cert = ssl.RemoteCertificate as X509Certificate2;

if (cert is not null)
{
    Console.WriteLine($"Subject: {cert.Subject}");
    Console.WriteLine($"Issuer: {cert.Issuer}");
    Console.WriteLine($"Valid until: {cert.NotAfter:yyyy-MM-dd}");
    Console.WriteLine($"DNS name: {cert.GetNameInfo(X509NameType.DnsName, false)}");
}

var request = $"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n";
await ssl.WriteAsync(Encoding.ASCII.GetBytes(request));

using var reader = new StreamReader(ssl);
Console.WriteLine(await reader.ReadLineAsync());

We connect to port 443, the HTTPS port of example.com.

await using var ssl = new SslStream(client.GetStream());
await ssl.AuthenticateAsClientAsync(host);

We wrap the network stream in an SslStream. The AuthenticateAsClientAsync method performs the TLS handshake. The host name is sent to the server, which uses it to choose the right certificate, and it is checked against the names in the certificate. The certificate chain is validated against the trusted root certificates of the operating system.

Console.WriteLine($"Protocol: {ssl.SslProtocol}");
Console.WriteLine($"Cipher suite: {ssl.NegotiatedCipherSuite}");

We print the negotiated protocol version and cipher suite.

var cert = ssl.RemoteCertificate as X509Certificate2;

The RemoteCertificate property holds the certificate of the server. We print its subject, issuer, expiration date, and DNS name.

var request = $"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n";
await ssl.WriteAsync(Encoding.ASCII.GetBytes(request));

using var reader = new StreamReader(ssl);
Console.WriteLine(await reader.ReadLineAsync());

After the handshake, we send a plain HTTP request; SslStream encrypts it. We print the status line of the response.

$ dotnet run
Protocol: Tls13
Cipher suite: TLS_AES_256_GCM_SHA384
Subject: CN=example.com
Issuer: CN=Cloudflare TLS Issuing ECC CA 3, O=SSL Corporation, C=US
Valid until: 2026-12-25
DNS name: example.com
HTTP/1.1 200 OK

C# SslStream invalid certificates

If the certificate cannot be validated, the handshake fails with an AuthenticationException. The badssl.com site provides hosts with various kinds of broken certificates for testing.

Program.cs
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;

string[] hosts = ["expired.badssl.com", "wrong.host.badssl.com", "self-signed.badssl.com"];

foreach (var host in hosts)
{
    using var client = new TcpClient();
    await client.ConnectAsync(host, 443);

    await using var ssl = new SslStream(client.GetStream());

    try
    {
        await ssl.AuthenticateAsClientAsync(host);
        Console.WriteLine($"{host}: OK");
    }
    catch (AuthenticationException e)
    {
        Console.WriteLine($"{host}: {e.Message}");
    }
}

We try to connect to a host with an expired certificate, a host whose certificate is issued for a different name, and a host with a self-signed certificate.

$ dotnet run
expired.badssl.com: The remote certificate is invalid because of errors in the certificate chain: NotTimeValid
wrong.host.badssl.com: The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch
self-signed.badssl.com: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot

The error messages tell us what is wrong with each certificate.

C# SslStream server

A TLS server needs a certificate with a private key. In production, the certificate is issued by a certificate authority, such as Let's Encrypt. For testing, we can create a self-signed certificate in code with the CertificateRequest class. In the following example, we run a TLS server and a client in one program.

Program.cs
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

using X509Certificate2 cert = CreateSelfSignedCertificate("localhost");
Console.WriteLine($"Server certificate thumbprint: {cert.Thumbprint}");

var listener = new TcpListener(IPAddress.Loopback, 5443);
listener.Start();

Task serverTask = RunServerAsync(listener, cert);

using (var client = new TcpClient())
{
    await client.ConnectAsync("localhost", 5443);

    var options = new SslClientAuthenticationOptions
    {
        TargetHost = "localhost",
        RemoteCertificateValidationCallback = (_, serverCert, _, errors) =>
        {
            Console.WriteLine($"Client: validation errors: {errors}");
            return serverCert?.GetCertHashString() == cert.Thumbprint;
        }
    };

    await using var ssl = new SslStream(client.GetStream());
    await ssl.AuthenticateAsClientAsync(options);

    using var reader = new StreamReader(ssl);
    using var writer = new StreamWriter(ssl) { AutoFlush = true };

    await writer.WriteLineAsync("Hello over TLS");
    Console.WriteLine($"Client: received '{await reader.ReadLineAsync()}'");
}

await serverTask;
listener.Stop();

static async Task RunServerAsync(TcpListener listener, X509Certificate2 cert)
{
    using TcpClient client = await listener.AcceptTcpClientAsync();

    await using var ssl = new SslStream(client.GetStream());
    await ssl.AuthenticateAsServerAsync(cert);

    Console.WriteLine($"Server: {ssl.SslProtocol}, {ssl.NegotiatedCipherSuite}");

    using var reader = new StreamReader(ssl);
    using var writer = new StreamWriter(ssl) { AutoFlush = true };

    string? line = await reader.ReadLineAsync();
    await writer.WriteLineAsync(line?.ToUpperInvariant());
}

static X509Certificate2 CreateSelfSignedCertificate(string name)
{
    using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
    var req = new CertificateRequest($"CN={name}", key, HashAlgorithmName.SHA256);

    var san = new SubjectAlternativeNameBuilder();
    san.AddDnsName(name);
    req.CertificateExtensions.Add(san.Build());

    using X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5),
        DateTimeOffset.UtcNow.AddDays(30));

    // on Windows, SslStream cannot use the ephemeral key of a new certificate
    return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null);
}

The client sends a line of text; the server sends it back in uppercase.

using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var req = new CertificateRequest($"CN={name}", key, HashAlgorithmName.SHA256);

var san = new SubjectAlternativeNameBuilder();
san.AddDnsName(name);
req.CertificateExtensions.Add(san.Build());

We create a certificate request with an elliptic curve key. Clients check the host name against the subject alternative name extension, so we add the localhost DNS name to it.

using X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5),
    DateTimeOffset.UtcNow.AddDays(30));

// on Windows, SslStream cannot use the ephemeral key of a new certificate
return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null);

The certificate is valid for 30 days. The start is set a few minutes back to tolerate small clock differences. We export the certificate with its private key to the PKCS#12 format and load it back with X509CertificateLoader, which replaces the obsolete X509Certificate2 constructors since .NET 9.

await using var ssl = new SslStream(client.GetStream());
await ssl.AuthenticateAsServerAsync(cert);

On the server side, the handshake is performed with AuthenticateAsServerAsync, which takes the server certificate.

RemoteCertificateValidationCallback = (_, serverCert, _, errors) =>
{
    Console.WriteLine($"Client: validation errors: {errors}");
    return serverCert?.GetCertHashString() == cert.Thumbprint;
}

A self-signed certificate is not trusted, so the default validation would fail. We provide our own validation callback. Instead of accepting any certificate, we accept exactly the certificate we expect, identified by its thumbprint. This technique is called certificate pinning.

Note: Never return true from the validation callback unconditionally. It turns off the protection against man-in-the-middle attacks.
$ dotnet run
Server certificate thumbprint: BE3CB5AAE4B0976B509AF929FB329B2063A746E5
Client: validation errors: RemoteCertificateChainErrors
Server: Tls13, TLS_AES_256_GCM_SHA384
Client: received 'HELLO OVER TLS'

The validation reports a chain error, because the certificate is not signed by a trusted authority. The pinned thumbprint matches, so the client accepts the connection.

Source

SslStream class - language reference

In this article we have secured TCP connections with TLS using the SslStream class in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.