28

一位用户最近在使用我的软件时报告了一个奇怪的错误。我使用 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();
  }
}

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

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

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

更新 2: DSA 提供程序似乎会查找存储在 %APPDATA%\Microsoft\Crypto\DSS\[SID] 下的密钥,即使使用现有密钥进行初始化也是如此。会不会和这个机制有冲突?任何人都知道更多关于密钥存储是如何运作的,以及为什么从字符串加载公钥时它会被命中?

4

1 回答 1

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]因为您只使用public keys。在 CryptoAPI 中,应该使用CryptAcquireContextwithNULL作为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;
}

该函数返回将在目录中创建的文件的名称,该文件%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构造函数参数的参数调用。因此,无法以 .NET 4.0 的不同设置CspParameters作为 .NET 4.0 的参数的解决方法来解决该问题DSACryptoServiceProvider

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

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

我很抱歉我的答案很长,并感谢所有能够在这个地方阅读它的人。:-)

我希望我的回答能帮助您 KristoferA 解决问题并改进您开发的软件。

于 2010-11-28T01:34:36.577 回答