Search code examples
c#sslsni

Set SNI in a Client for a StreamSocket or SslStream


I can connect successfully to our server using TLS 1.2. But we have an option to pass in a SNI (in the client hello message), which will redirect us to another server behind the firewall. How does one specify the SNI (server name indicator) in a C# StreamSocket or SslStream? I can't find any examples in for C# on the client side. Using Visual Studio 2017, C# 7.3, .NET 4.8.3752

Here is our connect command.

We've looked at dotnetty, curl.net, and possibly using openSSL.

await socket.ConnectAsync(new HostName(serverAddress), serverPort.ToString(), SocketProtectionLevel.Tls12);

Thanks in advance for any ideas or comments.


Solution

  • We were able to solve the problem. I pieced together several partial examples form 4 or 5 results found here on StackOverflow and other sources.

    Basically I was not able to use StreamSocket. I had to use a TcpClient and a SslStream, which I believe StreamSocket uses internally (from what I found in my many searches). Microsoft did not make it easy to do, like you can with ObjectiveC or Java.

    First you must create an instance of a TcpClient:

    _tcpClient = new TcpClient(_serverAddress, _serverPort);
    

    Then Create a SslStream:

     _socketStreamForReadSSL = new SslStream(
        _tcpClient.GetStream()
        , false
        , new RemoteCertificateValidationCallback(ValidateServerCertificate));
    

    Note in the above snippet, we have set a delegate to handle the remote server validation on our side of the code. This is because Microsoft will validate that the SNI name matches the host address. To override this behavior, they graciously allowed us to use a delegate (seen further down in answer).

    The next step is to set any certificates that are to be used in the handshake:

    X509CertificateCollection certs = BuildX509CertCollection();
    

    BuildX509CertCollection is a method I wrote that builds a collection of certificates, basically taking a Windows.Security.Cryptography.Certificates.Certificate and turning it into X509Certificate with the binary generated by Certificate.GetCertificateBlob.

    Now the next step is critical and where the magic happens. We will authenticate the client with the server. This is where the SNI gets set. We also set the protocol here (TLS 1.2).

    _socketStreamForReadSSL.AuthenticateAsClient(sniAddress, certs, SslProtocols.Tls12, false);
    

    If you're running WireShark using the following command, you can track communications, below is the sample command I used to see TLS 1.2 communications between my dev box and the server, where XXX.XXX.XXX.XXX is the ip addresses of your server and your target

    tls.record.version == "TLS 1.2" and ip.addr == XXX.XXX.XXX.XXX and ip.addr == XXX.XXX.XXX.XXX

    And a portion of the WireShark results demonstrating the SNI anme being set

    Internet Protocol Version 4, Src: XXX.XXX.XXX.XXX, Dst: XXX.XXX.XXX.XXX
    Transmission Control Protocol, Src Port: <port number here>, Dst Port: <port number here>, Seq: 1, Ack: 1, Len: 196
    Transport Layer Security
        TLSv1.2 Record Layer: Handshake Protocol: Client Hello
            Content Type: Handshake (22)
            Version: TLS 1.2 (0x0303)
            Length: 191
            Handshake Protocol: Client Hello
                Handshake Type: Client Hello (1)
                Length: 187
                Version: TLS 1.2 (0x0303)
                Random: blah...blah...blah
                Session ID Length: 0
                Cipher Suites Length: 42
                Cipher Suites (21 suites)
                Compression Methods Length: 1
                Compression Methods (1 method)
                Extensions Length: 104
                Extension: server_name (len=45)
                    Type: server_name (0)
                    Length: 45
                    Server Name Indication extension
                        Server Name list length: 43
                        Server Name Type: host_name (0)
                        Server Name length: 40
                        Server Name: <sni name shows up here>
    

    And as promised earlier, the remote certificate validation delegate. This is called by the SslStream when validating the server's certificate, and SNI name. In our instance we make sure the ending of the server address and ending of the SNI name match the common name (CN), but you can do whatever validation you see fit. Return true for passes and false for fails, if false, AuthenticateAsClient will throw an exception.

    private bool ValidateServerCertificate(
        object sender
        , X509Certificate certificate
        , X509Chain chain
        , SslPolicyErrors sslPolicyErrors)
    {
        // Do not allow this client to communicate with unauthenticated servers.
        bool result = false;
    
        switch (sslPolicyErrors)
        {
            case SslPolicyErrors.None:
                result = true;
                break;
    
            case SslPolicyErrors.RemoteCertificateNameMismatch:
                X509Certificate2 cert = new X509Certificate2(certificate);
                string cn = cert.GetNameInfo(X509NameType.SimpleName, false);
                string cleanName = cn.Substring(cn.LastIndexOf('*') + 1);
                string[] addresses = { _serverAddress, _serverSNIName };
    
                // if the ending of the sni and servername do match the common name of the cert, fail
                result = addresses.Where(item => item.EndsWith(cleanName)).Count() == addresses.Count();
    
                break;
    
            default:
                result = false;
                break;
        }
    
        return result;
    }