将生成的证书添加到存储并更新 IIS 站点绑定

Fiz*_*izz 6 c# iis ssl bouncycastle x509certificate2

我遇到了以下问题,在感觉我已经用尽了对 Google 和 Stack Overflow 的各种研究途径之后,我决定就它提出我自己的问题。

我正在尝试根据我已经拥有并拥有的 CA 证书生成个人证书(使用 BouncyCastle)。生成证书后,将其放在“我的”存储中,然后我尝试更新我的 IIS 网站的 SSL 绑定以使用此新证书。

我注意到的是,对 IIS 网站(使用ServerManager)的更新没有抛出异常,但是当我转到 IIS 管理器控制台时,我注意到网站的绑定没有选择 SSL 证书。当我尝试选择我创建的证书时(显示为一个可行的选项),我收到以下错误消息:

A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)

作为测试,我导出了生成的证书(使用私钥)并通过向导重新安装它,然后再次尝试设置绑定(通过 IIS 管理器),该绑定有效。

由于这种行为,我认为这是我如何生成证书或将证书添加到商店的问题。我希望有人可能对我遇到的问题有所了解。以下是用于创建证书、将其添加到商店以及以编程方式更新网站绑定的相关功能(我相信):

主要功能生成获取CA证书私钥,生成个人自签名证书,更新sites绑定:

public static bool GenerateServerCertificate(
    X509Certificate2 CACert, 
    bool addToStore,
    DateTime validUntil)
{
    try
    {
        if (CACert.PrivateKey == null)
        {
            throw new CryptoException("Authority certificate has no private key");
        }

        var key = DotNetUtilities.GetKeyPair(CACert.PrivateKey).Private;

        byte[] certHash = GenerateCertificateBasedOnCAPrivateKey(
            addToStore,
            key,
            validUntil);

        using (ServerManager manager = new ServerManager())
        {
            Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();

            if (site == null)
            {
                return false;
            }

            foreach (Binding binding in site.Bindings)
            {
                if (binding.Protocol == "https")
                {
                    binding.CertificateHash = certHash;
                    binding.CertificateStoreName = "MY";
                }
            }

            manager.CommitChanges();
        }
    }
    catch(Exception ex)
    {
        LOG.Error("Error generating certitifcate", ex);
        return false;
    }

    return true;
}
Run Code Online (Sandbox Code Playgroud)

基于CA私钥生成证书:

public static byte[] GenerateCertificateBasedOnCAPrivateKey(
    bool addToStore,
    AsymmetricKeyParameter issuerPrivKey,
    DateTime validUntil,
    int keyStrength = 2048)
{
    string subjectName = $"CN={CertSubjectName}";

    // Generating Random Numbers
    CryptoApiRandomGenerator randomGenerator = new CryptoApiRandomGenerator();
    SecureRandom random = new SecureRandom(randomGenerator);
    ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivKey, random);

    // The Certificate Generator
    X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
    certificateGenerator.AddExtension(
        X509Extensions.ExtendedKeyUsage, 
        true, 
        new ExtendedKeyUsage((new List<DerObjectIdentifier> { new DerObjectIdentifier("1.3.6.1.5.5.7.3.1") })));

    // Serial Number
    BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
    certificateGenerator.SetSerialNumber(serialNumber);

    // Issuer and Subject Name            
    X509Name subjectDN = new X509Name(subjectName);
    X509Name issuerDN = new X509Name(CACertificateName);
    certificateGenerator.SetIssuerDN(issuerDN);
    certificateGenerator.SetSubjectDN(subjectDN);

    // Valid For
    DateTime notBefore = DateTime.UtcNow.Date;
    DateTime notAfter = validUntil > notBefore ? validUntil : notBefore.AddYears(1);

    certificateGenerator.SetNotBefore(notBefore);
    certificateGenerator.SetNotAfter(notAfter);

    // Subject Public Key
    AsymmetricCipherKeyPair subjectKeyPair;
    var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
    var keyPairGenerator = new RsaKeyPairGenerator();
    keyPairGenerator.Init(keyGenerationParameters);
    subjectKeyPair = keyPairGenerator.GenerateKeyPair();

    certificateGenerator.SetPublicKey(subjectKeyPair.Public);

    // Generating the Certificate
    Org.BouncyCastle.X509.X509Certificate certificate = certificateGenerator.Generate(signatureFactory);

    // correcponding private key
    PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);

    // merge into X509Certificate2
    X509Certificate2 x509 = new X509Certificate2(certificate.GetEncoded());

    Asn1Sequence seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
    if (seq.Count != 9)
    {
        throw new PemException("Malformed sequence in RSA private key");
    }

    RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(seq);
    RsaPrivateCrtKeyParameters rsaparams = new RsaPrivateCrtKeyParameters(
        rsa.Modulus, 
        rsa.PublicExponent, 
        rsa.PrivateExponent, 
        rsa.Prime1,
        rsa.Prime2, 
        rsa.Exponent1, 
        rsa.Exponent2,
        rsa.Coefficient);

    x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);

    if (addToStore)
    {
        // Add certificate to the Personal store
        AddCertToStore(x509, StoreName.My, StoreLocation.LocalMachine, "Certificate Friendly Name");
    }

    return x509.GetCertHash();
}
Run Code Online (Sandbox Code Playgroud)

