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;
}