Replace SSL Certificate on Remote IIS 10 server from VB.NET - vb.net

I'm attempting to automate the process of renewing my SSL certificates for a few different publicly accessible endpoints. 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. 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. 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.
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. Here's what I'm currently using to try to accomplish that with the Microsoft.Web.Administration namespace:
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. I've checked both the binding settings in IIS and the site itself (browser) and confirmed that it's still using the "old" certificate.
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. Unfortunately, both of these methods throw a NotSupportedException stating: 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). 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. 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.
EDIT #1
I added the Site.Stop() and Site.Start() methods to restart the site from code, but it didn't make any difference. Plus, I'm sure there's probably a better way to implement that than what I've added to the code above.
EDIT #2
I've refactored some things to align with the suggestion from Joel Coehoorn in the comments. 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.
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. I know there's a lot of blur, but the one highlighted in blue is what I'm trying to apply to the binding:
EDIT #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:
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:'
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). 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(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.
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. I'll come back when I've had a chance to write/test that.
EDIT #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). 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. I got the SAME EXACT ERROR from 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. 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.
That's exactly what we should expect. Microsoft didn't develop that API to manage remote server's everything. As proof, you can see that even IIS Manager (built upon the same API) does not support managing server certificates of a remote machine.
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.

I've actually gotten this working! As identified in my multiple EDITs to the OP, it seems the main problem actually had to do with the original import of the certificate. 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:
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. error - I ran across this SO question: IIS 7 Error "A specified logon session does not exist. It may already have been terminated." when using https.
In the linked answer from user naimadswdn, they state that:
I fixed it by running:
certutil.exe -repairstore $CertificateStoreName $CertThumbPrint
where $CertificateStoreName is store name, and $CertThumbPrint is the thumbprint of imported certificate.
Another answer to that same question from user Ed Greaves provides a bit of explanation for the underlying cause of the problem:
This storage provider is a newer CNG provider and is not supported by IIS or .NET. You cannot access the key. Therefore you should use certutil.exe to install certificates in your scripts. Importing using the Certificate Manager MMC snap-in or IIS also works but for scripting, use certutil as follows:
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. I went back to IIS and, this time was able to successfully apply the certificate to the binding without error.
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. 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. So, now I have two options:
Keep my existing code for adding the certificate to the store, then repair the certificate (as per naimadswdn's answer), or
Take Ed Greaves' suggestion and use certutil to perform the actual import of the certificate.
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). 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).
(FINAL) TESTING
I reset the binding on the IIS site and deleted the certificate from the store to test my "new" process:
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.
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.
I allowed the CertUtilRepairCertificateInStore() method to run the certutil on the server through WMI. 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.
I manually reset the binding in IIS to use the old certificate and allowed my ApplyCertificateBinding() method to execute.
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. 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. It seems that, along with some other minor tweaks along the way, the certutil -repairstore call was the final solution.
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. 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. 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.)
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:
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.

Related

EWS Connection to Office365 fails - 401 Unauthorized

