在读卡器上查找智能卡上的证书。

17

我正在使用Visual Studio 2013 (C#),使用来自智能卡的证书对文档进行数字签名。

我无法识别当前插入读卡器中的证书 :(

Windows会从所有插入读卡器的卡片复制证书并将其保存在存储中。我只想使用当前插入的卡片。

我正在使用的代码是:

public static byte[] Sign(Stream inData, string certSubject)
{

    // Access Personal (MY) certificate store of current user
    X509Store my = new X509Store(StoreName.My, StoreLocation.CurrentUser);
    my.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

    // Find the certificate we'll use to sign            
    RSACryptoServiceProvider csp = null;
    foreach (X509Certificate2 cert in my.Certificates)
    {
        if (cert.Subject.Contains(certSubject))
        {
            // We found it. 
            // Get its associated CSP and private key
            if (cert.HasPrivateKey) {
                csp = (RSACryptoServiceProvider)cert.PrivateKey;
                if (csp.CspKeyContainerInfo.HardwareDevice)
                    Console.WriteLine("hardware");                              
                    Console.WriteLine(cert.ToString());
            }
        }
    }
    if (csp == null)
    {
        throw new Exception("No valid cert was found");
    }

    // Hash the data
    SHA1Managed sha1 = new SHA1Managed();
    byte[] hash = sha1.ComputeHash(inData);

    // Sign the hash
    return csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
}

但是,当用户访问cert.PrivateKey时,会提示插入读卡器中的卡。如何检测并跳过此提示以使用当前读卡器中对应卡上的证书?

我只想使用当前读卡器中智能卡上的证书。

3个回答

13

很遗憾,使用标准的 .NET API 无法检测读卡器中是否存在包含特定 X509Certificate2 对象的卡。我能想到的最好方法(非常不正规)是:

public static X509Certificate2 GetDefaultCertificateStoredOnTheCard() 
{ 
    // Acquire public key stored in the default container of the currently inserted card
    CspParameters cspParameters = new CspParameters(1, "Microsoft Base Smart Card Crypto Provider"); 
    RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(cspParameters); 
    string pubKeyXml = rsaProvider.ToXmlString(false); 

    // Find the certficate in the CurrentUser\My store that matches the public key
    X509Store x509Store = new X509Store(StoreName.My, StoreLocation.CurrentUser); 
    x509Store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); 
    foreach (X509Certificate2 cert in x509Store.Certificates) 
    { 
        if ((cert.PublicKey.Key.ToXmlString(false) == pubKeyXml) && cert.HasPrivateKey)
            return cert; 
    } 

    return null; 
}

然而,此方法仅在满足以下条件时可靠:

  1. 您的卡可通过minidriver和Microsoft Base Smart Card Crypto Provider访问。
  2. 只有一个读卡器连接到计算机,智能卡已插入其中。
  3. 当前插入读卡器的卡上只有一个证书。

当使用多个读卡器与智能卡或卡上存在多个证书时,无法确定此方法将返回哪个证书。

请注意还有其他可用于访问智能卡的API。其中一个例子是PKCS#11。它可能对于简单操作来说过于复杂,但可以让您完全控制卡和存储在其上的对象。如果您感兴趣并且您的智能卡带有PKCS#11库,您可以查看我的项目Pkcs11Interop,为.NET环境带来了PKCS#11 API的全部功能。

希望这会有所帮助 :)

编辑以删除“单证书”限制:

