You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET生成证书无法通过代码访问私钥,OpenSSL生成则正常

问题:.NET生成的证书无法被gRPC服务访问私钥,OpenSSL生成的可以

场景描述

我需要用.NET生成CA根证书和服务器证书,用于本地gRPC服务间的安全通信。生成服务器证书的PFX文件后,手动导入本地机器个人证书存储(标记私钥可导出),根证书导入受信任根存储,IIS和Chrome验证正常。

用OpenSSL生成的证书,所有环节(包括.NET gRPC服务)都正常工作;但用.NET生成的证书,IIS运行正常,gRPC服务却无法工作。原因是.NET代码访问证书的PrivateKey属性时,抛出System.Security.Cryptography.CryptographicException异常,提示“Keyset does not exist”,而OpenSSL生成的证书无此问题。

测试代码:

public void ReadDifferentCerts()
{
    var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
    store.Open(OpenFlags.ReadOnly);

    var certs = store.Certificates.Find(X509FindType.FindBySubjectName, "MyMachine", false);

    var dotNetCert = certs.Find(X509FindType.FindByIssuerName, "DotNetRootCA", false).First();
    var openSslCert = certs.Find(X509FindType.FindByIssuerName, "OpenSSLRootCA", false).First();
}

多数解决方案围绕密钥权限,但导入当前用户存储就能访问密钥。我想知道:
OpenSSL生成的证书有何不同?需要在.NET代码中设置什么标志?

更新:证书差异分析

用certutil -dump检查PFX文件,发现两者核心差异:

  • OpenSSL生成的证书使用Provider = Microsoft Enhanced Cryptographic Provider v1.0
  • .NET生成的证书使用Provider = Microsoft Software Key Storage Provider,且标记Private key is NOT plain text exportable

两者通过同一用户、相同方法导入同一存储,参数几乎一致,私钥确实存在(IIS可用,导入当前用户存储也正常)。

OpenSSL证书dump结果

c:\dev\certs>certutil -dump .\MyMachine-OpenSsl.pfx
Enter PFX password:

================ Certificate 0 ================
================ Begin Nesting Level 1 ================
Element 0:
Serial Number: 2f0f5985c17054b56c7fc42e8eb7048387536251
Issuer: C=GB, O=MyCompany, CN=OpenSSLRootCA
 NotBefore: 23/04/2025 09:36
 NotAfter: 23/04/2026 09:36
Subject: O=MyCompany, OU=DevSecOps, CN=LAPTOP-80
Non-root Certificate
Cert Hash(sha1): c065c3b09234933389714967f1d9dd2afc608f72
----------------  End Nesting Level 1  ----------------
  Provider = Microsoft Enhanced Cryptographic Provider v1.0
Encryption test passed
CertUtil: -dump command completed successfully.

.NET生成证书dump结果

c:\dev\certs>certutil -dump .\MyMachine-DotNet.pfx
Enter PFX password:

================ Certificate 0 ================
================ Begin Nesting Level 1 ================
Element 0:
Serial Number: 2a3781135c720ddfd1fae54ab3920bab915a8937
Issuer: C=GB, O=MyCompany, CN=DotNetRootCA
 NotBefore: 23/04/2025 15:39
 NotAfter: 23/04/2026 15:39
Subject: O=MyCompany, OU=DevSecOps, CN=LAPTOP-80
Non-root Certificate
Cert Hash(sha1): dabff030ecfcf8653b6ac5b45fa615fccf59cd19
----------------  End Nesting Level 1  ----------------
  Provider = Microsoft Software Key Storage Provider
Private key is NOT plain text exportable
Encryption test passed
CertUtil: -dump command completed successfully.

相关代码

.NET证书生成代码

public void GenerateAndWriteToDisk()
{
    var rootCA = BuildRootCertificate();
    
    Thread.Sleep(TimeSpan.FromSeconds(2));

    var serverCert = BuildServerCertificate(rootCA);

    var outputdir = @"C:\dev\certs";
    File.WriteAllBytes(Path.Combine(outputdir, "DotNetRootCACert.crt"), rootCA.Export(X509ContentType.Cert));
    File.WriteAllBytes(Path.Combine(outputdir, "LAPTOP-80-DotNet.pfx"), serverCert.Export(X509ContentType.Pfx, "word"));
}

public static X509Certificate2 BuildRootCertificate()
{
    using (var rsa = RSA.Create(3072))
    {
        var request = new CertificateRequest("C=GB,O=MyCompany,CN=DotNetRootCA", rsa, HashAlgorithmName.SHA256,
                RSASignaturePadding.Pkcs1);
        var subjectIdentifier = new X509SubjectKeyIdentifierExtension(new PublicKey(rsa), false);
        request.CertificateExtensions.Add(subjectIdentifier);
            request.CertificateExtensions.Add(X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectIdentifier));
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));

        var cert = request.CreateSelfSigned(DateTimeOffset.Now,
                DateTimeOffset.Now.AddDays(1825));
        return cert;
    }
}

