如何从PKCS#12字节数组构造X509Certificate2时会抛出CryptographicException("系统找不到指定的文件。")?

40

我正在尝试从字节数组中的PKCS#12 blob构造一个X509Certificate2,但是遇到了一个相当令人困惑的错误。这段代码在Windows XP上以管理员权限在桌面应用程序中运行。

以下是堆栈跟踪,但我陷入了困境,因为_LoadCertFromBlob被标记为[MethodImpl(MethodImplOptions.InternalCall)]

System.Security.Cryptography.CryptographicException: The system cannot find the file specified.
  at System.Security.Cryptography.CryptographicException.ThrowCryptogaphicException(Int32 hr)
  at System.Security.Cryptography.X509Certificates.X509Utils._LoadCertFromBlob(Byte[] rawData, IntPtr password, UInt32 dwFlags, Boolean persistKeySet, SafeCertContextHandle& pCertCtx)
  at System.Security.Cryptography.X509Certificates.X509Certificate.LoadCertificateFromBlob(Byte[] rawData, Object password, X509KeyStorageFlags keyStorageFlags)
  at System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(Byte[] rawData, String password, X509KeyStorageFlags keyStorageFlags)

编辑:这个blob是由BouncyCastle for C#生成的真正的PKCS#12,其中包含一个RSA私钥和证书(自签名或最近由CA注册)——我的目标是将BouncyCastle库中的私钥和证书转换为System.Security.Cryptography库,通过从一个库导出并导入到另一个库中。这段代码在大多数系统上都可以工作;我只是从来没有见过这个特定的错误从那个构造函数中抛出。它可能是某种环境上的奇怪问题。

编辑2:错误发生在另一个城市的不同环境中,我无法在本地重现它,所以我可能会将其归咎于破损的XP安装。

既然你问了,这里就是有问题的代码片段。该代码接受以BouncyCastle表示的私钥和证书,从个人密钥存储中删除任何先前具有相同Distinguished Name的证书,并通过一个中间的PKCS#12 blob导入新的私钥和证书到个人密钥存储中。

// open the personal keystore
var msMyStore = new X509Store(StoreName.My);
msMyStore.Open(OpenFlags.MaxAllowed);

// remove any certs previously issued for the same DN
var oldCerts =
    msMyStore.Certificates.Cast<X509Certificate2>()
        .Where(c => X509Name
                        .GetInstance(Asn1Object.FromByteArray(c.SubjectName.RawData))
                        .Equivalent(CurrentCertificate.SubjectDN))
        .ToArray();
if (oldCerts.Length > 0) msMyStore.RemoveRange(new X509Certificate2Collection(oldCerts));

// build a PKCS#12 blob from the private key and certificate
var pkcs12store = new Pkcs12StoreBuilder().Build();
pkcs12store.SetKeyEntry(_Pkcs12KeyName,
                        new AsymmetricKeyEntry(KeyPair.Private),
                        new[] {new X509CertificateEntry(CurrentCertificate)});
var pkcs12data = new MemoryStream();
pkcs12store.Save(pkcs12data, _Pkcs12Password.ToCharArray(), Random);

// and import it.  this constructor call blows up
_MyCertificate2 = new X509Certificate2(pkcs12data.ToArray(),
                                       _Pkcs12Password,
                                       X509KeyStorageFlags.Exportable);
msMyStore.Add(_MyCertificate2);
msMyStore.Close();
5个回答

48

您是否有PKCS#12或仅有PFX文件?在Microsoft世界中,它是相同的,但其他人则认为另一种方式(参见此归档页面)。

您可以尝试遵循以下步骤

X509Certificate2 cert = X509Certificate2(byte[] rawData, "password");
X509Certificate2 cert2 = X509Certificate2(byte[] rawData, "password",
              X509KeyStorageFlags.MachineKeySet |
              X509KeyStorageFlags.PersistKeySet |
              X509KeyStorageFlags.Exportable);

(X509Certificate2(Byte[])) 或者

X509Certificate2 cert = X509Certificate2("C:\Path\my.pfx", "password");

如果您需要使用一些标志,请参阅 Microsoft Docs 上的 X509Certificate2(String, String)Import(String, String, X509KeyStorageFlags)

更新:如果您只提供异常堆栈跟踪,那么插入代码片段会更有帮助。

您要使用哪个X509KeyStorageFlags?您可以使用Process Monitor来查找无法找到X509Certificate2构造函数的文件。例如,在出现问题的 Windows XP 上,当前用户可能没有默认密钥容器。您可以创建一个并重试导入操作。


