简体   繁体   English

从 VB.NET 替换远程 IIS 10 服务器上的 SSL 证书

[英]Replace SSL Certificate on Remote IIS 10 server from VB.NET

I'm attempting to automate the process of renewing my SSL certificates for a few different publicly accessible endpoints.我正在尝试为几个不同的可公开访问的端点自动更新我的 SSL 证书的过程。 I'm using Certify the Web 's Certify SSL/TLS Certificate Management to complete the CSR and SSL generation and validation via Let's Encrypt and Certify DNS .我正在使用Certify the WebCertify SSL/TLS Certificate Management通过Let's Encrypt and Certify DNS完成 CSR 和 SSL 的生成和验证。 This generates the .pfx files, which are then copied across the.network to a location where my main "daily processing" application can access them and try to install them.这会生成.pfx文件,然后将这些文件通过网络复制到我的主要“日常处理”应用程序可以访问它们并尝试安装它们的位置。 I've been able to successfully get that application to install the certificates to the remote servers' certificate stores, but I'm unable to get the IIS 10 bindings reconfigured on the sites to use the new certificates.我已经能够成功地让该应用程序将证书安装到远程服务器的证书存储区,但我无法在站点上重新配置 IIS 10 绑定以使用新证书。

For reference, here's the code for installing the certificate in the remote server's certificate store, which seems to work perfectly:作为参考,这里是在远程服务器的证书库中安装证书的代码,它似乎工作得很好:

Imports System.Security.Cryptography.X509Certificates
Imports System.Security.Permissions
Imports Microsoft.Web.Administration

Private Sub AddCertificateToRemoteStore(ByVal HostName As String, ByVal StoreName As String, ByVal Certificate As X509Certificate2)
    If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
        Throw New ArgumentNullException("HostName", "You must specify a server hostname")
    ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
        Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
    ElseIf Certificate Is Nothing Then
        Throw New ArgumentNullException("Certificate", "A valid X509Certificate2 object is required")
    Else
        Dim CertStorePath As String = String.Format("\\{0}\{1}", HostName, StoreName)
        Dim RootStorePath As String = String.Format("\\{0}\Root", HostName)
        Dim IntermediateStorePath As String = String.Format("\\{0}\CA", HostName)

        Using CertChain As New X509Chain
            If CertChain.Build(Certificate) Then
                Dim FindResults As X509Certificate2Collection
                Dim StorePermissions As New StorePermission(PermissionState.Unrestricted)

                With StorePermissions
                    .Flags = StorePermissionFlags.OpenStore Or StorePermissionFlags.AddToStore
                    .Assert()
                End With

                For C = 0 To CertChain.ChainElements.Count - 1
                    If C = 0 Then
                        Using CertificateStore As New X509Store(CertStorePath, StoreLocation.LocalMachine)
                            With CertificateStore
                                .Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
                                FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)

                                If FindResults.Count <= 0 Then
                                    .Add(CertChain.ChainElements(C).Certificate)
                                End If

                                FindResults.Clear()
                                .Close()
                            End With
                        End Using
                    ElseIf C = CertChain.ChainElements.Count - 1 Then
                        Using RootStore As New X509Store(RootStorePath, StoreLocation.LocalMachine)
                            With RootStore
                                .Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
                                FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)

                                If FindResults.Count <= 0 Then
                                    .Add(CertChain.ChainElements(C).Certificate)
                                End If

                                FindResults.Clear()
                                .Close()
                            End With
                        End Using
                    Else
                        Using IntermediateStore As New X509Store(IntermediateStorePath, StoreLocation.LocalMachine)
                            With IntermediateStore
                                .Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
                                FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)

                                If FindResults.Count <= 0 Then
                                    .Add(CertChain.ChainElements(C).Certificate)
                                End If

                                FindResults.Clear()
                                .Close()
                            End With
                        End Using
                    End If
                Next C
            End If
        End Using
    End If
End Sub

With the certificate successfully added to the store (I've verified that it's there through certlm.msc ), the next obvious step is to apply the new certificate to the existing IIS 10 site's bindings so it can actually be used for SSL/TLS communication.证书成功添加到商店后(我已经通过certlm.msc验证它在那里),下一个明显的步骤是将新证书应用于现有的 IIS 10 站点的绑定,以便它实际上可以用于 SSL/TLS 通信. Here's what I'm currently using to try to accomplish that with the Microsoft.Web.Administration namespace:这是我目前正在使用的尝试使用Microsoft.Web.Administration命名空间来完成此操作的方法:

