谈判了哪个TLS版本?

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

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

更新
ExtractSslProtocol()方法现在检测激活WebRequest AutomaticDecompression时可能返回的压缩GzipStreamDeflateStream

validation将在TlsValidationCallback ,当使用request.GetRequestStream()初始化请求时调用TlsValidationCallback

 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) { BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; Stream CompressedStream = null; if (stream.GetType().BaseType == typeof(GZipStream)) CompressedStream = (GZipStream)stream; else if (stream.GetType().BaseType == typeof(DeflateStream)) CompressedStream = (DeflateStream)stream; var objbaseStream = CompressedStream?.GetType().GetProperty("BaseStream").GetValue(stream); if (objbaseStream == null) objbaseStream = stream; var objConnection = objbaseStream.GetType().GetField("m_Connection", bindingFlags).GetValue(objbaseStream); var objTlsStream = objConnection.GetType().GetProperty("NetworkStream", bindingFlags).GetValue(objConnection); var objSslState = objTlsStream.GetType().GetField("m_Worker", bindingFlags).GetValue(objTlsStream); return (SslProtocols)objSslState.GetType().GetProperty("SslProtocol", bindingFlags).GetValue(objSslState); } 

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

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

 private bool TlsValidationCallback(object sender, X509Certificate CACert, X509Chain CAChain, SslPolicyErrors sslPolicyErrors) { List _OIDExtractor = CAChain .ChainElements .Cast() .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 – > QueryContextAttributesW()方法允许查询已初始化的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); 

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

 //[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不是公共的。
因此,您只能通过reflection或通过TcpClient.GetStream()返回的System.Net.Security.AuthenticatedStream派生类( System.Net.Security.SslStreamSystem.Net.Security.NegotiateStream )来获取TcpClient.GetStream()

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

我正在发布已组装的文档,它可能会帮助您找到另一条获取Context Handle的途径。

声明,结构,枚举器列表位于QueryContextAttributesW(PASTEBIN)中 。

Microsoft TechNet
身份validation结构

MSDN
使用Schannel创建安全连接

获取有关Schannel连接的信息

查询Schannel上下文的属性

QueryContextAttributes(Schannel)

代码库(部分)

.NET参考源

Internals.cs

内部结构SSPIHandle {}

内部枚举ContextAttribute {}


更新1:

我在你的评论中看到另一个答案,使用TcpClient()的解决方案对你来说是不可接受的。 无论如何我都会把它留在这里,所以Ben Voigt在这篇文章中的评论对任何有兴趣的人都有用。 此外,3种可能的解决方案优于2种。

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

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

TcpClient()和WebRequest都可以使用以下设置:
– 启用所有协议,让Tls Handshake确定将使用哪个协议。
– 定义RemoteCertificateValidationCallback()委托以validation服务器在X509Chain传递的X509Certificates

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

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

下面的解决方案肯定是“黑客”,因为它确实使用了reflection,但它目前涵盖了你可能使用HttpWebRequest的大多数情况。 如果无法确定Tls版本,它将返回null。 在您向请求流写入任何内容之前,它还会在同一请求中validationTls版本。 如果在调用方法时尚未发生流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; } 

我能弄明白的唯一方法是使用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相同。