指定的提供程序类型无效——尝试加载证书私钥时发生 CryptographicException

79
我正在尝试读取由第三方服务提供商共享给我的证书的私钥,以便在将其通过网络发送之前,我可以使用它加密一些XML。 我正在使用C#进行程序化操作,但我认为这是一个权限或配置问题,因此我将专注于似乎最相关的事实:
  • 我不认为这个问题与代码相关; 我的代码在其他计算机上运行良好,并且该问题影响了来自Microsoft的示例代码。
  • 证书以PFX文件的形式提供,仅用于测试目的,因此它还包括一个虚拟的认证机构。
  • 使用MMC.exe,我可以将证书导入到本地计算机的个人存储中,然后授予所有相关帐户的私钥权限,并将认证机构拖放到“受信任的根证书颁发机构”中。
  • 使用C#,我可以加载证书(由其指纹标识),并验证它是否具有私钥,使用X509Certificate2.HasPrivateKey。 但是,尝试读取密钥会导致错误。 在.NET中,当尝试访问属性X509Certificate2.PrivateKey时,会抛出CryptographicException异常,其中错误消息为“指定了无效的提供程序类型”。 在Win32中,调用方法CryptAcquireCertificatePrivateKey会返回相应的HRESULT,NTE_BAD_PROV_TYPE
  • 当使用Microsoft自己的两个代码示例读取证书的私钥时,也会出现此相同的异常。
  • 将相同的证书安装在当前用户等效存储中,而不是本地计算机中,可以成功加载私钥。
  • 我在Windows 8.1上拥有本地管理员权限,并尝试以正常和提升的模式运行我的代码。 使用同一证书的同事在Windows 7和Windows 8上能够从本地计算机存储中加载密钥。
  • 我可以成功读取位于相同存储位置的自签名IIS测试证书的私钥。
  • 我已经瞄准.NET 4.5(这个错误已经报告了某些旧版本的框架)。
  • 我不认为这是与证书模板有关的问题,因为我希望它会对本地计算机和当前用户存储都产生影响?

与我的同事不同,我已经多次尝试使用各种方法卸载和重新安装证书,包括通过IIS Manager并包括来自相同发行者的旧证书。 我在MMC中看不到任何旧的或重复的证书痕迹。 但是,我有许多大小相同的私钥文件,根据上一次写入时间,它们必须是在各种安装尝试之后留下的。 这些文件分别位于本地计算机和当前用户存储的以下位置:

c:\ ProgramData \ Microsoft \ Crypto \ RSA \ MachineKeys

c:\ Users \\ AppData \ Roaming \ Microsoft \ Crypto \ RSA \ S-1-5-21-[rest of user ID]

因此,是否有人可以建议:

  • 使用MMC卸载证书,删除所有看起来像是孤立的私钥的文件,然后重新安装证书并重试是否是一个好主意?
  • 还有其他文件应该尝试手动删除吗?
  • 是否还应尝试其他操作?

更新 - 添加了一个代码

