更新到 net core 3.1 后 SMTP 客户端停止工作

SMTP client stopped working after updating to netcore 3.1

将我的 asp.net-core web API 项目从 2.2 更新到 3.1 后,当我尝试通过 System.Net.Mail.SmtpClient 发送电子邮件时出现以下异常:

- 2020-02-09 14:40:26.8163  15  ( SmtpClient.OnSendCompleted => <>c__DisplayClass78_0.<SendMailAsync>b__0 => SmtpClient.HandleCompletion )  -  Error Failed to send E-Mail. -- ( Exception: System.Net.Mail.SmtpException: Failure sending mail.
 ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
   at System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)
   at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslStream.PartialFrameCallback(AsyncProtocolRequest asyncRequest)
--- End of stack trace from previous location where exception was thrown ---
   at System.Net.Security.SslStream.ThrowIfExceptional()
   at System.Net.Security.SslStream.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
   at System.Net.Security.SslStream.EndProcessAuthentication(IAsyncResult result)
   at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
   at System.Net.Mail.SmtpConnection.ConnectAndHandshakeAsyncResult.TlsStreamAuthenticateCallback(IAsyncResult result)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw(Exception source)
   at System.Net.Mail.SmtpConnection.ConnectAndHandshakeAsyncResult.End(IAsyncResult result)
   at System.Net.Mail.SmtpTransport.EndGetConnection(IAsyncResult result)
   at System.Net.Mail.SmtpClient.ConnectCallback(IAsyncResult result)
   --- End of inner exception stack trace ---

API 的端点是通过 appsettings.json

中的 Kestrel 部分配置的
  "Kestrel": {
    "EndPoints": {
      "Http": {
        "Url": "http://myapidomain:80"
      },
      "Https": {
        "Url": "https://www.myapidomain:443",
        "Certificate": {
          "Path": <path to pfx file>,
          "Password": <password>
        }
      }
    }
  },

我就是这样使用 SmtpClient:

using (var client = new SmtpClient(_options.MailServiceHost, _options.MailServicePort))
{
    client.EnableSsl = true;
    client.ClientCertificates.Add(_certificate);
    client.DeliveryMethod = SmtpDeliveryMethod.Network;
    client.UseDefaultCredentials = false;
    client.Credentials = new NetworkCredential(_options.MailAccountName, _options.MailAccountPassword);

    var mail = new MailMessage
    {
        From = new MailAddress(author),
        Body = body,
        Subject = subject,
        Sender = new MailAddress(author),
        SubjectEncoding = Encoding.UTF8,
        BodyEncoding = Encoding.UTF8,
        HeadersEncoding = Encoding.UTF8
    };

    mail.Headers.Add("Content-Type", "content=text/html; charset=\"UTF-8\"");
    mail.To.Add(receiver);
    mail.ReplyToList.Add(author);

    _logger.LogInformation($"Sending mail to: {receiver}");
    await client.SendMailAsync(mail);
    return true;
}

以上代码在我切换回 aspnetcore2.2 版本时有效,所以我怀疑这是证书本身的问题(我对两者使用完全相同的 .pfx 文件)。另一个重要的提示可能是,当在 Windows 机器上使用自签名开发证书进行本地调试时,它实际上可以在 aspnetcore3.1 上运行。在生产环境中,如果上述代码失败,我会 运行 在 Ubuntu 上设置 API。

我的猜测是 asp.net-core 3.x 中证书的处理方式发生了变化。或者 Windows 库的行为不同。如果需要,我还可以 post 两个 Startup.cs 文件。

是否有关于实际原因或如何解决此问题的更多详细信息的建议?

更新

按照 Adam 的建议,我已切换到 MailKit 的 SmtpClient 实现。该错误在生产中仍然存在,但我设法使用 client.ServerCertificateValidationCallback 获得了有关该错误的更多详细信息。在检查回调的 ChainStatus 属性:

时,我得到了以下日志打印
X509ChainStatusFlags: RevocationStatusUnknown
StatusInformation: unable to get certificate CRL

有人可以解释为什么无法获得证书 CRL 会导致我的连接失败吗?我该如何解决这个问题?

更新 2

此外,当我设置 client.CheckCertificateRevocation = false 时出现以下错误:

X509ChainStatusFlags: PartialChain
StatusInformation: unable to get local issuer certificate
Subject: CN=smtp.gmail.com, O=Google LLC, L=Mountain View, S=California, C=US 
Issuer: CN=GTS CA 1O1, O=Google Trust Services, C=US 

更新 3

似乎 netcore 框架找不到所需的 ca 证书,因为当我在 Ubuntu 机器上 运行 执行命令时,我收到与更新 2 中完全相同的错误消息:

openssl s_client -connect smtp.gmail.com:465
--> Verify return code: 20 (unable to get local issuer certificate)

但是,当我明确指定 ca 证书存储路径时验证通过。

openssl s_client -CApath /etc/ssl/certs/ -connect smtp.gmail.com:465
--> Verify return code: 0 (ok)

所以我想最后一个问题是:如何让我的 aspnet-core web API 找到 Ubuntu 的 ca 证书存储?

更新 4

我已经报告了这个问题 here,也许 dotnet 团队最终会解决这个问题。作为(希望)临时解决方法,我最终手动验证了证书的指纹。

因此,如果您查看 SmtpClient() 的文档,您会发现它现在已标记为已过时。我真的很惊讶你没有得到编译器错误,你应该有,但这可能是明天的问题。

Microsoft 推荐使用 MailKit 库。链接:Nuget - GitHub

我鼓励您使用 Mailkit 库而不是使用 System.Net.Mail.SmtpClient() 重新实现您的代码。除了不会过时之外,MailKit 还是一个非常易于使用的库,开发人员和贡献者投入了大量时间和思考。

使库过时并推荐 MailKit 的原因是

SmtpClient and its network of types are poorly designed

有趣的事实:.Net Core 1.0 无法访问 System.Net.Mail 命名空间,并且在之后添加了支持,但是当 System.Net.Mail 在 .NET Core 中可用时,MailKit 是 .NET Core 早期采用者的首选库。

我终于弄明白为什么它在 Linux 上停止工作了。 netcore3.1框架不是从OS的ca-certificates文件夹中读取的,而是从

中读取的

~/.dotnet/corefx/cryptography/x509stores/ca

因此该应用不信任任何证书。所以我从 this page 下载了 Google 的示例 PEM 文件,将其转换为 P7B 文件以确保整个证书链保持完整

openssl crl2pkcs7 -nocrl -certfile google-roots.pem -out google-roots.p7b

并在应用程序启动期间将整个链添加到框架的证书存储

using (var store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
    store.Open(OpenFlags.ReadWrite);

    options.CertificateChainFiles.ToList().ForEach(file =>
    {
        var collection = new X509Certificate2Collection();
        collection.Import(file);
        store.AddRange(collection);
    });
}