Private Sub ApplyCertificateBinding(ByVal HostName As String, ByVal StoreName As String, ByVal ActiveCertificate As X509Certificate2)
    If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
        Throw New ArgumentNullException("HostName", "You must specify a server hostname")
    ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
        Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
    ElseIf ActiveCertificate Is Nothing Then
        Throw New ArgumentNullException("ActiveCertificate", "A valid X509Certificate2 object is required")
    Else
        Dim SSLSiteName As String = ActiveCertificate.GetNameInfo(X509NameType.DnsName, False)
        Dim HostSites As New List(Of Site)

        Using HostManager As ServerManager = ServerManager.OpenRemote(HostName)
            For Each Site As Site In HostManager.Sites
                If Site.Name = SSLSiteName Then
                    HostSites.Add(Site)
                Else
                    For Each Binding In Site.Bindings
                        If Binding.Host = SSLSiteName Then
                            HostSites.Add(Site)
                            Exit For
                        End If
                    Next Binding
                End If
            Next Site

            For Each Site As Site In HostSites
                For Each SiteBinding In Site.Bindings
                    If SiteBinding.Protocol = "https" Then
                        Dim NewBinding As Binding = Site.Bindings.CreateElement

                        NewBinding.CertificateStoreName = StoreName
                        NewBinding.Protocol = "https"
                        NewBinding.CertificateHash = ActiveCertificate.GetCertHash
                        NewBinding.BindingInformation = SiteBinding.BindingInformation

                        SiteBinding = NewBinding
                        HostManager.CommitChanges()
                    End If
                Next SiteBinding

                Site.Stop()
                
                'PROBABLY A BETTER WAY TO HANDLE THIS 
                Do While Site.State <> ObjectState.Stopped
                Loop

                Site.Start()

                'AND THIS
                Do While Site.State <> ObjectState.Started
                Loop
            Next Site
        End Using
    End If
End Sub

This code gets all the way through the process without error, but it doesn't seem to actually make the necessary changes for the site to start using the new certificate.这段代码在整个过程中都没有错误,但它似乎并没有真正对站点进行必要的更改以开始使用新证书。 I manually restarted/refreshed the site from the IIS interface on the host, but it still doesn't seem to take effect.我在宿主机的IIS界面手动重启/刷新了站点,但是好像还是没有生效。 I've checked both the binding settings in IIS and the site itself (browser) and confirmed that it's still using the "old" certificate.我检查了 IIS 中的绑定设置和站点本身(浏览器)并确认它仍在使用“旧”证书。

I've also tried to directly set the certificate hash of the SiteBinding object to the X509Certificate2.GetCertHash value, as well as assigning the SiteBinding object to the NewBinding object before trying to set the CertificateHash property as above.我还尝试将SiteBinding object 的证书 hash 直接设置为X509Certificate2.GetCertHash值,以及在尝试设置CertificateHash属性之前将NewBinding object 分配给SiteBinding object。 Unfortunately, both of these methods throw a NotSupportedException stating: The specified operation is not supported when a server name is specified.不幸的是,这两种方法都抛出一个NotSupportedException声明: The specified operation is not supported when a server name is specified.

Additionally, there are settings from the "live" SiteBinding object that can't be set on the NewBinding object (like the .Host property).此外,“实时” SiteBinding object 中的一些设置无法在NewBinding object 上设置(如.Host属性)。 All I really want to be able to do is to change the active certificate on that site and not muck around with any of the binding's other properties.我真正想要做的就是更改该站点上的活动证书,而不是乱用绑定的任何其他属性。 The wording of the exception seems to indicate that what I'm trying to do can't be done remotely (at least, not with the Microsoft.Web.Administration API), but I can't imagine that there isn't a way to accomplish this goal.异常的措辞似乎表明我正在尝试做的事情无法远程完成(至少,不能使用Microsoft.Web.Administration API),但我无法想象没有办法来实现这个目标。 I'm sure I'm simply missing/overlooking something here, but my Google-fu is failing me and I need to get this project functional as soon as possible.我确定我只是在这里遗漏/忽略了一些东西,但是我的 Google-fu 使我失望了,我需要尽快让这个项目发挥作用。

EDIT #1编辑#1

I added the Site.Stop() and Site.Start() methods to restart the site from code, but it didn't make any difference.我添加了Site.Stop()Site.Start()方法以从代码重新启动站点,但没有任何区别。 Plus, I'm sure there's probably a better way to implement that than what I've added to the code above.另外,我确信可能有比我在上面的代码中添加的更好的方法来实现它。

EDIT #2编辑#2

I've refactored some things to align with the suggestion from Joel Coehoorn in the comments.我重构了一些内容以与评论中 Joel Coehoorn 的建议保持一致。 The code above represents the current state but still produces the same result: No exception occurs, but I cannot get the bindings updated to use the new certificate, even though it's apparently added to the store.上面的代码表示当前的 state 但仍然产生相同的结果:没有发生异常,但我无法更新绑定以使用新证书,即使它显然已添加到商店中。