public static X509Certificate2 BuildServerCertificate(X509Certificate2 rootCaCertificate)
{
    using (var rsa = RSA.Create(3072))
    {
        var request = new CertificateRequest("O=MyCompany,OU=DevSecOps,CN=MyMachine", rsa, HashAlgorithmName.SHA256,
                RSASignaturePadding.Pkcs1);
            
            request.CertificateExtensions.Add(X509AuthorityKeyIdentifierExtension.CreateFromCertificate(rootCaCertificate, true, false));
        request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(new PublicKey(rsa), false));
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false,
                0, false));

        var keyUsages = new X509KeyUsageExtension(
                X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment |
                X509KeyUsageFlags.DigitalSignature, false);
        request.CertificateExtensions.Add(keyUsages);

        var sanBuilder = new SubjectAlternativeNameBuilder();
        sanBuilder.AddDnsName("MyMachine");
        sanBuilder.AddDnsName("localhost");
        sanBuilder.AddIpAddress(IPAddress.Loopback);
        sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);

        request.CertificateExtensions.Add(sanBuilder.Build());

        var serialNumber = GetSerialNumber();

        var certificate = request.Create(rootCaCertificate, DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow.AddDays(365), serialNumber);

        return certificate.CopyWithPrivateKey(rsa);
    }
}

OpenSSL生成脚本(PowerShell)

function ExecOpenSsl([string]$sslArgs)
{
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = "openssl"
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.CreateNoWindow = $true
    $pinfo.Arguments = $sslArgs
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    $stdout = $p.StandardOutput.ReadToEnd()
    $stderr = $p.StandardError.ReadToEnd()
    Write-Host "stdout: $stdout"
    Write-Host "stderr: $stderr"
    Write-Host "exit code: " + $p.ExitCode
}


$dir = 'C:\dev\certs'
$caKeyName = "$dir\OpenSslRootCaKey.key"
$caCertName = "$dir\OpenSSlRootCaCert.crt"
$serverKeyName = "$dir\OpenSslServerKey.key"
$serverCsr = "$dir\OpenSslServerReq.csr"
$serverExtension = "$dir\OpenSslServerExtensions.ext"
$serverCert = "$dir\LAPTOP-80-OpenSsl.crt"
$serverPfx = "$dir\LAPTOP-80-OpenSsl.pfx"


Set-Location $dir

$caPass = 'word'  # (New-Guid).ToString()
$serverPass = 'word' #(New-Guid).ToString()

ExecOpenSsl -sslArgs "genrsa -aes128 -passout pass:$($caPass) -out $caKeyName 3072"

ExecOpenSsl -sslArgs "req -x509 -new -nodes -key $caKeyName -sha256 -days 1825 -out $caCertName -subj /CN=OpenSSLRootCA/O=MyCompany/C=GB/ -passin pass:$($caPass)"

ExecOpenSsl -sslArgs "genrsa -aes128 -passout pass:$($serverPass) -out $serverKeyName 3072"

$extension=@(
"authorityKeyIdentifier=keyid,issuer"
"basicConstraints=CA:FALSE"
"keyUsage = digitalSignature, keyEncipherment, dataEncipherment"
"subjectAltName = @alt_names"
""
"[alt_names]"
"DNS.1 = MyMachine"
"DNS.2 = localhost"
"IP.1 = 127.0.0.1"
"IP.2 = ::1"
)

Set-Content -Path $serverExtension -Value ($extension -join ([Environment]::NewLine))  -NoNewline

ExecOpenSsl -sslArgs "req -new -key $serverKeyName -passin pass:$($serverPass) -out $serverCsr -subj /CN=MyMachine/OU=DevSecOps/O=MyCompany/"

ExecOpenSsl -sslArgs "x509 -req -in $serverCsr -CA $caCertName -CAkey $caKeyName -passin pass:$($caPass) -CAcreateserial -out $serverCert -days 365 -sha256 -extfile $serverExtension"

ExecOpenSsl -sslArgs "pkcs12 -export -out $serverPfx -inkey $serverKeyName -in $serverCert -passin pass:$($serverPass) -passout pass:$($serverPass)"

解决方案

1. 证书差异本质

OpenSSL生成的证书使用传统CSP(加密服务提供程序)(Microsoft Enhanced Cryptographic Provider v1.0),而.NET默认生成的证书使用**CNG(下一代加密技术)**提供程序(Microsoft Software Key Storage Provider)。

CNG密钥导入到LocalMachine存储时,默认权限限制更严格,普通用户进程(如你的gRPC服务)无法访问,而IIS运行在高权限账户下可绕过该限制。另外,.NET生成的密钥默认标记为不可明文导出,也会影响部分场景下的访问。

