从X509证书中导出私钥/公钥到PEM

27

有没有一种方便的方法可以在.NET Core中以PEM格式从.p12证书中导出私钥/公钥?而不必在低级别处理字节?我已经谷歌了几个小时,几乎没有任何可用于.NET Core的东西,或者它没有被记录在任何地方。

让我们拥有一个X509Certificate2

var cert = new X509Certificate2(someBytes, pass);
var privateKey = cert.GetRSAPrivateKey();
var publicKey = cert.GetRSAPublicKey();
// assume everything is fine so far

现在我需要将密钥导出为两个单独的PEM密钥。我已经尝试过BouncyCastle中的PemWriter,但类型与Core中的System.Security.Cryptography不兼容...没有成功。

换句话说,我正在寻找一种方法来编写以下内容:

$ openssl pkcs12 -in path/to/cert.p12 -out public.pub -clcerts -nokeys
$ openssl pkcs12 -in path/to/cert.p12 -out private.key -nocerts

有人有想法吗?

谢谢


这个链接是关于编程的,是一个GitHub地址,包含了命名为“pem”的项目。知识点都隐藏在GitHub的问题和代码中。如果您找不到相关信息,可以创建一个新的问题并与开发人员讨论。 - Lex Li
4个回答

95

更新(2021-01-12):对于.NET 5来说,这非常容易。.NET Core 3.0甚至可以完成大部分工作。最初的答案是在.NET Core 1.1是最新版本的.NET Core时编写的。它解释了这些新方法在底层的操作。

更新(2023-08-30):而.NET 7则添加了直接转换为PEM的方法,使PEM输出变得更加简单。

.NET 7+:

string certificatePem = cert.ExportCertificatePem();

AsymmetricAlgorithm key = cert.GetRSAPrivateKey() ?? cert.GetECDsaPrivateKey();
string pubKeyPem = key.ExportSubjectPublicKeyInfoPem();
string privKeyPem = key.ExportPkcs8PrivateKeyPem();

.NET 7 还添加了 PemEncoding.WriteString,使得 .NET 5 风格的代码可以转换为 string 而不是 char[]

.NET 5+:

byte[] certificateBytes = cert.RawData;
char[] certificatePem = PemEncoding.Write("CERTIFICATE", certificateBytes);

AsymmetricAlgorithm key = cert.GetRSAPrivateKey() ?? cert.GetECDsaPrivateKey();
byte[] pubKeyBytes = key.ExportSubjectPublicKeyInfo();
byte[] privKeyBytes = key.ExportPkcs8PrivateKey();
char[] pubKeyPem = PemEncoding.Write("PUBLIC KEY", pubKeyBytes);
char[] privKeyPem = PemEncoding.Write("PRIVATE KEY", privKeyBytes);

new string(char[]) 可以将这些字符数组转换为 System.String 实例,如果需要的话。

对于加密的 PKCS#8,仍然很容易,但是你必须在如何加密它方面做出一些选择:

byte[] encryptedPrivKeyBytes = key.ExportEncryptedPkcs8PrivateKey(
    password,
    new PbeParameters(
        PbeEncryptionAlgorithm.Aes256Cbc,
        HashAlgorithmName.SHA256,
        iterationCount: 100_000));

.NET Core 3.0, .NET Core 3.1:

这与.NET 5的答案相同,只是PemEncoding类尚不存在。但没关系,旧答案中有一个用于PEM格式化的起点(尽管"CERTIFICATE"和cert.RawData需要从参数中获取)。

.NET Core 3.0是添加了额外密钥格式导出和导入方法的版本。

.NET Core 2.0, .NET Core 2.1:

与原始答案相同,只是您不需要编写DER编码器。您可以使用System.Formats.Asn1 NuGet package

原始答案(.NET Core 1.1是最新选项):

答案介于“不”和“不完全是”之间。

我假设您不希望在public.pubprivate.key的顶部有p12输出的冗余内容。

public.pub只是证书。 openssl命令行实用程序更喜欢PEM编码的数据,因此我们将编写一个PEM编码的证书(请注意,这是一个证书,而不是公钥。它包含一个公钥,但本身不是公钥):

using (var cert = new X509Certificate2(someBytes, pass))
{
    StringBuilder builder = new StringBuilder();
    builder.AppendLine("-----BEGIN CERTIFICATE-----");
    builder.AppendLine(
        Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks));
    builder.AppendLine("-----END CERTIFICATE-----");

    return builder.ToString();
}