I need to make a VB .net tool to read email and save attachments. My company recently migrated from on-premise Exchange to Office 365. I have been reading EWS tutorials for 2 days now, and searching StackOverflow, but cannot get past the most basic step of authorized access to the O365 mailbox.
I took the original code from this article: htp://www.c-sharpcorner.com/UploadFile/jj12345678910/reading-email-and-attachment-from-microsoft-exchange-server/. I had some trouble converting it to VB using the Telerik converter but I think I have it right. Each time I try using the FindItemsResults method it halts with "(401) Unauthorized."
The instructions for asking a question state that I should include links to what I have already found and why it did not work, but my SO reputation only allows me 2 links. Here is what I have tried:
I have tried every possible usercode and domain combination I can think after reading this page: htps://stackoverflow.com/questions/10107872/ews-connections-issues-401-unauthorized
I am trying to read my own mailbox, so this one does not help: htps://stackoverflow.com/questions/43346498/401-unauthorized-access-when-using-ews-to-connect-to-mailbox
My connection looks identical to the connection on this page, but using it in my project did not get past the same Unauthorized error as before:htps://stackoverflow.com/questions/29009295/ews-managed-api-retrieving-e-mails-from-office365-exchange-server
Here is my code:
Public Class Form1
Public Exchange As ExchangeService
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
lstMsg.Clear()
lstMsg.View = View.Details
lstMsg.Columns.Add("Date", 150)
lstMsg.Columns.Add("From", 250)
lstMsg.Columns.Add("Subject", 400)
lstMsg.Columns.Add("Has Attachment", 50)
lstMsg.Columns.Add("Id", 100)
lstMsg.FullRowSelect = True
End Sub
Public Sub ConnectToExchangeServer()
Me.lblMsg.Text = "Connecting to Exchange Server"
lblMsg.Refresh()
Try
'Exchange = New ExchangeService(ExchangeVersion.Exchange2013)
Exchange = New ExchangeService()
Exchange.TraceEnabled = True
'Exchange.UseDefaultCredentials = True
Exchange.Credentials = New WebCredentials("DoeJohn", "mypasswd", "mycorp.com")
'Exchange.AutodiscoverUrl("DoeJohn#mycorp.mail.onmicrosoft.com", AddressOf MyRedirectionURLValidationCallback)
'Exchange.AutodiscoverUrl("John.Doe#mycorp.com", AddressOf MyRedirectionURLValidationCallback)
Exchange.Url = New System.Uri("https://outlook.office365.com/ews/exchange.asmx")
lblMsg.Text = "Connected to Exchange Server"
lblMsg.Refresh()
Catch ex As Exception
MsgBox("Fatal Error in Connect: " & ex.Message)
End
End Try
End Sub
Public Function MyRedirectionURLValidationCallback(RedirectionURL As String) As Boolean
Dim Result As Boolean = False
Dim RedirectionURI As Uri = New Uri(RedirectionURL)
If RedirectionURI.Scheme = "https" Then
Return True
End If
Return False
End Function
Private Sub btnRead_Click(sender As Object, e As EventArgs) Handles btnRead.Click
Call ConnectToExchangeServer()
Dim ts As TimeSpan = New TimeSpan(0, -1, 0, 0)
Dim MyDate As DateTime = DateTime.Now.Add(ts)
Dim MyFilter As SearchFilter.IsGreaterThanOrEqualTo = New SearchFilter.IsGreaterThanOrEqualTo(ItemSchema.DateTimeReceived, MyDate)
If Exchange IsNot Nothing Then
Dim FindResults As FindItemsResults(Of Item) =
Exchange.FindItems(WellKnownFolderName.Inbox, MyFilter, New ItemView(50))
Dim NewRow As ListViewItem
For Each MyItem As Item In FindResults
Dim Message As EmailMessage = EmailMessage.Bind(Exchange, MyItem.Id)
NewRow = New ListViewItem(Message.DateTimeReceived.ToString())
NewRow.SubItems.Add(Message.From.Name.ToString())
NewRow.SubItems.Add(Message.Subject)
NewRow.SubItems.Add(Message.HasAttachments.ToString())
NewRow.SubItems.Add(Message.Id.ToString())
lstMsg.Items.Add(NewRow)
Next
Else
End If
End Sub
I have confirmed that AutoDiscover is correctly finding the server, comparing to Test Email AutoConfiguration in Outlook.
AutoConfig
An interesting sidenote -- after my company moved to Office 365, I noticed that I have two new SMTP mail addresses. If I open Outlook properties on myself, I see this:
Props
This means someone can send mail to me now either at the old address john.doe#mycorp.com, and also now at doejohn#mycorp.mail.onmicrosoft.com. The new address is based on my domain usercode. I tested the microsoft one from a gmail account and it works.
To sum up, here are my questions:
1. Why am I getting the (401) Unauthorized errors when I try to read my Inbox?
2. Does Office 365 expect me to use my domain account or my mailbox name for user credentials?
3. In the domain part of the WebCredentials statement, do I use my company's mycorp.com or instead do I use Office 365's domain outlook.office365.com?
If you have read this far, many thanks!
I found the answer to the problem above, so I'll share here in case anyone needs it. The problem is down to security protocols added by my IT organization that were not documented because of recent phishing attacks on our company.
The first clue was that IT stated if I want to check company email on my personal 4G device like smartphone or ipad, I must also install Microsoft InTune for MFA connection to the O365 mail server. Since I am unaware of how to integrate InTune into my .Net app, I had to look for another answer.
I requested and was approved to move my functional mailbox from O365 to an on-premise Exchange server for this app. That solved the authentication problem.

DocuSign Embedded RequestRecipientToken - 500 internal Server Error

