32

I have my app running in .NET 4.7. By default, it will try to use TLS1.2. Is it possible to know which TLS version was negotiated when performing, for example, an HTTP Request as below?

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(decodedUri);
if (requestPayload.Length > 0)
{
    using (Stream requestStream = request.GetRequestStream())
    {
        requestStream.Write(requestPayload, 0, requestPayload.Length);
    }
}

I only need this information for logging / debugging purposes, so it's not important that I have this information before writing to the request stream or receiving the response. I do not wish to parse net tracing logs for this information, and I also don't want to create a second connection (using SslStream or similar).

caesay
  • 16,932
  • 15
  • 95
  • 160
Frederic
  • 2,015
  • 4
  • 20
  • 37
  • 2
    I hope for your sake there is an easier way, but if you enable verbose `System.Net` tracing it will log that info and you could probably parse it out, i.e., `System.Net Information: 0 : [18984] EndProcessAuthentication(Protocol=Tls12, Cipher=Aes256 256 bit strength,....etc..` – Crowcoder Feb 02 '18 at 19:40
  • @Crowcoder: Could probably look in the Framework Source and see where the verbose logging is getting its information. – Robert Harvey Feb 02 '18 at 19:41
  • @Crowcoder : That would be a good start but I'm more looking to get that in the code itself and then log it myself – Frederic Feb 02 '18 at 19:52
  • Finding the data in the source gives some clues: _SslState :: internal SslProtocols SslProtocol {get;} -- TlsStream :: private SslState m_Worker; -- (TlsStream derives from NetworkStream) -- PooledStream :: internal NetworkStream NetworkStream; -- (Connection derives from PooledStream) -- but apparently it uses pooled Connection objects, which might be reused and describe some other TLS connection when you actually get to query it after the method already returned a result. – phi1010 Feb 06 '18 at 03:52
  • In the end, you probably want to get here, but I don't see a reliable way how to: https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3 – phi1010 Feb 06 '18 at 03:56
  • 1
    I don't think there is a way to get there without heavy reflection (as described above), not sure if you consider this a hack or not. – Evk Feb 06 '18 at 06:31
  • Maybe to use fiddler\wireshark and try analyze client and server hello messages. – Johnny Feb 06 '18 at 07:41
  • This information is returned by the [SslStream](https://msdn.microsoft.com/en-us/library/system.net.security.sslstream?f=255&MSPPError=-2147217396), see the `SslProtocol` Property. Also see the `TransportContext` Property. The abstract class from which derives is implemented internally by `SslStreamContext` class and `ConnectStreamContext` class. Look at the examples there, they're quite complete. – Jimi Feb 07 '18 at 10:03
  • 3
    It seems eminently useful to have this information surfaced through a documented API, and not just available through tracing, or by absurdly brittle reflection, or attempting to infer it from a certificate, or by reimplementing HTTP yourself on top of `SslStream`. It may be worth [opening an issue for it](https://github.com/dotnet/corefx/issues). – Jeroen Mostert Feb 09 '18 at 12:41

4 Answers4

29

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 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:

  • 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; }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • 1
    You must never use a separate connection for determining this information; that opens a huge TOCTOU vulnerability. – Ben Voigt Feb 08 '18 at 16:42
  • @Ben Voigt Can you be more specific? What race condition can occur with these timings and in this context? – Jimi Feb 08 '18 at 16:45
  • It's a separate TCP connection, so a load balancer can send it to an entirely different web server. TLS is supposed to guard against a compromised path, so in evaluating TLS, we assume that the path is controlled by a malicious actor, and in this scenario, we assume that the malicious actor is treating the two connections differently. For example, forcing a downgrade attack against only one of the two connections. – Ben Voigt Feb 08 '18 at 16:52
  • @Ben Voigt This could happen in MITM attack. Doesn't seem relevant in this context. The OP wants to know what protocol a secure Web Server negotiates. If security itself is already compromised, any information is to be considered compromised. – Jimi Feb 08 '18 at 16:58
  • If your threat model assumes the network is uncompromised, why are you wasting resources performing TLS in the first place? – Ben Voigt Feb 08 '18 at 17:01
  • @Ben Voigt Because it is required by the server you need to connect to? (I, personally, don't assume anything about my network. I have safeguards in place) I still don't know how this relates to the OP request. If his network is compromised, he will get bad responses anyway. Maybe two different connections with different results, may give him instead a hint. – Jimi Feb 08 '18 at 17:08
  • @Jimi Nice job too! I like your simplified solution that has a shorter method in the end and no recursive call. – Frederic Feb 09 '18 at 23:08
  • @Frederic Thanks. I can't say I like the idea of resorting to Reflection to solve this. And I know that the Cipher/Hash "analysis" can be annoying. I answered because I find interesting the research. I hope I'll come up with something more "elegant". – Jimi Feb 09 '18 at 23:33
  • @Jimi: Shouldn't it be m_NetworkStream instead of NetworkStream ? var _objTlsStream = _objConnection.GetType().GetProperty("NetworkStream", bindingFlags).GetValue(_objConnection); – Frederic Feb 12 '18 at 21:24
  • @Frederic Nope, `NetworkStream` is a property and that's its name. Do you have some kind of issue with it? (Usually, private field names have a `m_` prefix) – Jimi Feb 12 '18 at 21:27
  • @Jimi Never mind, I had not seen that property `NetworkStream` that accesses the private property [line 325 here](https://referencesource.microsoft.com/#System/net/System/Net/_PooledStream.cs,e88253d64c67abf0,references). I guess I meant : shouldn't we use `Connection` instead of `m_Connection` ? [see here](https://referencesource.microsoft.com/#System/net/System/Net/_ConnectStream.cs,5f027b70c70d93f6,references) Otherwise, it works great, just trying to dive into it to understand it perfectly. – Frederic Feb 12 '18 at 21:38
  • @Frederic As you can see from that code, it the exact same thing, they return the same object (`System.Net.PooledStream`). You can choose to get that object from the property (`Connection`) or the field (`m_Connection`). Are you trying to find a way aroud reflection? Mind that I'll keep digging myself. If (when) I find something, I'll let you know. – Jimi Feb 12 '18 at 21:47
  • @Jimi I was just wondering if there was a reason why you use a private property in one case (`m_Connection`) and the internal accessor in another case (`NetworkStream` instead of `m_NetworkStream`). I like consistency :-) – Frederic Feb 12 '18 at 21:49
  • @Frederic Well, that's a good thing. --The use of the field instead of the property seemed to shorten the path of one step to get to the m_Worker. But, as I said, it's really the same object. If you want to reference just the fields, for "naming convention" sake, I don't see way you shouldn't. The `m_NetworkStream` is the same object returned by the related property. You can't have all property names, though. – Jimi Feb 12 '18 at 22:01
2

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;
}
caesay
  • 16,932
  • 15
  • 95
  • 160
  • Like I mentioned to half of the people that came here, No, I'm not trying to enforce that I communicate over a certain protocol, I already know how to do that, thanks, read the question attentively please – Frederic Feb 09 '18 at 14:05
  • 3
    @Frederic Right, well, you're asking for something impossible, and you don't tell us why you need it. You don't explain this in question or in the comments. A likely reason for wanting this (since we need to guess) would be abandoning the connection or doing something differently if using a less-secure tls version, which is why I bring it up. It is by no means the sum of my answer and I put a fair amount of thought and testing into the posted solution. Take it or leave it. – caesay Feb 09 '18 at 14:09
  • 1
    Is that really important that you know why I need it ? Then here's the reason : Every time I connect to a secured URL, I want to log the TLS version that was used so it can be used when/if troubleshooting in the future (it can be helpful to know which TLS version each secured URL I connected to uses). If you're telling me that's impossible, then so be it, that's why I am asking the question. If I had known that it would be impossible, I would have not asked. Now i know, thanks! – Frederic Feb 09 '18 at 15:55
  • 1
    @Frederic: it's important for us to know why you need it simply because it allows us to suggest or disqualify alternative options. When you don't specify, and then get mad at someone for suggesting alternatives, that's not very nice. I've updated your question. – caesay Feb 09 '18 at 16:40
  • I think you misunderstood, when did I get mad ? I was just remind you that you're the 4th person (you cant see that because they all removed their answer by now) that tells me that we can specify the TLS versions to use. Hence, I was reminding you that this is not what I am trying to accomplish. I am not mad at all, sorry if that came across like this. Thanks for updating my question Peace! – Frederic Feb 09 '18 at 16:48
  • @Frederic Well, it's been demonstrated that it's not impossible (using the WebRequest context provided, I mean). You have at least 2 methods using reflection a one by inference (which also lets you store/analyze the Ciphers and Hashes used). I haven't seen anyone telling you what protocol version to use, probably for the reason you mentioned, but that's not important. Your feedback on what has been tried so solve your request is important, though. – Jimi Feb 09 '18 at 17:20
  • @caesay: I have just tested your solution and it works like a charm. You state that it is a hack but it looks pretty good to me. Nice job ! Note : It's the first bounty I create so I am not totally familiar with the process. I might wait in case people bring in different solutions – Frederic Feb 09 '18 at 21:30
1

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

0

The only way I can figure out is use SslStream to make a test connection, then check SslProtocol property.

TcpClient client = new TcpClient(decodedUri.DnsSafeHost, 443);
SslStream sslStream = new SslStream(client.GetStream());

// use this overload to ensure SslStream has the same scope of enabled protocol as HttpWebRequest
sslStream.AuthenticateAsClient(decodedUri.Host, null,
    (SslProtocols)ServicePointManager.SecurityProtocol, true);

// Check sslStream.SslProtocol here

client.Close();
sslStream.Close();

I have checked that sslStream.SslProtocl will always be as same as the TlsStream.m_worker.SslProtocol that used by HttpWebRequest's Connection.

Alex.Wei
  • 1,798
  • 6
  • 12
  • Well, you nailed it. There's another way, using a HttpWebRequest certificate Callback. Can you figure that out? – Jimi Feb 07 '18 at 13:11
  • @Jimi `RemoteCertificateValidationCallback` only give us the sender, the certification that need to be validated, the certification chain, and validation error detected by default validator, but didn't provide any other TLS connection information. So sadly, that was a dead end. – Alex.Wei Feb 07 '18 at 13:38
  • You're in the right path. It's a really cryptic information (just a number among other dotted numbers). Here's a hint: [WebSockets SSPIWrapper](https://referencesource.microsoft.com/#System/net/System/Net/_SSPIWrapper.cs,712). Look at the properties of the cerificates that come in the certification chain :) – Jimi Feb 07 '18 at 13:46
  • You need to check the stream used by the HTTP request, not a separate connection. Just because the two connections have the same TLS characteristics in your well-behaved test network does not allow you to conclude that they are "always the same" under the threat model. – Ben Voigt Feb 08 '18 at 16:55
  • @BenVoigt Correct, indeed I would like to know which TLS version was used in my actual HTTP request's stream and not a separate one. Though this solution provides good knowledge, it still requires a separate call to the uri which technically could return different results ? (I don't know about that) – Frederic Feb 08 '18 at 17:41
  • @Frederic Actually, http connection and SslStream both use SslState internally with only slice difference that would not effect ssl/tls connection behavior. But if you cann't satisfy with that, you have to reflect through RequestStream/ResponseStream.m_Connection.m_NetworkStream.m_Worker.SslProtocol. This way is the way that I had known from the beginning, and also is which way I had tried to avoid, because is too many internal classes and private members. – Alex.Wei Feb 09 '18 at 02:53