从一个用户导入xml字符串的DSA密钥失败.权限?安装破损?坏KSP?

Kri*_*erA 28 .net c# cryptography cryptoapi dsa

用户最近在使用我的软件时报告了一个奇怪的错误.我使用DSA签名来验证许可证.当软件导入公钥以验证签名时,DSA提供程序的FromXmlString方法将抛出CryptographicException,其描述为" 密钥无法在指定状态下使用 ".

从System.Security.Cryptography.Utils.CreateProvHandle调用的_OpenCSP方法似乎返回NTE_BAD_KEY_STATE(0x8009000b).这是第一次有人向我报告此错误,并且该代码多年来没有变化.

造成这种情况的可能原因是什么?屏蔽权限错误?CAPI安装破损?被.net信任/权限设置阻止?密钥存储提供商存储的垃圾,还是KSP向cryptoapi返回意外的东西?

我已经搜索了错误代码/说明/等,但没有找到任何真正的答案,可能会导致这个...

失败的代码的隔离版本在这里:http: //forum.huagati.com/getattachment.ashx?fileid = 78

using System;
using System.Security.Cryptography;
using System.Reflection;

public class Test
{
  public static void Main()
  {
    try
    {
      string key = "<DSAKeyValue><P>wrjxUnfKvH/1s5cbZ48vuhTjflRT5PjOFnr9GeUPZSIoZhYATYtME4JRKrXBtSkyioRNtE1xgghbGAyvAJ5jOWw88fLBF+P1ilsZyq72G1YcbB+co8ImQhAbWKmdCicO9/66Th2MB+7kms/oY3NaCzKEuR7J3b23dGrFpp4ccMM=</P><Q>xmxoSErIJCth91A3dSMjC6yQCu8=</Q><G>bwOLeEaoJHwSiC3i3qk9symlG/9kfzcgrkhRSWHqWhyPAfzqdV1KxJboMpeRoMoFr2+RqqKHgcdbzOypmTeN4QI/qh4nSsl5iEfVerarBOrFuRdOVcJO0d8WE233XQznd1K66nXa5L8d9SNZrM6umZ1YuBjhVsTFdPlIXKfGYhk=</G><Y>wZnEEdMUsF3U3NBQ8ebWHPOp37QRfiBn+7h5runN3YDee1e9bC7JbJf+Uq0eQmU8zDs+avEgD68NpxTKEHGr4nQ3rW6qqacj5SDbwO7nI6eN3wWrVhvrWcQm0tUO93m64HsEJREohfoL+LjqgrqIjZVT4D1KXE+k/iAb6WKAsIA=</Y><J>+zmcCCNm2kn1EXH9T45UcownEe7JH+gl3Lw2lhVzXuX/dYp5sGCA2lK119iQ+m3ogjOuwABATCVFLo6J66DsSlMd0I8WSD5WKPvypQ7QjY0Iv71J2N0FW0ZXpMlk/CE8zq4Z7arM1N564mNe</J><Seed>QDrZrUFowquY5Uay8YtUFOXnv28=</Seed><PgenCounter>Gg==</PgenCounter></DSAKeyValue>";

      DSACryptoServiceProvider csp2 = new DSACryptoServiceProvider();
      csp2.FromXmlString(key);

      Console.WriteLine("Success!");
    }
    catch (Exception ex)
    {
      int hResult = 0;
      try
      {
          PropertyInfo pi = typeof(Exception).GetProperty("HResult", BindingFlags.NonPublic | BindingFlags.Instance);
          hResult = (int)pi.GetValue(ex, null);
      }
      catch (Exception ex2)
      {
          Console.WriteLine("HResult lookup failed: " + ex2.ToString());
      }
      Console.WriteLine("Initializing CSP failed: " + ex.ToString() + "\r\nHResult: " + hResult.ToString("x"));
    }
    Console.WriteLine("\r\nPress Enter to continue");
    Console.ReadLine();
  }
}
Run Code Online (Sandbox Code Playgroud)

...并在受影响的用户的机器上返回:

Initializing CSP failed: System.Security.Cryptography.CryptographicException: Ke
y not valid for use in specified state.

at System.Security.Cryptography.Utils.CreateProvHandle(CspParameters paramete
rs, Boolean randomKeyContainer)
at System.Security.Cryptography.Utils.get_StaticDssProvHandle()
at System.Security.Cryptography.DSACryptoServiceProvider.ImportParameters(DSA
Parameters parameters)
at System.Security.Cryptography.DSA.FromXmlString(String xmlString)
at Test.Main()
HResult: 8009000b
Run Code Online (Sandbox Code Playgroud)

