谈判了哪个TLS版本?

时间:2018-02-02 19:25:56

标签: c# .net ssl

我的应用程序在.NET 4.7中运行。默认情况下,它将尝试使用TLS1.2。 是否有可能知道在执行时协商了哪个TLS版本,例如,如下所示的HTTP请求?

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

我只需要这些信息用于记录/调试,因此在写入请求流或接收响应之前获取此信息并不重要。我不想解析这些信息的网络跟踪日志,我也不想创建第二个连接(使用SslStream或类似的连接)。

4 个答案:

答案 0 :(得分:11)

您可以使用Reflection来获取TlsStream->SslState->SslProtocol属性值 此信息可以从HttpWebRequest.GetRequestStream()HttpWebRequest.GetResponseStream()返回的流中提取。

<强>更新
ExtractSslProtocol()方法现在正在检测激活GzipStream AutomaticDecompression时可能返回的压缩DeflateStreamWebRequest

验证将在TlsValidationCallback中进行,使用request.GetRequestStream()

初始化请求时会调用此验证
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

    //(...)
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | 
                                           SecurityProtocolType.Tls | 
                                           SecurityProtocolType.Tls11 | 
                                           SecurityProtocolType.Tls12;
    ServicePointManager.ServerCertificateValidationCallback += TlsValidationCallback;

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(decodedUri);
    if (requestPayload.Length > 0)
    {
        using (Stream requestStream = request.GetRequestStream())
        {
            //Here the request stream is already validated
            SslProtocols SslProtocol = ExtractSslProtocol(requestStream);
            requestStream.Write(requestPayload, 0, requestPayload.Length);
        }
    }
    //(...)

private SslProtocols ExtractSslProtocol(Stream stream)
{
    if (stream is null) return SslProtocols.None;
    BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
    Stream metaStream = stream;
    try
    {
        if (stream.GetType().BaseType == typeof(GZipStream))
            metaStream = stream as GZipStream;
        else if (stream.GetType().BaseType == typeof(DeflateStream))
            metaStream = stream as DeflateStream;

        var baseStream = metaStream?.GetType().GetProperty("BaseStream").GetValue(stream);

        var connection = baseStream.GetType().GetProperty("Connection", bindingFlags).GetValue(baseStream);
        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);
    }
    finally
    {
        metaStream.Dispose();
    }
}

RemoteCertificateValidationCallback包含有关所用安全协议的一些有用信息。 (参见:Transport Layer Security (TLS) Parameters (IANA)RFC 5246) 所使用的安全协议类型可以提供足够的信息,因为每个协议版本都支持哈希和加密算法的子集 Tls 1.2,介绍HMAC-SHA256并弃用IDEADES密码(所有变体都列在链接文档中)。

在这里,我插入了OIDExtractor,其中列出了正在使用的算法 请注意,TcpClient()和WebRequest()都将到达此处。

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();

    if (sslPolicyErrors == SslPolicyErrors.None) 
        return true;

    X509Certificate2 _Certificate = new X509Certificate2(CACert);

    //If you needed/have to pass a certificate, add it here.
    //X509Certificate2 _CACert = new X509Certificate2(@"[localstorage]/ca.cert");
    //CAChain.ChainPolicy.ExtraStore.Add(_CACert);
    CAChain.Build(_Certificate);
    foreach (X509ChainStatus CACStatus in CAChain.ChainStatus)
    {
        if ((CACStatus.Status != X509ChainStatusFlags.NoError) &
            (CACStatus.Status != X509ChainStatusFlags.UntrustedRoot))
            return false;
    }
    return true;
}

<小时/> 更新2:
secur32.dll - &gt; QueryContextAttributesW()方法允许查询已初始化的流的连接安全上下文。

[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);

从文档中可以看出,此方法返回引用void* buffer结构的SecPkgContext_ConnectionInfo

//[SuppressUnmanagedCodeSecurity]
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;
}

