【问题标题】:Which TLS version was negotiated?协商了哪个 TLS 版本?
【发布时间】:2018-07-13 08:41:57
【问题描述】:

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

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

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

【问题讨论】:

  • 我希望为您着想有一种更简单的方法,但如果您启用详细的System.Net 跟踪,它将记录该信息,您可能会解析出来,即System.Net Information: 0 : [18984] EndProcessAuthentication(Protocol=Tls12, Cipher=Aes256 256 bit strength,....etc..
  • @Crowcoder:可能会查看框架源代码并查看详细日志记录从何处获取其信息。
  • @Crowcoder :这将是一个好的开始,但我更希望在代码本身中得到它,然后自己记录它
  • 我不认为有一种方法可以不经过深思熟虑(如上所述)到达那里,不确定您是否认为这是一种黑客行为。
  • 通过记录的 API 显示这些信息似乎非常有用,而不仅仅是通过跟踪,或通过荒谬的脆弱反射,或尝试从证书推断它,或通过自己重新实现 HTTP SslStream 的顶部。可能值得opening an issue for it

标签: c# .net ssl


【解决方案1】:

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

ExtractSslProtocol() 还处理在激活WebRequest AutomaticDecompression 时返回的压缩GzipStreamDeflateStream

验证将发生在ServerCertificateValidationCallback中,当使用request.GetRequestStream()初始化请求时会调用它

注意SecurityProtocolType.Tls13 包含在 .Net Framework 4.8+ 和 .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);
}

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

更新 2:
secur32.dll -> 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
);

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

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 不是公开的。
因此,您只能通过反射或通过TcpClient.GetStream() 返回的System.Net.Security.AuthenticatedStream 派生类(System.Net.Security.SslStreamSystem.Net.Security.NegotiateStream)再次获得它。

很遗憾,WebRequest/WebResponse 返回的 Stream 无法转换为这些类。 Connections 和 Streams 类型仅通过非公共属性和字段引用。

我正在发布汇编的文档,它可能会帮助您找出到达该上下文句柄的另一条路径。

声明、结构、枚举器列表位于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 在这个中的 cmets 将对其他感兴趣的人有用。此外,3 种可能的解决方案优于 2 种。

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

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

TcpClient() 和 WebRequest 都可以使用这些设置:

  • 启用所有协议并让 TLS 握手确定将使用哪一个。
  • 定义一个RemoteCertificateValidationCallback()委托来验证服务器在X509Chain中传递的X509Certificates

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

设置TcpClient() 以接收和评估SslStream
checkCertificateRevocation 标志设置为 false,因此该过程不会浪费时间查找吊销列表。
证书验证回调与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);

//(...)

TlsInfo 类收集有关已建立的安全连接的一些信息:

  • TLS 协议版本
  • 密码和哈希算法
  • SSL 握手中使用的服务器证书

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

【讨论】:

  • 您绝不能使用单独的连接来确定此信息;这会打开一个巨大的 TOCTOU 漏洞。
  • @Ben Voigt 你能说得更具体点吗?这些时间和在这种情况下会发生什么竞争条件?
  • 这是一个单独的 TCP 连接,因此负载均衡器可以将其发送到完全不同的 Web 服务器。 TLS 应该防止受损路径,因此在评估 TLS 时,我们假设路径由恶意行为者控制,在这种情况下,我们假设恶意行为者对两个连接的处理方式不同。例如,强制对两个连接中的一个进行降级攻击。
  • @Ben Voigt 这可能发生在 MITM 攻击中。在这种情况下似乎不相关。 OP 想知道安全 Web 服务器协商什么协议。如果安全本身已经受到威胁,则任何信息都将被视为受到威胁。
  • 如果您的威胁模型假设网络不受影响,您为什么要浪费资源执行 TLS?
【解决方案2】:

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

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

希望这会有所帮助

【讨论】:

    【解决方案3】:

    下面的解决方案肯定是一个“hack”,因为它确实使用了反射,但它目前涵盖了使用 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;
    }
    

    【讨论】:

    • 就像我对一半来这里的人提到的那样,不,我不是要强制我通过某种协议进行通信,我已经知道该怎么做,谢谢,阅读问题请注意
    • @Frederic 是的,好吧,你要求的是不可能的事情,但你没有告诉我们你为什么需要它。您没有在问题中或在 cmets 中解释这一点。如果使用不太安全的 tls 版本,想要这个(因为我们需要猜测)的可能原因是放弃连接或做一些不同的事情,这就是我提出它的原因。这绝不是我的答案的总和,我对发布的解决方案进行了大量的思考和测试。要么接受,要么离开。
    • 知道我为什么需要它真的很重要吗?那么原因如下:每次我连接到安全 URL 时,我都想记录使用的 TLS 版本,以便将来/如果故障排除时可以使用(了解每个安全 URL 的哪个 TLS 版本会很有帮助)连接到用途)。如果你告诉我这是不可能的,那就这样吧,这就是我问这个问题的原因。如果我知道这是不可能的,我就不会问了。现在我知道了,谢谢!
    • @Frederic:了解您为什么需要它对我们来说很重要,因为它允许我们建议或取消替代选项的资格。当你没有具体说明,然后因为某人提出替代方案而生气,那不是很好。我已经更新了你的问题。
    • 我想你误会了,我什么时候生气了?我只是提醒您,您是第 4 个人(您看不到,因为他们现在都删除了他们的答案)告诉我我们可以指定要使用的 TLS 版本。因此,我提醒你,这不是我想要完成的。我一点也不生气,如果遇到这种情况,我很抱歉。感谢您更新我的问题和平!
    【解决方案4】:

    我能弄清楚的唯一方法是使用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 将始终与HttpWebRequestConnection 使用的TlsStream.m_worker.SslProtocol 相同。

    【讨论】:

    • 好吧,你成功了。还有另一种方法,使用 HttpWebRequest 证书回调。你能弄清楚吗?
    • @Jimi RemoteCertificateValidationCallback 只给了我们发送者、需要验证的证书、证书链以及默认验证器检测到的验证错误,而没有提供任何其他 TLS 连接信息。可悲的是,这是一条死胡同。
    • 您走在正确的道路上。这是一个非常神秘的信息(只是其他虚线数字中的一个数字)。这里有一个提示:WebSockets SSPIWrapper。查看认证链中证书的属性:)
    • 您需要检查HTTP请求使用的流,而不是单独的连接。仅仅因为这两个连接在您表现良好的测试网络中具有相同的 TLS 特征,并不能让您得出在威胁模型下它们“始终相同”的结论。
    • @BenVoigt 正确,确实我想知道在我的实际 HTTP 请求流中使用了哪个 TLS 版本,而不是单独的。虽然这个解决方案提供了很好的知识,但它仍然需要单独调用 uri,这在技术上可以返回不同的结果? (我不知道)
    猜你喜欢
    • 1970-01-01
    • 2011-03-05
    • 2017-04-28
    • 2019-05-30
    • 1970-01-01
    • 2018-07-24
    • 1970-01-01
    • 2021-09-11
    • 1970-01-01
    相关资源
    最近更新 更多