更新:在同一台机器上运行.net fx 2.0下相同的代码工作正常,但在.net fx 4.0下失败.

更新2: DSA提供程序似乎查找存储在%APPDATA%\ Microsoft\Crypto\DSS\[SID]下的密钥,即使然后使用现有密钥进行初始化.可能与这种机制发生冲突吗?任何人都知道更多关于密钥存储操作如何操作,以及从字符串加载公钥时它被击中的原因?

Ole*_*leg 85

你描述的这个问题对我来说非常有趣,但是如果没有一些额外的信息和一些实验,很难明确地说出原因是什么.所以我试着描述我是如何理解这个问题的.

首先,.NET加密类在内部使用非托管CryptoAPI.所以该方法在_OpenCSP内部调用CryptAcquireContext函数.在我们的文档中,我们可以阅读以下有关错误NTE_BAD_KEY_STATE(0x8009000BL):

自私钥加密以来,用户密码已更改.

DSA提供程序使用的用户私钥将作为文件保存在目录中,%APPDATA%\Microsoft\Crypto\DSS\[SID]并使用相对复杂的算法进行加密,您可以在此处阅读.重要的是要了解目录中的文件对应于用户密钥的密钥容器.通常,用户可以完全访问文件系统中的文件.文件将使用密钥加密,密钥取决于用户的密码.在许多标准情况下,文件将在密码更改后重新加密,但恢复算法取决于许多事情.如果重置密码而不是用户自己更改密码(来自域管理员/帐户操作员等),则旧的包含目录%APPDATA%\Microsoft\Crypto\DSS\[SID]可能没有用处.例如,如果用户不是Active Directory用户(本地用户)并且本地管理员重置了他的密码,那么将发生加密容器的问题.

因此,第一个建议是询问用户是否重置了他的Active Directory密码.接下来,您应该验证%APPDATA%\Microsoft\Crypto\DSS\[SID]用户配置文件中是否存在该目录,并且该用户对文件系统中的目录具有完全访问权限.您应该从目录中删除所有文件(先创建文件的备份副本).顺便说一下,知道用户是否具有中央保存的配置文件(保存在服务器上)是很有趣的.如果它具有中央配置文件,则可以验证您描述的同一问题是否存在于用户的另一台计算机上,而其他用户在其原始计算机上没有问题.

另一个对我来说不太清楚的问题是,为什么%APPDATA%\Microsoft\Crypto\DSS\[SID]使用目录中的密钥容器,因为你只使用公钥.CryptoAPI中应该使用CryptAcquireContextNULL作为pszContainer参数和CRYPT_VERIFYCONTEXTdwFlags.我不确定.NET是否使用该CRYPT_VERIFYCONTEXT标志,它可能是间接的问题.

您可以DSACryptoServiceProvider使用具有CspParameters参数的构造函数进行创建.另一方面的CspParameters具有Flags属性,该属性在.NET 4.0中使用值CreateEphemeralKey进行扩展.描述非常接近函数标志的描述.因此,使用可以尝试使用CspProviderFlags.CreateEphemeralKey或与(作为参数也是默认密钥容器)一起使用.CspProviderFlags.CreateEphemeralKeyCRYPT_VERIFYCONTEXTCryptAcquireContextCspProviderFlags.CreateEphemeralKeyCspProviderFlags.UseDefaultKeyContainerNULLpszContainerCryptAcquireContext

此外,如果可能,您可以尝试在可以重现问题的计算机上调试问题.对于调试,您可以使用可以启用的.NET源(请参阅此处此处)或从此处下载.然后,您可以回答有关程序中当前使用的值的一些问题,并比较.NET 3.5和.NET 4.0的值.CspParameters

如果我所写的内容无助于解决问题,请您附上其他信息:

  • 哪个操作系统和哪个服务包有计算机可以重现问题?
  • 问题是用户依赖的吗?我的意思是:在同一台计算机上有其他用户同样的问题吗?
  • 域(活动目录)用户或本地用户帐户是否存在问题?具有问题的用户是否具有保存在服务器上的中央用户配置文件?如果他有,那么用户可以在其他计算机上再现问题吗?
  • 你能描述一下你使用公钥验证的环境吗?特别是程序是在用户安全上下文中运行还是你冒充一些?

更新:在阅读了最初发布问题的论坛后,我对解决问题变得悲观.如果您没有直接联系到可以重现问题的计算机,并且只与每个帖子在论坛中与唯一有问题的用户进行通信......但是我一直在考虑这个问题,所以我决定尝试自己重现问题.我在这方面取得了成功.所以我会在这里仔细描述我的结果.我将描述如何重现问题,使其看起来与论坛帖子中描述的完全一样.