SchProtocols dwProtocol成员是SslProtocol。

捕获的是什么 引用连接上下文句柄的TlsStream.Context.m_SecurityContext._handle不公开 因此,您只能通过反思或通过System.Net.Security.AuthenticatedStream返回的System.Net.Security.SslStream派生类(System.Net.Security.NegotiateStreamTcpClient.GetStream())来获取它。

不幸的是,WebRequest / WebResponse返回的Stream无法强制转换为这些类。 Connections和Streams Types仅通过非公共属性和字段引用。

我发布了已组装的文档,它可能会帮助您找到另一条获取上下文句柄的途径。

声明,结构,枚举器列表位于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)

代码库(部分)

.NET Reference Source

Internals.cs

internal struct SSPIHandle { }

internal enum ContextAttribute { }

<小时/> 更新1:

  

我在你的评论中看到了解决方案使用的另一个答案   您无法接受TcpClient()。无论如何,我还是把它留在这里   Ben Voigt在这个评论中的评论对任何有兴趣的人都有用。此外,3种可能的解决方案优于2.

在提供的上下文中TcpClient() SslStream用法的一些实施细节。

如果在初始化WebRequest之前需要协议信息,则可以使用TLS连接所需的相同工具在同一上下文中建立TcpClient()连接。即,ServicePointManager.SecurityProtocol用于定义支持的协议,ServicePointManager.ServerCertificateValidationCallback用于验证服务器证书。

TcpClient()和WebRequest都可以使用这些设置:
- 启用所有协议,让Tls握手确定将使用哪一个 - 定义RemoteCertificateValidationCallback()委托以验证服务器在X509Certificates中传递的X509Chain

实际上,在建立TcpClient或WebRequest连接时,Tls Handshake是相同的 这种方法可以让您知道您的HttpWebRequest 与同一服务器协商的Tls协议。

设置TcpClient()以接收和评估SslStream checkCertificateRevocation标记设置为false,因此该过程不会浪费时间查找撤销列表。
证书验证回调与ServicePointManager

中指定的相同
TlsInfo TLSInfo;
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);

//(...)

TlsInfo类收集有关已建立的安全连接的一些信息:
- Tls协议版
- 密码和散列算法
- Ssl握手中使用的服务器证书

public class TlsInfo
{
    public TlsInfo(SslStream SecureStream)
    {
        this.ProtocolVersion = SecureStream.SslProtocol;
        this.CipherAlgorithm = SecureStream.CipherAlgorithm;
        this.HashAlgorithm = SecureStream.HashAlgorithm;
        this.RemoteCertificate = SecureStream.RemoteCertificate;
    }

    public SslProtocols ProtocolVersion { get; set; }
    public CipherAlgorithmType CipherAlgorithm { get; set; }
    public HashAlgorithmType HashAlgorithm { get; set; }
    public X509Certificate RemoteCertificate { get; set; }
}

答案 1 :(得分:2)

以下解决方案肯定是“黑客”,因为它确实使用了反射,但它目前涵盖了您可能使用HttpWebRequest的大多数情况。如果无法确定Tls版本,它将返回null。在您向请求流写入任何内容之前,它还会在同一请求中验证Tls版本。如果在调用方法时尚未发生流Tls握手,则会触发它。

您的样本用法如下:

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

方法:

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

答案 2 :(得分:0)

我能弄清楚的唯一方法是使用SslStream建立测试连接,然后检查SslProtocol属性。

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();

我已检查sslStream.SslProtocl将始终与TlsStream.m_worker.SslProtocol HttpWebRequest使用的Connection相同。

答案 3 :(得分:0)

在这里和那里汇集了一些想法,我做了一个简单的方法来测试每种可用的协议,每次尝试都强制一种特定类型的连接。 最后,我得到一个列表,其中包含需要使用的结果。

Ps:仅当您知道该网站在线时,该测试才有效-您可以进行先前的测试以进行检查。

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

希望这会有所帮助