私钥更难处理。假设该密钥是可导出的(如果您使用的是Windows或macOS,则不可导出,因为您没有声明X509KeyStorageFlags.Exportable),您可以使用privateKey.ExportParameters(true)获取参数。但现在您必须将其写下来。
RSA私钥将被写入一个PEM编码的文件中,其标签为"RSA PRIVATE KEY",有效负载为ASN.1(ITU-T X.680)RSAPrivateKey(PKCS#1 / RFC3447)结构,通常为DER编码(ITU-T X.690)--尽管由于它没有签名,所以没有特定的DER限制,但许多读者可能会假设为DER编码。
或者,它可以是PKCS#8(RFC 5208)的PrivateKeyInfo(标签:"PRIVATE KEY"),或者EncryptedPrivateKeyInfo(标签:"ENCRYPTED PRIVATE KEY")。由于EncryptedPrivateKeyInfo包装了PrivateKeyInfo,而PrivateKeyInfo封装了RSAPrivateKey,所以我们就从这里开始。
  RSAPrivateKey ::= SEQUENCE {
      version           Version,
      modulus           INTEGER,  -- n
      publicExponent    INTEGER,  -- e
      privateExponent   INTEGER,  -- d
      prime1            INTEGER,  -- p
      prime2            INTEGER,  -- q
      exponent1         INTEGER,  -- d mod (p-1)
      exponent2         INTEGER,  -- d mod (q-1)
      coefficient       INTEGER,  -- (inverse of q) mod p
      otherPrimeInfos   OtherPrimeInfos OPTIONAL
  }

现在忽略关于otherPrimeInfos的部分。 exponent1 是DP,exponent2 是DQ,而coefficient 是InverseQ。
我们来使用一个预发布的384位RSA密钥
RFC 3447说我们想要Version=0。其他所有信息都来自结构本身。
// SEQUENCE (RSAPrivateKey)
30 xa [ya [za]]
   // INTEGER (Version=0)
   02 01
         00
   // INTEGER (modulus)
   // Since the most significant bit of the most significant content byte is set,
   // add a padding 00 byte.
   02 31
         00
         DA CC 22 D8 6E 67 15 75 03 2E 31 F2 06 DC FC 19
         2C 65 E2 D5 10 89 E5 11 2D 09 6F 28 82 AF DB 5B
         78 CD B6 57 2F D2 F6 1D B3 90 47 22 32 E3 D9 F5
   // INTEGER publicExponent
   02 03
         01 00 01
   // INTEGER (privateExponent)
   // high bit isn't set, so no padding byte
   02 30
         7A 59 BD 02 9A 7A 3A 9D 7C 71 D0 AC 2E FA 54 5F
         1F 5C BA 43 BB 43 E1 3B 78 77 AF 82 EF EB 40 C3
         8D 1E CD 73 7F 5B F9 C8 96 92 B2 9C 87 5E D6 E1
   // INTEGER (prime1)
   // high bit is set, pad.
   02 19
         00
         FA DB D7 F8 A1 8B 3A 75 A4 F6 DF AE E3 42 6F D0
         FF 8B AC 74 B6 72 2D EF
   // INTEGER (prime2)
   // high bit is set, pad.
   02 19
         00
         DF 48 14 4A 6D 88 A7 80 14 4F CE A6 6B DC DA 50
         D6 07 1C 54 E5 D0 DA 5B
   // INTEGER (exponent1)
   // no padding
   02 18
         24 FF BB D0 DD F2 AD 02 A0 FC 10 6D B8 F3 19 8E
         D7 C2 00 03 8E CD 34 5D
   // INTEGER (exponent2)
   // padding required
   02 19
         00
         85 DF 73 BB 04 5D 91 00 6C 2D 45 9B E6 C4 2E 69
         95 4A 02 24 AC FE 42 4D
   // INTEGER (coefficient)
   // no padding
   02 18
         1A 3A 76 9C 21 26 2B 84 CA 9C A9 62 0F 98 D2 F4
         3E AC CC D4 87 9A 6F FD

现在我们计算进入RSAPrivateKey结构的字节数。我计算出是0xF2(242)。由于这个数大于0x7F,我们需要使用多字节长度编码:81 F2
所以现在,通过字节数组30 81 F2 02 01 00 ... 9A 6F FD,你可以将其转换为多行Base64,并用"RSA PRIVATE KEY" PEM装甲包裹起来。但也许你想要一个PKCS#8。
  PrivateKeyInfo ::= SEQUENCE {
    version                   Version,
    privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
    privateKey                PrivateKey,
    attributes           [0]  IMPLICIT Attributes OPTIONAL }

  Version ::= INTEGER
  PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
  PrivateKey ::= OCTET STRING

所以,让我们再做一次吧... RFC说我们在这里也想要version=0。AlgorithmIdentifier可以在RFC5280中找到。
// SEQUENCE (PrivateKeyInfo)
30 xa [ya [za]]
   // INTEGER (Version=0)
   02 01
         00
   // SEQUENCE (PrivateKeyAlgorithmIdentifier / AlgorithmIdentifier)
   30 xb [yb [zb]]
      // OBJECT IDENTIFIER id-rsaEncryption (1.2.840.113549.1.1.1)
      06 09 2A 86 48 86 F7 0D 01 01 01
      // NULL (per RFC 3447 A.1)
      05 00
   // OCTET STRING (aka byte[]) (PrivateKey)
   04 81 F5
      [the previous value here,
       note the length here is F5 because of the tag and length bytes of the payload]

回填长度:
“b”系列是13(0x0D),因为它只包含预定长度的内容。
“a”系列现在是(2 + 1)+(2 + 13)+(3 + 0xF5)= 266(0x010A)。
30 82 01 0A  02 01 00 30  0D ...

现在你可以将它作为“私钥”进行PEM格式化。
加密它?那是完全不同的事情。

7
很遗憾这篇答案没有更多的赞,你为这个很好的回答付出了很多努力。 - TheCatWhisperer
1
我在 .net 5.0 的代码中发现了两个错误。第一个是它不允许我使用 ?? 运算符,第二个是除非我使用 Exportable 打开证书,否则我无法导出 privateBytes:var cert = new X509Certificate2(bytesEntrada, pass, X509KeyStorageFlags.Exportable); - SonMauri
能否使用.NET Core 5 PemEncoding.Write()与ASN.1 (ITU-T X.680) RSAPrivateKey (PKCS#1 / RFC3447)结构,即标签为"RSA PRIVATE KEY"的文件?似乎唯一的方法是使用PKCS#8。 - acarlon
1
@acarlon 是的。你需要传递RSAPrivateKey值(例如rsa.ExportRSAPrivateKey()),PEM编码器并不理解翻译,它只会按照你的指示执行。如果有什么问题,你应该提出问题而不是在这里留言。 - bartonjs

8
我找到了一个可行的解决方案。我在Windows系统中没有找到一个确切的示例来实现从证书存储到pem文件的转换。虽然这种方法可能不适用于某些证书,但如果您使用的是自己创建的证书(例如,如果您只需要在两个您控制的计算机之间提供安全性,而终端用户看不到),那么这是一种很好的返回pem / pk(类似Linux风格)的方法。
我利用了在http://www.bouncycastle.org/csharp/找到的工具。
X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
certStore.Open(OpenFlags.ReadOnly);

X509Certificate2 caCert = certStore.Certificates.Find(X509FindType.FindByThumbprint, "3C97BF2632ACAB5E35B48CB94927C4A7D20BBEBA", true)[0];


RSACryptoServiceProvider pkey = (RSACryptoServiceProvider)caCert.PrivateKey;


AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetRsaKeyPair(pkey);
using (TextWriter tw = new StreamWriter("C:\\private.pem"))
{
    PemWriter pw = new PemWriter(tw);
    pw.WriteObject(keyPair.Private);
    tw.Flush();
}

1

X509certificate2 -> 私钥、公钥和证书pem...我刚刚发现你可以用5或6行小白代码实现它!

有一个免费的包叫Chilkat(它有一些冷静的品牌)。它有一些非常直观的 证书类。这里有一些示例代码,说明如何创建自签名的PFX格式证书并将其导出到PEM!这样就可以将一个带有证书和关联公钥以及签署它的私钥的X509Certificate2实例导出为三个单独的Pem文件。其中一个是证书(包括公钥),一个是公钥,一个是私钥。非常容易(花了一周时间阅读才弄明白,哈哈)。然后看一下https://github.com/patrickpr/YAOG,这是一个很好的OpenSSL Windows Gui,用于查看/创建证书(如结果截图所示)。

using Chilkat;
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;


namespace CertPractice
{
static public class CertificateUtilityExample
{

    public static X509Certificate2 GenerateSelfSignedCertificate()
    {


        string secp256r1Oid = "1.2.840.10045.3.1.7";  //oid for prime256v1(7)  other identifier: secp256r1
        
        string subjectName = "Self-Signed-Cert-Example";

        var ecdsa = ECDsa.Create(ECCurve.CreateFromValue(secp256r1Oid));

        var certRequest = new CertificateRequest($"CN={subjectName}", ecdsa, HashAlgorithmName.SHA256);

        //add extensions to the request (just as an example)
        //add keyUsage
        certRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true));

        X509Certificate2 generatedCert = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddYears(10)); // generate the cert and sign!
//----------------end certificate generation, ie start here if you already have an X509Certificate2 instance----------------


        X509Certificate2 pfxGeneratedCert = new X509Certificate2(generatedCert.Export(X509ContentType.Pfx)); //has to be turned into pfx or Windows at least throws a security credentials not found during sslStream.connectAsClient or HttpClient request...

        Chilkat.Cert chilkatVersionOfPfxGeneratedCert = new Chilkat.Cert(); // now use Chilcat Cert to get pems
        chilkatVersionOfPfxGeneratedCert.LoadPfxData(generatedCert.Export(X509ContentType.Pfx), null); // export as binary pfx to load into a Chilkat Cert

        PrivateKey privateKey = chilkatVersionOfPfxGeneratedCert.ExportPrivateKey(); // get the private key
        privateKey.SavePemFile(@"filepath"); //save the private key to a pem file
        
        Chilkat.PublicKey publicKey = chilkatVersionOfPfxGeneratedCert.ExportPublicKey(); //get the public key
        publicKey.SavePemFile(true, @"filepath"); //save the public key

        chilkatVersionOfPfxGeneratedCert.ExportCertPemFile(@"filepath"); //save the public Cert to pem file
        


        return pfxGeneratedCert;


    }
}

