Question

I am using Visual Studio 2013 (C#) to digitally sign document using certificate from smartcard. I cannot identify certificate currently inserted in the card reader :(

Windows copy certificates from all card inserted in the reader and keep it in the store. I want to use just card cuurently in the reader.

code I using is

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"));
}

But when access cert.PrivateKey user get prompted to insert card in the reader. How to detect and skip this prompt for card or detect that cert HAS corresponding card currently in the reader?

I just want to use certificate from smartcard currently in the reader.

Was it helpful?

Solution

I am afraid it is not possible to detect if the card containing specific X509Certificate2 object is present in the reader by using standard .NET APIs. The best thing (very hackish) I could come up with is this:

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; 
}

However this method is reliable only when the following conditions are met:

  1. You card is accessible via minidriver and Microsoft Base Smart Card Crypto Provider.
  2. There is only one reader connected to your computer with the smartcard present.
  3. There is only one certificate present on the card currently inserted in the reader.

When there are multiple readers with smartcards connected or multiple certificates present on the card you cannot be sure which one will be returned by this method.

Please note there are also other APIs available that can access the smartcard. One example of such API is PKCS#11. It may be an overkill for simple operations but it can give you total control over your card and the objects stored on it. If you are interested and your smartcard comes with the PKCS#11 library you can take a look at my project Pkcs11Interop that brings full power of PKCS#11 API to the .NET environment.

Hope this helps :)

Edited to remove the "single certificate" restriction:

I have slightly modified the code. It now uses unmanaged Crypto API to enumerate the names of all containers managed by Microsoft Base Smart Card Crypto Provider and then searches for the corresponding X509Certificate2 objects in the CurrentUser\My store. Please note that this approach is also very hackish and provided code may not work reliably with all the cards/minidrivers available on the market. It is usually better and easier to let the user pick correct certificate from the built-in certificate selection dialog.

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;
        }
    }
}

Just call GetCertificates() method of the provided class to check out whether this code works with your card:

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

OTHER TIPS

I was wondering why you do foreach through all certificates in a store when you know cert subject. My suggestion would be:

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"));
}

If you don't know exact subject or are expecting to find another certificate with this subject this will probably not work for you.

In case you SmartCard is compatible with PKCS11Interop, you can retrieve certificates from smartcards with it's API.

You can retrieve .Net X509Certificate2 instances from them using the Pkcs11Interop.X509Store nuGet package:

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();
        }
    }
}

You can filter smartcards out as on the tests and examples.

The pkcsLibPath is a path to the driver dll file that implements the PKCS API for your smartcard. If comptabile, possible your smartcard drivers would have added it to your system, you may have to find them out. The one I use was installed by the SafeNet Software, I use device named eToken, also named as Aladin Token. The dll file ended up on:

c:\Windows\System32\eTPKCS11.dll

As an important note I want to write down for search engines as it was very hard for me to find this info. This allows you to retrieve X509Certificate2 through the PKCS11Interop API.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top