我稍微修改了代码。现在它使用非托管的Crypto API列举由Microsoft Base Smart Card Crypto Provider管理的所有容器的名称,然后在CurrentUser\My存储区域中搜索相应的X509Certificate2对象。请注意,这种方法也非常hackish,提供的代码可能无法可靠地使用市场上所有可用的卡/ minidriver。通常最好并且更容易让用户从内置证书选择对话框中选择正确的证书。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace CSP
{
    public static class BaseSmartCardCryptoProvider
    {
        private const string _providerName = "Microsoft Base Smart Card Crypto Provider";

        private static class NativeMethods
        {
            public const uint PROV_RSA_FULL = 0x00000001;
            public const uint CRYPT_VERIFYCONTEXT = 0xF0000000;
            public const uint CRYPT_FIRST = 0x00000001;
            public const uint CRYPT_NEXT = 0x00000002;
            public const uint ERROR_NO_MORE_ITEMS = 0x00000103;
            public const uint PP_ENUMCONTAINERS = 0x00000002;

            [DllImport("advapi32.dll", BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
            public static extern bool CryptAcquireContext(
            ref IntPtr phProv,
            [MarshalAs(UnmanagedType.LPStr)] string pszContainer,
            [MarshalAs(UnmanagedType.LPStr)] string pszProvider,
            uint dwProvType,
            uint dwFlags);

            [DllImport("advapi32.dll", BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
            public static extern bool CryptGetProvParam(
            IntPtr hProv,
            uint dwParam,
            [MarshalAs(UnmanagedType.LPStr)] StringBuilder pbData,
            ref uint pdwDataLen,
            uint dwFlags);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern bool CryptReleaseContext(
            IntPtr hProv,
            uint dwFlags);
        }

        public static List<X509Certificate2> GetCertificates()
        {
            List<X509Certificate2> certs = new List<X509Certificate2>();

            X509Store x509Store = null;

            try
            {
                x509Store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                x509Store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

                List<string> containers = GetKeyContainers();

                foreach (string container in containers)
                {
                    CspParameters cspParameters = new CspParameters((int)NativeMethods.PROV_RSA_FULL, _providerName, container);
                    cspParameters.Flags = CspProviderFlags.UseExistingKey;
                    string pubKeyXml = null;
                    using (RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(cspParameters))
                        pubKeyXml = rsaProvider.ToXmlString(false);

                    foreach (X509Certificate2 cert in x509Store.Certificates)
                    {
                        if ((cert.PublicKey.Key.ToXmlString(false) == pubKeyXml) && cert.HasPrivateKey)
                            certs.Add(cert);
                    }
                }
            }
            finally
            {
                if (x509Store != null)
                {
                    x509Store.Close();
                    x509Store = null;
                }
            }

            return certs;
        }

        private static List<string> GetKeyContainers()
        {
            List<string> containers = new List<string>();

            IntPtr hProv = IntPtr.Zero;

            try
            {
                if (!NativeMethods.CryptAcquireContext(ref hProv, null, _providerName, NativeMethods.PROV_RSA_FULL, NativeMethods.CRYPT_VERIFYCONTEXT))
                    throw new Win32Exception(Marshal.GetLastWin32Error());

                uint pcbData = 0;
                uint dwFlags = NativeMethods.CRYPT_FIRST;
                if (!NativeMethods.CryptGetProvParam(hProv, NativeMethods.PP_ENUMCONTAINERS, null, ref pcbData, dwFlags))
                    throw new Win32Exception(Marshal.GetLastWin32Error());

                StringBuilder sb = new StringBuilder((int)pcbData + 1);
                while (NativeMethods.CryptGetProvParam(hProv, NativeMethods.PP_ENUMCONTAINERS, sb, ref pcbData, dwFlags))
                {
                    containers.Add(sb.ToString());
                    dwFlags = NativeMethods.CRYPT_NEXT;
                }

                int err = Marshal.GetLastWin32Error();
                if (err != NativeMethods.ERROR_NO_MORE_ITEMS)
                    throw new Win32Exception(err);

                if (hProv != IntPtr.Zero)
                {
                    if (!NativeMethods.CryptReleaseContext(hProv, 0))
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    hProv = IntPtr.Zero;
                }
            }
            catch
            {
                if (hProv != IntPtr.Zero)
                {
                    if (!NativeMethods.CryptReleaseContext(hProv, 0))
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    hProv = IntPtr.Zero;
                }

                throw;
            }

            return containers;
        }
    }
}

只需调用提供的类的GetCertificates()方法,即可检查此代码是否与您的卡兼容:

List<X509Certificate2> certs = CSP.BaseSmartCardCryptoProvider.GetCertificates();

这将解决我的问题。我曾考虑过 RSACryptoServiceProvider 和 x509Store 的组合,但我希望有更像 cert.CardInTheReader 这样的逻辑解决方案 :) 谢谢。 - Nebojsa Susic
我太快了:( 我需要使用的某些卡有两个证书。在RSACryptoServiceProvider中,我可以选择/过滤/处理证书吗? - Nebojsa Susic
对我来说,你最初的代码就是我所需要的,我非常感谢。之前,我只在浏览器中创建智能卡签名,并需要从CSP-->X509并不知道我会在常规存储中找到它。谢谢! - Yablargo
例如CSP实现的内部错误。 - jariq
3
尝试枚举卡中的容器时,我收到了“访问被拒绝”的消息。 - codenamezero
显示剩余5条评论

0

如果您的智能卡兼容PKCS11Interop,则可以使用其API从智能卡中检索证书。

您可以使用Pkcs11Interop.X509Store nuGet包从中检索.Net X509Certificate2实例:

public class EtokenCertRetriever
{
    private readonly string PkcsLibPath;
    private readonly ETokenPinProvider PProvider;

    public EtokenCertRetriever(string pkcsLibPath, string pin)
    {
        PkcsLibPath = pkcsLibPath;
        PProvider = new ETokenPinProvider(pin);
    }

    public List<X509Certificate2> GetEtokenCertificates()
    {
        using Pkcs11X509Store store = new Pkcs11X509Store(PkcsLibPath, PProvider);
        List<X509Certificate2> netCertificates = new List<X509Certificate2>();
        foreach (var slot in store.Slots)
        {
            //You may want to filter out slots according to the token info, names and things like that
            //For sample, check https://github.com/Pkcs11Interop/Pkcs11Interop.X509Store/blob/master/src/Pkcs11Interop.X509Store.Tests/Helpers.cs
            if (slot.Token != null && slot.Token.Certificates != null) //This operation triggers pin request
            {
                foreach (var pkcsCertificate in slot.Token.Certificates)
                {
                    if (pkcsCertificate.Info.ParsedCertificate != null)
                    {
                        netCertificates.Add(pkcsCertificate.Info.ParsedCertificate);
                    }
                }
            }
        }
        return netCertificates;
    }

    //This implementation assumes same password for the two type of pins
    private class ETokenPinProvider : IPinProvider
    {
        private readonly string Pin;

        public ETokenPinProvider(string pin)
        {
            Pin = pin;
        }

        private GetPinResult GetPin()
        {
            return new GetPinResult(false, Pin.Select((ch) => (byte)ch).ToArray()); //Convert to bytes
        }

        public GetPinResult GetKeyPin(
            Pkcs11X509StoreInfo storeInfo,
            Pkcs11SlotInfo slotInfo,
            Pkcs11TokenInfo tokenInfo,
            Pkcs11X509CertificateInfo certificateInfo
        )
        {
            return GetPin();
        }

        public GetPinResult GetTokenPin(
            Pkcs11X509StoreInfo storeInfo,
            Pkcs11SlotInfo slotInfo,
            Pkcs11TokenInfo tokenInfo
        )
        {
            return GetPin();
        }
    }
}

您可以像 测试和示例 中所示,过滤智能卡。

pkcsLibPath 是指向实现智能卡 PKCS API 的驱动程序 dll 文件的路径。如果兼容,则可能您的智能卡驱动程序已将其添加到系统中,您可能需要找到它们。 我使用的那个是由 SafeNet 软件安装的,我使用名为 eToken 的设备,也称为 Aladin Token。dll 文件位于以下位置:

c:\Windows\System32\eTPKCS11.dll

作为一个重要的提示,我想为搜索引擎写下这个信息,因为对我来说很难找到这个信息。这允许您通过PKCS11Interop API检索X509Certificate2。

0
我在想为什么您要遍历存储中的所有证书,而已知道证书的主题。我的建议是:
public static byte[] Sign(Stream inData, string certSubject)
{

    // Access Personal (MY) certificate store of current user
    X509Store my = new X509Store(StoreName.My, StoreLocation.CurrentUser);
    my.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

    var foundCerts = my.Certificates.Find(X509FindType.FindBySubjectName, certSubject, true);
    if (foundCerts.Count == 0)
        throw new Exception("No valid cert was found");

    var cert = foundCerts[0];
    RSACryptoServiceProvider csp = null;
    // let us assume that certSubject is unique
    if (cert.HasPrivateKey)
    {
        csp = (RSACryptoServiceProvider)cert.PrivateKey;
        if (csp.CspKeyContainerInfo.HardwareDevice)
            Console.WriteLine("hardware");
        Console.WriteLine(cert.ToString());
    }
    else
    {
        throw new Exception("No private key assigned to this certificate");
    }

    // Hash the data
    SHA1Managed sha1 = new SHA1Managed();
    byte[] hash = sha1.ComputeHash(inData);

    // Sign the hash
    return csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
}

如果你不知道具体的主题,或者希望找到另一种与此主题相关的证书,那么这可能对你没有用。


很遗憾,我不知道证书的确切主题。无论如何,谢谢。 - Nebojsa Susic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接