Screen shot of output pem files


1

基于@bartonjs的知识(他的答案),我编写了一个小类,应该很容易使用。

现在也有一个完整的示例,无需使用外部dll / nuget包

我所做的唯一更改是:

  • 在创建X509Certificate2实例时,我必须将“X509KeyStorageFlags.Exportable”添加到StorageFlags中,以便方法“ExportPkcs8PrivateKey()”不会失败。

使用我的类,可以将Let's Encrypt证书从PFX格式转换为带有证书和私钥的PEM格式。

如何使用我的类

var certificateLogic = new CertificateLogic("fileName.pfx", "privateKeyOfPfx");
certificateLogic.LoadCertificate();
certificateLogic.GenerateSaveCertificatePem();
certificateLogic.GenereateSavePrivateKeyPem();

我的代码在那个类后面

public class CertificateLogic {

    private readonly FileInfo CertificateFile;
    private readonly SecureString CertificatePassword;
    public X509Certificate2 Certificate { get; private set; }

    public CertificateLogic(FileInfo certificationFile, string password) {
        if (!certificationFile.Exists) {
            throw new FileNotFoundException(certificationFile.FullName);
        }

        CertificateFile = certificationFile;
        CertificatePassword = ConvertPassword(password);
    }

    public CertificateLogic(string certificationFullFileName, string password) {
        var certificateFile = new FileInfo(certificationFullFileName);
        if (certificateFile == null || !certificateFile.Exists) {
            throw new FileNotFoundException(certificationFullFileName);
        }

        CertificateFile = certificateFile;
        CertificatePassword = ConvertPassword(password);
    }

