Which TLS version was negotiated?
Asked Answered
C

4

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).

Cheesy answered 2/2, 2018 at 19:25 Comment(9)
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..Haycock
@Crowcoder: Could probably look in the Framework Source and see where the verbose logging is getting its information.Horlacher
@Haycock : That would be a good start but I'm more looking to get that in the code itself and then log it myselfCheesy
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.Timmy
In the end, you probably want to get here, but I don't see a reliable way how to: referencesource.microsoft.com/#System/net/System/Net/…Timmy
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.Caracole
Maybe to use fiddler\wireshark and try analyze client and server hello messages.Lyell
This information is returned by the SslStream, 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.Bron
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.Hotheaded
B
32

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; }
}
Bron answered 8/2, 2018 at 0:19 Comment(14)
You must never use a separate connection for determining this information; that opens a huge TOCTOU vulnerability.Ripon
@Ben Voigt Can you be more specific? What race condition can occur with these timings and in this context?Bron
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.Ripon
@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.Bron
If your threat model assumes the network is uncompromised, why are you wasting resources performing TLS in the first place?Ripon
@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.Bron
@Bron Nice job too! I like your simplified solution that has a shorter method in the end and no recursive call.Cheesy
@Cheesy 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".Bron
@Jimi: Shouldn't it be m_NetworkStream instead of NetworkStream ? var _objTlsStream = _objConnection.GetType().GetProperty("NetworkStream", bindingFlags).GetValue(_objConnection);Cheesy
@Cheesy 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)Bron
@Bron Never mind, I had not seen that property NetworkStream that accesses the private property line 325 here. I guess I meant : shouldn't we use Connection instead of m_Connection ? see here Otherwise, it works great, just trying to dive into it to understand it perfectly.Cheesy
@Cheesy 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.Bron
@Bron 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 :-)Cheesy
@Cheesy 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.Bron
G
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;
}
Girt answered 9/2, 2018 at 13:35 Comment(7)
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 pleaseCheesy
@Cheesy 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.Girt
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!Cheesy
@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.Girt
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!Cheesy
@Cheesy 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.Bron
@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 solutionsCheesy
S
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

Selenaselenate answered 31/1, 2019 at 11:56 Comment(0)
C
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.

Contrecoup answered 7/2, 2018 at 11:32 Comment(6)
Well, you nailed it. There's another way, using a HttpWebRequest certificate Callback. Can you figure that out?Bron
@Bron 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.Contrecoup
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. Look at the properties of the cerificates that come in the certification chain :)Bron
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.Ripon
@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)Cheesy
@Cheesy 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.Contrecoup

© 2022 - 2024 — McMap. All rights reserved.