2. 修改.NET代码的方法

要让.NET生成的证书与OpenSSL一致,需强制使用传统CSP,并允许密钥导出。

修改点1:创建RSA实例时指定传统CSP

替换原有的RSA.Create(3072),创建使用传统CSP的RSA实例:

// 替换原来的RSA.Create(3072)
var cspParams = new CspParameters
{
    ProviderType = 24, // Microsoft Enhanced RSA and AES Cryptographic Provider
    ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0",
    KeyContainerName = Guid.NewGuid().ToString(),
    Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.Exportable
};
using (var rsa = new RSACryptoServiceProvider(3072, cspParams))

修改点2:导出PFX时允许密钥明文导出

导出PFX时,指定允许私钥导出的选项:

// 导出服务器证书PFX时,确保密钥可明文导出
var exportOptions = new X509Certificate2ExportOptions
{
    ExportOptions = X509ExportOptions.ExportPrivateKey | X509ExportOptions.AllowPlainTextExport
};
File.WriteAllBytes(Path.Combine(outputdir, "LAPTOP-80-DotNet.pfx"), serverCert.Export(X509ContentType.Pfx, "word", exportOptions));

完整修改后的BuildRootCertificate方法

public static X509Certificate2 BuildRootCertificate()
{
    var cspParams = new CspParameters
    {
        ProviderType = 24,
        ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0",
        KeyContainerName = Guid.NewGuid().ToString(),
        Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.Exportable
    };
    using (var rsa = new RSACryptoServiceProvider(3072, cspParams))
    {
        var request = new CertificateRequest("C=GB,O=MyCompany,CN=DotNetRootCA", rsa, HashAlgorithmName.SHA256,
                RSASignaturePadding.Pkcs1);
        var subjectIdentifier = new X509SubjectKeyIdentifierExtension(new PublicKey(rsa), false);
        request.CertificateExtensions.Add(subjectIdentifier);
        request.CertificateExtensions.Add(X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectIdentifier));
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));

        var cert = request.CreateSelfSigned(DateTimeOffset.Now,
                DateTimeOffset.Now.AddDays(1825));
        return cert;
    }
}

完整修改后的BuildServerCertificate方法

public static X509Certificate2 BuildServerCertificate(X509Certificate2 rootCaCertificate)
{
    var cspParams = new CspParameters
    {
        ProviderType = 24,
        ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0",
        KeyContainerName = Guid.NewGuid().ToString(),
        Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.Exportable
    };
    using (var rsa = new RSACryptoServiceProvider(3072, cspParams))
    {
        var request = new CertificateRequest("O=MyCompany,OU=DevSecOps,CN=MyMachine", rsa, HashAlgorithmName.SHA256,
                RSASignaturePadding.Pkcs1);
            
        request.CertificateExtensions.Add(X509AuthorityKeyIdentifierExtension.CreateFromCertificate(rootCaCertificate, true, false));
        request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(new PublicKey(rsa), false));
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false,
                0, false));

        var keyUsages = new X509KeyUsageExtension(
                X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment |
                X509KeyUsageFlags.DigitalSignature, false);
        request.CertificateExtensions.Add(keyUsages);

        var sanBuilder = new SubjectAlternativeNameBuilder();
        sanBuilder.AddDnsName("MyMachine");
        sanBuilder.AddDnsName("localhost");
        sanBuilder.AddIpAddress(IPAddress.Loopback);
        sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);

        request.CertificateExtensions.Add(sanBuilder.Build());

        var serialNumber = GetSerialNumber();

        var certificate = request.Create(rootCaCertificate, DateTimeOffset.UtcNow,
                DateTimeOffset.UtcNow.AddDays(365), serialNumber);

        return certificate.CopyWithPrivateKey(rsa);
    }
}

修改导出逻辑

public void GenerateAndWriteToDisk()
{
    var rootCA = BuildRootCertificate();
    
    Thread.Sleep(TimeSpan.FromSeconds(2));

    var serverCert = BuildServerCertificate(rootCA);

    var outputdir = @"C:\dev\certs";
    File.WriteAllBytes(Path.Combine(outputdir, "DotNetRootCACert.crt"), rootCA.Export(X509ContentType.Cert));
    
    // 导出可明文导出的PFX
    var exportOptions = new X509Certificate2ExportOptions
    {
        ExportOptions = X509ExportOptions.ExportPrivateKey | X509ExportOptions.AllowPlainTextExport
    };
    File.WriteAllBytes(Path.Combine(outputdir, "LAPTOP-80-DotNet.pfx"), serverCert.Export(X509ContentType.Pfx, "word", exportOptions));
}

3. 验证修改效果

重新生成证书后,用certutil -dump检查PFX文件,应显示:

  • Provider = Microsoft Enhanced Cryptographic Provider v1.0
  • 不再显示`Private key is NOT plain text export
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 07:49:50