古董 Win7 上跑 .Net 应用的 TLS 版本问题
背景
最近捣鼓了一个查询 TFS 上工作项的小工具,查询完工作项之后会自动发个邮件出来给相关的同事,然后问题来了,在我工作电脑上可以正常发送,在我一台老测试机上就发送不了,报如下的错误。
前几天找 IT 同事一起确认过是不是哪里需要配置下,也没搞定,怀疑是测试机软件环境的问题(比如还是 Win7),但没有具体的怀疑方向。
System.Net.Mail.SmtpException: 发送邮件失败。 ---> System.IO.IOException: 由于远程方已关闭传输流,身份验证失败。
在 System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
在 System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
在 System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
在 System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
在 System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
在 System.Net.TlsStream.ProcessAuthentication(LazyAsyncResult result)
在 System.Net.TlsStream.Write(Byte[] buffer, Int32 offset, Int32 size)
在 System.Net.PooledStream.Write(Byte[] buffer, Int32 offset, Int32 size)
在 System.Net.Mail.SmtpConnection.Flush()
在 System.Net.Mail.ReadLinesCommand.Send(SmtpConnection conn)
在 System.Net.Mail.EHelloCommand.Send(SmtpConnection conn, String domain)
在 System.Net.Mail.SmtpConnection.GetConnection(ServicePoint servicePoint)
在 System.Net.Mail.SmtpClient.GetConnection()
在 System.Net.Mail.SmtpClient.Send(MailMessage message)
调查
今天下班前有会空,就把问题捡起来继续调查了。
先尝试用 WireShark 抓了下包,有问题的机器上是这样的:
72424 2023-04-27 15:19:03.930917 CLIENT1 SERVER SMTP 64 C: STARTTLS
72425 2023-04-27 15:19:03.931315 SERVER CLIENT1 SMTP 83 S: 220 2.0.0 SMTP server ready
72426 2023-04-27 15:19:03.931555 CLIENT1 SERVER TLSv1 185 Client Hello
72427 2023-04-27 15:19:03.932556 SERVER CLIENT1 TCP 60 25 → 35467 [FIN, ACK] Seq=402 Ack=159 Win=64082 Len=0
而没问题的机器上是这样的:
36302 35.125573 CLIENT2 SERVER SMTP 64 C: STARTTLS
36303 35.127572 SERVER CLIENT2 SMTP 83 S: 220 2.0.0 SMTP server ready
36304 35.129940 CLIENT2 SERVER TLSv1.2 239 Client Hello
36305 35.136641 SERVER CLIENT2 TCP 1514 25 → 62789 [ACK] Seq=335 Ack=214 Win=64027 Len=1460 [TCP segment of a reassembled PDU]
36306 35.136641 SERVER CLIENT2 TCP 1514 25 → 62789 [ACK] Seq=1795 Ack=214 Win=64027 Len=1460 [TCP segment of a reassembled PDU]
36307 35.136641 SERVER CLIENT2 TCP 1514 25 → 62789 [ACK] Seq=3255 Ack=214 Win=64027 Len=1460 [TCP segment of a reassembled PDU]
36308 35.136641 SERVER CLIENT2 TLSv1.2 939 Server Hello, Certificate, Server Key Exchange, Server Hello Done
问题机器上是 TLSv1,而且 Hello 之后就被服务器主动断开了连接,而正常机器上是 TLSv1.2,随后也有正常的一些交互信息,问题应该就在这了!
然后一开始先是怀疑是不是两台机器使用的 .Net 版本不一样啊(原理上不应该怀疑,原谅一下小白),根据这里的介绍,找了个工具 DotNetVersions,问题机器是:
2.0.50727.5420 Service Pack 2
3.0.30729.5420 Service Pack 2
3.5.30729.5420 Service Pack 1
4.0.0.0
4.8.03761
而正常机器是:
4.0.0.0
4.8.04084
好像也没啥大问题。然后继续查到这个帖子,说修改注册表就可以让 .Net 应用使用更新的 TLS 版本:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319]
"SchUseStrongCrypto"=dword:00000001
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319]
"SchUseStrongCrypto"=dword:00000001
试了下,没用(后面还有相关的继续尝试)。
然后看到这个帖子建议可以使用 MailKit(不想用那个 System.Web.Mail.SmtpMail),晚点再试吧,再然后看到微软官网的几个帖子:
-
.NET Framework 中的传输层安全性 (TLS) 最佳做法
为确保 .NET Framework 应用程序的安全性,TLS 版本不应 被硬编码。 .NET Framework 应用程序应使用操作系统 (OS) 支持的 TLS 版本。
对于 TLS 1.2,在你的应用上面向 .NET Framework 4.7 或更高版本,在 WCF 应用上面向 .NET Framework 4.7.1 或更高版本。
-
Windows 8.1、Windows Server 2012 R2、Windows 10、Windows Server 2016 及更高版本的 Windows 本机支持 TLS 1.2,以便通过 WinHTTP 进行客户端与服务器通信。
NET Framework 4.6.2 及更高版本支持 TLS 1.1 和 TLS 1.2。 确认注册表设置,但无需进行其他更改。
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v2.0.50727] "SystemDefaultTlsVersions" = dword:00000001 "SchUseStrongCrypto" = dword:00000001 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319] "SystemDefaultTlsVersions" = dword:00000001 "SchUseStrongCrypto" = dword:00000001
设置 SchUseStrongCrypto 允许 .NET 使用 TLS 1.1 和 TLS 1.2。 设置 SystemDefaultTlsVersions 允许 .NET 使用 OS 配置。
编注:如果是 64 位平台跑 32 位应用,还需要改
Wow6432Node
下的。
看着都挺有道理的,但试下来,还是没用。。。包括谨遵医嘱在修改之后重启也没有效果。甚至还试了下面这个 WinHttp 相关的,重启后还给我自动改回去了。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp\
DefaultSecureProtocols = (DWORD): 0xAA0
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp\
DefaultSecureProtocols = (DWORD): 0xAA0
MailKit
虽然还是觉得可能自己哪里没弄对,但折腾不起了,还是改用第三方库吧,照猫画虎后,碰到第一个问题,正常机器都发不出去邮件了!
MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection
如下的代码里不是指明了 SSL 是 true 么,咋还不对呢?
client.Connect (host, 25, true);
被错误信息引导到官网上的 FAQ,似懂非懂,结合万能的 Stackoverflow 才明白:
When you specify useSsl as true in MailKit's Connect method, it assumes that you meant to use an SSL-wrapped connection (which is different from STARTTLS).
To make this less confusing, MailKit has a Connect method that takes a SecureSocketOptions argument instead of a bool.
把代码改成下面这样,正常机器就可以发出邮件了:
client.Connect(host, 25, MailKit.Security.SecureSocketOptions.StartTls);
然后把程序部署到测试机器上,继续报错,我太难了。。。
The host name did not match the name given in the server's SSL certificate
还是回到上面那个 FAQ,图省事的话,加上如下这句就好了,相当于忽略证书相关的错误(作者说了:生产环境强烈不建议!):
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
尾声
我是应该继续使用 MailKit,还是该直接给测试机升级个 Win10?
可能两个都得做吧,既然微软官方也不建议继续使用 System.Net.Mail.SmtpClient。