Just to triple-check, I went to the site's bindings in IIS and the new certificate does show up as available to apply to the site.只是为了三重检查,我去了 IIS 中的站点绑定,新证书确实显示为可用于该站点。 I know there's a lot of blur, but the one highlighted in blue is what I'm trying to apply to the binding:我知道有很多模糊之处,但以蓝色突出显示的是我要应用于绑定的内容: IIS 10 站点绑定对话框

EDIT #3编辑#3

After reading Conrado Clark's Developer Log entry titled Adding SSL Binding to a remote website using Microsoft.Web.Administration , I decided to try to add the NewBinding to IIS instead of just updating the existing one:在阅读 Conrado Clark 的开发者日志条目后,标题为使用 Microsoft.Web.Administration 添加 SSL 绑定到远程网站,我决定尝试将NewBinding添加到 IIS 而不是仅仅更新现有的:

    Site.Bindings.Add(NewBinding)
    Site.Bindings.Remove(SiteBinding)
    HostManager.CommitChanges()

This produced a different exception: Cannot add duplicate collection entry of type 'binding' with combined key attributes 'protocol, bindingInformation' respectively set to 'https, XXX.XXX.XXX.XXX:443:'这产生了一个不同的异常: Cannot add duplicate collection entry of type 'binding' with combined key attributes 'protocol, bindingInformation' respectively set to 'https, XXX.XXX.XXX.XXX:443:'

So, I tried removing the existing binding first:因此,我首先尝试删除现有绑定:

    Site.Bindings.Remove(SiteBinding)
    Site.Bindings.Add(NewBinding)
    HostManager.CommitChanges()

This time it made it through the first two steps ( Site.Bindings.Remove() and Site.Bindings.Add() ), but when it tried to execute HostManager.CommitChanges() , I got another "new" exception: A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)这次它完成了前两个步骤( Site.Bindings.Remove()Site.Bindings.Add() ),但是当它尝试执行HostManager.CommitChanges()时,我得到了另一个“新”异常: A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) . A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) Additionally, it "reset" the binding so there was no certificate installed on the site.此外,它“重置”了绑定,因此站点上没有安装证书。

Just to see what would happen, I tried to commit the Site.Bindings.Remove() before trying to add it back.只是为了看看会发生什么,我尝试在尝试将其添加回来之前提交Site.Bindings.Remove()

    Site.Bindings.Remove(SiteBinding)
    HostManager.CommitChanges()
    Site.Bindings.Add(NewBinding)
    HostManager.CommitChanges()

The initial commit seemed to work fine (and the binding disappeared completely from IIS), but when it went to add the new binding, I got this: The configuration object is read only, because it has been committed by a call to ServerManager.CommitChanges(). If write access is required, use ServerManager to get a new reference.初始提交似乎工作正常(并且绑定从 IIS 中完全消失),但是当它添加新绑定时,我得到了这个: The configuration object is read only, because it has been committed by a call to ServerManager.CommitChanges(). If write access is required, use ServerManager to get a new reference. The configuration object is read only, because it has been committed by a call to ServerManager.CommitChanges(). If write access is required, use ServerManager to get a new reference.

I manually recreated the binding ( thankfully I had taken a quick screenshot before I started messing with it ), but that last error has given me an idea for my next attempt.我手动重新创建了绑定(谢天谢地,在我开始弄乱它之前我已经快速截屏了),但是最后一个错误让我对下一次尝试有了一个想法。 I'm going to try to break the Add() and Remove() methods out to new methods where I can open new instances of the ServerManager object specifically for this purpose.我将尝试将Add()Remove()方法分解为新方法,在这些方法中我可以专门为此目的打开ServerManager object 的新实例。 I'll come back when I've had a chance to write/test that.当我有机会编写/测试它时,我会回来。

EDIT #4编辑#4

I tried the above and still ended up with an error stating that A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)我尝试了上面的方法,但仍然以错误告终,指出A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) . A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520) So, just to see if I could determine the cause of the problem, I went to IIS and manually tried to apply the new certificate.所以,为了看看我是否能确定问题的原因,我去了 IIS 并手动尝试申请新证书。 I got the SAME EXACT ERROR from IIS!我从 IIS 得到了同样的错误 ( Yes, I know... I probably should have checked that bit a long time ago, but here we are ) 是的,我知道......我可能早就应该检查一下,但我们到了

It looks like there's a problem with the certificate in the store.商店中的证书似乎有问题。 Digging around a little deeper, I found an old reference on the MSDN forums talking about this error being related to a missing private key.深入挖掘后,我在 MSDN 论坛上发现了一篇旧参考资料,其中提到此错误与丢失的私钥有关。 This makes it sound like I missed a step in the certificate installation process, so I guess I need to take another step back and figure out what's wrong with that method before proceeding.这听起来好像我错过了证书安装过程中的一个步骤,所以我想我需要再退一步,在继续之前弄清楚方法有什么问题。