将证书添加到商店:

private static void AddCertToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, string friendlyName)
{
    X509Store store = new X509Store(storeName, storeLocation);

    try
    {
        store.Open(OpenFlags.ReadWrite);
        store.Add(cert);

        if (!string.IsNullOrWhiteSpace(friendlyName)) {
            var certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, cert.Subject, true);
            if (certs.Count > 0)
            {
                certs[0].FriendlyName = friendlyName;
            }
        }
    }
    finally
    {
        store.Close();
    }
}
Run Code Online (Sandbox Code Playgroud)

只是最后一点,我从我在各种网站上看到的关于该错误的内容中尝试了一些事情(似乎不太清楚问题是什么):

  • 这适用于不同的机器(我的个人开发机器),但我在服务器机器(运行 Windows Server 2012 R2)上遇到了这些障碍
  • IIS 帮助对话框通知我机器正在运行 IIS 8.5
  • 使用 CertUtil.exe 验证生成的证书和 CA 证书的有效性
  • 验证生成的证书和 CA 证书有一个可以找到的私钥
  • 经过验证的管理员(最终甚至是我登录的帐户)可以访问 CA 证书和生成的证书的私钥文件所在的位置。

任何想法我的问题可能是什么?


更新:

通过执行以下操作,我能够得到一些结果:

通过执行以编程方式将我的证书导出到文件 File.WriteAllBytes(filePath, cert.Export(X509ContentType.Pkcs12, password));

然后我通过执行以下操作将此证书文件导入商店:

var cert = new X509Certificate2(certFilePath, certPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);

// My original AddCertToStore function
AddCertToStore(cert, StoreName.My, StoreLocation.LocalMachine, "Friendly Name"); 
Run Code Online (Sandbox Code Playgroud)

最后,我像之前一样设置了绑定:

using (ServerManager manager = new ServerManager())
{
    Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();

    if (site == null)
    {
        return false;
    }

    foreach (Binding binding in site.Bindings)
    {
        if (binding.Protocol == "https")
        {
            binding.CertificateHash = certHash;
            binding.CertificateStoreName = "MY";
        }
    }

    manager.CommitChanges();
 }
Run Code Online (Sandbox Code Playgroud)

这样做是可行的,但我不明白为什么要将证书导出到文件,然后将其加载到 X509Certificate2 对象中,添加到存储中,最后设置绑定。

bar*_*njs 3

ToRSA方法很可能会创建一个临时 RSA 密钥,因此当引用全部消失时,该密钥将被删除。将临时结构导出到 PFX,然后使用 PersistKeySet 重新导入它是将其转换为持久密钥的一种方法。还存在其他一些,但其中一个是不太复杂的之一。

不过,您实际上不必将其写入文件。

byte[] pkcs12Blob = cert.Export(X509ContentType.Pkcs12, password);
ver certWithPersistedKey = new X509Certificate2(pkcs12Blob, password, allTheFlagsYouAlreadySet);
Run Code Online (Sandbox Code Playgroud)

还有其他微妙之处,例如设置 PrivateKey 属性对于从存储加载的证书实例和从字节加载的证书实例具有不同的行为...... PFX/PKCS#12 导出/导入围绕所有这些进行工作。