提问人:Olivier Cueilliez 提问时间:10/25/2023 最后编辑:jstedfastOlivier Cueilliez 更新时间:10/25/2023 访问量:34
拔出/重新插入具有用于 TLS 连接的嵌入式身份验证证书的智能卡时引发的 MailKit.Security.SslHandshakeException
MailKit.Security.SslHandshakeException thrown when unplugging / replugging a smartcard with embedded authentication certificate for TLS connection
问:
我在尝试使用 Mailkit 连接到电子邮件服务器时收到 MailKit.Security.SslHandshakeException。身份验证基于 TLS 1.2,并使用嵌入在智能卡中的证书。
只要卡保留在读卡器中,我就可以连接并获取一些电子邮件文件夹。但是,如果我在不重新启动应用程序的情况下拔下并重新插入它,则会引发异常。
我编写了一个简单的测试控制台应用程序来测试使用 IMAP 协议的连接/获取电子邮件文件夹/断开连接序列。
using Agm.Commun.MSS;
using Agm.Commun.Utils.Certificates;
using MailKit;
using System;
namespace Agm.Commun.TestConsole
{
class Program
{
const string IMAP_HOST = "frontimap-igcsante.formation.mssante.fr"; // This is a test server
const int IMAP_PORT = 143;
const string userEMailAdress = "[email protected]"; // This is a fake email address
static void Main(string[] args)
{
Console.WriteLine("Simple console test app");
do
{
Console.WriteLine("Plug a smartcard into the reader then press [Enter]. [Q]+[Enter] to quit");
string input = Console.ReadLine();
if (input.ToLower() == "q")
break;
MssManagerBis mssManager = ConfigureMssManager();
var emailFolders = mssManager.GetFolders();
Console.WriteLine("Email Folders for " + userEMailAdress);
foreach (IMailFolder folder in emailFolders)
{
Console.WriteLine(folder.FullName);
}
Console.WriteLine();
} while (true);
}
private static MssManagerBis ConfigureMssManager()
{
CertificateManager certificateManager = new CertificateManager(Utils.Certificates.CertificateType.CPS);
MssManagerBis mssManager = new MssManagerBis(certificateManager.AuthenticationCertificate, userEMailAdress, IMAP_HOST, IMAP_PORT);
return mssManager;
}
}
}
CertificateManager 从 CurrentUser/Personal Windows 证书存储中获取证书。当智能卡插入读卡器时,此证书会自动添加到存储中,并在拔下智能卡时从存储中删除。
以下是管理 IMAP 连接/获取文件夹/断开连接序列的类:
using MailKit;
using MailKit.Net.Imap;
using MailKit.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Agm.Commun.MSS
{
public class MssManagerBis
{
#region Properties
private X509Certificate2 AuthenticationCertificate;
private string Email;
private string HostImap;
private int PortImap;
// Allowed cipher suites to be used with TLS 1.2 connections
private static readonly string[] CIPHERSUITEALLOWED = {
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"
};
#endregion
public MssManagerBis(X509Certificate2 pCertificate, string pEmail, string pHostImap, int pPortImap)
{
AuthenticationCertificate = pCertificate;
Email = pEmail;
HostImap = pHostImap;
PortImap = pPortImap;
}
public ImapClient ImapConnect()
{
ImapClient imapClient = new ImapClient();
// set authentication certificate that is embedded in the smartcard
imapClient.ClientCertificates = new X509Certificate2Collection();
imapClient.ClientCertificates.Add(AuthenticationCertificate);
// Set configuration to check certificate revocation
imapClient.CheckCertificateRevocation = true;
// TLS 1.2 required by the email server
imapClient.SslProtocols = SslProtocols.Tls12;
// Add handler to callback method for validation
imapClient.ServerCertificateValidationCallback += CertificateValidationCallBack;
// Actual connection to the email server - Fails on second attempt after the same smartcard was unplugged / replugged
imapClient.Connect(HostImap, PortImap, SecureSocketOptions.Auto);
// Use a cipher that is compatible with the email server
string cipherAlgorithmUsed = GetCipherSuite(imapClient);
if (CIPHERSUITEALLOWED.Contains(cipherAlgorithmUsed))
{
// Authenticate user
imapClient.Authenticate(Email, "");
return imapClient;
}
else
{
IMAPDisconnect(imapClient);
throw new Exception(string.Format("Cipher suite {0} not supported, IMAP connexion cancelled.", cipherAlgorithmUsed));
}
}
public IList<IMailFolder> GetFolders()
{
IList<IMailFolder> folders = new List<IMailFolder>();
ImapClient imapClient = ImapConnect();
if (imapClient == null)
return folders;
foreach (FolderNamespace ns in imapClient.PersonalNamespaces)
{
folders = imapClient.GetFolders(ns);
}
IMAPDisconnect(imapClient);
return folders;
}
public void IMAPDisconnect(ImapClient imapClient)
{
if (imapClient != null && imapClient.IsConnected)
{
imapClient.Disconnect(true);
imapClient.ClientCertificates.Clear();
imapClient.Dispose();
}
}
private bool CertificateValidationCallBack(object pSender, X509Certificate pCertificate, X509Chain pChain, SslPolicyErrors pSslPolicyErrors)
{
// Vérification de la validité de la période de validité du certificat
if (pCertificate is X509Certificate2 certificate2)
{
if (certificate2.NotBefore > DateTime.Now || certificate2.NotAfter < DateTime.Now)
{
throw new Exception("Le certificat n'est pas valide à cette date.");
}
}
// Vérification de l'identité de l'émetteur du certificat
if (pChain.ChainElements.Count > 1)
{
if (pCertificate.Issuer != pChain.ChainElements[1].Certificate.Subject)
{
throw new Exception("L'émetteur du certificat n'a pas pu être vérifié.");
}
}
// Vérification de la chaîne de certificats
pChain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
pChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
pChain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(1000);
pChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority | X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown | X509VerificationFlags.IgnoreEndRevocationUnknown;
pChain.ChainPolicy.VerificationTime = DateTime.Now;
bool isChainValid = pChain.Build((X509Certificate2)pCertificate);
if (!isChainValid)
{
throw new Exception("La chaîne de certificats n'a pas pu être vérifiée.");
}
return true;
}
private string GetCipherSuite(ImapClient imapClient)
{
StringBuilder stringBuilder = new StringBuilder();
if (imapClient.SslProtocol == SslProtocols.Tls12)
stringBuilder.Append("TLS_");
if ((int)imapClient.SslKeyExchangeAlgorithm == 44550)
stringBuilder.Append("ECDHE_RSA_WITH_");
switch (imapClient.SslCipherAlgorithm)
{
case CipherAlgorithmType.Aes256:
stringBuilder.Append("AES_256_");
break;
case CipherAlgorithmType.Aes128:
stringBuilder.Append("AES_128_");
break;
default:
break;
}
if (imapClient.SslProtocol == SslProtocols.Tls12)
stringBuilder.Append("GCM_");
stringBuilder.Append(imapClient.SslHashAlgorithm.ToString().ToUpper());
return stringBuilder.ToString();
}
}
}
第一次打开连接时,没有问题:连接打开,GetFolders() 方法返回文件夹列表。只要我不从读卡器中取出智能卡,我就可以无限次这样做。如果我删除它,然后将其放回阅读器中,则会引发 SslHandshakeException。 我尝试了不同的连接模式(SecureSocketOptions.Auto、SecureSocketOptions.StartTlsWhenAvailable),无论是否使用 certificationValidationCallbak(因为如果没有正确删除,它就无法释放某些资源)。我还在两次尝试之间等待了长达 10 分钟。
请注意,这是一个安全的电子邮件服务器,仅接受 Tls 1.2(及更高版本)和特定密码套件。只有智能卡证书才能用于安全地打开连接。
我正在使用 .NET core 2.2,但我认为它并不重要。如果可以更好地工作,我可能会转向更新的框架。
我猜 Windows PC/SC 层会保留某种与智能卡相关的缓存,因为如果我第二次尝试使用不同的卡,它会起作用。但是,一旦我再次尝试相同的方法,就会出现异常。有没有办法强制清理或刷新这样的缓存?
使用以下方法检索证书:
private List<X509Certificate2> GetAuthenticationCertificateList(CertificateType certificateType)
{
StoreLocation certificatelocation = StoreLocation.CurrentUser;
List<X509Certificate2> certificateList;
using (X509Store x509Store = new X509Store(StoreName.My, certificatelocation))
{
x509Store.Open(OpenFlags.ReadOnly);
certificateList = x509Store.Certificates.OfType<X509Certificate2>()
.Where(certificate => certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>()
.Any(extension => extension.EnhancedKeyUsages.OfType<Oid>()
.Any(usage => usage.Value == ExternalOids.OID_CLIENT_AUTHENTICATION))).ToList();
// Pour les authentifications avec carte, il faut aussi vérifier le rôle de type "smart card".
if (certificateType == CertificateType.CPS || certificateType == CertificateType.CPE)
{
certificateList = certificateList
.Where(certificate => certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>()
.Any(extension => extension.EnhancedKeyUsages.OfType<Oid>()
.Any(usage => usage.Value == ExternalOids.OID_SMART_CARD_LOGON)))
.ToList();
}
}
return certificateList;
}
也许应该以不同的方式检索证书,即通过 PKCS11 库?
任何帮助将不胜感激:)
答: 暂无答案
评论
var certificate = new X509Certificate2(actualCertificate.RawData)