a NotSupportedException stating: The specified operation is not supported when a server name is specified . NotSupportedException声明: The specified operation is not supported when a server name is specified

That's exactly what we should expect.这正是我们应该期待的。 Microsoft didn't develop that API to manage remote server's everything.微软没有开发 API 来管理远程服务器的一切。 As proof, you can see that even IIS Manager (built upon the same API) does not support managing server certificates of a remote machine.作为证明,您可以看到即使是 IIS Manager(基于相同的 API)也不支持管理远程机器的服务器证书。

If I were you, I will actually use other approaches, such as developing a dedicated small demon app to run on each IIS machines, so that actual communication via Microsoft.Web.Administration happens locally, not remotely.如果我是你,我实际上会使用其他方法,例如开发一个专用的小恶魔应用程序在每台 IIS 机器上运行,以便通过Microsoft.Web.Administration进行实际通信。管理发生在本地,而不是远程。

I've actually gotten this working!我实际上已经开始工作了! As identified in my multiple EDIT s to the OP, it seems the main problem actually had to do with the original import of the certificate.正如我在 OP 的多个EDIT中所确定的那样,似乎主要问题实际上与证书的原始导入有关。 Once that issue was resolved, everything else pretty much fell into place.一旦这个问题得到解决,其他一切都差不多就位了。 I've provided the full working code at the end if you want the TL;DR version, but here's what I found:如果你想要TL;DR版本,我在最后提供了完整的工作代码,但这是我发现的:

EXPLANATION/TROUBLESHOOTING解释/故障排除


After a bunch of further research into the individual errors I encountered in my initial testing - specifically the A specified logon session does not exist. It may already have been terminated.在对我在初始测试中遇到的个别错误进行了大量进一步研究之后——特别是A specified logon session does not exist. It may already have been terminated. A specified logon session does not exist. It may already have been terminated. error - I ran across this SO question: IIS 7 Error "A specified logon session does not exist. It may already have been terminated."错误 - 我遇到了这个 SO 问题: IIS 7 错误“指定的登录 session 不存在。它可能已经终止。” when using https .使用 https 时

In the linked answer from user naimadswdn , they state that:在用户naimadswdn的链接答案中,他们 state 认为:

I fixed it by running:我通过运行修复它:

certutil.exe -repairstore $CertificateStoreName $CertThumbPrint

where $CertificateStoreName is store name, and $CertThumbPrint is the thumbprint of imported certificate.其中$CertificateStoreName是存储名称, $CertThumbPrint是导入证书的指纹。

Another answer to that same question from user Ed Greaves provides a bit of explanation for the underlying cause of the problem:用户Ed Greaves对同一问题的另一个回答对问题的根本原因提供了一些解释:

This storage provider is a newer CNG provider and is not supported by IIS or .NET. You cannot access the key.此存储提供程序是较新的 CNG 提供程序,不受 IIS 或 .NET 的支持。您无法访问密钥。 Therefore you should use certutil.exe to install certificates in your scripts.因此,您应该使用 certutil.exe 在脚本中安装证书。 Importing using the Certificate Manager MMC snap-in or IIS also works but for scripting, use certutil as follows:使用 Certificate Manager MMC 管理单元或 IIS 导入也可以,但对于脚本,请使用 certutil,如下所示:

The original question was asked about trying to import the certificate directly in IIS and, since my testing showed that I wasn't able to do that either, I went ahead and tried to repair the certificate from the command line on the server with certutil .最初的问题是关于尝试直接在 IIS 中导入证书,由于我的测试表明我也无法做到这一点,因此我继续尝试使用certutil从服务器上的命令行修复证书。 I went back to IIS and, this time was able to successfully apply the certificate to the binding without error.又回到了IIS,这次成功的将证书应用到绑定上,没有报错。

Since that worked, I reset the binding and tried again to set it to the new certificate through my code.由于这样做有效,我重置了绑定并再次尝试通过我的代码将其设置为新证书。 No exception was encountered and, when I went to check the binding in IIS, it showed the correct certificate selected.没有遇到异常,当我去检查 IIS 中的绑定时,它显示选择了正确的证书。 I verified through my browser that it was showing the new certificate and everything seems to be working as expected/intended.我通过我的浏览器验证它显示了新证书并且一切似乎都按预期/预期工作。