4
在从字节数组加载时,将X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable添加到构造函数中对我有用。 - Muxa
2
我在Windows Server 2012上遇到了相同的错误,但在Windows 7或Azure云中完全相同的代码没有问题。将标志更改为MachineKeySet解决了该问题。 - RMD
2
在这个例子中要注意PersistKetSet标志 - 它会在磁盘上留下永远不会被清理的关键文件。如果您经常进行此调用,那么最终将面临巨大的清理任务。(问我怎么知道的。)如果您只在内存中使用证书,则只需指定MachineKeySet即可。如果您在代码中完成了内存中的证书,请调用Reset方法立即删除密钥文件。最好将其添加到存储中并重新加载。这就是它的作用。 - James McLachlan
@JamesMcLachlan:抱歉,我已经多年没有在这个方向上做过任何事情了。导入证书的情况不止一次非常特殊。如果PersistKeySet在您的情况下不好用,那么您应该不使用它。另一方面,如果使用PersistKeySet导入证书,则密钥将被放置在密钥存储中作为文件(在用户配置文件中)。使用CertGetCertificateContextPropertyCERT_KEY_PROV_INFO_PROP_ID获取信息。可以使用PP_UNIQUE_CONTAINER获取文件名。该文件位于CommonApplicationData下(带有Microsoft\Crypto\RSA\MachineKeys后缀)。 - Oleg

11

我也遇到了相同的问题。

根据这篇旧的KB文章,问题在于构造函数尝试将证书加载到当前用户的配置文件中,但是我的.NET代码正在模拟用户,因此它尚未加载用户配置文件。构造函数需要加载用户配置文件才能正常工作。

从文章中可以看出:

X509Certificate2类的构造函数尝试将证书导入应用程序所运行的用户帐户的用户配置文件中。许多时候,ASP.NET和COM+应用程序会模拟客户端。当他们这样做时,由于性能原因,他们不会加载模拟用户的用户配置文件。因此,他们无法访问模拟用户的“用户”证书存储区。

加载用户配置文件可以修复错误。


问题中涉及的代码片段运行在桌面应用程序内部,因此我怀疑未加载用户配置文件是引起问题的原因。 - Jeffrey Hantin
@Zain,感谢你的回答。你也知道这是修复此问题的最佳方式吗?这让我考虑到性能影响方面。谢谢。 - curiousBoy
@curiousBoy 像我在答案的最后一行所说的那样,调用API来加载用户资料。 - Zain Rizvi

5

我曾经遇到过同样的问题。

  1. 在托管网站的服务器上打开IIS。
  2. 找到该网站的应用程序池。
  3. 点击“高级设置”。
  4. 将“加载用户配置文件”更改为true。(可能需要重新启动或重启)

这样可以让加密子系统正常工作。

enter image description here


1
这是第四个指向IIS应用程序池问题的答案。我在桌面应用程序中遇到了这个问题。用户配置文件可能出现了某些损坏,但它肯定已经加载了。 - Jeffrey Hantin
有趣的是,从你的问题中并不明显它是一个桌面应用程序。此修复方式在我的 IdentityServer3 MVC 应用程序上起作用。该项目在本地运行良好,但一旦上传到服务器上就会崩溃。这个标志是唯一的解决办法。可能是桌面应用程序的权限问题? - crthompson
它在XP机器上以管理员组帐户运行,因此权限和UAC都不应该成为问题。我无法在任何其他机器上重现它,因此我目前的最佳猜测是损坏的配置文件或损坏的操作系统配置。 - Jeffrey Hantin

4

在Windows 2012的web应用程序中遇到了这个问题,将应用程序池选项Load User Profile设置为true即可解决问题。

要做到这一点,请运行inetmgr.exe,进入正确应用程序池的高级设置,并将Process Model下的Load User Profile更改为true。


这发生在桌面应用程序中,而不是Web应用程序或服务中。 - Jeffrey Hantin

1

我曾经遇到过完全相同的问题。在特定用户下,相同的代码和数据/证书可以在 Windows 2003 x86 上正常运行,但在另一个帐户下失败(该帐户也用于运行 IIS 应用程序池)。

显然,在 Windows 上有一些其他的东西消耗了资源,因此失败的用户无法真正加载用户的个人资料(他的桌面看起来很奇怪),尽管在事件查看器中没有相关事件。

重新启动暂时解决了问题。虽然这不是问题的永久解决方案,但它表明还有其他东西(例如,COM+ 组件、本机代码服务等)正在消耗资源,需要进行调查。它还显示了 Windows 平台的不稳定性...


这种特定类型的不稳定性似乎与依赖于进程间通信的桌面软件结合以及在资源耗尽时缺乏强大的稳健性有关。我以前在Linux上见过与GNOME和特别是Evolution相同类别的问题。 - Jeffrey Hantin

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