您应该执行以下步骤:

1)您在计算机上创建本地测试帐户.2)您使用测试帐户登录并为DSA提供程序生成默认密钥容器.你可以这样做,例如关于调用以下简单函数的小型.NET程序:

static string GenerateDsaKeyInDefaultContainer()
{
    const int PROV_DSS_DH = 13;
    CspParameters cspParam = new CspParameters(PROV_DSS_DH);
    cspParam.KeyContainerName = null;
    cspParam.KeyNumber = (int)KeyNumber.Signature;
    cspParam.Flags = CspProviderFlags.UseDefaultKeyContainer;
    DSACryptoServiceProvider csp = new DSACryptoServiceProvider(cspParam);
    return csp.CspKeyContainerInfo.UniqueKeyContainerName;
}
Run Code Online (Sandbox Code Playgroud)

该函数返回将在目录中创建的文件的名称,该文件%APPDATA%\Microsoft\Crypto\DSS\[SID]将包含生成的密钥对.3)您注销测试帐户并使用具有本地管理权限的另一个帐户登录.您重置测试帐户的密码.4)您再次使用测试帐户登录,并验证您在Visual Studio 2010 for .NET 4.0中编译的测试程序是否产生错误NTE_BAD_KEY_STATE(0x8009000b),并且将抛出相应的异常.5)如果重新编译.NET 3.5而不是.NET 4.0的程序(也可以使用Visual Studio 2010),测试程序将运行而不会出现任何错误.

所以所有结果都与论坛帖子中描述的完全一样.

如果使用DSA提供程序的默认密钥容器删除或重命名该文件,则问题将得到解决.就像我在重置用户密码之前所描述的那样,默认密钥容器的包含将无法被解密.因此,如果您知道默认容器的名称(对于用户来说是唯一的)(例如,您可以从Process Monitor的跟踪中看到名称),您可以将任何其他用户和任何其他计算机上的任何密钥容器文件复制到目录%APPDATA%\Microsoft\Crypto\DSS\[SID],重命名文件,以便名称将是默认的容器名称... ...您将具有与重置用户密码完全相同的结果.

我做了一些实验,用不同的设置CspParameters作为参数DSACryptoServiceProvider(参见我关于CspProviderFlags.CreateEphemeralKey使用的第一个建议),但没有任何成功.之后,我调试了.NET 4.0的源代码,并且可以明确地说_OpenCSP,将使用与构造函数的参数无关的参数调用函数调用(未打开哪个代码)DSACryptoServiceProvider.因此,无法使用不同CspParameters的参数设置找到.NET 4.0问题的解决方法DSACryptoServiceProvider.

因此,如果您想要修改代码,以便它在损坏的默认密钥提供程序的情况下也能正常工作,我看到目前只有两种方法可以解决问题:

  1. 使用CryptAcquireContext带有CRYPT_VERIFYCONTEXT标志的非托管函数实现整个或部分代码.
  2. 检测错误的问题NTE_BAD_KEY_STATE(0x8009000b)并包括删除或临时重命名%APPDATA%\Microsoft\Crypto\DSS\[SID]包含损坏的默认密钥容器的文件的代码部分.要检测文件的名称,我认为您可以尝试使用CryptGetProvParamPP_UNIQUE_CONTAINER和/和PP_ENUMCONTAINERS参数的函数.

我很抱歉我的回答很长,并感谢所有能够阅读到这个地方的人.:-)

我希望我的回答可以帮助您克服KristoferA解决问题并改进您开发的软件.

  • @Oleg:非常好,非常感谢您的详尽研究和出色的答案.目标是找到导致此问题的原因,如果它可能会影响其他用户,则为用户提供正确的错误消息,回退或解决方法.你的回答为我提供了这个; 密码重置并不罕见,所以虽然它只是由一个用户报告,但其他人很可能已经体验过它.了解这一点,作为一种解决方法,我可以简单地弹出一个对话框或消息框,为用户提供解决此问题所需的信息...... +1! (10认同)
  • @FreshCode:不客气!多年来,我花了很多时间研究如何使用CryptoAPI以及它如何在内部工作.我并没有真正使用这些知识.所以我很高兴找到这样的问题.我也很高兴有这么多人发现我的回答很有趣或有帮助.感谢您和所有人阅读和提升我的答案. (3认同)
  • @KristoferA - Huagati.com:我从论坛上读到了帖子,经过一些实验,我可以重现/模拟所描述的问题.在我的答案的"更新"部分中查看更多内容. (2认同)
  • +1疯狂的回答.很抱歉毁了漂亮的圆形64 upvotes :) (2认同)