You can use Reflection to get to the TlsStream->SslState->SslProtocol
property value.
This information can be extracted from the Stream returned by both HttpWebRequest.GetRequestStream()
and HttpWebRequest.GetResponseStream()
.
The ExtractSslProtocol()
also handles the compressed GzipStream
or DeflateStream
that are returned when the WebRequest
[AutomaticDecompression][1] is activated.
The validation will occur in the ServerCertificateValidationCallback
, which is called when the request is initialized with request.GetRequestStream()
Note: SecurityProtocolType.Tls13
is include in .Net Framework 4.8+
and .Net Core 3.0+
.
using System.IO.Compression;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
//(...)
// Allow all, to then check what the Handshake will agree upon
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 |
SecurityProtocolType.Tls |
SecurityProtocolType.Tls11 |
SecurityProtocolType.Tls12 |
SecurityProtocolType.Tls13;
// Handle the Server certificate exchange, to inspect the certificates received
ServicePointManager.ServerCertificateValidationCallback += TlsValidationCallback;
Uri requestUri = new Uri("https://somesite.com");
var request = WebRequest.CreateHttp(requestUri);
request.Method = WebRequestMethods.Http.Post;
request.ServicePoint.Expect100Continue = false;
request.AllowAutoRedirect = true;
request.CookieContainer = new CookieContainer();
request.ContentType = "application/x-www-form-urlencoded";
var postdata = Encoding.UTF8.GetBytes("Some postdata here");
request.ContentLength = postdata.Length;
request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident / 7.0; rv: 11.0) like Gecko";
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip, deflate;q=0.8");
request.Headers.Add(HttpRequestHeader.CacheControl, "no-cache");
using (var requestStream = request.GetRequestStream()) {
//Here the request stream is already validated
SslProtocols sslProtocol = ExtractSslProtocol(requestStream);
if (sslProtocol < SslProtocols.Tls12)
{
// Refuse/close the connection
}
}
//(...)
private SslProtocols ExtractSslProtocol(Stream stream)
{
if (stream is null) return SslProtocols.None;
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
Stream metaStream = stream;
if (stream.GetType().BaseType == typeof(GZipStream)) {
metaStream = (stream as GZipStream).BaseStream;
}
else if (stream.GetType().BaseType == typeof(DeflateStream)) {
metaStream = (stream as DeflateStream).BaseStream;
}
var connection = metaStream.GetType().GetProperty("Connection", bindingFlags).GetValue(metaStream);
if (!(bool)connection.GetType().GetProperty("UsingSecureStream", bindingFlags).GetValue(connection)) {
// Not a Https connection
return SslProtocols.None;
}
var tlsStream = connection.GetType().GetProperty("NetworkStream", bindingFlags).GetValue(connection);
var tlsState = tlsStream.GetType().GetField("m_Worker", bindingFlags).GetValue(tlsStream);
return (SslProtocols)tlsState.GetType().GetProperty("SslProtocol", bindingFlags).GetValue(tlsState);
}
The RemoteCertificateValidationCallback
has some useful information on the security protocols used. (see: [Transport Layer Security (TLS) Parameters (IANA)][3] and [RFC 5246][4]).
The types of security protocols used can be informative enough, since each protocol version supports a subset of Hashing and Encryption algorithms.
Tls 1.2, introduces HMAC-SHA256
and deprecates IDEA
and DES
ciphers (all variants are listed in the linked documents).
Here, I inserted an OIDExtractor
, which lists the algorithms in use.
Note that both TcpClient() and WebRequest() will get here.
private bool TlsValidationCallback(object sender, X509Certificate CACert, X509Chain CAChain, SslPolicyErrors sslPolicyErrors)
{
List<Oid> oidExtractor = CAChain
.ChainElements
.Cast<X509ChainElement>()
.Select(x509 => new Oid(x509.Certificate.SignatureAlgorithm.Value))
.ToList();
// Inspect the oidExtractor list
var certificate = new X509Certificate2(CACert);
//If you needed/have to pass a certificate, add it here.
//X509Certificate2 cert = new X509Certificate2(@"[localstorage]/[ca.cert]");
//CAChain.ChainPolicy.ExtraStore.Add(cert);
CAChain.Build(certificate);
foreach (X509ChainStatus CACStatus in CAChain.ChainStatus)
{
if ((CACStatus.Status != X509ChainStatusFlags.NoError) &
(CACStatus.Status != X509ChainStatusFlags.UntrustedRoot))
return false;
}
return true;
}
**UPDATE 2:**
The `secur32.dll` -> `QueryContextAttributesW()` method, allows to query the Connection Security Context of an initialized Stream.
[DllImport("secur32.dll", CharSet = CharSet.Auto, ExactSpelling=true, SetLastError=false)]
private static extern int QueryContextAttributesW(
SSPIHandle contextHandle,
[In] ContextAttribute attribute,
[In] [Out] ref SecPkgContext_ConnectionInfo ConnectionInfo
);
As you can see from the documentation, this method returns a void* buffer
that references a SecPkgContext_ConnectionInfo
structure:
private struct SecPkgContext_ConnectionInfo
{
public SchProtocols dwProtocol;
public ALG_ID aiCipher;
public int dwCipherStrength;
public ALG_ID aiHash;
public int dwHashStrength;
public ALG_ID aiExch;
public int dwExchStrength;
}
The SchProtocols dwProtocol
member is the SslProtocol.
What's the catch.
The TlsStream.Context.m_SecurityContext._handle
that references the Connection Context Handle is not public.
Thus, you can get it, again, only through reflection or through the System.Net.Security.AuthenticatedStream
derived classes (System.Net.Security.SslStream
and System.Net.Security.NegotiateStream
) returned by TcpClient.GetStream()
.
Unfortunately, the Stream returned by WebRequest/WebResponse cannot be cast to these classes. The Connections and Streams Types are only referenced through non-public properties and fields.
I'm publishing the assembled documentation, it maybe help you figure out another path to get to that Context Handle.
The declarations, structures, enumerator lists are in [QueryContextAttributesW (PASTEBIN)][5].
Microsoft TechNet
[Authentication Structures][6]
MSDN
[Creating a Secure Connection Using Schannel][7]
[Getting Information About Schannel Connections][8]
[Querying the Attributes of an Schannel Context][9]
[QueryContextAttributes (Schannel)][10]
Code Base (Partial)
[.NET Reference Source][11]
[Internals.cs][12]
[internal struct SSPIHandle { }][13]
[internal enum ContextAttribute { }][14]
UPDATE 1:
I saw in your comment to another answer that the solution using
TcpClient()
is not acceptable for you. I'm leaving it here anyway so
the comments of [Ben Voigt][15] in this one will be useful to anyone else interested. Also, 3 possible solutions are better than 2.
Some implementation details on the [TcpClient()][16] [SslStream][17] usage in the context provided.
If protocol informations are required before initializing a WebRequest, a TcpClient() connection can be established in the same context using the same tools required for a TLS connection. Namely, the [ServicePointManager.SecurityProtocol
][18] to define the supported protocols and the [ServicePointManager.ServerCertificateValidationCallback
][19] to validate the server certificate.
Both TcpClient() and WebRequest can use these settings:
- enable all protocols and let the TLS Handshake determine which one will be used.
- define a
RemoteCertificateValidationCallback()
delegate to validate the X509Certificates
the Server passes in a X509Chain
.
In practice, the TLS Handshake is the same when establishing a TcpClient or a WebRequest connection.
This approach lets you know what Tls Protocol your HttpWebRequest will negotiate with the same server.
Setup a TcpClient()
to receive and evaluate the SslStream
.
The checkCertificateRevocation
flag is set to false
, so the process won't waste time looking up the revocation list.
The certificate validation Callback is the same specified in ServicePointManager
.
TlsInfo tlsInfo = null;
IPHostEntry dnsHost = await Dns.GetHostEntryAsync(HostURI.Host);
using (TcpClient client = new TcpClient(dnsHost.HostName, 443))
{
using (SslStream sslStream = new SslStream(client.GetStream(), false,
TlsValidationCallback, null))
{
sslstream.AuthenticateAsClient(dnsHost.HostName, null,
(SslProtocols)ServicePointManager.SecurityProtocol, false);
tlsInfo = new TlsInfo(sslStream);
}
}
//The HttpWebRequest goes on from here.
HttpWebRequest httpRequest = WebRequest.CreateHttp(HostURI);
//(...)
The TlsInfo
Class collects some information on the established secure connection: