65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# Bouncy Castle 库生成证书

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (7投票s)

2019 年 4 月 1 日

CPOL

11分钟阅读

viewsIcon

34136

downloadIcon

2194

使用 .NET C# Bouncy Castle 库生成用于 Web 服务器和 CA 服务器的证书文件

引言

大约一年前,我和一位同事的一次谈话中,我们萌生了创建一个图形用户界面(GUI)应用程序的想法,该应用程序可以让我们为 Web 服务器生成证书请求文件,然后将其发送给外部或内部证书颁发机构(CA)来生成 Web 服务器证书,用于服务器身份验证和服务器与 Web 客户端之间的 Web 流量加密。
创建服务器证书的整个过程如下:

  • 生成了一个证书请求文件和一个包含私钥的文件,用于 Web 服务器。证书请求文件包含访问 Web 服务器的备用名称。
  • 之后,生成的请求文件将被发送给外部 CA(例如 GoDaddy)或内部 Active Directory (AD) CA 进行签名(生成公钥证书文件)。
  • CA 服务器会签署请求文件(生成公钥证书)并将其发送回发送者。
  • 签名后的请求文件将与先前生成的文件(包含私钥)合并,从而生成 Web 服务器证书,该证书用于身份验证 Web 服务器以及加密客户端和服务器之间的通信。

背景

对于上述过程,我们使用了一台安装了 OpenSSL 应用程序的 Linux 虚拟机(VM)。

由于 Internet Information Server (IIS) 用作 Windows 平台上的 Web 服务器,而 Windows 是我们开发软件的平台,因此我们得出结论,使用 Windows 操作系统 (OS) 而不是 Linux VM 来生成证书请求文件会更实用。首先,我们在 Windows 桌面计算机上安装了 OpenSSL。主要想法是创建一个 GUI,该 GUI 根据应用程序表单中输入的信息生成参数文件。然后,该参数文件将用作调用 *openssl.exe* 应用程序时的参数。之后,*openssl.exe* 应用程序将直接从 GUI 中调用,并带上必要的参数。然后,*openssl.exe* 应用程序将直接从 GUI 中调用,并带上必要的参数。

当从 CA 服务器获取签名文件后,想法是通过 GUI 导入该文件,并将其与上一步生成的私钥合并。

这就是要在 .NET 平台和 C# 中实现的应用程序的主要想法。

总的来说,我不喜欢从 GUI 调用外部命令行应用程序的想法,因为要检查证书请求文件的生成是否成功,只能通过解析 *oppenssl.exe* 命令行应用程序的输出消息来实现。

我当时想,最好是完全使用 C# 来实现整个应用程序,而无需调用 *openssl.exe* 外部命令行应用程序。由 *openssl.exe* 应用程序完成的部分工作必须直接用 C# 实现。

于是我开始在网上搜索,希望能找到实现这一目标的示例代码。起初,我找到了许多用 Java 编程语言编写的创建证书文件的代码示例。由于我以前用 Java 编程过,一种方法是分析 Java 代码并用 C# 编写相同的代码,但后来我意识到这简直是“西西弗斯的工作”。

我继续在网上搜索,找到了使用 Microsoft 原始证书处理函数的 C# 代码示例。这些示例实现起来似乎非常复杂,而且通常不包含或解释我需要的内容。

首要的要求是创建一个带有 Web 服务器备用名称的证书。

第一次发现

阅读了各种关于证书的文章一段时间后,我找到了文章“Bouncy Castle - Subject Alternative Names”,这篇文章准确地解释了我需要的内容——如何为带有备用名称的 Web 服务器创建证书。我阅读了该博客上所有与此主题相关的文章:“Using Bouncy Castle from .NET”,并第一次听说了“Bouncy Castle”项目。

Bouncy Castle”项目最初是作为 Java 代码创建的,后来被转换为 C#。它是一个很棒的库,有大量的优秀示例来说明如何使用该库中的代码。C# 源代码可以在 GitHub 位置 https://github.com/bcgit/bc-csharp 找到。

Using the Code

于是我开始开发应用程序。对于 GUI 的实现,我决定使用 MahApps.Metro 库,对于证书处理,我使用了 Bouncy Castle C# 库。

最初创建了一个带有两个菜单选项的应用程序。第一个菜单选项用于生成证书请求文件和包含私钥的文件。之后,证书请求文件将被发送到外部或内部 CA 服务器以生成签名证书文件(公钥文件)。
菜单的第二个选项是根据从 CA 服务器获取的签名证书文件和先前生成的私钥文件来生成包含公钥和私钥的证书文件。
为了测试生成的证书请求文件是否有效,我们使用安装了 CA 角色 (CA Roll) 的 Windows VM。每次生成证书请求文件后,都需要将其复制到安装了 CA 角色的 VM 中,在那里生成签名文件,然后再将文件返回到启动应用程序的计算机。然后将生成的文件与私钥文件合并,生成证书。然后将生成的证书导入本地证书存储,以检查生成的证书文件是否包含有效数据。

这是一个非常繁琐且复杂的过程。因此,我一直在思考如何简化测试生成请求文件的整个过程,并使验证过程更简单。在阅读上述博客文章“Bouncy Castle - Being a Certificate Authority”时,我发现 CA 服务器证书与其他用途(例如 Web 服务器身份验证和数据加密)的证书之间的区别只有一个参数。对于 CA 服务器参数,“基本约束”(Basic Constraints)扩展设置为 `true`,而对于其他证书,此值设置为 `false`。

因此,我产生了创建根 CA 服务器证书的想法,该证书将签署生成的请求文件,从而加速验证生成证书请求文件的过程。此外,有必要实现一个新的菜单选项,用于使用我们之前生成的根 CA 文件签署请求文件。这个额外的菜单选项将加速测试和生成 Web 服务器证书的整个过程。

代码

因此,在生成证书请求的表单中添加了以下选项:

  1. 用于证书的备用名称
    string[] subjectAlternativeNames = new string[alternativSubjectNames.Count];
    int i = 0;
    foreach (var item in alternativSubjectNames)
    {
      subjectAlternativeNames[i++] = item.AlternativSubjectName;
    }
    GeneralNames names = new GeneralNames(subjectAlternativeNames.Select(n => 
                                          new GeneralName(GeneralName.DnsName, n)).ToArray());
    Asn1OctetString asn1ost = new DerOctetString(names);
    extensions.Add(X509Extensions.SubjectAlternativeName, 
                   new Org.BouncyCastle.Asn1.X509.X509Extension(false, asn1ost));
  2. 决定是否创建证书请求文件是请求服务器成为 CA 的参数
    var extensions = new Dictionary<DerObjectIdentifier, Org.BouncyCastle.Asn1.X509.X509Extension>();
    extensions.Add(X509Extensions.BasicConstraints,
                    new Org.BouncyCastle.Asn1.X509.X509Extension(true, asn1ost0));

在申请证书时,可以选择以下“选择密钥用法”参数:

KeyUsageCCBData = new ObservableCollection<KeyUsageData>();
KeyUsageCCBData.Add(new KeyUsageData("DigitalSignature", KeyUsage.DigitalSignature));
KeyUsageCCBData.Add(new KeyUsageData("NonRepudiation", KeyUsage.NonRepudiation));
KeyUsageCCBData.Add(new KeyUsageData("KeyEncipherment", KeyUsage.KeyEncipherment));
KeyUsageCCBData.Add(new KeyUsageData("DataEncipherment", KeyUsage.DataEncipherment));
KeyUsageCCBData.Add(new KeyUsageData("KeyAgreement", KeyUsage.KeyAgreement));
KeyUsageCCBData.Add(new KeyUsageData("KeyCertSign", KeyUsage.KeyCertSign));
KeyUsageCCBData.Add(new KeyUsageData("CrlSign", KeyUsage.CrlSign));
KeyUsageCCBData.Add(new KeyUsageData("EncipherOnly", KeyUsage.EncipherOnly));
KeyUsageCCBData.Add(new KeyUsageData("DecipherOnly", KeyUsage.DecipherOnly));

并选择以下“选择扩展密钥用法”参数:

ExtendedKeyUsageCCBData = new ObservableCollection<ExtendedKeyUsageData>();
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData
   ("AnyExtendedKeyUsage", KeyPurposeID.AnyExtendedKeyUsage));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData
   ("ServerAuthetification", KeyPurposeID.IdKPServerAuth));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData
   ("ClientAuthetification", KeyPurposeID.IdKPClientAuth));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData("CodeSigning", KeyPurposeID.IdKPCodeSigning));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData
   ("PEmailProtection", KeyPurposeID.IdKPEmailProtection));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData
   ("IpsecEndSystem", KeyPurposeID.IdKPIpsecEndSystem));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData("IpsecTunnel", KeyPurposeID.IdKPIpsecTunnel));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData("IpsecUser", KeyPurposeID.IdKPIpsecUser));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData("TimeStamping", KeyPurposeID.IdKPTimeStamping));
ExtendedKeyUsageCCBData.Add(new ExtendedKeyUsageData("OcspSigning", KeyPurposeID.IdKPOcspSigning));

Certificate request file

此证书是否为 CA 证书”参数的想法是为了能够生成一个证书请求文件,该文件将作为 CA 服务器链中的下一个 CA 权威。Windows 平台上的典型 CA 服务器配置包含 2 或 3 个级别的 CA 服务器。第一个服务器安装为根 CA(主 CA)服务器,它是一个工作组计算机,下一个或两个 CA 服务器是 Windows AD 域成员计算机,实际负责颁发和管理证书。第一个根 CA 服务器通常安装为 VM,仅在需要发布已撤销证书列表时才开启,而下一个或两个服务器则始终在线。

第一个根 CA 服务器证书始终实现为自签名证书,而证书链中的每个后续 CA 证书都由更高级别的证书签名,这意味着存在以下证书结构:

II level CA certificate: Root CA + intermediate CA
  masterCA (self-sign certificate)
    intermediateCA (certificate signed with masterCA certificate)

III level CA certificate: Root CA + intermediate CA + issuer CA
  masterCA (self-sign certificate)
    intermediateCA (certificate signed with masterCA certificate)
      issuerCA (certificate signed with intermediateCA certificate)

为了在 Windows 平台上实现“II 级 CA 证书”的 CA 基础结构,我们需要 3 台 VM(根 CA-> 工作组 VM,DC - AD 域控制器 VM,中间 CA-> 安装了 CA 角色的 AD 计算机)或者对于“III 级 CA 证书”需要 4 台 VM(根 CA-> 工作组 VM,DC - AD 域控制器 VM,中间 CA-> 安装了 CA 角色的 AD 计算机,颁发 CA-> 安装了 CA 角色的 AD 计算机)。

注意

Microsoft 不建议在安装了 DC 角色的 AD 计算机上安装 CA 角色。因此,有必要将 DC 角色计算机作为单独的 VM。

由于为中间 CA 和颁发 CA 服务器生成证书很复杂(您需要使用整个过程来生成单独的请求文件,然后单独为每个 CA 服务器签名和颁发证书),所以我添加了一个额外的菜单选项。

应用程序的第五个菜单选项可用于生成 I、II 或 III 级 CA 证书,这些证书随后可用于签名包含它们签名链的生成的证书请求文件。

通过应用程序为 CA 服务器生成的 CA 服务器文件在技术上与从安装了 CA 角色的 VM 中使用的证书没有区别。将包含 CA 证书链(用于签名请求文件的 *.pfx* 扩展名的文件)的证书导入 Web 服务器后,CA 证书会自动存储在 Web 服务器计算机上的相应证书存储中。为了让客户端的这部分能够正常工作,有必要将 CA 证书链导入到访问 Web 服务器的客户端计算机上的相应证书存储中。

代码中最有趣的部分是基于生成请求文件中包含的数据来颁发证书的部分。我需要从文件中读取数据,然后基于这些数据生成签名证书文件。

//
// Add certificate extensions
//
Asn1Set attributes = cerRequest.GetCertificationRequestInfo().Attributes;
if (attributes != null)
{
  for (int i = 0; i != attributes.Count; i++)
  {
    AttributePkcs attr = AttributePkcs.GetInstance(attributes[i]);
    if (attr.AttrType.Equals(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest))
    {
       X509Extensions extensions1 = X509Extensions.GetInstance(attr.AttrValues[0]);
       foreach (DerObjectIdentifier oid in extensions1.ExtensionOids)
       {
         Org.BouncyCastle.Asn1.X509.X509Extension ext = extensions1.GetExtension(oid);
         certGen.AddExtension(oid, ext.IsCritical, ext.GetParsedValue());
       }
     }
  }
}

为了在创建存储私钥的文件时提供额外的安全性,我添加了输入密码的功能,该密码将用于加密该文件的内容。

var textWriter = new StringWriter();
var pemWriter = new PemWriter(textWriter);
if (!String.IsNullOrEmpty(password))
{
  pemWriter.WriteObject(subjectKeyPair.Private, "DESEDE", password.ToCharArray(), new SecureRandom());
}
else
{
  pemWriter.WriteObject(subjectKeyPair.Private);
}

pemWriter.Writer.Flush();
string privateKeyPem = textWriter.ToString();
using (var writer = new StreamWriter(outputPrivateKeyName))
{
  writer.WriteLine(privateKeyPem);
}

如果私钥文件的内容是用密码加密的,那么在读取该文件中的数据时,就需要使用相应的密码。

static AsymmetricKeyParameter ReadPrivateKey(string privateKeyFileName, string password=null)
{
   AsymmetricCipherKeyPair keyPair;
   if (password == null)
   {
      using (var reader = File.OpenText(privateKeyFileName))
       keyPair=(AsymmetricCipherKeyPair)new PemReader(reader).ReadObject();
   }
   else
   {
     using (var reader = File.OpenText(privateKeyFileName))
        keyPair=(AsymmetricCipherKeyPair) new PemReader(reader, 
                                                    new PasswordFinder(password)).ReadObject();
   }
   return keyPair.Private;
}

生成的 *.pfx* 扩展名的证书文件包含在签名请求证书文件时使用的 CA 证书链。在 Windows 平台上,双击此生成的文件将打开证书导入向导,该向导会自动将 *.pfx* 文件中包含的所有证书存储到相应的证书存储中。

Import wizard

如何使用?

GitHub 上的 GenCert 项目 的二进制发行版中,您可以在“GenCert”文件中找到应用程序用户手册,并在“GenCert steps”文件中找到有关如何生成证书的分步说明。

启动应用程序后,激活第一个菜单选项“创建请求”。使用适当的数据填写表单,然后按“生成”按钮。之后,按“继续”按钮,并在每个打开的表单上重复此过程。

结论

此应用程序具有教育和实际用途。

  • 教育用途 - 了解 CA 服务器证书的生成过程以及 CA 服务器为不同目的颁发证书的过程。
  • 实际用途 - 创建证书请求文件,将其发送给内部或外部 CA 服务器以生成证书公钥,并创建包含公钥和私钥的证书(*.pfx* 扩展名的文件)。

如果证书用于内部目的,也可以使用应用程序本身生成的 CA 证书。生成的 Web 服务器证书需要安装在 Web 服务器计算机上,并且签署 Web 服务器证书的 CA 服务器的证书需要安装在访问 Web 服务器的客户端计算机上。

在功能和技术上,使用此应用程序生成的 CA 证书与外部或内部 CA 服务器生成的证书没有任何区别。

© . All rights reserved.