-Issue Resolved
On Live accounts that Use Embedded Signing, The Account Manager will need to either disable In Session or apply the accounts X.509 Certificate.
There is no way to bypass without the DocuSign Account Managers/Customer Support making updates to non forward facing settings.
-Using SOAP API in a VB.NET application.
I have recently moved our application to live after endless testing on the staging environment. Everything is working as expected except when I get too opening the recipient signature page.
When I make the RequestRecipientToken call I receive the error "One or both of Username and Password are invalid."
The API log give me 00_Internal Server Error_RequestRecipientToken.txt
the log doesn't really give me any info just shows that call.
I know this all works on staging, and I have tried to have all my account settings the same on both environments.
After looking all over I saw that X509 Certificate was recommended, so I added this last line in my DSAPI
-update code:
Protected Overrides Function GetWebRequest(uri As Uri) As WebRequest
IntegratorKey = SettingsHelper.sIntegrationKey
Password = SettingsHelper.sAPIPassword
Username = SettingsHelper.sAPIUserName
Dim r As System.Net.HttpWebRequest = MyBase.GetWebRequest(uri)
r.Headers.Add("X-DocuSign-Authentication", "<DocuSignCredentials><Username>" & Username & "</Username><Password>" & Password & "</Password><IntegratorKey>" & IntegratorKey & "</IntegratorKey></DocuSignCredentials>")
Dim store As X509Store = New X509Store(StoreName.My, StoreLocation.CurrentUser)
store.Open(OpenFlags.ReadOnly)
Dim certs As X509Certificate2Collection = store.Certificates.Find(X509FindType.FindByIssuerDistinguishedName, <{OUR VALUE}>, False)
Dim cert As New X509Certificate2
If certs.Count > 0 Then
cert = certs(0)
Dim securityToken = New X509SecurityToken(cert)
r.ClientCertificates.Add(securityToken.Certificate)
End If
store.Close()
Return r
End Function
This did not help.
Any help would be appreciated.

How do I check if an ftp server is online and get the error that it generates if it is not connected?

I am new to programming in vb.net. I have come a long ways in my development and understanding of vb, but there is one hurtle I can not seem to fix. I am hosting an ftp server on my pc and I am making an app for it to connect to my server and download files. The problem with all the sample code is that everyone ASSUMES the server WILL be ONLINE. My pc may not be running 24/7 and I also may not have the ftp service running.In the first case it shouldnt even register that it is connected. In the second case, it WILL say that is connected b/c the pc is on, but it will return that the machine ou are trying to connect to is actively refusing the connection. Is there a way to TRULY check if the program is indeed connected to the server WITHOUT generating a bunch of Exceptions in the debugger? All I want is a call like:
Dim ftponline As Boolean = False 'Set default to false
ftponline = checkftp()
If ftponline Then
'continue program
Else
'try a different server
End If
So it would be a function called checkftp that returns a boolean value of true or false.
Here is my info:
Using Visual Studio 2010 Pro
Using .Net framework 4
Can anyone help?
Thanks!
I have tried the rebex ftp pack as well as the Ultimate FTP Pack.
Here is the updated code:
Public Function CheckConnection(address As String) As Boolean
Dim logonServer As New System.Net.Sockets.TcpClient()
Try
logonServer.Connect(address, 21)
Catch generatedExceptionName As Exception
MessageBox.Show("Failed to connect to: " & address)
End Try
If logonServer.Connected Then
MessageBox.Show("Connected to: " & address)
Return True
logonServer.Close()
Else
Return False
End If
End Function
Public Sub ConnectFtp()
types.Clear()
models.Clear()
ListBox1.Items.Clear()
ListBox2.Items.Clear()
TextBox2.Clear()
Dim request As New Rebex.Net.Ftp
If CheckConnection(*) Then
Dim tempString As String()
request.Connect(*)
request.Login(*, *)
request.ChangeDirectory("/atc3/HD_Models")
Dim list As Array
list = request.GetNameList()
Dim item As String = ""
For Each item In list
tempString = item.Split(New Char() {" "c})
If types.Contains(tempString(0)) = False Then
types.Add(tempString(0))
End If
If models.Contains(item) = False Then
models.Add(item)
End If
Next
request.Disconnect()
request.Dispose()
ElseIf CheckConnection(*) Then
request.Connect(*)
request.Login(*, *)
request.ChangeDirectory(*)
Dim list2 As Array
list2 = request.GetNameList()
Dim item2 As String = ""
Dim tempString2 As String()
For Each item2 In list2
MessageBox.Show(item2)
tempString2 = item2.Split(New Char() {" "c})
If types.Contains(tempString2(0)) = False Then
types.Add(tempString2(0))
End If
If models.Contains(item2) = False Then
models.Add(item2)
End If
Next
request.Disconnect()
request.Dispose()
End If
End Sub
No matter what I do, the second server will not connect. I even put a messagebox to show what items were being returned in the second server, but there are no messageboxes apearing when I run the program with my server offline. Is there anyone who can help?
If your code is designed with proper exception catching, it shouldn't be generating a "bunch" of exceptions. The first exception you catch should be your indication that the connection failed and your code should cease attempting to communicate at that point. If for some reason you really need to check the connectivity before attempting the FTP connection, you should be able to simply attempt to synchronously open a TCP socket to the FTP server's port. If that works, it's up and running.
You could simply open a socket to the server's IP address on Port 21 (assuming default FTP port).
I'm not much of a VB.Net programmer, but here's a link to sample code:
http://vb.net-informations.com/communications/vb.net_Client_Socket.htm
If you can establish the socket connection, you know that something is listening on that port (though you have not yet proven it's an FTP server, or that it will accept your login credentials...).
If you wish to simply avoid exceptions in the debugger, you could place the connection code in a method and apply the DebuggerHidden attribute to that method.

Accessing Files On Server By Specifying Credentials

Our company has a share point document server where the UNC looks something like this: \\theserver.ourdomain.com\rootdirectory
Currently this drive is mapped to the Z:\ on my local computer. To access the Z:\ you have to specify (each time you login) credentials (in our case is it our username and password we logged on with) to access the folders and files in the rootdirectory.
I am in a situation where I need to copy files onto the share point server. I want to be able to copy files onto the server without using the mapped network drive (not have to specify Z:\ in the path). How can I supply credentials so that I can perform basic IO functions like GetDirectories(), GetFiles(), IO.File.Copy() etc...?
I have looked into the following things but was unsuccessful in making them work:
LogonUser API call by specifying plain text user name and password, then taking the token from that call and impersonating that user using a new instance of the WindowsIdentity class. Was able to get the token, but the impersonation didn't seem to work. Kept getting access denied errors.
CredUIPromptForCredentials/CredUIPromptForWindowsCredentials API calls, but I realize these are just for a fancy Windows UI where you can enter your credentials into and actually don't do anything.
<DllImport("advapi32.dll", SetLastError:=True)> _
Private Shared Function LogonUser(lpszUsername As String, lpszDomain As String, _
lpszPassword As String, dwLogonType As Integer, _
dwLogonProvider As Integer, ByRef phToken As IntPtr) As Boolean
End Function
<DllImport("kernel32.dll", CharSet:=CharSet.Auto)> _
Private Shared Function CloseHandle(handle As IntPtr) As Boolean
End Function
'// logon types
Public Const LOGON32_LOGON_NETWORK As Integer = 3
Public Const LOGON32_LOGON_NEW_CREDENTIALS As Integer = 9
'// logon providers
Public Const LOGON32_PROVIDER_WINNT50 As Integer = 3
Public Const LOGON32_PROVIDER_WINNT40 As Integer = 2
Public Const LOGON32_PROVIDER_WINNT35 As Integer = 1
Public Const LOGON32_PROVIDER_DEFAULT As Integer = 0
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Dim token = IntPtr.Zero
Dim success = LogonUser("username", "domain", "password", _
LOGON32_LOGON_NEW_CREDENTIALS, _
LOGON32_PROVIDER_DEFAULT, token)
If Not success Then
Me.RaiseLastWin32Error()
End If
Using identity = New WindowsIdentity(token)
Using impersonated = identity.Impersonate()
Try
Dim info = New DirectoryInfo("\\theserver.ourdomain.com\rootdirectory\")
Dim files = info.GetDirectories()
Catch ex As Exception
Finally
impersonated.Undo()
End Try
If Not CloseHandle(token) Then
Me.RaiseLastWin32Error()
End If
End Using
End Using
End Sub
Private Sub RaiseLastWin32Error()
Dim hr = Marshal.GetLastWin32Error()
Dim ex = Marshal.GetExceptionForHR(hr)
If ex IsNot Nothing Then
Throw ex
End If
Throw New SystemException(String.Format("Call resulted in error code {0}", hr))
End Sub
This isn't a direct answer to your question as it is a wildly different approach. If it doesn't work for your situation sorry to bother, but have you considered using the SharePoint web services to load the files and retrieve information?
I suggest this approach for a few reasons:
The issue you are experiencing may be occurring because SharePoint implements WebDav which might not be 100% compatible with System.IO. I'm not an expert on the innards here, I don't know about the compatibility for sure, but it seems plausible.
The UNC location you have could easily be massaged into a URL that the web service requires.
You can set the credentials directly on the proxy and might have an easier time. (though we make these calls from another web server and so the app pool credentials in the example are good enough for us)
Here's some sanitized and simplified code just in case:
// location takes the form http://server.name.com/site/library/folder/document.ext
public string UploadDocument(string location, byte[] fileContents)
{
var result = String.empty;
var destination = new string[1];
destination[0] = location;
var fileName = Path.GetFileName(location);
var fieldInfo = new FieldInformation[0];
CopyResult[] copyResults;
_copyService.Url = "http://server.name.com/_vti_bin/Copy.asmx";
_copyService.Credentials = CredentialCache.DefaultCredentials;
_copyService.CopyIntoItems(fileName, destination, fieldInfo, fileContents, out copyResults);
var errorCode = copyResults[0].ErrorCode;
if (errorCode != CopyErrorCode.Success)
{
if (errorCode == CopyErrorCode.DestinationCheckedOut)
result = "File is currently checked out. Please try again later.";
else
result = "Error uploading content.";
}
return result;
}
_copyService is a dependency we inject where the run-time implementation is the proxy generated by Visual Studio tools from the Copy.asmx SharePoint web service.
You can also get folder contents and document metadata using the Lists.asmx web service. The biggest downsides to this approach are that querying the information requires some CAML knowledge and processing the results is not as easy. But the services are reasonably documented on MSDN and the operations are all working in our application.
Well, I was able to solve this with the help of the WNetAddConnection2 API. This API is used for mapping network drives as well, however you can call this method without specifying a drive letter so that it just adds the connection.
Say for example you had drive X: mapped to \\server\share
Lets also say that it requires username & password to access the files on the server. When you restart Windows 7, you will probably lose that connection (you will get a notification saying that Windows was unable to reconnect some of the network drives). If you have an application that requires access to that server's files and you attempt to access it without supplying your credentials you will get access denied exceptions. If you do a successful call to WNetAddConnection2, not only will it fix your unmapped network drive, you will also be able to access the files/directories via the System.IO namespace.
We use Sharepoint and this worked for me. Thanks to the other guys for replying also.

FtpWebRequest.GetRequestStream hang up and fails.

I have wrote a web service, in a nutshell it uses openpop to get email messages does stuff with the content to insert into databases and saves attachments which are images. That works fine when i save images locally, it does exactley what it is suppose to. Now an added requirment was to save images to an FTP directory, so i can create my folders dynamically (they are created based upon timestamp) and that works well. My problem comes from when i try to save them to the ftp. Yes my user name and password are correct, otherwise i wouldn't be creating the directory.
Private Sub UploadFile(ByVal fileToSave As FileInfo, ByVal path As String)
Dim UploadRequest As FtpWebRequest = DirectCast(WebRequest.Create("ftp://UserName:Passowrd#999.99.999.9" & path), FtpWebRequest)
UploadRequest.Credentials = New NetworkCredential("PicService", "grean.matching18")
UploadRequest.Method = System.Net.WebRequestMethods.Ftp.UploadFile
UploadRequest.UseBinary = True
UploadRequest.UsePassive = True
' Const BufferSize As Integer = 2048
' Dim content(BufferSize - 1) As Byte, dataRead As Integer
Dim bFile() As Byte = System.IO.File.ReadAllBytes(fileToSave.ToString)
'UploadRequest.ContentLength = content.Length
Using FileStream1 As FileStream = fileToSave.OpenRead()
Try
'open request to send
Using RequestStream As Stream = UploadRequest.GetRequestStream
End Using
Catch ex As Exception
Finally
'ensure file closed
FileStream1.Close()
End Try
End Using
End Sub
I have tried using Passive False and Binary False as well, i did more research on my stack trace.
And found this article but no solution as of yet. Any input would be appreciated, i am also posting another question on windows services for different issue. If you would like to take a shot at it, the other question isnt about ftp but permissions for a service on windows server 2003
This may not be the solution but I've found that the URI string has to be 'just right' and that what is 'just right' varies by the ftp server.
So ftp://server/directory/file works on some servers but needs to be ftp://server//directory/file to work on others (note the double slash after the server name)
Aso, your URI has 'password' spelled incorrectly: ftp://UserName:Passowrd#999.99.999.9 and you are supplying the credentials in a separate code line as well.