【问题标题】:Export private/public keys from X509 certificate to PEM将 X509 证书中的私钥/公钥导出到 PEM
【发布时间】:2017-05-12 01:46:12
【问题描述】:

是否有任何便捷的方法可以使用 .NET Core 从 .p12 证书中以 PEM 格式导出私钥/公钥?不使用低级别的字节操作?我用谷歌搜索了几个小时,几乎没有任何东西在 .net 核心中可用,或者它没有在任何地方记录..

让我们有一个 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.com/dotnet/corefx/search?utf8=✓&q=pem&type= 知识隐藏在GitHub问题和代码中。如果您不能轻松地挖掘出来,您可以创建一个新问题并与开发该问题的人讨论。

标签: c# .net-core pem x509certificate2


【解决方案1】:

更新 (2021-01-12):对于 .NET 5,这很容易。 .NET Core 3.0 甚至可以实现大部分目标。最初的答案是在 .NET Core 1.1 是 .NET Core 的最新版本时编写的。它解释了这些新方法在幕后的作用。

.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[]) 可以根据需要将这些 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-ifier 的开头(尽管“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 是最新的选项):

答案介于“不”和“不是真的”之间。

我假设您不希望 p12 输出粘在 public.pub 和 private.key 的顶部。

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,后者封装了 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。

让我们使用pre-published 384-bit RSA key。

RFC 3447 说我们想要 Version=0。其他一切都来自结构。

// SEQUENCE (RSAPrivateKey)
30 xa [ya [za]]
   // INTEGER (Version=0)
   02 01
         00
   // INTEGER (modulus)
   // Since the most significant bit if 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
         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 (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。算法标识符可以在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。

加密它?那是完全不同的球赛。

【讨论】:

  • 很遗憾没有更多的赞成票,您为这个出色的答案付出了很多努力。
  • .net 5.0 的代码有 2 个错误。第一个,它不允许我使用 ??运算符和第二个,除非我使用 Exportable 打开证书,否则我无法导出 privateBytes: var cert = new X509Certificate2(bytesEntrada, pass, X509KeyStorageFlags.Exportable);
  • 是否可以将 .NET Core 5 PemEncoding.Write() 与 ASN.1 (ITU-T X.680) RSAPrivateKey (PKCS#1 / RFC3447) 结构一起使用 - 即带有文件的文件谁的标签是“RSA PRIVATE KEY”?看来唯一的办法就是PKCS#8。
  • @acarlon 是的。不过,由您决定传入 RSAPrivateKey 值(例如rsa.ExportRSAPrivateKey())。 PEM 编码器不理解翻译,它只是按照你说的去做。如果出现问题,您需要提出问题,而不是在此处使用 cmets。
【解决方案2】:

我想出了一个行之有效的解决方案。我找不到如何在 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();
}

【讨论】:

    【解决方案3】:

    X509certificate2 -> Private、Public 和 Cert pems...我刚刚发现你可以用 5 或 6 行外行代码来完成!

    有一个名为 Chilkat 的免费包(它有一些令人毛骨悚然的品牌)。它有一些非常直观的Certificate Classes 这是一些关于如何创建自签名 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;
    
    
        }
    }
    

    【讨论】:

      【解决方案4】:

      基于@bartonjs的知识(his answer),我写了一个小类,应该很好用。

      所以现在也有一个完整的例子,不用使用外部的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;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2011-01-29
        • 1970-01-01
        • 1970-01-01
        • 2014-09-27
        • 2019-12-06
        • 1970-01-01
        • 2013-01-06
        • 1970-01-01
        • 2016-10-07
        相关资源
        最近更新 更多