    private static SecureString ConvertPassword(string password) {
        var secure = new SecureString();
        foreach (char c in password) {
            secure.AppendChar(c);
        }

        return secure;
    }

    public void LoadCertificate() {
        LoadCertificate(X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
    }

    public void LoadCertificate(X509KeyStorageFlags keyStorageFlags) {
        Certificate = new X509Certificate2(CertificateFile.FullName, CertificatePassword, keyStorageFlags);
    }

    public byte[] GenerateCertificatePem() {
        var certData = Certificate.RawData;
        var newPemData = PemEncoding.Write("CERTIFICATE", certData);

        return newPemData.Select(c => (byte)c).ToArray();
    }

    public byte[] GeneratePrivateKeyPem() {
        var privateCertKey = Certificate.GetRSAPrivateKey();
        var privateCertKeyBytes = privateCertKey.ExportPkcs8PrivateKey();

        char[] newPemData = PemEncoding.Write("PRIVATE KEY", privateCertKeyBytes);

        return newPemData.Select(c => (byte)c).ToArray();
    }

    public FileInfo GenerateSaveCertificatePem() {
        var newData = GenerateCertificatePem();

        var oldFile = Path.GetFileNameWithoutExtension(CertificateFile.FullName);
        var newCertPemFile = new FileInfo($@"{CertificateFile.DirectoryName}\{oldFile} Certificate.pem");
        return SaveNewCertificate(newCertPemFile, newData);
    }

    public FileInfo GenereateSavePrivateKeyPem() {
        var newData = GeneratePrivateKeyPem();

        var oldFile = Path.GetFileNameWithoutExtension(CertificateFile.FullName);
        var newPrivateKeyPemFile = new FileInfo($@"{CertificateFile.DirectoryName}\{oldFile} PrivateKey.pem");
        return SaveNewCertificate(newPrivateKeyPemFile, newData);
    }

    public FileInfo SaveNewCertificate(FileInfo newFile, byte[] data) {
        File.WriteAllBytes(newFile.FullName, data);

        return newFile;
    }
}

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