拔出/重新插入具有用于 TLS 连接的嵌入式身份验证证书的智能卡时引发的 MailKit.Security.SslHandshakeException

MailKit.Security.SslHandshakeException thrown when unplugging / replugging a smartcard with embedded authentication certificate for TLS connection

提问人:Olivier Cueilliez 提问时间:10/25/2023 最后编辑:jstedfastOlivier Cueilliez 更新时间:10/25/2023 访问量:34

问:

我在尝试使用 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 库?

任何帮助将不胜感激:)

C# TLS1.2 智能卡 邮件工具包

评论

0赞 jstedfast 10/25/2023
我不知道这是否有效,因为我没有任何智能卡或任何接受客户端SSL证书的邮件服务器,但也许您可以尝试:以便从内存而不是文件系统/缓存加载证书?var certificate = new X509Certificate2(actualCertificate.RawData)
0赞 Olivier Cueilliez 10/25/2023
@jstedfast 谢谢你的建议。我试过了,但行为保持不变:连接成功,直到卡插入/重新插入。
0赞 jstedfast 10/26/2023
的,希望这对你有用。

答: 暂无答案