Verify Private Key Protection before signing with RSACryptoServiceProvider

1.3k views Asked by At

When signing data with RSACryptoServiceProvider in C#, I have a requirement to ensure the certificate was imported with strong key protection and a high security level to require the user enters the password every time they sign with the key. Here's a quick simplified sample of the signing code:

X509Store myCurrentUserStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
myCurrentUserStore.Open(OpenFlags.MaxAllowed);
X509Certificate2 currentCertificate = myCurrentUserStore.Certificates[4];

RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.FromXmlString(currentCertificate.PrivateKey.ToXmlString(true));

byte[] signedData = Encoding.UTF8.GetBytes(originalFileContent);
byte[] signature = key.SignData(signedData, CryptoConfig2.CreateFromName("SHA256CryptoServiceProvider") as HashAlgorithm);    

So what's the best way to go about checking how the certificate was installed so I can display an error message if it was not installed with strong private key protection with a high security level?

1

There are 1 answers

4
bartonjs On

There are a couple things in your snippet that I don't understand.

  1. Why you're opening with MaxAllowed. If you just want to read, use ReadOnly.
  2. Why you're reading store.Certificates[4]. But presumably this is just a placeholder for "read a cert".
  3. Why you're exporting and re-importing the key. (Especially since that would have had to prompt, which would defeat your "it needs to prompt" goal).

For #3 I'm assuming you are just looking to have a unique instance, in which case: Good news! .NET 4.6 added a GetRSAPrivateKey (extension) method to X509Certificate2 which always returns a unique instance. (And you might be excited to know about the new overload to SignData which doesn't encourage sending objects to the finalizer queue: https://msdn.microsoft.com/en-us/library/mt132675(v=vs.110).aspx)

Anyways, what I wrote here works for medium (consent) or high (password) protection. The CngKey based approach can distinguish medium from high, but the classic CAPI fallback can't tell which is which. (The classic CAPI fallback will only happen with obscure HSMs which don't have a CNG-compatible driver).

private static bool HasProtectedKey(X509Certificate2 cert)
{
    if (!cert.HasPrivateKey)
    {
        return false;
    }

    using (RSA rsa = cert.GetRSAPrivateKey())
    {
        return HasProtectedKey(rsa);
    }
}

private static bool HasProtectedKey(RSA rsa)
{
    RSACng rsaCng = rsa as RSACng;

    if (rsaCng != null)
    {
        return rsaCng.Key.UIPolicy.ProtectionLevel != CngUIProtectionLevels.None;
    }

    RSACryptoServiceProvider rsaCsp = rsa as RSACryptoServiceProvider;

    if (rsaCsp != null)
    {
        CspKeyContainerInfo info = rsaCsp.CspKeyContainerInfo;

        // First, try with the CNG API, it can answer the question directly:
        try
        {
            var openOptions = info.MachineKeyStore
                ? CngKeyOpenOptions.MachineKey
                : CngKeyOpenOptions.UserKey;

            var cngProvider = new CngProvider(info.ProviderName);

            using (CngKey cngKey =
                CngKey.Open(info.KeyContainerName, cngProvider, openOptions))
            {
                return cngKey.UIPolicy.ProtectionLevel != CngUIProtectionLevels.None;
            }
        }
        catch (CryptographicException)
        {
        }

        // Fallback for CSP modules which CNG cannot load:
        try
        {
            CspParameters silentParams = new CspParameters
            {
                KeyContainerName = info.KeyContainerName,
                KeyNumber = (int)info.KeyNumber,
                ProviderType = info.ProviderType,
                ProviderName = info.ProviderName,
                Flags = CspProviderFlags.UseExistingKey | CspProviderFlags.NoPrompt,
            };

            if (info.MachineKeyStore)
            {
                silentParams.Flags |= CspProviderFlags.UseMachineKeyStore;
            }

            using (new RSACryptoServiceProvider(silentParams))
            {
            }

            return false;
        }
        catch (CryptographicException e)
        {
            const int NTE_SILENT_CONTEXT = unchecked((int)0x80090022);

            if (e.HResult == NTE_SILENT_CONTEXT)
            {
                return true;
            }

            throw;
        }
    }

    // Some sort of RSA we don't know about, assume false.
    return false;
}