static void Main()
{
    // Exception occurs when trying to read the private key after loading certificate from here:
    X509Store store = new X509Store("MY", StoreLocation.LocalMachine);
    // Exception does not occur if certificate was installed to, and loaded from, here:
    //X509Store store = new X509Store("MY", StoreLocation.CurrentUser);

    store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

    X509Certificate2Collection collection = (X509Certificate2Collection)store.Certificates;
    X509Certificate2Collection fcollection = (X509Certificate2Collection)collection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
    X509Certificate2Collection scollection = X509Certificate2UI.SelectFromCollection(fcollection, "Test Certificate Select", "Select a certificate from the following list to get information on that certificate", X509SelectionFlag.MultiSelection);
    Console.WriteLine("Number of certificates: {0}{1}", scollection.Count, Environment.NewLine);

    foreach (X509Certificate2 x509 in scollection)
    {
        try
        {
            Console.WriteLine("Private Key: {0}", x509.HasPrivateKey ? x509.PrivateKey.ToXmlString(false) : "[N/A]");
            x509.Reset();
        }
        catch (CryptographicException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
    store.Close();

    Console.ReadLine();
}

1
我建议你发布相关的代码... - David W
嗯...如果这不是与编程有关的问题,那么要诊断起来就非常困难了,但我稍微搜索了一下,找到了以下内容:http://blogs.msdn.com/b/alejacma/archive/2009/12/22/invalid-provider-type-specified-error-when-accessing-x509certificate2-privatekey.aspx,听起来似乎与此有关(读取私钥时出现模糊的无效提供程序类型异常)。 - David W
@DavidW 谢谢David,但我已经看过那篇文章了,并且我已经确保我正在针对.NET 4.5。 - Simon13
2
@DavidW 好主意,谢谢!在尝试访问可疑证书的密钥时,进程监视器中没有出现“ACCESS DENIED”。不过,我通过拥有我的IIS测试证书的密钥文件并拒绝自己访问来证明了你的建议。进程监视器检测到了它,但抛出的异常略有不同:“密钥集不存在”。 - Simon13
1
抱歉,我们没有找到解决方案,最后只好向IT部门申请了一台新系统。祝好运! - Simon13
显示剩余6条评论
21个回答

58

我曾在Windows 8和Server 2012/2012 R2上遇到同样的问题,这是由于我最近收到了两个新证书。在Windows 10中,这个问题不再存在(但对我没有帮助,因为操作证书的代码是在服务器上使用的)。尽管Joe Strommen的解决方案原则上可行,但不同的私钥模型需要大量修改使用证书的代码。我认为更好的解决方案是将私钥从CNG转换为RSA,如Remy Blok here所解释的。

Remy使用OpenSSL和两个旧工具来完成私钥转换,我们想要自动化它并开发了一个仅使用OpenSSL的解决方案。假设有一个以CNG格式存储、私钥密码为MYPWDMYCERT.pfx,以下是获取新的CONVERTED.pfx并将私钥转换为RSA格式且保持相同密码的步骤:

  1. 提取公钥和完整的证书链:
OpenSSL pkcs12 -in "MYCERT.pfx" -nokeys -out "MYCERT.cer" -passin "pass:MYPWD"
  1. 提取私钥:
OpenSSL pkcs12 -in "MYCERT.pfx" -nocerts -out "MYCERT.pem" -passin "pass:MYPWD" -passout "pass:MYPWD"
  1. 将私钥转换为RSA格式:
OpenSSL rsa -inform PEM -in "MYCERT.pem" -out "MYCERT.rsa" -passin "pass:MYPWD" -passout "pass:MYPWD"

4. 将公钥与RSA私钥合并到新的PFX中:
OpenSSL pkcs12 -export -in "MYCERT.cer" -inkey "MYCERT.rsa" -out "CONVERTED.pfx" -passin "pass:MYPWD" -passout "pass:MYPWD"

如果您加载转换后的pfx文件或将其导入到Windows证书存储中,而不是CNG格式的pfx文件,则问题就会消失,C#代码也不需要更改。
我在自动化过程中遇到的一个额外的问题是:我们为私钥使用长生成密码,密码可能包含"。对于OpenSSL命令行,密码中的"字符必须转义为""

3
这解决了我的问题,真希望几个小时前就能找到它! - Dan Smith
1
我在这里找到了类似的指令:http://www.componentspace.com/Forums/ComponentSpace-Knowledge-Bases/Knowledge-Base-SAML-SSO-for-ASP.NET,因此看起来这可能有一些价值。 - Owen Johnson
3
@Berend,在上面的步骤1中创建了.cert文件。步骤2没有创建.pem文件,没有显示错误,但它说,“pkcs12:使用-help获取摘要。”我做错了什么? 您的操作有误吗?在第二步中可能出现了错误或缺失的命令。建议仔细检查并确保所有命令都正确执行。另外,您可以尝试通过输入“-help”来查看pkcs12的帮助信息以了解更多信息。 - vkelman
1
这个答案的总结是:“基于旧的pfx创建一个新的pfx”?嗯。 - sports
这个救了我的命,我在IIS中加载SAML/ADFS登录响应解密证书时出现了问题。 - klaudyuxxx
显示剩余3条评论

42
在我的情况下,我尝试使用PowerShell的New-SelfSignedCertificate命令来使用自签名证书。默认情况下,它将使用CNG(Crypto-Next Generation)API生成证书,而不是旧版/经典加密CAPI。一些旧代码可能会遇到问题;在我的情况下,这是一个旧版本的IdentityServer STS提供程序。
通过在我的New-SelfSignedCertificate命令末尾添加-KeySpec KeyExchange,我解决了这个问题。
关于powershell命令开关的参考:

https://learn.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps


1
这很有帮助,谢谢。我还需要确保用户可以访问私钥,请参阅https://dev59.com/0mct5IYBdhLWcg3wa8zp - wal
2
爱你!谢谢 ;) - Keytrap
3
非常准确,谢谢!事实上,在 -KeySpec 参数中除了 None 之外的所有选项都可以使用。例如:New-SelfSignedCertificate -DnsName "IssuedToName" -CertStoreLocation "cert:\CurrentUser\My" -KeySpec Signature - Palo Mraz
1
感谢上帝,在这么多小时之后终于搞定了。对于未来有类似问题的人,如果你正在尝试为一个网站创建本地代理,你的程序可能需要以管理员身份运行才能访问私钥,证书也可能需要放在/Root而不是/My目录下才能被接受。 - leumasme
1
我们在部署到Azure应用服务的一个应用程序中遇到了这个问题,而这个标志解决了它。 - Matthew Steeples
谢谢你,你帮我省了很多的烦恼! - iamkl00t

19

这是另一个可能发生的原因,经过一天的挣扎,我解决了这个奇怪的问题。作为一个实验,我更改了存储机器密钥的"C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys"文件夹的权限,用于证书的私有密钥数据。当您更改此文件夹的权限时,所有的私有密钥都会显示为“Microsoft Software KSP provider”,而非供应商(在我的情况下,它们应该是“ Microsoft RSA Schannel Cryptographic Provider”)。

解决方案:重置Machinekeys文件夹的权限

此文件夹的原始权限可以在这里找到。在我的情况下,我更改了“Everyone”的权限,给了读取权限,其中删除了“特殊权限”的勾选。所以我与我的团队成员检查过(右键单击文件夹>属性>安全>高级>选择“Everyone”>编辑>在权限复选框列表中单击“高级设置”)

Special permissions

希望这可以节省其他人的时间!

我在这里找到答案,感谢他记录了此问题。


1
干杯,大耳朵。 - Moustachio
你真是救星!我第一次部署到 Amazon EC2 机器时遇到了这个问题,完全不知道为什么我的代码突然开始失败。 - BlueMaegi
这解决了我的问题。不过,我只授予“ IIS AppPool \ DefaultAppPool”用户完全控制权限,这是托管我的应用程序的应用程序池正在运行的用户帐户。 - WithMetta

17
在我的情况下,以下代码在本地主机上运行良好(包括NET 3.5和NET 4.7):

 var certificate = new X509Certificate2(certificateBytes, password);

 string xml = "....";
 XmlDocument xmlDocument = new XmlDocument();
 xmlDocument.PreserveWhitespace = true;
 xmlDocument.LoadXml(xml);

 SignedXml signedXml = new SignedXml(xmlDocument);
 signedXml.SigningKey = certificate.PrivateKey;

 //etc...

但是在部署到Azure Web App时失败了,在certificate.PrivateKey处。

通过以下方式更改代码后它可以正常工作:

 var certificate = new X509Certificate2(certificateBytes, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
                                                                   //^ Here
 string xml = "....";
 XmlDocument xmlDocument = new XmlDocument();
 xmlDocument.PreserveWhitespace = true;
 xmlDocument.LoadXml(xml);

 SignedXml signedXml = new SignedXml(xmlDocument);
 signedXml.SigningKey = certificate.GetRSAPrivateKey();
                                      // ^ Here too

 //etc...

由于微软Azure云平台的问题,又一次浪费了我整整一天的工作时间。


1
感谢您的评论。我只浪费了3个小时,然后偶然看到了您的评论。它挽救了我的整个周末。 - whatsinaname
1
GetRSAPrivateKey() 对我来说非常关键。 - Ian1971
2
在Azure上,为我添加X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable起作用了。 - rcruz
1
在 Windows Server 2016 上的 IIS 上实现 API 时遇到了同样的问题。第一种修改对我起到了作用。非常感谢。 - SonMauri
谢谢,伙计。这里的主要问题是缺少一些X509KeyStorageFlags。 - Sruit A.Suk
显示剩余2条评论

12

我的解决方法是: IIS / 应用程序池 / 载入用户配置文件 = true


也适用于我... - Edo
我曾经遇到过同样的问题,但只出现在IIS中的asp.net应用程序中。使用相同代码的控制台应用程序可以正常加载PrivateKey,但尝试从asp.net应用程序中加载时会失败,结果发现我的应用程序池上的LoadUserProfile设置为false...这真是救了我一命,谢谢! - Crackerjack

10

链接到Alejandro的博客非常重要。

我认为这是因为证书存储在使用CNG(“Crypto Next-Generation”)API的计算机上。旧的.NET API与之不兼容,因此无法正常工作。

您可以使用Security.Cryptography包装器来使用此API(可在Codeplex上获得)。 这会为X509Certificate / X509Certificate2添加扩展方法,因此您的代码将类似于:

using Security.Cryptography.X509Certificates; // Get extension methods

X509Certificate cert; // Populate from somewhere else...
if (cert.HasCngKey())
{
    var privateKey = cert.GetCngPrivateKey();
}
else
{
    var privateKey = cert.PrivateKey;
}

不幸的是,CNG私钥的对象模型有相当大的差异。我不确定是否可以像您原始代码示例中那样将其导出为XML...在我的情况下,我只需要使用私钥对某些数据进行签名。


1
尽管我已经使用了using子句,但我的证书类上没有.HasCngKey()方法。 - Jepzen
一定有问题。你能明确地调用它作为 Security.Cryptography.X509Certificates.X509CertificateExtensionMethods.HasCngKey(cert) 吗? - Joe Strommen
7
在最新的.NET(4.6)中,这仍然是正确的方法吗? - MichaelChan
@MichaelChan 除了 GetCngPrivateKey 被重命名为 GetRSAPrivateKey,其他都没变。我在使用 EncryptedXml 时遇到了这个错误,后来发现代码已经更新到 net462 版本,使用了这个新方法,所以不再抛出错误。 - Michael

8
正如其他答案所指出的那样,当私钥是Windows Cryptography: Next Generation (CNG)密钥而不是“经典”Windows Cryptographic API (CAPI)密钥时,就会出现此问题。
从.NET Framework 4.6开始,可以通过X509Certificate2的扩展方法访问私钥(假设它是RSA密钥):cert.GetRSAPrivateKey()
当私钥由CNG持有时,GetRSAPrivateKey扩展方法将返回一个RSACng对象(在4.6中新添加到框架)。因为CNG可以通过传递来读取旧的CAPI软件密钥,即使对于CAPI密钥,GetRSAPrivateKey通常也会返回RSACng;但如果CNG无法加载它(例如,它是没有CNG驱动程序的HSM密钥),则GetRSAPrivateKey将返回RSACryptoServiceProvider
请注意,GetRSAPrivateKey的返回类型为RSA。从.NET Framework v4.6开始,您不应需要超出RSA进行标准操作;只有当您需要与使用或密钥标识符(或按名称打开持久化密钥)的程序或库交互时,才需要使用RSACngRSACryptoServiceProvider。(.NET Framework v4.6还有许多地方仍将输入对象强制转换为RSACryptoServiceProvider,但这些在4.6.2中全部被消除了(当然,这已经是2年前的事情了))。
通过GetECDsaPrivateKey扩展方法于4.6.1中添加了ECDSA证书支持,并通过GetDSAPrivateKey于4.6.2中升级了DSA。
在.NET Core中,Get[Algorithm]PrivateKey的返回值取决于操作系统。对于RSA,在Windows上是RSACng/RSACryptoServiceProvider,在Linux上(或除macOS外的任何类UNIX操作系统)是RSAOpenSsl,在macOS上是一个非公开类型(这意味着您无法把它强制转换超出RSA)。

4
使用Visual Studio 2019和IISExpress,我成功地通过在加载.pfx | .p12文件时删除以下标记来解决了这个问题:
X509KeyStorageFlags.MachineKeySet

之前:

X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable

之后:

X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable

通常,我会以这种方式加载证书:

var myCert = new X509Certificate2("mykey.pfx", "mypassword", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);

这不会引发异常,相反,当尝试使用证书(或在我的情况下,尝试获取.PrivateKey时)时才会引发异常。我发现当调用用户权限不足时可能会导致此问题。
由于我在某些环境中依赖于MachineKeySet标志,所以我的当前解决方案是吞咽异常,更改标志,然后重试。:/
更有机的解决方案是测试权限级别并动态设置此标志,但是我不知道简单的方法来做到这一点,因此使用了回退方案。
注意:我正在使用的.pfx文件是从使用JavaScript库(digitalbazaar/forge)的网站创建的,该库不使用CNG(密码学下一代)密钥存储提供程序。有许多常见原因导致同样的错误(最常见的修复与CNG扩展相关,这些扩展甚至已经在.NET版本中改变了名称空间),它们会抛出相同的错误。Microsoft在抛出这些类型的错误时应该更加详细。

1
假设已通过chocolatey安装了openssl,以下是来自@berend-engelbrecht的答案的Powershell版本。
function Fix-Certificates($certPasswordPlain)
{
    $certs = Get-ChildItem -path "*.pfx" -Exclude "*.converted.pfx"
    $certs | ForEach-Object{
        $certFile = $_

        $shortName = [io.path]::GetFileNameWithoutExtension($certFile.Name)
        Write-Host "Importing $shortName"
        $finalPfx = "$shortName.converted.pfx"


        Set-Alias openssl "C:\Program Files\OpenSSL\bin\openssl.exe"

        # Extract public key
        OpenSSL pkcs12 -in $certFile.Fullname -nokeys -out "$shortName.cer" -passin "pass:$certPasswordPlain"

        # Extract private key
        OpenSSL pkcs12 -in $certFile.Fullname -nocerts -out "$shortName.pem" -passin "pass:$certPasswordPlain" -passout "pass:$certPasswordPlain"

        # Convert private key to RSA format
        OpenSSL rsa -inform PEM -in "$shortName.pem" -out "$shortName.rsa" -passin "pass:$certPasswordPlain" -passout "pass:$certPasswordPlain" 2>$null

        # Merge public keys with RSA private key to new PFX
        OpenSSL pkcs12 -export -in "$shortName.cer" -inkey "$shortName.rsa" -out $finalPfx -passin "pass:$certPasswordPlain" -passout "pass:$certPasswordPlain"

        # Clean up
        Remove-Item "$shortName.pem"
        Remove-Item "$shortName.cer"
        Remove-Item "$shortName.rsa"

        Write-Host "$finalPfx created"
    }
}

# Execute in cert folder
Fix-Certificates password

1

在遵循接受的答案(指定KeySpec)后,异常变为System.Security.Cryptography.CryptographicException: Invalid provider type specified.。我通过授予我的Web应用程序访问私钥(IIS_IUSRS)来解决了这个异常。我发现这也会解决我的原始证书问题。因此,在生成和部署新证书之前,请检查私钥的权限。


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