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.
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.
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.
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.
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
List all C# tutorials.