Of course, I want this process automated, so I can't be logging in to the server to repair certificates every 90 days.当然,我希望这个过程自动化,所以我不能每 90 天登录一次服务器来修复证书。 So, now I have two options:所以,现在我有两个选择:

  1. Keep my existing code for adding the certificate to the store, then repair the certificate (as per naimadswdn 's answer), or保留我现有的用于将证书添加到商店的代码,然后修复证书(根据naimadswdn的回答),或者
  2. Take Ed Greaves ' suggestion and use certutil to perform the actual import of the certificate.采纳Ed Greaves的建议并使用certutil执行证书的实际导入。

Since I already have it importing the certificate into the store without any exception being thrown, I decided to go with the former solution ( for now, at least ).由于我已经将证书导入商店,没有任何异常被抛出,我决定使用前一个解决方案 go(至少现在)。 Of course, I want to do this remotely, so I've chosen to use WMI to execute certutil on the server ( see the CertUtilRepairCertificateInStore() method in the full code listing below ).当然,我想远程执行此操作,因此我选择使用 WMI 在服务器上执行certutil请参阅下面完整代码清单中的CertUtilRepairCertificateInStore()方法)。

(FINAL) TESTING (最终)测试

I reset the binding on the IIS site and deleted the certificate from the store to test my "new" process:我重置了 IIS 站点上的绑定并从商店中删除了证书以测试我的“新”过程:

  • The certificate was successfully added to the store by the AddCertificateToRemoteStore() method, but I paused execution before allowing it to "repair" the certificate in the store.证书已通过AddCertificateToRemoteStore()方法成功添加到存储中,但在允许它“修复”存储中的证书之前我暂停了执行。
  • While debugging was paused, I tried to manually apply the certificate to the binding in IIS. This resulted in the same logon session does not exist error.调试暂停时,我尝试手动将证书应用于 IIS 中的绑定。这导致了相同的logon session does not exist错误。
  • I allowed the CertUtilRepairCertificateInStore() method to run the certutil on the server through WMI.我允许CertUtilRepairCertificateInStore()方法通过 WMI 在服务器上运行certutil I didn't get any exceptions.我没有得到任何例外。
  • I paused debugging again after the certificate was "repaired" and tried to manually apply the certifiate to the binding in IIS. Now the binding was successfully updated to use the new certificate.证书“修复”后我再次暂停调试,并尝试手动将证书应用于 IIS 中的绑定。现在绑定已成功更新为使用新证书。
  • I manually reset the binding in IIS to use the old certificate and allowed my ApplyCertificateBinding() method to execute.我手动重置 IIS 中的绑定以使用旧证书并允许执行我的ApplyCertificateBinding()方法。

This time, the method completed without throwing any exceptions, so I went into IIS and verified that it does, indeed, have the new certificate applied to the appropriate binding.这一次,该方法完成且没有抛出任何异常,因此我进入 IIS 并验证它确实将新证书应用于适当的绑定。 As one last bit of verification, I went to my browser and checked the certificate from the site itself and it shows the correct new Let's Encrypt certificate.作为最后一点验证,我转到浏览器并检查了网站本身的证书,它显示了正确的新Let's Encrypt证书。 It seems that, along with some other minor tweaks along the way, the certutil -repairstore call was the final solution.似乎certutil -repairstore调用是最终的解决方案,再加上其他一些小的调整。

SOLUTION (CODE)解决方案(代码)


After all of that, I decided to keep my original code as-is and simply add the WMI bit to "repair" the certificate immediately after importing it to ensure it's ready to be applied to the binding.所有这一切之后,我决定保持原始代码不变,并在导入证书后立即添加 WMI 位以“修复”证书,以确保它已准备好应用于绑定。 Yes, I could allow that to be handled in a Try/Catch block for the binding, but I'd rather just avoid the issue altogether.是的,我可以允许在绑定的Try/Catch块中处理它,但我宁愿完全避免这个问题。 Here's a (mostly) complete listing of the functional code I'm using now and, so far, it seems to work exactly as I require/expect.这是我现在使用的功能代码的(大部分)完整列表,到目前为止,它似乎完全按照我的要求/期望工作。

( quick edit - I moved the HostManager.CommitChanges() execution outside of the For / Next loop through the HostSites list because once the commit is executed, the code can't make any more changes until a new connection is opened. ) 快速编辑- 我通过HostSites列表将HostManager.CommitChanges()执行移到了For / Next循环之外,因为一旦执行了提交,代码就无法再进行任何更改,直到打开新连接。

Imports System.Security.Cryptography.X509Certificates
Imports System.Security.Permissions
Imports Microsoft.Web.Administration

Friend Async Function InstallSSLCertificate(ByVal PFXFile As IO.FileInfo) As Task(Of X509Certificate2)
    If PFXFile Is Nothing Then
        Throw New ArgumentNullException("PFXFile", "You must provide a valid PFX certificate file")
    ElseIf Not PFXFile.Exists OrElse PFXFile.Length <= 0 Then
        Throw New ArgumentException("PFXFile", "You must provide a valid PFX certificate file")
    Else
        Dim CertPFX As X509Certificate2 = Nothing
        Dim CertSubject As String = String.Empty
        
        'THE GetOperationalCredentials() METHOD (not defined here) IS A CUSTOM UTILITY METHOD FOR RETRIEVING A SPECIFIC SET OF CREDENTIALS STORED ELSEWHERE
        Dim PFXCredentials As Net.NetworkCredential = GetOperationalCredentials(SecurityOperation.PFX)
        Dim UserCredentials As Net.NetworkCredential = GetOperationalCredentials(SecurityOperation.Server)

        If Not PFXCredentials Is Nothing Then
            'BUILD/EXTRACT THE X509Certificate2 OBJECT INFORMATION FROM THE PFX FILE
            'MAKE SURE TO SET THE X509KeyStorageFlags.MachineKeySet AND X509KeyStorageFlags.PersistKeySet FLAGS TO
            '  ENSURE THE CERTIFICATE PERSISTS IN THE STORE
            CertPFX = New X509Certificate2(PFXFile.FullName, PFXCredentials.SecurePassword, X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet)
            CertSubject = CertPFX.GetNameInfo(X509NameType.DnsName, False)

            If Not CertSubject Is Nothing AndAlso Not String.IsNullOrEmpty(CertSubject.Trim) Then
                If CertSubject.ToLower.Trim.StartsWith("ftps") Then
                    'THIS CONDITIONAL (not defined here) IS FOR HANDLING CERTIFICATE(S) THAT CANNOT BE INSTALLED FROM A CERTIFICATE STORE
                    If Not Await InstallSSLCertificateFromPEM(CertSubject, PFXFile, UserCredentials) Then
                        Return Nothing
                    End If
                Else
                    If Not InstallCertificateFromPFX(CertPFX, UserCredentials) Then
                        Return Nothing
                    End If
                End If
            Else
                Return Nothing
            End If
        Else
            Return Nothing
        End If

        Return CertPFX
    End If
End Function

Private Function InstallCertificateFromPFX(ByVal Certificate As X509Certificate2, ByVal Credentials As Net.NetworkCredential) As Boolean
    Dim Subject As String = Certificate.GetNameInfo(X509NameType.DnsName, False)
    Dim Hostname As String = String.Empty
    Dim StoreName As String = String.Empty

    If Subject.ToLower.Trim.StartsWith("www") Then
        Hostname = "<WEBSITE_SERVER_NAME>"
        StoreName = "WebHosting"
    ElseIf Subject.ToLower.Trim.StartsWith("rdp") Then
        Hostname = "<REMOTE_DESKTOP_SERVER_NAME>"
        StoreName = "My"
    End If

    Try
        AddCertificateToRemoteStore(Hostname, StoreName, Certificate, Credentials)
        ApplyCertificateBinding(Hostname, StoreName, Certificate)
        'THE CleanUpCertifcateStore() METHOD (not defined here) IS SOMETHING I INTEND TO
        '  IMPLEMENT TO GET RID OF OLD UNUSED/EXPIRED CERTIFICATES
        CleanUpCertifcateStore(Hostname, StoreName, Certificate)
        Return True
    Catch ex As Exception
        MessageBox.Show(ex.Message)
        Return False
    End Try
End Function

''' <summary>
''' Connect to the remote certificate store and import the details from a valid <see cref="X509Certificate2"/> object
''' </summary>
''' <param name="HostName">The hostname of the server where the certificate store is located</param>
''' <param name="StoreName">The name of the certificate store into which the certificate should be imported</param>
''' <param name="Certificate">A valid <see cref="X509Certificate2"/> object containing the details of the certificate to be imported</param>
''' <param name="Credentials">A valid <see cref="Net.NetworkCredential"/> object for passing to the certificate repair method for establishing a WMI connection</param>
Private Sub AddCertificateToRemoteStore(ByVal HostName As String, ByVal StoreName As String, ByVal Certificate As X509Certificate2, ByVal Credentials As Net.NetworkCredential)
    If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
        Throw New ArgumentNullException("HostName", "You must specify a server hostname")
    ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
        Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
    ElseIf Certificate Is Nothing Then
        Throw New ArgumentNullException("Certificate", "A valid X509Certificate2 object is required")
    Else
        'SET UP THE PATHS TO THE APPROPRIATE CERTIFICATE STORES
        Dim CertStorePath As String = String.Format("\\{0}\{1}", HostName, StoreName)
        Dim RootStorePath As String = String.Format("\\{0}\Root", HostName)
        Dim IntermediateStorePath As String = String.Format("\\{0}\CA", HostName)

        'USE THE X509Chain OBJECT TO MAKE IT EASIER TO IDENTIFY THE APPROPRIATE STORE FOR
        '  EACH CERTIFICATE EXTRACTED FROM THE PFX
        Using CertChain As New X509Chain
            If CertChain.Build(Certificate) Then
                Dim FindResults As X509Certificate2Collection
                Dim StorePermissions As New StorePermission(PermissionState.Unrestricted)

                With StorePermissions
                    .Flags = StorePermissionFlags.OpenStore Or StorePermissionFlags.AddToStore
                    .Assert()
                End With

                For C = 0 To CertChain.ChainElements.Count - 1
                    If C = 0 Then
                        'FIRST ELEMENT IN THE CHAIN = CERTIFICATE FOR THE SITE
                        Using CertificateStore As New X509Store(CertStorePath, StoreLocation.LocalMachine)
                            With CertificateStore
                                .Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
                                FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)

                                If FindResults.Count <= 0 Then
                                    .Add(CertChain.ChainElements(C).Certificate)
                                End If

                                FindResults.Clear()
                                .Close()
                            End With
                        End Using

                        'REPAIR THE CERTIFICATE'S PROVIDER/PRIVATE KEY IN THE REMOTE STORE
                        CertUtilRepairCertificateInStore(HostName, StoreName, CertChain.ChainElements(C).Certificate, Credentials)
                    ElseIf C = CertChain.ChainElements.Count - 1 Then
                        'LAST ELEMENT IN THE CHAIN = ROOT CA CERTIFICATE
                        Using RootStore As New X509Store(RootStorePath, StoreLocation.LocalMachine)
                            With RootStore
                                .Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
                                FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)

                                If FindResults.Count <= 0 Then
                                    .Add(CertChain.ChainElements(C).Certificate)
                                End If

                                FindResults.Clear()
                                .Close()
                            End With
                        End Using
                    Else
                        'ANY ELEMENT BETWEEN THE FIRST AND LAST IN THE CHAIN = INTERMEDIATE CA CERTIFICATE(S)
                        Using IntermediateStore As New X509Store(IntermediateStorePath, StoreLocation.LocalMachine)
                            With IntermediateStore
                                .Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
                                FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)

                                If FindResults.Count <= 0 Then
                                    .Add(CertChain.ChainElements(C).Certificate)
                                End If

                                FindResults.Clear()
                                .Close()
                            End With
                        End Using
                    End If
                Next C
            End If
        End Using
    End If
End Sub

''' <summary>
''' Use WMI to execute certutil.exe on the remote server to "repair" the certificate and correct issues with the provider/private key
''' </summary>
''' <param name="HostName">The hostname of the server where the certificate store is located</param>
''' <param name="StoreName">The name of the certificate store into which the certificate has been imported</param>
''' <param name="ActiveCertificate">A valid <see cref="X509Certificate2"/> object containing the details of the certificate to be repaired</param>
''' <param name="Credentials">A valid <see cref="Net.NetworkCredential"/> object for establishing the WMI connection</param>
Private Sub CertUtilRepairCertificateInStore(ByVal HostName As String, ByVal StoreName As String, ByVal ActiveCertificate As X509Certificate2, ByVal Credentials As Net.NetworkCredential)
    Dim WMIOptions As New Management.ConnectionOptions
    Dim WMIScope As Management.ManagementScope
    Dim GetOptions As New Management.ObjectGetOptions
    Dim WMIProcess As Management.ManagementClass
    Dim WMIParameters As Management.ManagementBaseObject

    With WMIOptions
        .Username = Credentials.Domain & "\" & Credentials.UserName
        .Password = Credentials.Password
        .Impersonation = Management.ImpersonationLevel.Impersonate
        .Authentication = Management.AuthenticationLevel.PacketPrivacy
        .EnablePrivileges = True
    End With

    WMIScope = New Management.ManagementScope(String.Format("\\{0}\root\cimv2", HostName), WMIOptions)
    WMIScope.Connect()

    WMIProcess = New Management.ManagementClass(WMIScope, New Management.ManagementPath("root\cimv2:Win32_Process"), GetOptions)

    WMIParameters = WMIProcess.GetMethodParameters("Create")
    WMIParameters("CommandLine") = String.Format("cmd.exe /c C:\Windows\System32\certutil.exe -repairstore {0} {1}", StoreName, ActiveCertificate.Thumbprint)

    WMIProcess.InvokeMethod("Create", WMIParameters, Nothing)
End Sub

''' <summary>
''' Connect to IIS on a remote host to apply a new certificate to a site's SSL bindings
''' </summary>
''' <param name="HostName">The hostname of the server where the certificate store is located</param>
''' <param name="StoreName">The name of the certificate store into which the certificate has been imported</param>
''' <param name="ActiveCertificate">A valid <see cref="X509Certificate2"/> object containing the details of the certificate that has been imported</param>
Private Sub ApplyCertificateBinding(ByVal HostName As String, ByVal StoreName As String, ByVal ActiveCertificate As X509Certificate2)
    If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
        Throw New ArgumentNullException("HostName", "You must specify a server hostname")
    ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
        Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
    ElseIf ActiveCertificate Is Nothing Then
        Throw New ArgumentNullException("ActiveCertificate", "A valid X509Certificate2 object is required")
    Else
        Dim SSLSiteName As String = ActiveCertificate.GetNameInfo(X509NameType.DnsName, False)
        Dim HostSites As New List(Of Site)

        Using HostManager As ServerManager = ServerManager.OpenRemote(HostName)
            'FIND THE SITE(S) IN IIS THAT MATCH(ES) THE DETAILS FROM THE SSL CERTIFICATE
            '>>> THIS IS **FAR FROM** BULLET-PROOF, BUT I AM NOT SURE HOW TO MAKE IT BETTER <<<
            For Each Site As Site In HostManager.Sites
                If Site.Name = SSLSiteName Then
                    HostSites.Add(Site)
                Else
                    For Each Binding In Site.Bindings
                        If Binding.Host = SSLSiteName Then
                            HostSites.Add(Site)
                            Exit For
                        End If
                    Next Binding
                End If
            Next Site

            For Each Site As Site In HostSites
                'USE THE .ToList() METHOD TO BASICALLY MAKE AN IN-MEMORY "COPY" OF THE
                '  BindingCollection OBJECT WHERE CHANGES CAN BE MADE WITHOUT MODIFYING THE
                '  COLLECTION ITSELF (which would cause the For/Next loop to generate an
                '  exception whenever any changes are made to the site's bindings)
                For Each SiteBinding In Site.Bindings.ToList
                    If SiteBinding.Protocol = "https" Then
                        If Not SiteBinding.CertificateHash.SequenceEqual(ActiveCertificate.GetCertHash) Then
                            'CANNOT JUST EDIT OR "REPLACE" AN EXISTING BINDING ON A REMOTE IIS HOST SO
                            '  CREATE A NEW BINDING TO ADD TO THE SITE AFTER THE EXISTING BINDING HAS
                            '  BEEN REMOVED
                            Dim NewBinding As Binding = Site.Bindings.CreateElement

                            NewBinding.CertificateStoreName = StoreName
                            NewBinding.Protocol = "https"
                            NewBinding.CertificateHash = ActiveCertificate.GetCertHash
                            NewBinding.BindingInformation = SiteBinding.BindingInformation

                            'THIS PROCESS MUST BE COMPLETED IN THIS ORDER
                            Site.Bindings.Remove(SiteBinding)
                            Site.Bindings.Add(NewBinding)
                        End If
                    End If
                Next SiteBinding

                'RESTARTING THE SITE IN IIS (there is almost certainly a better way to do this)
                Site.Stop()

                Do While Site.State <> ObjectState.Stopped
                Loop

                Site.Start()

                Do While Site.State <> ObjectState.Started
                Loop
            Next Site

            HostManager.CommitChanges()
        End Using
    End If
End Sub

I'm stepping into all of that from a method that looks for "pending" .pfx files that are waiting to be processd:我正在从一种寻找等待处理的“待定” .pfx文件的方法中进入所有这些:

Private Async Sub CheckForNewSSLCertificates()
    Dim PendingSSLFolder As DirectoryInfo = New DirectoryInfo("\\SERVER\Certify\PendingSSL\")

    For Each PFXFile As FileInfo In PendingSSLFolder.GetFiles("*.pfx")
        Dim CertPFX As Security.Cryptography.X509Certificates.X509Certificate2 = Await InstallSSLCertificate(PFXFile)
        
        If Not CertPFX Is Nothing Then
            '[ARCHIVE THE PFX]
        End If
    Next PFXFile
End Sub

I know the documentation is a bit sparse but, if you find ways to make this more effective/efficient, or if you have any questions about what it's doing here, please feel free to let me know.我知道文档有点稀疏,但是,如果您找到使它更有效/高效的方法,或者如果您对它在这里做什么有任何疑问,请随时告诉我。 Eventually I may try to just use certutil to import the PFX and one-shot that process, but for now I just wanted to leave this here for anyone else who's trying to implement some "centralized" automation for SSL certificate management.最终我可能会尝试只使用certutil来导入 PFX 并一次性完成该过程,但现在我只想将它留给任何其他试图为 SSL 证书管理实施“集中式”自动化的人。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM