我的应用程序在.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属性值。 这些信息可以从双方返回的流中提取HttpWebRequest.GetRequestStream()和HttpWebRequest.GetResponseStream()。
TlsStream->SslState->SslProtocol
HttpWebRequest.GetRequestStream()
HttpWebRequest.GetResponseStream()
该ExtractSslProtocol()还处理压缩GzipStream或者DeflateStream当被返回WebRequest AutomaticDecompression被激活。
ExtractSslProtocol()
GzipStream
DeflateStream
WebRequest
验证将在中进行ServerCertificateValidationCallback,当使用初始化请求时会调用。request.GetRequestStream()
ServerCertificateValidationCallback
request.GetRequestStream()
注意 : SecurityProtocolType.Tls13 包含在.Net Framework 4.8+和.Net Core中3.0+。
SecurityProtocolType.Tls13
4.8+
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; //(...) ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; 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对所使用的安全协议的一些有用的信息。(请参阅:传输层安全性(TLS)参数(IANA)和RFC 5246)。 使用的安全协议的类型可以提供足够的信息,因为每个协议版本都支持哈希和加密算法的子集。 Tls 1.2引入HMAC-SHA256并弃用IDEA和DES加密(所有变体在链接的文档中列出)。
RemoteCertificateValidationCallback
HMAC-SHA256
IDEA
DES
在这里,我插入了OIDExtractor,其中列出了正在使用的算法。 请注意,TcpClient()和WebRequest()都将到达此处。
OIDExtractor
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 if (sslPolicyErrors == SslPolicyErrors.None) return true; X509Certificate2 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()方法,允许查询一个初始化流的连接安全性上下文。
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结构的:
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。
SchProtocols dwProtocol
有什么收获。 该TlsStream.Context.m_SecurityContext._handle引用连接上下文句柄是不公开的。 因此,您只能再次通过反射或通过返回的System.Net.Security.AuthenticatedStream派生类(System.Net.Security.SslStream和System.Net.Security.NegotiateStream)来获取它TcpClient.GetStream()。
TlsStream.Context.m_SecurityContext._handle
System.Net.Security.AuthenticatedStream
System.Net.Security.SslStream
System.Net.Security.NegotiateStream
TcpClient.GetStream()
不幸的是,WebRequest / WebResponse返回的Stream不能转换为这些类。仅通过非公共属性和字段引用连接和流类型。
我正在发布汇编的文档,它可能会帮助您找到获取该Context Handle的另一条路径。
声明,结构,枚举器列表位于QueryContextAttributesW(PASTEBIN)中。
Microsoft TechNet 身份验证结构
MSDN 使用Schannel创建安全连接
获取有关Schannel连接的信息
查询Schannel上下文的属性
QueryContextAttributes(Schannel)
代码库(部分)
.NET参考源
Internals.cs
内部结构SSPIHandle {}
内部枚举ContextAttribute {}
更新1:
我在您对另一个答案的评论中看到,您TcpClient()不接受使用该解决方案 。我还是把它留在这里,所以本·沃伊特(Ben Voigt)在这本书中的评论 将对其他感兴趣的人有用。另外,3种可能的解决方案要优于2种。
TcpClient()
在提供的上下文中,有关TcpClient() SslStream用法的一些实现细节。
如果在初始化WebRequest之前需要协议信息,则可以使用TLS连接所需的相同工具在相同的上下文中建立TcpClient()连接。即,ServicePointManager.SecurityProtocol定义支持的协议和ServicePointManager.ServerCertificateValidationCallback验证服务器证书。
ServicePointManager.SecurityProtocol
ServicePointManager.ServerCertificateValidationCallback
TcpClient()和WebRequest都可以使用以下设置: -启用所有协议,并让Tls握手确定将使用哪个协议。 -定义一个RemoteCertificateValidationCallback()委托来验证X509Certificates服务器传递的X509Chain。
RemoteCertificateValidationCallback()
X509Certificates
X509Chain
实际上,建立TcpClient或WebRequest连接时,Tls握手是相同的。 这种方法使您知道HttpWebRequest 将 与同一服务器协商哪些Tls协议。
设置一个TcpClient()以接收和评估SslStream。 该checkCertificateRevocation标志设置为false,因此该过程不会浪费时间来查找吊销列表。 证书验证回调与在中指定的相同ServicePointManager
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协议版本 -密码和Hash算法 - SSL握手中使用的服务器证书
TlsInfo
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; } }