我正在尝试读取第三方服务提供商与我共享的证书的私钥,因此我可以使用它来加密某些XML,然后再将其发送给他们。我在C#中以编程方式这样做,但我认为这是一个权限或配置错误的问题,因此我将重点关注似乎最相关的事实:
X509Certificate2.HasPrivateKey
验证它是否具有私钥。但是,尝试读取密钥会导致错误。在.NET中,在尝试访问属性CryptographicException
时,会抛出X509Certificate2.PrivateKey
,并显示消息“指定了无效的提供程序类型”。在Win32中,调用方法CryptAcquireCertificatePrivateKey
返回等效的HRESULT,NTE_BAD_PROV_TYPE
。与我的同事不同,我之前曾多次尝试以各种方式卸载和重新安装证书,包括通过IIS管理器,还包括来自同一发行人的旧证书。我在MMC中看不到任何旧证书或重复证书的痕迹。但是,我确实有许多相同大小的私钥文件,基于最后写入时间,在我的各种安装尝试后必须留下。这些位于以下位置,分别用于本地计算机和当前用户存储:
C:\ ProgramData \微软\加密\ RSA \ MachineKeys的
c:\ Users \\ AppData \ Roaming \ Microsoft \ Crypto \ RSA \ S-1-5-21- [其余用户ID]
所以,任何人都可以建议是否:
更新 - 添加了一个代码示例,显示尝试读取私钥:
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();
}
我在Windows 8和Server 2012/2012 R2上遇到了同样的问题,我最近收到了两个新证书。在Windows 10上,问题不再出现(但这对我没有帮助,因为在服务器上使用了操纵证书的代码)。虽然Joe Strommen的解决方案原则上有效,但不同的私钥模型需要使用证书对代码进行大量更改。我发现更好的解决方案是将私钥从CNG转换为RSA,正如Remy Blok here所解释的那样。
Remy使用OpenSSL和两个较旧的工具来完成私钥转换,我们希望自动化它并开发一个仅支持OpenSSL的解决方案。鉴于MYCERT.pfx
具有CNG格式的私钥密码MYPWD
,这些是获取带有RSA格式的私钥和相同密码的新CONVERTED.pfx
的步骤:
OpenSSL pkcs12 -in "MYCERT.pfx" -nokeys -out "MYCERT.cer" -passin "pass:MYPWD"
OpenSSL pkcs12 -in "MYCERT.pfx" -nocerts –out “MYCERT.pem" -passin "pass:MYPWD" -passout "pass:MYPWD"
OpenSSL rsa -inform PEM -in "MYCERT.pem" -out "MYCERT.rsa" -passin "pass:MYPWD" -passout "pass:MYPWD"
OpenSSL pkcs12 -export -in "MYCERT.cer" -inkey "MYCERT.rsa" -out "CONVERTED.pfx" -passin "pass:MYPWD" -passout "pass:MYPWD"
如果加载转换后的pfx或将其导入Windows证书库而不是CNG格式pfx,问题就会消失,C#代码不需要更改。
在自动执行此操作时遇到的另一个问题是:我们使用长密码生成私钥,密码可能包含"
。对于OpenSSL命令行,密码中的"
字符必须作为""
进行转义。
与Alejandro's blog的链接是关键。
我相信这是因为证书使用CNG(“加密下一代”)API存储在您的计算机上。旧的.NET API与它不兼容,因此它不起作用。
您可以对此API使用Security.Cryptography包装器(available on 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 ...在我的情况下,我只需要使用私钥签署一些数据。
这是另一个原因,这可能发生,这是一个奇怪的问题,经过一天的努力,我解决了这个问题。作为实验,我更改了“C:\ ProgramData \ Microsoft \ Crypto \ RSA \ MachineKeys”文件夹的权限,该文件夹使用机器密钥库保存证书的私钥数据。当您更改此文件夹的权限时,所有私钥都显示为“Microsoft Software KSP提供程序”,而不是提供程序(在我的情况下,它们应该是“Microsoft RSA Schannel Cryptographic Provider”)。
解决方案:重置Machinekeys文件夹的权限
可以在here中找到此文件夹的原始权限。在我的情况下,我已经更改了“Everyone”的权限,在删除“特殊权限”时给出了读取权限。所以我检查了我的团队成员之一(右键单击文件夹>属性>安全性>高级>选择“所有人”>编辑>单击权限复选框列表中的“高级设置”
希望这能拯救某人的一天!
在这里我找到了答案source,归功于他记录这一点。
就我而言,我试图在PowerShell的New-SelfSignedCertificate命令中使用自签名证书。默认情况下,它将使用CNG(加密 - 下一代)API而不是旧版/经典加密CAPI生成证书。一些较旧的代码会遇到麻烦;就我而言,它是IdentityServer STS提供程序的旧版本。
通过在New-SelfSignedCertificate命令的末尾添加它,我遇到了问题:
KEYSpec KeyExchange
在powershell命令的开关上引用:
https://docs.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps
我也有这个问题,在尝试了这篇文章中的建议后没有成功。我可以通过使用Digicert证书实用程序https://www.digicert.com/util/重新加载证书来解决我的问题。这允许选择要将证书加载到的提供程序。在我的情况下,将证书加载到Microsoft RSA Schannel加密提供程序提供程序中,我原本预计它将首先解决问题。
正如许多其他答案所指出的,当私钥是Windows密码术:下一代(CNG)密钥而不是“经典”Windows密码API(CAPI)密钥时,会出现此问题。
从.NET Framework 4.6开始,可以通过X509Certificate2:cert.GetRSAPrivateKey()
上的扩展方法访问私钥(假设它是RSA密钥)。
当私有密钥由CNG持有时,GetRSAPrivateKey
扩展方法将返回一个RSACng
对象(4.6中的框架的新内容)。因为CNG有一个传递来读取旧的CAPI软件密钥,GetRSAPrivateKey
通常会返回一个RSACng
,即使是CAPI密钥;但如果CNG无法加载它(例如它是没有CNG驱动程序的HSM密钥),那么GetRSAPrivateKey
将返回RSACryptoServiceProvider
。
请注意,GetRSAPrivateKey
的返回类型是RSA
。从.NET Framework v4.6开始,您不需要在RSA
之外进行标准操作;使用RSACng
或RSACryptoServiceProvider
的唯一原因是当您需要与使用NCRYPT_KEY_HANDLE
或密钥标识符(或按名称打开持久密钥)的程序或库互操作时。 (.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上的RSAOpenSsl
(或除macOS之外的任何类UNIX操作系统),以及macOS上的非公共类型(意味着你无法将其强制转换为RSA
)。
在我的例子中,以下代码在localhost(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...
但是在certificate.PrivateKey
上部署到Azure Web App时失败了
它通过更改代码如下工作:
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...
感谢Microsoft Azure,我的生活再次失去了一整天的工作。
我在IIS应用程序中遇到了同样的问题:
System.Security.Cryptography.Pkcs.PkcsUtils.CreateSignerEncodeInfo(CmsSigner signer, Boolean silent)
System.Security.Cryptography.Pkcs.SignedCms.Sign(CmsSigner signer, Boolean silent)
System.Security.Cryptography.Pkcs.SignedCms.ComputeSignature(CmsSigner signer, Boolean silent)
如此处提到的重新生成证书没有帮助。我还注意到测试控制台应用程序在池用户下工作正常。
清除IIS应用程序池的“启用32位应用程序”设置后问题消失。
来自@ berend-engelbrecht的答案的Powershell版本,假设openssl
通过chocolatey安装
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