Which TLS version was negotiated?
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 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) and RFC 5246).
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).
Microsoft TechNet
Authentication Structures
MSDN
Creating a Secure Connection Using Schannel
Getting Information About Schannel Connections
Querying the Attributes of an Schannel Context
QueryContextAttributes (Schannel)
Code Base (Partial)
.NET Reference Source
Internals.cs
internal struct SSPIHandle { }
internal enum ContextAttribute { }
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 in this one will be useful to anyone else interested. Also, 3 possible solutions are better than 2.
Some implementation details on the TcpClient() SslStream 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
to define the supported protocols and the ServicePointManager.ServerCertificateValidationCallback
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 theX509Certificates
the Server passes in aX509Chain
.
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:
- TLS protocol version
- Cipher and Hash Algorithms
- The Server certificate used in the SSL Handshake
public class TlsInfo
{
public TlsInfo(SslStream secStream)
{
this.ProtocolVersion = secStream.SslProtocol;
this.CipherAlgorithm = secStream.CipherAlgorithm;
this.HashAlgorithm = secStream.HashAlgorithm;
this.RemoteCertificate = secStream.RemoteCertificate;
}
public SslProtocols ProtocolVersion { get; set; }
public CipherAlgorithmType CipherAlgorithm { get; set; }
public HashAlgorithmType HashAlgorithm { get; set; }
public X509Certificate RemoteCertificate { get; set; }
}
The below solution is most certainly a "hack" in that it does use reflection, but it currently covers most situations that you could be in with an HttpWebRequest. It will return null if the Tls version could not be determined. It also verifies the Tls version in the same request, before you've written anything to the request stream. If the stream Tls handshake has not yet occurred when you call the method, it will trigger it.
Your sample usage would look like this:
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("...");
request.Method = "POST";
if (requestPayload.Length > 0)
{
using (Stream requestStream = request.GetRequestStream())
{
SslProtocols? protocol = GetSslProtocol(requestStream);
requestStream.Write(requestPayload, 0, requestPayload.Length);
}
}
And the method:
public static SslProtocols? GetSslProtocol(Stream stream)
{
if (stream == null)
return null;
if (typeof(SslStream).IsAssignableFrom(stream.GetType()))
{
var ssl = stream as SslStream;
return ssl.SslProtocol;
}
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
if (stream.GetType().FullName == "System.Net.ConnectStream")
{
var connection = stream.GetType().GetProperty("Connection", flags).GetValue(stream);
var netStream = connection.GetType().GetProperty("NetworkStream", flags).GetValue(connection) as Stream;
return GetSslProtocol(netStream);
}
if (stream.GetType().FullName == "System.Net.TlsStream")
{
// type SslState
var ssl = stream.GetType().GetField("m_Worker", flags).GetValue(stream);
if (ssl.GetType().GetProperty("IsAuthenticated", flags).GetValue(ssl) as bool? != true)
{
// we're not authenticated yet. see: https://referencesource.microsoft.com/#System/net/System/Net/_TLSstream.cs,115
var processAuthMethod = stream.GetType().GetMethod("ProcessAuthentication", flags);
processAuthMethod.Invoke(stream, new object[] { null });
}
var protocol = ssl.GetType().GetProperty("SslProtocol", flags).GetValue(ssl) as SslProtocols?;
return protocol;
}
return null;
}
Putting together some ideas here and there, I did a simple method to test each protocol available, forcing one specific type of connection each try. At the end, I get a list with the results to use as I need.
Ps: The test is valid only if you know that the website is online - you can make a previously test to check this.
public static IEnumerable<T> GetValues<T>()
{
return Enum.GetValues(typeof(T)).Cast<T>();
}
private Dictionary<SecurityProtocolType, bool> ProcessProtocols(string address)
{
var protocolResultList = new Dictionary<SecurityProtocolType, bool>();
var defaultProtocol = ServicePointManager.SecurityProtocol;
ServicePointManager.Expect100Continue = true;
foreach (var protocol in GetValues<SecurityProtocolType>())
{
try
{
ServicePointManager.SecurityProtocol = protocol;
var request = WebRequest.Create(address);
var response = request.GetResponse();
protocolResultList.Add(protocol, true);
}
catch
{
protocolResultList.Add(protocol, false);
}
}
ServicePointManager.SecurityProtocol = defaultProtocol;
return protocolResultList;
}
Hope this will be helpfull