c# - 协商了哪个 TLS 版本?

标签 c# .net ssl

我的应用程序在 .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 或类似的)。

最佳答案

您可以使用反射到达 TlsStream->SslState->SslProtocol适当的值(value)。
此信息可以从 HttpWebRequest.GetRequestStream() 返回的 Stream 中提取。和 HttpWebRequest.GetResponseStream() .ExtractSslProtocol()还处理压缩的 GzipStreamDeflateStream WebRequest 时返回的AutomaticDecompression被激活。
验证将在 ServerCertificateValidationCallback 中进行,在使用 request.GetRequestStream() 初始化请求时调用
备注 : SecurityProtocolType.Tls13 包含在 .Net Framework 4.8+ 中和 .Net 核心 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有一些关于使用的安全协议(protocol)的有用信息。 (参见:Transport Layer Security (TLS) Parameters (IANA)RFC 5246)。
使用的安全协议(protocol)类型可以提供足够的信息,因为每个协议(protocol)版本都支持散列和加密算法的子集。
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
);
从文档中可以看出,此方法返回 void* buffer引用 SecPkgContext_ConnectionInfo结构体:
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 类型仅通过非公共(public)属性和字段引用。
我正在发布汇编的文档,它可能会帮助您找出到达该上下文句柄的另一条路径。
声明、结构、枚举器列表位于 QueryContextAttributesW (PASTEBIN) .
微软技术网
Authentication Structures
微软
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:

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.


TcpClient() 上的一些实现细节SslStream在提供的上下文中使用。
如果在初始化 WebRequest 之前需要协议(protocol)信息,则可以使用 TLS 连接所需的相同工具在相同的上下文中建立 TcpClient() 连接。即 ServicePointManager.SecurityProtocol 定义支持的协议(protocol)和 ServicePointManager.ServerCertificateValidationCallback 验证服务器证书。
TcpClient() 和 WebRequest 都可以使用这些设置:
  • 启用所有协议(protocol)并让 TLS 握手确定将使用哪一个。
  • 定义一个 RemoteCertificateValidationCallback()委托(delegate)验证 X509Certificates服务器传入 X509Chain .

  • 实际上,在建立 TcpClient 或 WebRequest 连接时,TLS 握手是相同的。
    这种方法让您知道您的 HttpWebRequest 是什么 Tls 协议(protocol)将与同一服务器协商。
    设置 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 协议(protocol)版本
  • 密码和哈希算法
  • 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; }
    }
    

    关于c# - 协商了哪个 TLS 版本?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48589590/

    相关文章:

    security - 为什么允许没有子域的站点使用通配符证书?

    python - 无法使用 pyenv 安装 Python 3.8.5

    c# - 在 C# 中的 Xml 签名中配置 TSA

    c# - TabControl 底部的选项卡

    .net - 为什么 List<T>.IndexOf() 比 List<T>.Contains() 快很多?

    c# - 如何过滤项目的子列表

    c# - 带有 String keySelector 的 OrderBy

    ssl - 使用 SSL 的雅司给出错误 "SSL accept failed: timeout"

    c# - 我应该在执行 SQL 语句之前从中删除空格吗?

    c# - 是否可以在不创建对象实例的情况下获取对象属性名称字符串?