From 986b6a8c91e24e65cc2a5fa78c0947e4529d09f5 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 31 Aug 2022 14:20:08 +0000 Subject: [PATCH 01/17] Update generated README --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index af92f07..b71e417 100644 --- a/README.md +++ b/README.md @@ -140,3 +140,26 @@ WinRm Port |Port to run WinRm on Default for http is 5985 Orchestrator |This is the orchestrator server registered with the appropriate capabilities to manage this certificate store type. Inventory Schedule |The interval that the system will use to report on what certificates are currently in the store. + +#### TEST CASES +Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot +----|------------------------|------------------------------------|--------------|----------------|------------------------- +1 |New Cert Enrollment To New Binding|**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsite.com
**Sni Flag:** 0 - No SNI
**Protocol:** https|New Binding Created with Enrollment Params specified|True|![](images/TestCase1Results.gif) +2 |New Cert Enrollment To Existing Binding|**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsite.com
**Sni Flag:** 0 - No SNI
**Protocol:** https|Existing Binding From Case 1 Updated with New Cert|True|![](images/TestCase2Results.gif) +3 |New Cert Enrollment To Existing Binding Enable SNI |**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsite.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Will Update Site In Case 2 to Have Sni Enabled|True|![](images/TestCase3Results.gif) +4 |New Cert Enrollment New IP Address|**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsite.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|New Binding Created With New IP and New SNI on Same Port|True|![](images/TestCase4Results.gif) +5 |New Cert Enrollment New Host Name|**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.newhostname.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|New Binding Created With different host on Same Port and IP Address|True|![](images/TestCase5Results.gif) +6 |New Cert Enrollment Same Site New Port |**Site Name:** FirstSite
**Port:** 4443
**IP Address:**`192.168.58.162`
**Host Name:** www.newhostname.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|New Binding on different port will be created with new cert enrolled|True|![](images/TestCase6Results.gif) +7 |Remove Cert and Binding From Test Case 6|**Site Name:** FirstSite
**Port:** 4443
**IP Address:**`192.168.58.162`
**Host Name:** www.newhostname.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert and Binding From Test Case 6 Removed|True|![](images/TestCase7Results.gif) +8 |Renew Same Cert on 2 Different Sites|`SITE 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsite.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`SITE 2`
**First Site**
**Site Name:** SecondSite
**Port:** 443
**IP Address:**`*`
**Host Name:** cstiis04.cstpki.int
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both sites because it has the same thrumbprint|True|![](images/TestCase8Site1.gif)![](images/TestCase8Site2.gif) +9 |Renew Same Cert on Same Site Same Binding Settings Different Hostname|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding2.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase9Binding1.gif)![](images/TestCase9Binding2.gif) +10 |Renew Single Cert on Same Site Same Binding Settings Different Hostname Different Certs|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding2.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on only one binding because the other binding does not match thrumbprint|True|![](images/TestCase10Binding1.gif)![](images/TestCase10Binding2.gif) +11 |Renew Same Cert on Same Site Same Binding Settings Different IPs|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.160`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase11Binding1.gif)![](images/TestCase11Binding2.gif) +12 |Renew Same Cert on Same Site Same Binding Settings Different Ports|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 543
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase12Binding1.gif)![](images/TestCase12Binding2.gif) + + + + + + + From 6624b551187b2e4be4914136b08110c6bd943127 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Wed, 31 Aug 2022 11:42:14 -0400 Subject: [PATCH 02/17] ReEnrollment Stub --- IISWithBindings/Jobs/ReEnrollment.cs | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 IISWithBindings/Jobs/ReEnrollment.cs diff --git a/IISWithBindings/Jobs/ReEnrollment.cs b/IISWithBindings/Jobs/ReEnrollment.cs new file mode 100644 index 0000000..6b7e720 --- /dev/null +++ b/IISWithBindings/Jobs/ReEnrollment.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs +{ + public class ReEnrollment:IReenrollmentJobExtension + { + private readonly ILogger _logger; + + public ReEnrollment(ILogger logger) + { + _logger = logger; + } + + public string ExtensionName => "IISBindings"; + + public JobResult ProcessJob(ReenrollmentJobConfiguration jobConfiguration, SubmitReenrollmentCSR submitReEnrollmentUpdate) + { + _logger.MethodEntry(); + throw new NotImplementedException(); + + } + } +} From 29db221b68b44220c00c2aa89e16d7703b989ee4 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 1 Sep 2022 07:07:07 -0700 Subject: [PATCH 03/17] Adding Re-Enrollment --- IISWithBindings/Jobs/Inventory.cs | 4 ++-- IISWithBindings/Jobs/Management.cs | 4 ++-- IISWithBindings/Jobs/ReEnrollment.cs | 4 ++-- IISWithBindings/manifest.json | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/IISWithBindings/Jobs/Inventory.cs b/IISWithBindings/Jobs/Inventory.cs index 043b7e8..ebbed68 100644 --- a/IISWithBindings/Jobs/Inventory.cs +++ b/IISWithBindings/Jobs/Inventory.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs +namespace Keyfactor.Extensions.Orchestrator.IISU.Jobs { public class Inventory : IInventoryJobExtension { @@ -195,7 +195,7 @@ private JobResult PerformInventory(InventoryJobConfiguration config, SubmitInven } } - public string ExtensionName => "IISBindings"; + public string ExtensionName => "IISU"; public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitInventoryUpdate submitInventoryUpdate) { return PerformInventory(jobConfiguration, submitInventoryUpdate); diff --git a/IISWithBindings/Jobs/Management.cs b/IISWithBindings/Jobs/Management.cs index 82bc092..ab7a446 100644 --- a/IISWithBindings/Jobs/Management.cs +++ b/IISWithBindings/Jobs/Management.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs +namespace Keyfactor.Extensions.Orchestrator.IISU.Jobs { public class Management : IManagementJobExtension { @@ -23,7 +23,7 @@ public Management(ILogger logger) _logger = logger; } - public string ExtensionName => "IISBindings"; + public string ExtensionName => "IISU"; public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) { diff --git a/IISWithBindings/Jobs/ReEnrollment.cs b/IISWithBindings/Jobs/ReEnrollment.cs index 6b7e720..6a2f56a 100644 --- a/IISWithBindings/Jobs/ReEnrollment.cs +++ b/IISWithBindings/Jobs/ReEnrollment.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs +namespace Keyfactor.Extensions.Orchestrator.IISU.Jobs { public class ReEnrollment:IReenrollmentJobExtension { @@ -21,7 +21,7 @@ public ReEnrollment(ILogger logger) _logger = logger; } - public string ExtensionName => "IISBindings"; + public string ExtensionName => "IISU"; public JobResult ProcessJob(ReenrollmentJobConfiguration jobConfiguration, SubmitReenrollmentCSR submitReEnrollmentUpdate) { diff --git a/IISWithBindings/manifest.json b/IISWithBindings/manifest.json index 81d7653..af4a3a6 100644 --- a/IISWithBindings/manifest.json +++ b/IISWithBindings/manifest.json @@ -8,6 +8,10 @@ "CertStores.IISBindings.Management": { "assemblypath": "IISWithBindings.dll", "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs.Management" + }, + "CertStores.IISBindings.ReEnrollment": { + "assemblypath": "IISWithBindings.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs.ReEnrollment" } } } From 75928d5b010b6fb87b73592c3cbafd62472c64e9 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 1 Sep 2022 10:17:48 -0400 Subject: [PATCH 04/17] Changed naming to reflect project --- IISWithBindings.sln => IISU.sln | 100 +++++++++--------- .../IISU.csproj | 52 ++++----- {IISWithBindings => IISU}/Jobs/Inventory.cs | 0 {IISWithBindings => IISU}/Jobs/Management.cs | 20 ++-- .../Jobs/ReEnrollment.cs | 0 .../PSCertStoreException.cs | 2 +- {IISWithBindings => IISU}/PSCertificate.cs | 2 +- .../PowerShellCertStore.cs | 2 +- {IISWithBindings => IISU}/StorePath.cs | 74 ++++++------- IISU/manifest.json | 18 ++++ IISWithBindings/manifest.json | 18 ---- 11 files changed, 144 insertions(+), 144 deletions(-) rename IISWithBindings.sln => IISU.sln (91%) rename IISWithBindings/IISWithBindings.csproj => IISU/IISU.csproj (87%) rename {IISWithBindings => IISU}/Jobs/Inventory.cs (100%) rename {IISWithBindings => IISU}/Jobs/Management.cs (99%) rename {IISWithBindings => IISU}/Jobs/ReEnrollment.cs (100%) rename {IISWithBindings => IISU}/PSCertStoreException.cs (90%) rename {IISWithBindings => IISU}/PSCertificate.cs (82%) rename {IISWithBindings => IISU}/PowerShellCertStore.cs (98%) rename {IISWithBindings => IISU}/StorePath.cs (88%) create mode 100644 IISU/manifest.json delete mode 100644 IISWithBindings/manifest.json diff --git a/IISWithBindings.sln b/IISU.sln similarity index 91% rename from IISWithBindings.sln rename to IISU.sln index 3e42e26..8c6a7ed 100644 --- a/IISWithBindings.sln +++ b/IISU.sln @@ -1,50 +1,50 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30717.126 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISWithBindings", "IISWithBindings\IISWithBindings.csproj", "{33FBC5A1-3466-4F10-B9A6-7186F804A65A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A6C93E7-24FD-47FD-883D-EDABF5CEE4C6}" - ProjectSection(SolutionItems) = preProject - CHANGELOG.md = CHANGELOG.md - integration-manifest.json = integration-manifest.json - .github\workflows\keyfactor-extension-release.yml = .github\workflows\keyfactor-extension-release.yml - README.md = README.md - README.md.tpl = README.md.tpl - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{6302034E-DF8C-4B65-AC36-CED24C068999}" - ProjectSection(SolutionItems) = preProject - Images\Image1.png = Images\Image1.png - Images\Image2.png = Images\Image2.png - Images\Image3.png = Images\Image3.png - Images\Image4.png = Images\Image4.png - Images\Image5.png = Images\Image5.png - Images\Image6.png = Images\Image6.png - Images\Image7.png = Images\Image7.png - Images\Image8.png = Images\Image8.png - Images\Image9.png = Images\Image9.png - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6302034E-DF8C-4B65-AC36-CED24C068999} = {1A6C93E7-24FD-47FD-883D-EDABF5CEE4C6} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E0FA12DA-6B82-4E64-928A-BB9965E636C1} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30717.126 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISU", "IISU\IISU.csproj", "{33FBC5A1-3466-4F10-B9A6-7186F804A65A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A6C93E7-24FD-47FD-883D-EDABF5CEE4C6}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + integration-manifest.json = integration-manifest.json + .github\workflows\keyfactor-extension-release.yml = .github\workflows\keyfactor-extension-release.yml + README.md = README.md + README.md.tpl = README.md.tpl + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{6302034E-DF8C-4B65-AC36-CED24C068999}" + ProjectSection(SolutionItems) = preProject + Images\Image1.png = Images\Image1.png + Images\Image2.png = Images\Image2.png + Images\Image3.png = Images\Image3.png + Images\Image4.png = Images\Image4.png + Images\Image5.png = Images\Image5.png + Images\Image6.png = Images\Image6.png + Images\Image7.png = Images\Image7.png + Images\Image8.png = Images\Image8.png + Images\Image9.png = Images\Image9.png + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33FBC5A1-3466-4F10-B9A6-7186F804A65A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6302034E-DF8C-4B65-AC36-CED24C068999} = {1A6C93E7-24FD-47FD-883D-EDABF5CEE4C6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E0FA12DA-6B82-4E64-928A-BB9965E636C1} + EndGlobalSection +EndGlobal diff --git a/IISWithBindings/IISWithBindings.csproj b/IISU/IISU.csproj similarity index 87% rename from IISWithBindings/IISWithBindings.csproj rename to IISU/IISU.csproj index 5ffb2c7..191424a 100644 --- a/IISWithBindings/IISWithBindings.csproj +++ b/IISU/IISU.csproj @@ -1,26 +1,26 @@ - - - - netcoreapp3.1 - Keyfactor.Extensions.Orchestrator.IISWithBinding - true - - - - none - false - - - - - - - - - - - PreserveNewest - - - - + + + + netcoreapp3.1 + Keyfactor.Extensions.Orchestrator.IISU + true + + + + none + false + + + + + + + + + + + PreserveNewest + + + + diff --git a/IISWithBindings/Jobs/Inventory.cs b/IISU/Jobs/Inventory.cs similarity index 100% rename from IISWithBindings/Jobs/Inventory.cs rename to IISU/Jobs/Inventory.cs diff --git a/IISWithBindings/Jobs/Management.cs b/IISU/Jobs/Management.cs similarity index 99% rename from IISWithBindings/Jobs/Management.cs rename to IISU/Jobs/Management.cs index ab7a446..9eb0504 100644 --- a/IISWithBindings/Jobs/Management.cs +++ b/IISU/Jobs/Management.cs @@ -4,7 +4,7 @@ using System.Management.Automation.Runspaces; using System.Net; using System.Security.Cryptography.X509Certificates; -using Keyfactor.Logging; +using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; @@ -47,7 +47,7 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) } _logger.LogTrace("Before PerformAddition..."); complete = PerformAddition(jobConfiguration, _thumbprint); - _logger.LogTrace("After PerformAddition..."); + _logger.LogTrace("After PerformAddition..."); break; case CertStoreOperationType.Remove: _logger.LogTrace("After PerformRemoval..."); @@ -57,16 +57,16 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) } _logger.MethodExit(); return complete; - } - + } + private JobResult PerformRemoval(ManagementJobConfiguration config) { try - { - _logger.MethodEntry(); + { + _logger.MethodEntry(); var siteName = config.JobProperties["Site Name"]; - var port = config.JobProperties["Port"]; - var hostName = config.JobProperties["Host Name"]; + var port = config.JobProperties["Port"]; + var hostName = config.JobProperties["Host Name"]; var protocol = config.JobProperties["Protocol"]; _logger.LogTrace($"Removing Site: {siteName}, Port:{port}, hostName:{hostName}, protocol:{protocol}"); @@ -146,8 +146,8 @@ private JobResult PerformRemoval(ManagementJobConfiguration config) .AddParameter("Name", "WebAdministration") .AddStatement(); - _logger.LogTrace("Imported WebAdministration Module"); - + _logger.LogTrace("Imported WebAdministration Module"); + foreach (var binding in foundBindings) { ps.AddCommand("Remove-WebBinding") diff --git a/IISWithBindings/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs similarity index 100% rename from IISWithBindings/Jobs/ReEnrollment.cs rename to IISU/Jobs/ReEnrollment.cs diff --git a/IISWithBindings/PSCertStoreException.cs b/IISU/PSCertStoreException.cs similarity index 90% rename from IISWithBindings/PSCertStoreException.cs rename to IISU/PSCertStoreException.cs index b3a43ea..702dc8f 100644 --- a/IISWithBindings/PSCertStoreException.cs +++ b/IISU/PSCertStoreException.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding +namespace Keyfactor.Extensions.Orchestrator.IISU { [Serializable] internal class PsCertStoreException : Exception diff --git a/IISWithBindings/PSCertificate.cs b/IISU/PSCertificate.cs similarity index 82% rename from IISWithBindings/PSCertificate.cs rename to IISU/PSCertificate.cs index f6cde11..832bee7 100644 --- a/IISWithBindings/PSCertificate.cs +++ b/IISU/PSCertificate.cs @@ -1,6 +1,6 @@ using System; -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding +namespace Keyfactor.Extensions.Orchestrator.IISU { public class PsCertificate { diff --git a/IISWithBindings/PowerShellCertStore.cs b/IISU/PowerShellCertStore.cs similarity index 98% rename from IISWithBindings/PowerShellCertStore.cs rename to IISU/PowerShellCertStore.cs index f561951..ecf11f7 100644 --- a/IISWithBindings/PowerShellCertStore.cs +++ b/IISU/PowerShellCertStore.cs @@ -3,7 +3,7 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding +namespace Keyfactor.Extensions.Orchestrator.IISU { internal class PowerShellCertStore { diff --git a/IISWithBindings/StorePath.cs b/IISU/StorePath.cs similarity index 88% rename from IISWithBindings/StorePath.cs rename to IISU/StorePath.cs index 6e4408f..7de509c 100644 --- a/IISWithBindings/StorePath.cs +++ b/IISU/StorePath.cs @@ -1,38 +1,38 @@ -using System.ComponentModel; -using Newtonsoft.Json; - -namespace Keyfactor.Extensions.Orchestrator.IISWithBinding -{ - internal class StorePath - { - - public StorePath() - { - } - - [JsonProperty("spnwithport")] - [DefaultValue(false)] - public bool SpnPortFlag { get; set; } - - [JsonProperty("WinRm Protocol")] - [DefaultValue("http")] - public string WinRmProtocol { get; set; } - - [JsonProperty("WinRm Port")] - [DefaultValue("5985")] - public string WinRmPort { get; set; } - - [JsonProperty("sniflag")] - [DefaultValue(SniFlag.None)] - public SniFlag SniFlag { get; set; } - - } - - internal enum SniFlag - { - None = 0, - Sni = 1, - NoneCentral = 2, - SniCentral = 3 - } +using System.ComponentModel; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.Orchestrator.IISU +{ + internal class StorePath + { + + public StorePath() + { + } + + [JsonProperty("spnwithport")] + [DefaultValue(false)] + public bool SpnPortFlag { get; set; } + + [JsonProperty("WinRm Protocol")] + [DefaultValue("http")] + public string WinRmProtocol { get; set; } + + [JsonProperty("WinRm Port")] + [DefaultValue("5985")] + public string WinRmPort { get; set; } + + [JsonProperty("sniflag")] + [DefaultValue(SniFlag.None)] + public SniFlag SniFlag { get; set; } + + } + + internal enum SniFlag + { + None = 0, + Sni = 1, + NoneCentral = 2, + SniCentral = 3 + } } \ No newline at end of file diff --git a/IISU/manifest.json b/IISU/manifest.json new file mode 100644 index 0000000..b09b129 --- /dev/null +++ b/IISU/manifest.json @@ -0,0 +1,18 @@ +{ + "extensions": { + "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": { + "CertStores.IISU.Inventory": { + "assemblypath": "IISU.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISU.Jobs.Inventory" + }, + "CertStores.IISU.Management": { + "assemblypath": "IISU.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISU.Jobs.Management" + }, + "CertStores.IISU.ReEnrollment": { + "assemblypath": "IISU.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISU.Jobs.ReEnrollment" + } + } + } +} \ No newline at end of file diff --git a/IISWithBindings/manifest.json b/IISWithBindings/manifest.json deleted file mode 100644 index af4a3a6..0000000 --- a/IISWithBindings/manifest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extensions": { - "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": { - "CertStores.IISBindings.Inventory": { - "assemblypath": "IISWithBindings.dll", - "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs.Inventory" - }, - "CertStores.IISBindings.Management": { - "assemblypath": "IISWithBindings.dll", - "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs.Management" - }, - "CertStores.IISBindings.ReEnrollment": { - "assemblypath": "IISWithBindings.dll", - "TypeFullName": "Keyfactor.Extensions.Orchestrator.IISWithBinding.Jobs.ReEnrollment" - } - } - } -} \ No newline at end of file From 7b92bfecb55c3d25559cb87a0294efb8e1b25892 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Thu, 1 Sep 2022 17:47:12 -0400 Subject: [PATCH 05/17] Code Checkpoint --- IISU/IISManager.cs | 353 ++++++++++++++++++++++++ IISU/{StorePath.cs => JobProperties.cs} | 4 +- IISU/Jobs/Inventory.cs | 2 +- IISU/Jobs/Management.cs | 224 +-------------- IISU/Jobs/ReEnrollment.cs | 9 +- 5 files changed, 371 insertions(+), 221 deletions(-) create mode 100644 IISU/IISManager.cs rename IISU/{StorePath.cs => JobProperties.cs} (91%) diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs new file mode 100644 index 0000000..faf28dc --- /dev/null +++ b/IISU/IISManager.cs @@ -0,0 +1,353 @@ +using System; +using System.Configuration; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.Orchestrator.IISU +{ + public class IISManager + { + public IISManager() + { + Logger = LogHandler.GetClassLogger(); + } + + private ILogger Logger { get; } + private string SiteName { get; set; } + private string IpAddress { get; set; } + private string HostName { get; set; } + private long JobHistoryId { get; set; } + private string Port { get; set; } + private string SniFlag { get; set; } + private string Path { get; set; } + private string ClientMachine { get; set; } + private string Protocol { get; set; } + private string CertContents { get; set; } + private string PrivateKeyPassword { get; set; } + private string ServerUserName { get; set; } + private string ServerPassword { get; set; } + private JobProperties Properties { get; set; } + private string Thumbprint { get; set; } + private WSManConnectionInfo ConnectionInfo { get; set; } + + + public JobResult ReEnrollCertificate(ReenrollmentJobConfiguration config) + { + try + { + SiteName = config.JobProperties["SiteName"].ToString(); + Port = config.JobProperties["Port"].ToString(); + HostName = config.JobProperties["HostName"].ToString(); + Protocol = config.JobProperties["Protocol"].ToString(); + SniFlag= config.JobProperties["SniFlag"].ToString(); + IpAddress = config.JobProperties["IpAddress"].ToString(); + + PrivateKeyPassword = ""; //Todo set the private Key Password + ServerUserName = config.ServerUsername; + ServerPassword = config.ServerPassword; + Thumbprint = ""; //todo Set the Thumbprint + ClientMachine = config.CertificateStoreDetails.ClientMachine; + Path = config.CertificateStoreDetails.StorePath; + CertContents = ""; //Todo Generate CSR and Get Cert Contents + JobHistoryId = config.JobHistoryId; + + Properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, + new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + + ConnectionInfo = + new WSManConnectionInfo( + new Uri($"{Properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{Properties?.WinRmPort}/wsman")); + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" + }; + } + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "UnExpected Error Occured" + }; + } + + public JobResult AddCertificate(ManagementJobConfiguration config) + { + try + { + SiteName = config.JobProperties["SiteName"].ToString(); + Port = config.JobProperties["Port"].ToString(); + HostName = config.JobProperties["HostName"].ToString(); + Protocol = config.JobProperties["Protocol"].ToString(); + SniFlag = config.JobProperties["SniFlag"].ToString(); + IpAddress = config.JobProperties["IpAddress"].ToString(); + + PrivateKeyPassword = ""; //Todo set the private Key Password + ServerUserName = config.ServerUsername; + ServerPassword = config.ServerPassword; + Thumbprint = ""; //todo Set the Thumbprint + ClientMachine = config.CertificateStoreDetails.ClientMachine; + Path = config.CertificateStoreDetails.StorePath; + CertContents = config.JobCertificate.Contents; + JobHistoryId = config.JobHistoryId; + + Properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, + new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + + ConnectionInfo = + new WSManConnectionInfo( + new Uri($"{Properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{Properties?.WinRmPort}/wsman")); + + if (config.JobProperties.ContainsKey("RenewalThumbprint")) + { + Thumbprint = config.JobProperties["RenewalThumbprint"].ToString(); + Logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {Thumbprint}"); + } + + return InstallCertificate(); + + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" + }; + } + } + + + private JobResult InstallCertificate() + { + Logger.LogTrace($"IncludePortInSPN: {Properties.SpnPortFlag}"); + ConnectionInfo.IncludePortInSPN = Properties.SpnPortFlag; + Logger.LogTrace($"Credentials: UserName:{ServerUserName} Password:{ServerPassword}"); + var pw = new NetworkCredential(ServerUserName, ServerPassword) + .SecurePassword; + ConnectionInfo.Credential = new PSCredential(ServerUserName, pw); + Logger.LogTrace($"PSCredential Created {pw}"); + + Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); + var x509Cert = new X509Certificate2( + Convert.FromBase64String(CertContents), + PrivateKeyPassword, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); + Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); + Logger.LogTrace( + $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); + + using var runSpace = RunspaceFactory.CreateRunspace(ConnectionInfo); + Logger.LogTrace("RunSpace Created"); + runSpace.Open(); + Logger.LogTrace("RunSpace Opened"); + Logger.LogTrace( + $"Creating Cert Store with ClientMachine: {ClientMachine}, JobProperties: {Path}"); + var _ = new PowerShellCertStore( + ClientMachine, Path, + runSpace); + Logger.LogTrace("Cert Store Created"); + using var ps = PowerShell.Create(); + Logger.LogTrace("ps created"); + ps.Runspace = runSpace; + Logger.LogTrace("RunSpace Assigned"); + + var funcScript = @" + $ErrorActionPreference = ""Stop"" + + function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) { + $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, ""LocalMachine"" + $certStore.Open(5) + $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #> + $certStore.Add($cert) + $certStore.Close(); + }"; + + ps.AddScript(funcScript).AddStatement(); + Logger.LogTrace("InstallPfxToMachineStore Statement Added..."); + ps.AddCommand("InstallPfxToMachineStore") + .AddParameter("bytes", Convert.FromBase64String(CertContents)) + .AddParameter("password", PrivateKeyPassword) + .AddParameter("storeName", + $@"\\{ClientMachine}\{Path}"); + Logger.LogTrace("InstallPfxToMachineStore Command Added..."); + + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + Logger.LogTrace("Invoking ps..."); + ps.Invoke(); + Logger.LogTrace("ps Invoked..."); + if (ps.HadErrors) + { + Logger.LogTrace("ps Has Errors"); + var psError = ps.Streams.Error.ReadAll() + .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + $"Site {Path} on server {ClientMachine}: {psError}" + }; + } + } + + Logger.LogTrace("Clearing Commands..."); + ps.Commands.Clear(); + Logger.LogTrace("Commands Cleared.."); + + //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all + if (Thumbprint.Length > 0) + { + Logger.LogTrace($"Thumbprint Length > 0 {Thumbprint}"); + ps.AddCommand("Import-Module") + .AddParameter("Name", "WebAdministration") + .AddStatement(); + + Logger.LogTrace("WebAdministration Imported"); + var searchScript = + "Foreach($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]@{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation;thumbprint=$Bind.certificateHash;sniFlg=$Bind.sslFlags}}}"; + ps.AddScript(searchScript).AddStatement(); + Logger.LogTrace($"Search Script: {searchScript}"); + var bindings = ps.Invoke(); + foreach (var binding in bindings) + { + Logger.LogTrace("Looping Bindings...."); + var bindingSiteName = binding.Properties["name"].Value.ToString(); + var bindingIpAddress = binding.Properties["Bindings"].Value.ToString()?.Split(':')[0]; + var bindingPort = binding.Properties["Bindings"].Value.ToString()?.Split(':')[1]; + var bindingHostName = binding.Properties["Bindings"].Value.ToString()?.Split(':')[2]; + var bindingProtocol = binding.Properties["Protocol"].Value.ToString(); + var bindingThumbprint = binding.Properties["thumbprint"].Value.ToString(); + var bindingSniFlg = binding.Properties["sniFlg"].Value.ToString(); + + Logger.LogTrace( + $"bindingSiteName: {bindingSiteName}, bindingIpAddress: {bindingIpAddress}, bindingPort: {bindingPort}, bindingHostName: {bindingHostName}, bindingProtocol: {bindingProtocol}, bindingThumbprint: {bindingThumbprint}, bindingSniFlg: {bindingSniFlg}"); + + //if the thumbprint of the renewal request matches the thumbprint of the cert in IIS, then renew it + if (Thumbprint == bindingThumbprint) + { + Logger.LogTrace($"Thumbprint Match {Thumbprint}={bindingThumbprint}"); + funcScript = string.Format(@" + $ErrorActionPreference = ""Stop"" + + $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} + if($IISInstalled) {{ + Import-Module WebAdministration + Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | + ForEach-Object {{ Remove-WebBinding -BindingInformation $_.bindingInformation }} + + New-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" -SslFlags ""{7}"" + Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | + ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} + }}", bindingSiteName, //{0} + bindingIpAddress, //{1} + bindingPort, //{2} + bindingProtocol, //{3} + bindingHostName, //{4} + x509Cert.Thumbprint, //{5} + Path, //{6} + bindingSniFlg); //{7} + + Logger.LogTrace($"funcScript {funcScript}"); + ps.AddScript(funcScript); + Logger.LogTrace("funcScript added..."); + ps.Invoke(); + Logger.LogTrace("funcScript Invoked..."); + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + ps.Commands.Clear(); + Logger.LogTrace("Commands Cleared.."); + } + } + } + else + { + funcScript = string.Format(@" + $ErrorActionPreference = ""Stop"" + + $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} + if($IISInstalled) {{ + Import-Module WebAdministration + Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -Port ""{2}"" -Protocol ""{3}"" -HostHeader ""{4}"" | + ForEach-Object {{ Remove-WebBinding -BindingInformation $_.bindingInformation }} + + New-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" -SslFlags ""{7}"" + Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | + ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} + }}", SiteName, //{0} + IpAddress, //{1} + Port, //{2} + Protocol, //{3} + HostName, //{4} + x509Cert.Thumbprint, //{5} + Path, //{6} + Convert.ToInt16(SniFlag)); //{7} + //Convert.ToInt16(config.JobProperties["Sni Flag"].ToString()?.Substring(0, 1))); //{7} + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + Logger.LogTrace($"funcScript {funcScript}"); + ps.AddScript(funcScript); + Logger.LogTrace("funcScript added..."); + ps.Invoke(); + Logger.LogTrace("funcScript Invoked..."); + } + + if (ps.HadErrors) + { + var psError = ps.Streams.Error.ReadAll() + .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + $"Site {Path} on server {ClientMachine}: {psError}" + }; + } + } + + Logger.LogTrace("closing RunSpace..."); + runSpace.Close(); + Logger.LogTrace("RunSpace Closed..."); + + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + "Unexpected Error Occured" + }; + } + } +} \ No newline at end of file diff --git a/IISU/StorePath.cs b/IISU/JobProperties.cs similarity index 91% rename from IISU/StorePath.cs rename to IISU/JobProperties.cs index 7de509c..aa3ccc0 100644 --- a/IISU/StorePath.cs +++ b/IISU/JobProperties.cs @@ -3,10 +3,10 @@ namespace Keyfactor.Extensions.Orchestrator.IISU { - internal class StorePath + internal class JobProperties { - public StorePath() + public JobProperties() { } diff --git a/IISU/Jobs/Inventory.cs b/IISU/Jobs/Inventory.cs index ebbed68..dbf75ac 100644 --- a/IISU/Jobs/Inventory.cs +++ b/IISU/Jobs/Inventory.cs @@ -25,7 +25,7 @@ private JobResult PerformInventory(InventoryJobConfiguration config, SubmitInven { _logger.MethodEntry(); _logger.LogTrace($"Job Configuration: {JsonConvert.SerializeObject(config)}"); - var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); var inventoryItems = new List(); _logger.LogTrace($"Begin Inventory for Cert Store {$@"\\{config.CertificateStoreDetails.ClientMachine}\{config.CertificateStoreDetails.StorePath}"}"); diff --git a/IISU/Jobs/Management.cs b/IISU/Jobs/Management.cs index 9eb0504..e01bf00 100644 --- a/IISU/Jobs/Management.cs +++ b/IISU/Jobs/Management.cs @@ -70,7 +70,7 @@ private JobResult PerformRemoval(ManagementJobConfiguration config) var protocol = config.JobProperties["Protocol"]; _logger.LogTrace($"Removing Site: {siteName}, Port:{port}, hostName:{hostName}, protocol:{protocol}"); - var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, + var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, new JsonSerializerSettings {DefaultValueHandling = DefaultValueHandling.Populate}); _logger.LogTrace( @@ -207,226 +207,14 @@ private JobResult PerformRemoval(ManagementJobConfiguration config) } } - private JobResult PerformAddition(ManagementJobConfiguration config,string thumpPrint) + private JobResult PerformAddition(ManagementJobConfiguration config) { try { _logger.MethodEntry(); - var protocol = config.JobProperties["Protocol"]; - _logger.LogTrace($"Protocol: {protocol}"); - - _logger.LogTrace( - $"Begin Addition for Cert Store {$@"\\{config.CertificateStoreDetails.ClientMachine}\{config.CertificateStoreDetails.StorePath}"}"); - - var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, - new JsonSerializerSettings {DefaultValueHandling = DefaultValueHandling.Populate}); - - _logger.LogTrace($"WinRm Url: {storePath?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{storePath?.WinRmPort}/wsman"); - - var connInfo = - new WSManConnectionInfo( - new Uri($"{storePath?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{storePath?.WinRmPort}/wsman")); - if (storePath != null) - { - _logger.LogTrace($"IncludePortInSPN: {storePath.SpnPortFlag}"); - connInfo.IncludePortInSPN = storePath.SpnPortFlag; - _logger.LogTrace($"Credentials: UserName:{config.ServerUsername} Password:{config.ServerPassword}"); - var pw = new NetworkCredential(config.ServerUsername, config.ServerPassword) - .SecurePassword; - connInfo.Credential = new PSCredential(config.ServerUsername, pw); - _logger.LogTrace($"PSCredential Created {pw}"); - - _logger.LogTrace($"Creating X509 Cert from: {config.JobCertificate.Contents}"); - var x509Cert = new X509Certificate2( - Convert.FromBase64String(config.JobCertificate.Contents), - config.JobCertificate.PrivateKeyPassword, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable); - _logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); - _logger.LogTrace( - $"Begin Add for Cert Store {$@"\\{config.CertificateStoreDetails.ClientMachine}\{config.CertificateStoreDetails.StorePath}"}"); - - using var runSpace = RunspaceFactory.CreateRunspace(connInfo); - _logger.LogTrace("RunSpace Created"); - runSpace.Open(); - _logger.LogTrace("RunSpace Opened"); - _logger.LogTrace($"Creating Cert Store with ClientMachine: {config.CertificateStoreDetails.ClientMachine}, StorePath: {config.CertificateStoreDetails.StorePath}"); - var _ = new PowerShellCertStore( - config.CertificateStoreDetails.ClientMachine, config.CertificateStoreDetails.StorePath, - runSpace); - _logger.LogTrace("Cert Store Created"); - using var ps = PowerShell.Create(); - _logger.LogTrace("ps created"); - ps.Runspace = runSpace; - _logger.LogTrace("RunSpace Assigned"); - - var funcScript = @" - $ErrorActionPreference = ""Stop"" - - function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) { - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, ""LocalMachine"" - $certStore.Open(5) - $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #> - $certStore.Add($cert) - $certStore.Close(); - }"; - - ps.AddScript(funcScript).AddStatement(); - _logger.LogTrace("InstallPfxToMachineStore Statement Added..."); - ps.AddCommand("InstallPfxToMachineStore") - .AddParameter("bytes", Convert.FromBase64String(config.JobCertificate.Contents)) - .AddParameter("password", config.JobCertificate.PrivateKeyPassword) - .AddParameter("storeName", - $@"\\{config.CertificateStoreDetails.ClientMachine}\{config.CertificateStoreDetails.StorePath}"); - _logger.LogTrace("InstallPfxToMachineStore Command Added..."); - - foreach (var cmd in ps.Commands.Commands) - { - _logger.LogTrace("Logging PowerShell Command"); - _logger.LogTrace(cmd.CommandText); - } - _logger.LogTrace("Invoking ps..."); - ps.Invoke(); - _logger.LogTrace("ps Invoked..."); - if (ps.HadErrors) - { - _logger.LogTrace("ps Has Errors"); - var psError = ps.Streams.Error.ReadAll().Aggregate(String.Empty, (current, error) => current + error.ErrorDetails.Message); - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = - $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: {psError}" - }; - } - _logger.LogTrace("Clearing Commands..."); - ps.Commands.Clear(); - _logger.LogTrace("Commands Cleared.."); - - //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all - if (thumpPrint.Length > 0) - { - _logger.LogTrace($"Thumbprint Length > 0 {thumpPrint}"); - ps.AddCommand("Import-Module") - .AddParameter("Name", "WebAdministration") - .AddStatement(); - - _logger.LogTrace("WebAdministration Imported"); - var searchScript = "Foreach($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]@{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation;thumbprint=$Bind.certificateHash;sniFlg=$Bind.sslFlags}}}"; ps.AddScript(searchScript).AddStatement(); - _logger.LogTrace($"Search Script: {searchScript}"); - var bindings = ps.Invoke(); - foreach (var binding in bindings) - { - _logger.LogTrace($"Looping Bindings...."); - var bindingSiteName = binding.Properties["name"].Value.ToString(); - var bindingIpAddress = binding.Properties["Bindings"].Value.ToString()?.Split(':')[0]; - var bindingPort = binding.Properties["Bindings"].Value.ToString()?.Split(':')[1]; - var bindingHostName = binding.Properties["Bindings"].Value.ToString()?.Split(':')[2]; - var bindingProtocol = binding.Properties["Protocol"].Value.ToString(); - var bindingThumbprint = binding.Properties["thumbprint"].Value.ToString(); - var bindingSniFlg = binding.Properties["sniFlg"].Value.ToString(); - - _logger.LogTrace($"bindingSiteName: {bindingSiteName}, bindingIpAddress: {bindingIpAddress}, bindingPort: {bindingPort}, bindingHostName: {bindingHostName}, bindingProtocol: {bindingProtocol}, bindingThumbprint: {bindingThumbprint}, bindingSniFlg: {bindingSniFlg}"); - - //if the thumprint of the renewal request matches the thumprint of the cert in IIS, then renew it - if (_thumbprint == bindingThumbprint) - { - _logger.LogTrace($"Thumbprint Match {_thumbprint}={bindingThumbprint}"); - funcScript = string.Format(@" - $ErrorActionPreference = ""Stop"" - - $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} - if($IISInstalled) {{ - Import-Module WebAdministration - Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | - ForEach-Object {{ Remove-WebBinding -BindingInformation $_.bindingInformation }} - - New-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" -SslFlags ""{7}"" - Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | - ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} - }}", bindingSiteName, //{0} - bindingIpAddress, //{1} - bindingPort, //{2} - bindingProtocol, //{3} - bindingHostName, //{4} - x509Cert.Thumbprint, //{5} - config.CertificateStoreDetails.StorePath, //{6} - bindingSniFlg); //{7} - - _logger.LogTrace($"funcScript {funcScript}"); - ps.AddScript(funcScript); - _logger.LogTrace("funcScript added..."); - ps.Invoke(); - _logger.LogTrace("funcScript Invoked..."); - foreach (var cmd in ps.Commands.Commands) - { - _logger.LogTrace("Logging PowerShell Command"); - _logger.LogTrace(cmd.CommandText); - } - ps.Commands.Clear(); - _logger.LogTrace("Commands Cleared.."); - } - } - } - else - { - funcScript = string.Format(@" - $ErrorActionPreference = ""Stop"" - - $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} - if($IISInstalled) {{ - Import-Module WebAdministration - Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -Port ""{2}"" -Protocol ""{3}"" -HostHeader ""{4}"" | - ForEach-Object {{ Remove-WebBinding -BindingInformation $_.bindingInformation }} - - New-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" -SslFlags ""{7}"" - Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | - ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} - }}", config.JobProperties["Site Name"], //{0} - config.JobProperties["IP Address"], //{1} - config.JobProperties["Port"], //{2} - protocol, //{3} - config.JobProperties["Host Name"], //{4} - x509Cert.Thumbprint, //{5} - config.CertificateStoreDetails.StorePath, //{6} - Convert.ToInt16(config.JobProperties["Sni Flag"].ToString()?.Substring(0, 1))); //{7} - - foreach (var cmd in ps.Commands.Commands) - { - _logger.LogTrace("Logging PowerShell Command"); - _logger.LogTrace(cmd.CommandText); - } - - _logger.LogTrace($"funcScript {funcScript}"); - ps.AddScript(funcScript); - _logger.LogTrace("funcScript added..."); - ps.Invoke(); - _logger.LogTrace("funcScript Invoked..."); - } - - if (ps.HadErrors) - { - var psError = ps.Streams.Error.ReadAll().Aggregate(String.Empty, (current, error) => current + error.ErrorDetails.Message); - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = - $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: {psError}" - }; - } - _logger.LogTrace("closing RunSpace..."); - runSpace.Close(); - _logger.LogTrace("RunSpace Closed..."); - } - _logger.LogTrace("Returning Success..."); - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Success, - JobHistoryId = config.JobHistoryId, - FailureMessage = "" - }; + + var iisManager=new IISManager(); + return iisManager.AddCertificate(config); } catch (Exception ex) { @@ -441,5 +229,7 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st }; } } + + } } \ No newline at end of file diff --git a/IISU/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs index 6a2f56a..f563e5a 100644 --- a/IISU/Jobs/ReEnrollment.cs +++ b/IISU/Jobs/ReEnrollment.cs @@ -23,9 +23,16 @@ public ReEnrollment(ILogger logger) public string ExtensionName => "IISU"; - public JobResult ProcessJob(ReenrollmentJobConfiguration jobConfiguration, SubmitReenrollmentCSR submitReEnrollmentUpdate) + public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReEnrollmentUpdate) { _logger.MethodEntry(); + _logger.LogTrace($"Job Configuration: {JsonConvert.SerializeObject(config)}"); + var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + _logger.LogTrace($"WinRm Url: {storePath?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{storePath?.WinRmPort}/wsman"); + + + + throw new NotImplementedException(); } From 117d88a9812504d69e51641957d875e4833e6627 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Fri, 2 Sep 2022 13:43:55 -0400 Subject: [PATCH 06/17] Re-Enrollment Changes --- IISU/Jobs/Management.cs | 3 +-- IISU/Jobs/ReEnrollment.cs | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/IISU/Jobs/Management.cs b/IISU/Jobs/Management.cs index e01bf00..ce13081 100644 --- a/IISU/Jobs/Management.cs +++ b/IISU/Jobs/Management.cs @@ -3,7 +3,6 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Net; -using System.Security.Cryptography.X509Certificates; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -46,7 +45,7 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) _logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {_thumbprint}"); } _logger.LogTrace("Before PerformAddition..."); - complete = PerformAddition(jobConfiguration, _thumbprint); + complete = PerformAddition(jobConfiguration); _logger.LogTrace("After PerformAddition..."); break; case CertStoreOperationType.Remove: diff --git a/IISU/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs index f563e5a..3a31e6e 100644 --- a/IISU/Jobs/ReEnrollment.cs +++ b/IISU/Jobs/ReEnrollment.cs @@ -1,11 +1,5 @@ using System; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Net; -using System.Security.Cryptography.X509Certificates; using Keyfactor.Logging; -using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; From e5f3d8796db4892c121c49dd564f99b8c8a6b0f6 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 6 Sep 2022 08:50:21 -0700 Subject: [PATCH 07/17] Checkpoint --- IISU/IISManager.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs index faf28dc..bf5d992 100644 --- a/IISU/IISManager.cs +++ b/IISU/IISManager.cs @@ -47,8 +47,8 @@ public JobResult ReEnrollCertificate(ReenrollmentJobConfiguration config) Port = config.JobProperties["Port"].ToString(); HostName = config.JobProperties["HostName"].ToString(); Protocol = config.JobProperties["Protocol"].ToString(); - SniFlag= config.JobProperties["SniFlag"].ToString(); - IpAddress = config.JobProperties["IpAddress"].ToString(); + SniFlag= config.JobProperties["SniFlag"].ToString()?.Substring(0, 1); + IpAddress = config.JobProperties["IPAddress"].ToString(); PrivateKeyPassword = ""; //Todo set the private Key Password ServerUserName = config.ServerUsername; @@ -92,10 +92,10 @@ public JobResult AddCertificate(ManagementJobConfiguration config) Port = config.JobProperties["Port"].ToString(); HostName = config.JobProperties["HostName"].ToString(); Protocol = config.JobProperties["Protocol"].ToString(); - SniFlag = config.JobProperties["SniFlag"].ToString(); - IpAddress = config.JobProperties["IpAddress"].ToString(); + SniFlag = config.JobProperties["SniFlag"].ToString()?.Substring(0, 1); + IpAddress = config.JobProperties["IPAddress"].ToString(); - PrivateKeyPassword = ""; //Todo set the private Key Password + PrivateKeyPassword = config.JobCertificate.PrivateKeyPassword; //Todo set the private Key Password ServerUserName = config.ServerUsername; ServerPassword = config.ServerPassword; Thumbprint = ""; //todo Set the Thumbprint @@ -307,7 +307,6 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st x509Cert.Thumbprint, //{5} Path, //{6} Convert.ToInt16(SniFlag)); //{7} - //Convert.ToInt16(config.JobProperties["Sni Flag"].ToString()?.Substring(0, 1))); //{7} foreach (var cmd in ps.Commands.Commands) { Logger.LogTrace("Logging PowerShell Command"); @@ -343,10 +342,9 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st return new JobResult { - Result = OrchestratorJobStatusJobResult.Failure, + Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = JobHistoryId, - FailureMessage = - "Unexpected Error Occured" + FailureMessage = "" }; } } From 1fad11a2e83e5a57f14add68a45839f8fcf08c66 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 6 Sep 2022 12:46:00 -0400 Subject: [PATCH 08/17] Checkpoint --- IISU/IISManager.cs | 327 +++++++++++++++++++++++---------------------- 1 file changed, 169 insertions(+), 158 deletions(-) diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs index bf5d992..ef7e4fd 100644 --- a/IISU/IISManager.cs +++ b/IISU/IISManager.cs @@ -1,5 +1,4 @@ using System; -using System.Configuration; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -134,40 +133,42 @@ public JobResult AddCertificate(ManagementJobConfiguration config) private JobResult InstallCertificate() { - Logger.LogTrace($"IncludePortInSPN: {Properties.SpnPortFlag}"); - ConnectionInfo.IncludePortInSPN = Properties.SpnPortFlag; - Logger.LogTrace($"Credentials: UserName:{ServerUserName} Password:{ServerPassword}"); - var pw = new NetworkCredential(ServerUserName, ServerPassword) - .SecurePassword; - ConnectionInfo.Credential = new PSCredential(ServerUserName, pw); - Logger.LogTrace($"PSCredential Created {pw}"); - - Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); - var x509Cert = new X509Certificate2( - Convert.FromBase64String(CertContents), - PrivateKeyPassword, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable); - Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); - Logger.LogTrace( - $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); - - using var runSpace = RunspaceFactory.CreateRunspace(ConnectionInfo); - Logger.LogTrace("RunSpace Created"); - runSpace.Open(); - Logger.LogTrace("RunSpace Opened"); - Logger.LogTrace( - $"Creating Cert Store with ClientMachine: {ClientMachine}, JobProperties: {Path}"); - var _ = new PowerShellCertStore( - ClientMachine, Path, - runSpace); - Logger.LogTrace("Cert Store Created"); - using var ps = PowerShell.Create(); - Logger.LogTrace("ps created"); - ps.Runspace = runSpace; - Logger.LogTrace("RunSpace Assigned"); - - var funcScript = @" + try + { + Logger.LogTrace($"IncludePortInSPN: {Properties.SpnPortFlag}"); + ConnectionInfo.IncludePortInSPN = Properties.SpnPortFlag; + Logger.LogTrace($"Credentials: UserName:{ServerUserName} Password:{ServerPassword}"); + var pw = new NetworkCredential(ServerUserName, ServerPassword) + .SecurePassword; + ConnectionInfo.Credential = new PSCredential(ServerUserName, pw); + Logger.LogTrace($"PSCredential Created {pw}"); + + Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); + var x509Cert = new X509Certificate2( + Convert.FromBase64String(CertContents), + PrivateKeyPassword, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); + Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); + Logger.LogTrace( + $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); + + using var runSpace = RunspaceFactory.CreateRunspace(ConnectionInfo); + Logger.LogTrace("RunSpace Created"); + runSpace.Open(); + Logger.LogTrace("RunSpace Opened"); + Logger.LogTrace( + $"Creating Cert Store with ClientMachine: {ClientMachine}, JobProperties: {Path}"); + var _ = new PowerShellCertStore( + ClientMachine, Path, + runSpace); + Logger.LogTrace("Cert Store Created"); + using var ps = PowerShell.Create(); + Logger.LogTrace("ps created"); + ps.Runspace = runSpace; + Logger.LogTrace("RunSpace Assigned"); + + var funcScript = @" $ErrorActionPreference = ""Stop"" function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$storeName) { @@ -178,77 +179,77 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st $certStore.Close(); }"; - ps.AddScript(funcScript).AddStatement(); - Logger.LogTrace("InstallPfxToMachineStore Statement Added..."); - ps.AddCommand("InstallPfxToMachineStore") - .AddParameter("bytes", Convert.FromBase64String(CertContents)) - .AddParameter("password", PrivateKeyPassword) - .AddParameter("storeName", - $@"\\{ClientMachine}\{Path}"); - Logger.LogTrace("InstallPfxToMachineStore Command Added..."); + ps.AddScript(funcScript).AddStatement(); + Logger.LogTrace("InstallPfxToMachineStore Statement Added..."); + ps.AddCommand("InstallPfxToMachineStore") + .AddParameter("bytes", Convert.FromBase64String(CertContents)) + .AddParameter("password", PrivateKeyPassword) + .AddParameter("storeName", + $@"\\{ClientMachine}\{Path}"); + Logger.LogTrace("InstallPfxToMachineStore Command Added..."); - foreach (var cmd in ps.Commands.Commands) - { - Logger.LogTrace("Logging PowerShell Command"); - Logger.LogTrace(cmd.CommandText); - } + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } - Logger.LogTrace("Invoking ps..."); - ps.Invoke(); - Logger.LogTrace("ps Invoked..."); - if (ps.HadErrors) - { - Logger.LogTrace("ps Has Errors"); - var psError = ps.Streams.Error.ReadAll() - .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); + Logger.LogTrace("Invoking ps..."); + ps.Invoke(); + Logger.LogTrace("ps Invoked..."); + if (ps.HadErrors) { - return new JobResult + Logger.LogTrace("ps Has Errors"); + var psError = ps.Streams.Error.ReadAll() + .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = JobHistoryId, - FailureMessage = - $"Site {Path} on server {ClientMachine}: {psError}" - }; + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + $"Site {Path} on server {ClientMachine}: {psError}" + }; + } } - } - Logger.LogTrace("Clearing Commands..."); - ps.Commands.Clear(); - Logger.LogTrace("Commands Cleared.."); + Logger.LogTrace("Clearing Commands..."); + ps.Commands.Clear(); + Logger.LogTrace("Commands Cleared.."); - //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all - if (Thumbprint.Length > 0) - { - Logger.LogTrace($"Thumbprint Length > 0 {Thumbprint}"); - ps.AddCommand("Import-Module") - .AddParameter("Name", "WebAdministration") - .AddStatement(); - - Logger.LogTrace("WebAdministration Imported"); - var searchScript = - "Foreach($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]@{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation;thumbprint=$Bind.certificateHash;sniFlg=$Bind.sslFlags}}}"; - ps.AddScript(searchScript).AddStatement(); - Logger.LogTrace($"Search Script: {searchScript}"); - var bindings = ps.Invoke(); - foreach (var binding in bindings) + //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all + if (Thumbprint.Length > 0) { - Logger.LogTrace("Looping Bindings...."); - var bindingSiteName = binding.Properties["name"].Value.ToString(); - var bindingIpAddress = binding.Properties["Bindings"].Value.ToString()?.Split(':')[0]; - var bindingPort = binding.Properties["Bindings"].Value.ToString()?.Split(':')[1]; - var bindingHostName = binding.Properties["Bindings"].Value.ToString()?.Split(':')[2]; - var bindingProtocol = binding.Properties["Protocol"].Value.ToString(); - var bindingThumbprint = binding.Properties["thumbprint"].Value.ToString(); - var bindingSniFlg = binding.Properties["sniFlg"].Value.ToString(); - - Logger.LogTrace( - $"bindingSiteName: {bindingSiteName}, bindingIpAddress: {bindingIpAddress}, bindingPort: {bindingPort}, bindingHostName: {bindingHostName}, bindingProtocol: {bindingProtocol}, bindingThumbprint: {bindingThumbprint}, bindingSniFlg: {bindingSniFlg}"); - - //if the thumbprint of the renewal request matches the thumbprint of the cert in IIS, then renew it - if (Thumbprint == bindingThumbprint) + Logger.LogTrace($"Thumbprint Length > 0 {Thumbprint}"); + ps.AddCommand("Import-Module") + .AddParameter("Name", "WebAdministration") + .AddStatement(); + + Logger.LogTrace("WebAdministration Imported"); + var searchScript = + "Foreach($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]@{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation;thumbprint=$Bind.certificateHash;sniFlg=$Bind.sslFlags}}}"; + ps.AddScript(searchScript).AddStatement(); + Logger.LogTrace($"Search Script: {searchScript}"); + var bindings = ps.Invoke(); + foreach (var binding in bindings) { - Logger.LogTrace($"Thumbprint Match {Thumbprint}={bindingThumbprint}"); - funcScript = string.Format(@" + Logger.LogTrace("Looping Bindings...."); + var bindingSiteName = binding.Properties["name"].Value.ToString(); + var bindingIpAddress = binding.Properties["Bindings"].Value.ToString()?.Split(':')[0]; + var bindingPort = binding.Properties["Bindings"].Value.ToString()?.Split(':')[1]; + var bindingHostName = binding.Properties["Bindings"].Value.ToString()?.Split(':')[2]; + var bindingProtocol = binding.Properties["Protocol"].Value.ToString(); + var bindingThumbprint = binding.Properties["thumbprint"].Value.ToString(); + var bindingSniFlg = binding.Properties["sniFlg"].Value.ToString(); + + Logger.LogTrace( + $"bindingSiteName: {bindingSiteName}, bindingIpAddress: {bindingIpAddress}, bindingPort: {bindingPort}, bindingHostName: {bindingHostName}, bindingProtocol: {bindingProtocol}, bindingThumbprint: {bindingThumbprint}, bindingSniFlg: {bindingSniFlg}"); + + //if the thumbprint of the renewal request matches the thumbprint of the cert in IIS, then renew it + if (Thumbprint == bindingThumbprint) + { + Logger.LogTrace($"Thumbprint Match {Thumbprint}={bindingThumbprint}"); + funcScript = string.Format(@" $ErrorActionPreference = ""Stop"" $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} @@ -261,33 +262,33 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} }}", bindingSiteName, //{0} - bindingIpAddress, //{1} - bindingPort, //{2} - bindingProtocol, //{3} - bindingHostName, //{4} - x509Cert.Thumbprint, //{5} - Path, //{6} - bindingSniFlg); //{7} - - Logger.LogTrace($"funcScript {funcScript}"); - ps.AddScript(funcScript); - Logger.LogTrace("funcScript added..."); - ps.Invoke(); - Logger.LogTrace("funcScript Invoked..."); - foreach (var cmd in ps.Commands.Commands) - { - Logger.LogTrace("Logging PowerShell Command"); - Logger.LogTrace(cmd.CommandText); + bindingIpAddress, //{1} + bindingPort, //{2} + bindingProtocol, //{3} + bindingHostName, //{4} + x509Cert.Thumbprint, //{5} + Path, //{6} + bindingSniFlg); //{7} + + Logger.LogTrace($"funcScript {funcScript}"); + ps.AddScript(funcScript); + Logger.LogTrace("funcScript added..."); + ps.Invoke(); + Logger.LogTrace("funcScript Invoked..."); + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + ps.Commands.Clear(); + Logger.LogTrace("Commands Cleared.."); } - - ps.Commands.Clear(); - Logger.LogTrace("Commands Cleared.."); } } - } - else - { - funcScript = string.Format(@" + else + { + funcScript = string.Format(@" $ErrorActionPreference = ""Stop"" $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} @@ -300,52 +301,62 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} }}", SiteName, //{0} - IpAddress, //{1} - Port, //{2} - Protocol, //{3} - HostName, //{4} - x509Cert.Thumbprint, //{5} - Path, //{6} - Convert.ToInt16(SniFlag)); //{7} - foreach (var cmd in ps.Commands.Commands) - { - Logger.LogTrace("Logging PowerShell Command"); - Logger.LogTrace(cmd.CommandText); - } + IpAddress, //{1} + Port, //{2} + Protocol, //{3} + HostName, //{4} + x509Cert.Thumbprint, //{5} + Path, //{6} + Convert.ToInt16(SniFlag)); //{7} + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } - Logger.LogTrace($"funcScript {funcScript}"); - ps.AddScript(funcScript); - Logger.LogTrace("funcScript added..."); - ps.Invoke(); - Logger.LogTrace("funcScript Invoked..."); - } + Logger.LogTrace($"funcScript {funcScript}"); + ps.AddScript(funcScript); + Logger.LogTrace("funcScript added..."); + ps.Invoke(); + Logger.LogTrace("funcScript Invoked..."); + } - if (ps.HadErrors) - { - var psError = ps.Streams.Error.ReadAll() - .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); + if (ps.HadErrors) { - return new JobResult + var psError = ps.Streams.Error.ReadAll() + .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = JobHistoryId, - FailureMessage = - $"Site {Path} on server {ClientMachine}: {psError}" - }; + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + $"Site {Path} on server {ClientMachine}: {psError}" + }; + } } - } - Logger.LogTrace("closing RunSpace..."); - runSpace.Close(); - Logger.LogTrace("RunSpace Closed..."); + Logger.LogTrace("closing RunSpace..."); + runSpace.Close(); + Logger.LogTrace("RunSpace Closed..."); - return new JobResult + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Success, + JobHistoryId = JobHistoryId, + FailureMessage = "" + }; + } + catch (Exception e) { - Result = OrchestratorJobStatusJobResult.Success, - JobHistoryId = JobHistoryId, - FailureMessage = "" - }; + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" + }; + } } } } \ No newline at end of file From c7c76fd3904c7c99395067540f2d8dc5c7f41f05 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 6 Sep 2022 10:52:13 -0700 Subject: [PATCH 09/17] Commit Checkpoint --- IISU/IISManager.cs | 8 ++------ IISU/Jobs/Management.cs | 5 ++--- IISU/Jobs/ReEnrollment.cs | 37 ++++++++++++++++++++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs index ef7e4fd..7a67dcf 100644 --- a/IISU/IISManager.cs +++ b/IISU/IISManager.cs @@ -64,6 +64,8 @@ public JobResult ReEnrollCertificate(ReenrollmentJobConfiguration config) ConnectionInfo = new WSManConnectionInfo( new Uri($"{Properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{Properties?.WinRmPort}/wsman")); + + return InstallCertificate(); } catch (Exception e) { @@ -75,12 +77,6 @@ public JobResult ReEnrollCertificate(ReenrollmentJobConfiguration config) }; } - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = "UnExpected Error Occured" - }; } public JobResult AddCertificate(ManagementJobConfiguration config) diff --git a/IISU/Jobs/Management.cs b/IISU/Jobs/Management.cs index ce13081..4886726 100644 --- a/IISU/Jobs/Management.cs +++ b/IISU/Jobs/Management.cs @@ -227,8 +227,7 @@ private JobResult PerformAddition(ManagementJobConfiguration config) FailureMessage = failureMessage }; } - } - - + } + } } \ No newline at end of file diff --git a/IISU/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs index 3a31e6e..75fd589 100644 --- a/IISU/Jobs/ReEnrollment.cs +++ b/IISU/Jobs/ReEnrollment.cs @@ -1,5 +1,6 @@ using System; using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -22,13 +23,35 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm _logger.MethodEntry(); _logger.LogTrace($"Job Configuration: {JsonConvert.SerializeObject(config)}"); var storePath = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); - _logger.LogTrace($"WinRm Url: {storePath?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{storePath?.WinRmPort}/wsman"); - - - - - throw new NotImplementedException(); - + _logger.LogTrace($"WinRm Url: {storePath?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{storePath?.WinRmPort}/wsman"); + + _logger.LogTrace("Entering ReEnrollment..."); + _logger.LogTrace("Before ReEnrollment..."); + return PerformReEnrollment(config); + + } + + private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config) + { + try + { + _logger.MethodEntry(); + + var iisManager = new IISManager(); + return iisManager.ReEnrollCertificate(config); + } + catch (Exception ex) + { + var failureMessage = $"Add job failed for Site '{config.CertificateStoreDetails.StorePath}' on server '{config.CertificateStoreDetails.ClientMachine}' with error: '{LogHandler.FlattenException(ex)}'"; + _logger.LogWarning(failureMessage); + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = failureMessage + }; + } } } } From 7e7fe9da2cf37cb195786d2736928bf94cc3f6aa Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 6 Sep 2022 14:39:54 -0400 Subject: [PATCH 10/17] Commit Checkpoint --- IISU/IISManager.cs | 52 +++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs index 7a67dcf..6491e43 100644 --- a/IISU/IISManager.cs +++ b/IISU/IISManager.cs @@ -65,7 +65,7 @@ public JobResult ReEnrollCertificate(ReenrollmentJobConfiguration config) new WSManConnectionInfo( new Uri($"{Properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{Properties?.WinRmPort}/wsman")); - return InstallCertificate(); + return InstallCertificate(true); } catch (Exception e) { @@ -90,10 +90,9 @@ public JobResult AddCertificate(ManagementJobConfiguration config) SniFlag = config.JobProperties["SniFlag"].ToString()?.Substring(0, 1); IpAddress = config.JobProperties["IPAddress"].ToString(); - PrivateKeyPassword = config.JobCertificate.PrivateKeyPassword; //Todo set the private Key Password + PrivateKeyPassword = config.JobCertificate.PrivateKeyPassword; ServerUserName = config.ServerUsername; ServerPassword = config.ServerPassword; - Thumbprint = ""; //todo Set the Thumbprint ClientMachine = config.CertificateStoreDetails.ClientMachine; Path = config.CertificateStoreDetails.StorePath; CertContents = config.JobCertificate.Contents; @@ -112,7 +111,7 @@ public JobResult AddCertificate(ManagementJobConfiguration config) Logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {Thumbprint}"); } - return InstallCertificate(); + return InstallCertificate(false); } catch (Exception e) @@ -127,7 +126,7 @@ public JobResult AddCertificate(ManagementJobConfiguration config) } - private JobResult InstallCertificate() + private JobResult InstallCertificate(bool reEnrollment) { try { @@ -139,15 +138,40 @@ private JobResult InstallCertificate() ConnectionInfo.Credential = new PSCredential(ServerUserName, pw); Logger.LogTrace($"PSCredential Created {pw}"); - Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); - var x509Cert = new X509Certificate2( - Convert.FromBase64String(CertContents), - PrivateKeyPassword, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable); - Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); - Logger.LogTrace( - $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); + X509Certificate2 x509Cert; + + //If ReEnrollment + if (reEnrollment) + { + //***************** Cert content not coming from Keyfactor Enrollment UI You Must Create on the Machine Instead ************************************** + + //1. Get whatever new Properties are needed from the Cert Store Params for Alogo and such to generate the CSR and Keypair + + //2. responseContent=SomeWindowsCryptoFunction.GenerateCSR(subject from ReEnroll UI) //ToDo Generate a CSR + + //3. sign CSR in Keyfactor + //string body = $"{{\"CSR\": \"{responseContent}\",\"CertificateAuthority\": \"{storeProperties.CA}\", \"IncludeChain\": false, \"Metadata\": {{}}, \"Timestamp\": \"{DateTime.UtcNow.ToString("s")}\", \"Template\": \"{storeProperties.template}\"}}"; + //enrollResponse resp = MakeWebRequest(storeProperties.keyfactorHost + "/KeyfactorAPI/Enrollment/CSR", storeProperties.keyfactorUser, jobConfiguration.CertificateStoreDetails.StorePassword, body, skipCertCheck: true); + //string cert = resp.CertificateInformation.Certificates[0]; + //cert = cert.Substring(cert.IndexOf("-----")); + //_logger.LogDebug(cert); + + //4. Try Loading cert contents from step 2. into an X509Certificate2 object + x509Cert = new X509Certificate2(); //ToDo Replace this + } + else + { + Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); + x509Cert = new X509Certificate2( + Convert.FromBase64String(CertContents), + PrivateKeyPassword, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); + Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); + Logger.LogTrace( + $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); + } + using var runSpace = RunspaceFactory.CreateRunspace(ConnectionInfo); Logger.LogTrace("RunSpace Created"); From c44edd772fd78b5e57f6f99550e67b56c187e01d Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Wed, 19 Oct 2022 14:41:07 -0500 Subject: [PATCH 11/17] Added general code for ReEnrollment --- IISU/IISManager.cs | 17 +++- IISU/Jobs/Inventory.cs | 8 +- IISU/Jobs/ReEnrollment.cs | 43 +++++++++- IISU/PowerShellCertException.cs | 27 +++++++ IISU/PowerShellCertRequest.cs | 119 ++++++++++++++++++++++++++++ IISU/Properties/launchSettings.json | 11 +++ 6 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 IISU/PowerShellCertException.cs create mode 100644 IISU/PowerShellCertRequest.cs create mode 100644 IISU/Properties/launchSettings.json diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs index 6491e43..39c9cd6 100644 --- a/IISU/IISManager.cs +++ b/IISU/IISManager.cs @@ -143,6 +143,21 @@ private JobResult InstallCertificate(bool reEnrollment) //If ReEnrollment if (reEnrollment) { + ////// + // Create the private, public key value (from the local machine) + // .INF File + // Subject = "CN=Keyfactor SSL Certificate + // ProviderName = "Fortanix KMS CNG Provider" + // MachineKeySet = true + // CertificateTemplate = DDKeyfactorDatabaseEncrypt2yr + // SAN = "dns=fortanix.thedemodrive.com&dns=keyfactor.thedemodrive.com" + + // Generate the CSR and send it to get signed + // Phase I: Sign in Keyfactor + // Phase II: Get it signed by Fortanix + // Bind the new cert to IIS + ////// + //***************** Cert content not coming from Keyfactor Enrollment UI You Must Create on the Machine Instead ************************************** //1. Get whatever new Properties are needed from the Cert Store Params for Alogo and such to generate the CSR and Keypair @@ -238,7 +253,7 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st Logger.LogTrace("Commands Cleared.."); //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all - if (Thumbprint.Length > 0) + if (Thumbprint?.Length > 0) { Logger.LogTrace($"Thumbprint Length > 0 {Thumbprint}"); ps.AddCommand("Import-Module") diff --git a/IISU/Jobs/Inventory.cs b/IISU/Jobs/Inventory.cs index dbf75ac..934780c 100644 --- a/IISU/Jobs/Inventory.cs +++ b/IISU/Jobs/Inventory.cs @@ -132,11 +132,11 @@ private JobResult PerformInventory(InventoryJobConfiguration config, SubmitInven var siteSettingsDict = new Dictionary { - { "Site Name", binding.Properties["Name"]?.Value }, + { "SiteName", binding.Properties["Name"]?.Value }, { "Port", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[1] }, - { "IP Address", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[0] }, - { "Host Name", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[2] }, - { "Sni Flag", sniValue }, + { "IPAddress", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[0] }, + { "HostName", binding.Properties["Bindings"]?.Value.ToString()?.Split(':')[2] }, + { "SniFlag", sniValue }, { "Protocol", binding.Properties["Protocol"]?.Value } }; diff --git a/IISU/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs index 75fd589..882eb26 100644 --- a/IISU/Jobs/ReEnrollment.cs +++ b/IISU/Jobs/ReEnrollment.cs @@ -1,4 +1,7 @@ using System; +using System.Management.Automation.Runspaces; +using System.Net; +using System.Security.Cryptography.X509Certificates; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -27,18 +30,54 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm _logger.LogTrace("Entering ReEnrollment..."); _logger.LogTrace("Before ReEnrollment..."); - return PerformReEnrollment(config); + return PerformReEnrollment(config, submitReEnrollmentUpdate); } - private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config) + private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment) { try { _logger.MethodEntry(); + // Extract values necessary to create remote PS connection + JobProperties properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, + new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + + WSManConnectionInfo connectionInfo = new WSManConnectionInfo(new Uri($"{properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{properties?.WinRmPort}/wsman")); + connectionInfo.IncludePortInSPN = properties.SpnPortFlag; + var pw = new NetworkCredential(config.ServerUsername, config.ServerPassword).SecurePassword; + _logger.LogTrace($"Credentials: UserName:{config.ServerUsername} Password:{config.ServerPassword}"); + + connectionInfo.Credential = new System.Management.Automation.PSCredential(config.ServerUsername, pw); + _logger.LogTrace($"PSCredential Created {pw}"); + + // Establish new remote ps session + _logger.LogTrace("Creating remote PS Workspace"); + using var runSpace = RunspaceFactory.CreateRunspace(connectionInfo); + _logger.LogTrace("Workspace created"); + runSpace.Open(); + _logger.LogTrace("Workspace opened"); + + using var _ = new PowerShellCertRequest(config.CertificateStoreDetails.ClientMachine, config.CertificateStoreDetails.StorePath, runSpace); + + // Build INF file and create CSR + string CSRFilename = _.AddNewCertificate(config); + + // Sign CSR in Keyfactor + X509Certificate2 myCert = submitReenrollment.Invoke(CSRFilename); + + // Accept the signed cert + //x509object as encoded string to send back to powershell + //X509Certificate2 myCert = X509Certificate2.CreateFromCertFile("Myfile"); + _.AcceptCertificate(myCert.GetRawCertDataString()); + + runSpace.Close(); + + // Bind the certificate to IIS var iisManager = new IISManager(); return iisManager.ReEnrollCertificate(config); + } catch (Exception ex) { diff --git a/IISU/PowerShellCertException.cs b/IISU/PowerShellCertException.cs new file mode 100644 index 0000000..160176d --- /dev/null +++ b/IISU/PowerShellCertException.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +namespace Keyfactor.Extensions.Orchestrator.IISU +{ + [Serializable] + internal class PowerShellCertException : Exception + { + public PowerShellCertException() + { + } + + public PowerShellCertException(string message) : base(message) + { + } + + public PowerShellCertException(string message, Exception innerException) : base(message, innerException) + { + } + + protected PowerShellCertException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/IISU/PowerShellCertRequest.cs b/IISU/PowerShellCertRequest.cs new file mode 100644 index 0000000..5c963b6 --- /dev/null +++ b/IISU/PowerShellCertRequest.cs @@ -0,0 +1,119 @@ +using Keyfactor.Orchestrators.Extensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; + +namespace Keyfactor.Extensions.Orchestrator.IISU +{ + internal class PowerShellCertRequest : IDisposable + { + public Runspace RunSpace { get; set; } + public string ServerName { get; set; } + public string StorePath { get; set; } + + private PowerShell ps { get; set; } + + public PowerShellCertRequest(string serverName, string storePath, Runspace runspace) + { + ServerName = serverName; + StorePath = storePath; + RunSpace = runspace; + + ps = PowerShell.Create(); + ps.Runspace = runspace; + } + + /// + /// Executes the certreq -new command to create the CSR file + /// + /// Unsigned CSR Filename + public string AddNewCertificate(ReenrollmentJobConfiguration config) + { + // Define the variables sent from config argument + // Todo: Set the values that come from the Reenrollment object + string subject = "CN=Bobs Test Win Cert"; + string providerName = "Microsoft Enhanced RSA and AES Cryptographic Provider"; + string machineKeySet = "true"; + string certificateTemplate = "ExportableWebServer"; + string SAN = "SAN=\"dns=www.bobs.com\"&dns=bobs.com"; + + // Create the script file + ps.AddScript("$infFilename = New-TemporaryFile"); + ps.AddScript("$csrFilename = New-TemporaryFile"); + + ps.AddScript("if (Test-Path $csrFilename) { Remove-Item $csrFilename }"); + + ps.AddScript($"Set-Content $infFilename [NewRequest]"); + ps.AddScript($"Add-Content $infFilename 'Subject = \"{subject}\"'"); + ps.AddScript($"Add-Content $infFilename 'ProviderName = \"{providerName}\"'"); + ps.AddScript($"Add-Content $infFilename 'MachineKeySet = {machineKeySet}'"); + + ps.AddScript($"Add-Content $infFilename [RequestAttributes]"); + ps.AddScript($"Add-Content $infFilename 'CertificateTemplate = {certificateTemplate}'"); + ps.AddScript($"Add-Content $infFilename 'SAN = \"{SAN}\"'"); + + // Execute the -new command + ps.AddScript($"certreq -new -q $infFilename $csrFilename"); + ps.AddScript($"$CSR = Get-Content $csrFilename"); + + // Get the returned results back from Powershell + Collection results = ps.Invoke(); + + if (ps.HadErrors) + { + var psError = ps.Streams.Error.ReadAll().Aggregate(String.Empty, (current, error) => current + error.ErrorDetails.Message); + throw new PowerShellCertException($"Error creating CSR File. {psError}"); + } + + // Get the byte array + var CSRArray = ps.Runspace.SessionStateProxy.PSVariable.GetValue("CSR"); + string CSR = string.Empty; + foreach(object o in (IEnumerable)(CSRArray)) + { + CSR += o.ToString() + "\n"; + } + return CSR; + } + + public void SubmitCertificate() + { + // This gets done in KF Commnad + } + + /// + /// Executes the certreq -submit command to bind the signed certificate with the CA + /// + public void AcceptCertificate(string myCertificate) + { + ps.AddScript("$cerFilename = New-TemporaryFile"); + ps.Runspace.SessionStateProxy.SetVariable("$certBytes", myCertificate); + ps.AddScript("$Set-Content $cerFilename $certBytes"); + ps.Invoke(); + + ps.AddScript("certreq-accept $cerFilename"); + ps.Invoke(); + } + + public void Dispose() + { + try + { + ps.AddScript("if (Test-Path $infFilename) { Remove-Item $infFilename }"); + ps.AddScript("if (Test-Path $csrFilename) { Remove-Item $csrFilename }"); + ps.Invoke(); + } + catch (Exception) + { + } + finally + { + ps.Dispose(); + } + } + } +} diff --git a/IISU/Properties/launchSettings.json b/IISU/Properties/launchSettings.json new file mode 100644 index 0000000..e85a2ae --- /dev/null +++ b/IISU/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "IISU": { + "commandName": "Project", + "remoteDebugEnabled": true, + "authenticationMode": "None", + "remoteDebugMachine": "WIN-KFO100:4026", + "nativeDebugging": true + } + } +} \ No newline at end of file From ac2db8c59ecd54930dac51e942898f65fa2c5640 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 20 Oct 2022 21:44:32 +0000 Subject: [PATCH 12/17] Update generated README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b71e417..9dc8962 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The IIS Orchestrator treats the certificates bound (actively in use) on a Micros ## About the Keyfactor Universal Orchestrator Capability -This repository contains a Universal Orchestrator Capability which is a plugin to the Keyfactor Universal Orchestrator. Within the Keyfactor Platform, Orchestrators are used to manage “certificate stores” — collections of certificates and roots of trust that are found within and used by various applications. +This repository contains a Universal Orchestrator Extension which is a plugin to the Keyfactor Universal Orchestrator. Within the Keyfactor Platform, Orchestrators are used to manage “certificate stores” — collections of certificates and roots of trust that are found within and used by various applications. The Universal Orchestrator is part of the Keyfactor software distribution and is available via the Keyfactor customer portal. For general instructions on installing Capabilities, see the “Keyfactor Command Orchestrator Installation and Configuration Guide” section of the Keyfactor documentation. For configuration details of this specific Capability, see below in this readme. @@ -16,6 +16,7 @@ The Universal Orchestrator is the successor to the Windows Orchestrator. This Ca + ## Platform Specific Notes The Keyfactor Universal Orchestrator may be installed on either Windows or Linux based platforms. The certificate operations supported by a capability may vary based what platform the capability is installed on. The table below indicates what capabilities are supported based on which platform the encompassing Universal Orchestrator is running. From ff32c80e49558763a5800ee5d0beb567552eea4c Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 14 Nov 2022 14:55:07 +0000 Subject: [PATCH 13/17] Added ReEnrollment logic for Fortanix HSM --- IISU/IISManager.cs | 597 +++++++++++++++++----------------- IISU/IISU.csproj | 4 + IISU/Jobs/Management.cs | 4 +- IISU/Jobs/ReEnrollment.cs | 131 +++++++- IISU/PowerShellCertRequest.cs | 110 ++++--- 5 files changed, 487 insertions(+), 359 deletions(-) diff --git a/IISU/IISManager.cs b/IISU/IISManager.cs index 39c9cd6..59abc55 100644 --- a/IISU/IISManager.cs +++ b/IISU/IISManager.cs @@ -12,13 +12,8 @@ namespace Keyfactor.Extensions.Orchestrator.IISU { - public class IISManager + public class IISManager { - public IISManager() - { - Logger = LogHandler.GetClassLogger(); - } - private ILogger Logger { get; } private string SiteName { get; set; } private string IpAddress { get; set; } @@ -34,53 +29,62 @@ public IISManager() private string ServerUserName { get; set; } private string ServerPassword { get; set; } private JobProperties Properties { get; set; } - private string Thumbprint { get; set; } - private WSManConnectionInfo ConnectionInfo { get; set; } - - - public JobResult ReEnrollCertificate(ReenrollmentJobConfiguration config) - { - try - { - SiteName = config.JobProperties["SiteName"].ToString(); - Port = config.JobProperties["Port"].ToString(); - HostName = config.JobProperties["HostName"].ToString(); - Protocol = config.JobProperties["Protocol"].ToString(); - SniFlag= config.JobProperties["SniFlag"].ToString()?.Substring(0, 1); - IpAddress = config.JobProperties["IPAddress"].ToString(); - - PrivateKeyPassword = ""; //Todo set the private Key Password - ServerUserName = config.ServerUsername; - ServerPassword = config.ServerPassword; - Thumbprint = ""; //todo Set the Thumbprint - ClientMachine = config.CertificateStoreDetails.ClientMachine; - Path = config.CertificateStoreDetails.StorePath; - CertContents = ""; //Todo Generate CSR and Get Cert Contents - JobHistoryId = config.JobHistoryId; - - Properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, - new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); - - ConnectionInfo = - new WSManConnectionInfo( - new Uri($"{Properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{Properties?.WinRmPort}/wsman")); - - return InstallCertificate(true); - } - catch (Exception e) - { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" - }; - } - + private string RenewalThumbprint { get; set; } + private WSManConnectionInfo ConnectionInfo { get; set; } + + + private X509Certificate2 x509Cert; + private Runspace runSpace; + private PowerShell ps; + + #region Constructors + /// + /// Performs a Reenrollment of a certificate in IIS + /// + /// + public IISManager(ReenrollmentJobConfiguration config) + { + Logger = LogHandler.GetClassLogger(); + + try + { + SiteName = config.JobProperties["SiteName"].ToString(); + Port = config.JobProperties["Port"].ToString(); + HostName = config.JobProperties["HostName"].ToString(); + Protocol = config.JobProperties["Protocol"].ToString(); + SniFlag = config.JobProperties["SniFlag"].ToString()?.Substring(0, 1); + IpAddress = config.JobProperties["IPAddress"].ToString(); + + PrivateKeyPassword = ""; // A reenrollment does not have a PFX Password + ServerUserName = config.ServerUsername; + ServerPassword = config.ServerPassword; + RenewalThumbprint = ""; // A reenrollment will always be empty + ClientMachine = config.CertificateStoreDetails.ClientMachine; + Path = config.CertificateStoreDetails.StorePath; + CertContents = ""; // Not needed for a reenrollment + JobHistoryId = config.JobHistoryId; + + Properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties, + new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + + ConnectionInfo = + new WSManConnectionInfo( + new Uri($"{Properties?.WinRmProtocol}://{config.CertificateStoreDetails.ClientMachine}:{Properties?.WinRmPort}/wsman")); + } + catch (Exception e) + { + throw new Exception($"Error when initiating an IIS ReEnrollment Job: {e.Message}", e.InnerException); + } } - public JobResult AddCertificate(ManagementJobConfiguration config) - { + /// + /// Performs Management functions of Adding or updating certificates in IIS + /// + /// + public IISManager(ManagementJobConfiguration config) + { + Logger = LogHandler.GetClassLogger(); + try { SiteName = config.JobProperties["SiteName"].ToString(); @@ -90,7 +94,7 @@ public JobResult AddCertificate(ManagementJobConfiguration config) SniFlag = config.JobProperties["SniFlag"].ToString()?.Substring(0, 1); IpAddress = config.JobProperties["IPAddress"].ToString(); - PrivateKeyPassword = config.JobCertificate.PrivateKeyPassword; + PrivateKeyPassword = config.JobCertificate.PrivateKeyPassword; ServerUserName = config.ServerUsername; ServerPassword = config.ServerPassword; ClientMachine = config.CertificateStoreDetails.ClientMachine; @@ -107,102 +111,60 @@ public JobResult AddCertificate(ManagementJobConfiguration config) if (config.JobProperties.ContainsKey("RenewalThumbprint")) { - Thumbprint = config.JobProperties["RenewalThumbprint"].ToString(); - Logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {Thumbprint}"); + RenewalThumbprint = config.JobProperties["RenewalThumbprint"].ToString(); + Logger.LogTrace($"Found Thumbprint Will Renew all Certs with this thumbprint: {RenewalThumbprint}"); } - return InstallCertificate(false); - } catch (Exception e) { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" - }; + throw new Exception($"Error when initiating an IIS Management Job: {e.Message}", e.InnerException); + } + } + + #endregion + + public JobResult ReEnrollCertificate(X509Certificate2 certificate) + { + x509Cert = certificate; + + try + { + // Instanciate a new Powershell instance + CreatePowerShellInstance(); + + return BindCertificate(); + + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = $"Error Occurred in ReEnrollCertification {LogHandler.FlattenException(e)}" + }; } } - - private JobResult InstallCertificate(bool reEnrollment) - { - try - { - Logger.LogTrace($"IncludePortInSPN: {Properties.SpnPortFlag}"); - ConnectionInfo.IncludePortInSPN = Properties.SpnPortFlag; - Logger.LogTrace($"Credentials: UserName:{ServerUserName} Password:{ServerPassword}"); - var pw = new NetworkCredential(ServerUserName, ServerPassword) - .SecurePassword; - ConnectionInfo.Credential = new PSCredential(ServerUserName, pw); - Logger.LogTrace($"PSCredential Created {pw}"); - - X509Certificate2 x509Cert; - - //If ReEnrollment - if (reEnrollment) - { - ////// - // Create the private, public key value (from the local machine) - // .INF File - // Subject = "CN=Keyfactor SSL Certificate - // ProviderName = "Fortanix KMS CNG Provider" - // MachineKeySet = true - // CertificateTemplate = DDKeyfactorDatabaseEncrypt2yr - // SAN = "dns=fortanix.thedemodrive.com&dns=keyfactor.thedemodrive.com" - - // Generate the CSR and send it to get signed - // Phase I: Sign in Keyfactor - // Phase II: Get it signed by Fortanix - // Bind the new cert to IIS - ////// - - //***************** Cert content not coming from Keyfactor Enrollment UI You Must Create on the Machine Instead ************************************** - - //1. Get whatever new Properties are needed from the Cert Store Params for Alogo and such to generate the CSR and Keypair - - //2. responseContent=SomeWindowsCryptoFunction.GenerateCSR(subject from ReEnroll UI) //ToDo Generate a CSR - - //3. sign CSR in Keyfactor - //string body = $"{{\"CSR\": \"{responseContent}\",\"CertificateAuthority\": \"{storeProperties.CA}\", \"IncludeChain\": false, \"Metadata\": {{}}, \"Timestamp\": \"{DateTime.UtcNow.ToString("s")}\", \"Template\": \"{storeProperties.template}\"}}"; - //enrollResponse resp = MakeWebRequest(storeProperties.keyfactorHost + "/KeyfactorAPI/Enrollment/CSR", storeProperties.keyfactorUser, jobConfiguration.CertificateStoreDetails.StorePassword, body, skipCertCheck: true); - //string cert = resp.CertificateInformation.Certificates[0]; - //cert = cert.Substring(cert.IndexOf("-----")); - //_logger.LogDebug(cert); - - //4. Try Loading cert contents from step 2. into an X509Certificate2 object - x509Cert = new X509Certificate2(); //ToDo Replace this - } - else - { - Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); - x509Cert = new X509Certificate2( - Convert.FromBase64String(CertContents), - PrivateKeyPassword, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.Exportable); - Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); - Logger.LogTrace( - $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); - } - - - using var runSpace = RunspaceFactory.CreateRunspace(ConnectionInfo); - Logger.LogTrace("RunSpace Created"); - runSpace.Open(); - Logger.LogTrace("RunSpace Opened"); - Logger.LogTrace( - $"Creating Cert Store with ClientMachine: {ClientMachine}, JobProperties: {Path}"); - var _ = new PowerShellCertStore( - ClientMachine, Path, - runSpace); - Logger.LogTrace("Cert Store Created"); - using var ps = PowerShell.Create(); - Logger.LogTrace("ps created"); - ps.Runspace = runSpace; - Logger.LogTrace("RunSpace Assigned"); - + public JobResult AddCertificate() + { + try + { + Logger.LogTrace($"Creating X509 Cert from: {CertContents}"); + x509Cert = new X509Certificate2( + Convert.FromBase64String(CertContents), + PrivateKeyPassword, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.Exportable); + Logger.LogTrace($"X509 Cert Created With Subject: {x509Cert.SubjectName}"); + Logger.LogTrace( + $"Begin Add for Cert Store {$@"\\{ClientMachine}\{Path}"}"); + + // Instanciate a new Powershell instance + CreatePowerShellInstance(); + + // Add Certificate var funcScript = @" $ErrorActionPreference = ""Stop"" @@ -212,79 +174,123 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $password, 18 <# Persist, Machine #> $certStore.Add($cert) $certStore.Close(); - }"; - - ps.AddScript(funcScript).AddStatement(); - Logger.LogTrace("InstallPfxToMachineStore Statement Added..."); - ps.AddCommand("InstallPfxToMachineStore") - .AddParameter("bytes", Convert.FromBase64String(CertContents)) - .AddParameter("password", PrivateKeyPassword) - .AddParameter("storeName", - $@"\\{ClientMachine}\{Path}"); - Logger.LogTrace("InstallPfxToMachineStore Command Added..."); - - foreach (var cmd in ps.Commands.Commands) - { - Logger.LogTrace("Logging PowerShell Command"); - Logger.LogTrace(cmd.CommandText); - } - - Logger.LogTrace("Invoking ps..."); - ps.Invoke(); - Logger.LogTrace("ps Invoked..."); - if (ps.HadErrors) - { - Logger.LogTrace("ps Has Errors"); - var psError = ps.Streams.Error.ReadAll() - .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); - { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = JobHistoryId, - FailureMessage = - $"Site {Path} on server {ClientMachine}: {psError}" - }; - } - } - - Logger.LogTrace("Clearing Commands..."); - ps.Commands.Clear(); - Logger.LogTrace("Commands Cleared.."); - - //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all - if (Thumbprint?.Length > 0) - { - Logger.LogTrace($"Thumbprint Length > 0 {Thumbprint}"); - ps.AddCommand("Import-Module") - .AddParameter("Name", "WebAdministration") - .AddStatement(); - - Logger.LogTrace("WebAdministration Imported"); - var searchScript = - "Foreach($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]@{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation;thumbprint=$Bind.certificateHash;sniFlg=$Bind.sslFlags}}}"; - ps.AddScript(searchScript).AddStatement(); - Logger.LogTrace($"Search Script: {searchScript}"); - var bindings = ps.Invoke(); - foreach (var binding in bindings) - { - Logger.LogTrace("Looping Bindings...."); - var bindingSiteName = binding.Properties["name"].Value.ToString(); - var bindingIpAddress = binding.Properties["Bindings"].Value.ToString()?.Split(':')[0]; - var bindingPort = binding.Properties["Bindings"].Value.ToString()?.Split(':')[1]; - var bindingHostName = binding.Properties["Bindings"].Value.ToString()?.Split(':')[2]; - var bindingProtocol = binding.Properties["Protocol"].Value.ToString(); - var bindingThumbprint = binding.Properties["thumbprint"].Value.ToString(); - var bindingSniFlg = binding.Properties["sniFlg"].Value.ToString(); + }"; + + ps.AddScript(funcScript).AddStatement(); + Logger.LogTrace("InstallPfxToMachineStore Statement Added..."); + ps.AddCommand("InstallPfxToMachineStore") + .AddParameter("bytes", Convert.FromBase64String(CertContents)) + .AddParameter("password", PrivateKeyPassword) + .AddParameter("storeName", + $@"\\{ClientMachine}\{Path}"); + Logger.LogTrace("InstallPfxToMachineStore Command Added..."); + + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + Logger.LogTrace("Invoking ps..."); + ps.Invoke(); + Logger.LogTrace("ps Invoked..."); + if (ps.HadErrors) + { + Logger.LogTrace("ps Has Errors"); + var psError = ps.Streams.Error.ReadAll() + .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + $"Site {Path} on server {ClientMachine}: {psError}" + }; + } + } + + Logger.LogTrace("Clearing Commands..."); + ps.Commands.Clear(); + Logger.LogTrace("Commands Cleared.."); + + // Install the certifiacte + return BindCertificate(); + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" + }; + } + } - Logger.LogTrace( - $"bindingSiteName: {bindingSiteName}, bindingIpAddress: {bindingIpAddress}, bindingPort: {bindingPort}, bindingHostName: {bindingHostName}, bindingProtocol: {bindingProtocol}, bindingThumbprint: {bindingThumbprint}, bindingSniFlg: {bindingSniFlg}"); + private void CreatePowerShellInstance() + { + Logger.LogTrace($"IncludePortInSPN: {Properties.SpnPortFlag}"); + ConnectionInfo.IncludePortInSPN = Properties.SpnPortFlag; + Logger.LogTrace($"Credentials: UserName:{ServerUserName} Password:{ServerPassword}"); + var pw = new NetworkCredential(ServerUserName, ServerPassword) + .SecurePassword; + ConnectionInfo.Credential = new PSCredential(ServerUserName, pw); + Logger.LogTrace($"PSCredential Created {pw}"); + + runSpace = RunspaceFactory.CreateRunspace(ConnectionInfo); + Logger.LogTrace("RunSpace Created"); + runSpace.Open(); + Logger.LogTrace("RunSpace Opened"); + Logger.LogTrace( + $"Creating Cert Store with ClientMachine: {ClientMachine}, JobProperties: {Path}"); + var _ = new PowerShellCertStore( + ClientMachine, Path, + runSpace); + Logger.LogTrace("Cert Store Created"); + ps = PowerShell.Create(); + Logger.LogTrace("ps created"); + ps.Runspace = runSpace; + Logger.LogTrace("RunSpace Assigned"); + } - //if the thumbprint of the renewal request matches the thumbprint of the cert in IIS, then renew it - if (Thumbprint == bindingThumbprint) - { - Logger.LogTrace($"Thumbprint Match {Thumbprint}={bindingThumbprint}"); - funcScript = string.Format(@" + private JobResult BindCertificate() + { + try + { + //if thumbprint is there it is a renewal so we have to search all the sites for that thumbprint and renew them all + if (RenewalThumbprint?.Length > 0) + { + Logger.LogTrace($"Thumbprint Length > 0 {RenewalThumbprint}"); + ps.AddCommand("Import-Module") + .AddParameter("Name", "WebAdministration") + .AddStatement(); + + Logger.LogTrace("WebAdministration Imported"); + var searchScript = + "Foreach($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]@{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation;thumbprint=$Bind.certificateHash;sniFlg=$Bind.sslFlags}}}"; + ps.AddScript(searchScript).AddStatement(); + Logger.LogTrace($"Search Script: {searchScript}"); + var bindings = ps.Invoke(); + foreach (var binding in bindings) + { + Logger.LogTrace("Looping Bindings...."); + var bindingSiteName = binding.Properties["name"].Value.ToString(); + var bindingIpAddress = binding.Properties["Bindings"].Value.ToString()?.Split(':')[0]; + var bindingPort = binding.Properties["Bindings"].Value.ToString()?.Split(':')[1]; + var bindingHostName = binding.Properties["Bindings"].Value.ToString()?.Split(':')[2]; + var bindingProtocol = binding.Properties["Protocol"].Value.ToString(); + var bindingThumbprint = binding.Properties["thumbprint"].Value.ToString(); + var bindingSniFlg = binding.Properties["sniFlg"].Value.ToString(); + + Logger.LogTrace( + $"bindingSiteName: {bindingSiteName}, bindingIpAddress: {bindingIpAddress}, bindingPort: {bindingPort}, bindingHostName: {bindingHostName}, bindingProtocol: {bindingProtocol}, bindingThumbprint: {bindingThumbprint}, bindingSniFlg: {bindingSniFlg}"); + + //if the thumbprint of the renewal request matches the thumbprint of the cert in IIS, then renew it + if (RenewalThumbprint == bindingThumbprint) + { + Logger.LogTrace($"Thumbprint Match {RenewalThumbprint}={bindingThumbprint}"); + var funcScript = string.Format(@" $ErrorActionPreference = ""Stop"" $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} @@ -296,34 +302,34 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st New-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" -SslFlags ""{7}"" Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} - }}", bindingSiteName, //{0} - bindingIpAddress, //{1} - bindingPort, //{2} - bindingProtocol, //{3} - bindingHostName, //{4} - x509Cert.Thumbprint, //{5} - Path, //{6} - bindingSniFlg); //{7} - - Logger.LogTrace($"funcScript {funcScript}"); - ps.AddScript(funcScript); - Logger.LogTrace("funcScript added..."); - ps.Invoke(); - Logger.LogTrace("funcScript Invoked..."); - foreach (var cmd in ps.Commands.Commands) - { - Logger.LogTrace("Logging PowerShell Command"); - Logger.LogTrace(cmd.CommandText); - } - - ps.Commands.Clear(); - Logger.LogTrace("Commands Cleared.."); - } - } - } - else - { - funcScript = string.Format(@" + }}", bindingSiteName, //{0} + bindingIpAddress, //{1} + bindingPort, //{2} + bindingProtocol, //{3} + bindingHostName, //{4} + x509Cert.Thumbprint, //{5} + Path, //{6} + bindingSniFlg); //{7} + + Logger.LogTrace($"funcScript {funcScript}"); + ps.AddScript(funcScript); + Logger.LogTrace("funcScript added..."); + ps.Invoke(); + Logger.LogTrace("funcScript Invoked..."); + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + ps.Commands.Clear(); + Logger.LogTrace("Commands Cleared.."); + } + } + } + else + { + var funcScript = string.Format(@" $ErrorActionPreference = ""Stop"" $IISInstalled = Get-Module -ListAvailable | where {{$_.Name -eq ""WebAdministration""}} @@ -335,63 +341,62 @@ function InstallPfxToMachineStore([byte[]]$bytes, [string]$password, [string]$st New-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" -SslFlags ""{7}"" Get-WebBinding -Name ""{0}"" -IPAddress ""{1}"" -HostHeader ""{4}"" -Port ""{2}"" -Protocol ""{3}"" | ForEach-Object {{ $_.AddSslCertificate(""{5}"", ""{6}"") }} - }}", SiteName, //{0} - IpAddress, //{1} - Port, //{2} - Protocol, //{3} - HostName, //{4} - x509Cert.Thumbprint, //{5} - Path, //{6} - Convert.ToInt16(SniFlag)); //{7} - foreach (var cmd in ps.Commands.Commands) - { - Logger.LogTrace("Logging PowerShell Command"); - Logger.LogTrace(cmd.CommandText); - } - - Logger.LogTrace($"funcScript {funcScript}"); - ps.AddScript(funcScript); - Logger.LogTrace("funcScript added..."); - ps.Invoke(); - Logger.LogTrace("funcScript Invoked..."); - } - - if (ps.HadErrors) - { - var psError = ps.Streams.Error.ReadAll() - .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); - { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = JobHistoryId, - FailureMessage = - $"Site {Path} on server {ClientMachine}: {psError}" - }; - } - } - - Logger.LogTrace("closing RunSpace..."); - runSpace.Close(); - Logger.LogTrace("RunSpace Closed..."); - - - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Success, - JobHistoryId = JobHistoryId, - FailureMessage = "" - }; - } - catch (Exception e) - { - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = JobHistoryId, - FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" - }; - } + }}", SiteName, //{0} + IpAddress, //{1} + Port, //{2} + Protocol, //{3} + HostName, //{4} + x509Cert.Thumbprint, //{5} + Path, //{6} + Convert.ToInt16(SniFlag)); //{7} + foreach (var cmd in ps.Commands.Commands) + { + Logger.LogTrace("Logging PowerShell Command"); + Logger.LogTrace(cmd.CommandText); + } + + Logger.LogTrace($"funcScript {funcScript}"); + ps.AddScript(funcScript); + Logger.LogTrace("funcScript added..."); + ps.Invoke(); + Logger.LogTrace("funcScript Invoked..."); + } + + if (ps.HadErrors) + { + var psError = ps.Streams.Error.ReadAll() + .Aggregate(string.Empty, (current, error) => current + error.ErrorDetails.Message); + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = + $"Site {Path} on server {ClientMachine}: {psError}" + }; + } + } + + Logger.LogTrace("closing RunSpace..."); + runSpace.Close(); + Logger.LogTrace("RunSpace Closed..."); + + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Success, + JobHistoryId = JobHistoryId, + FailureMessage = "" + }; + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = JobHistoryId, + FailureMessage = $"Error Occurred in InstallCertificate {LogHandler.FlattenException(e)}" + }; + } } } } \ No newline at end of file diff --git a/IISU/IISU.csproj b/IISU/IISU.csproj index 191424a..45621eb 100644 --- a/IISU/IISU.csproj +++ b/IISU/IISU.csproj @@ -11,6 +11,10 @@ false + + + + diff --git a/IISU/Jobs/Management.cs b/IISU/Jobs/Management.cs index 4886726..64e651c 100644 --- a/IISU/Jobs/Management.cs +++ b/IISU/Jobs/Management.cs @@ -212,8 +212,8 @@ private JobResult PerformAddition(ManagementJobConfiguration config) { _logger.MethodEntry(); - var iisManager=new IISManager(); - return iisManager.AddCertificate(config); + var iisManager=new IISManager(config); + return iisManager.AddCertificate(); } catch (Exception ex) { diff --git a/IISU/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs index 882eb26..ec3415c 100644 --- a/IISU/Jobs/ReEnrollment.cs +++ b/IISU/Jobs/ReEnrollment.cs @@ -1,7 +1,12 @@ using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Net; using System.Security.Cryptography.X509Certificates; +using System.Text; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -49,7 +54,7 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi var pw = new NetworkCredential(config.ServerUsername, config.ServerPassword).SecurePassword; _logger.LogTrace($"Credentials: UserName:{config.ServerUsername} Password:{config.ServerPassword}"); - connectionInfo.Credential = new System.Management.Automation.PSCredential(config.ServerUsername, pw); + connectionInfo.Credential = new PSCredential(config.ServerUsername, pw); _logger.LogTrace($"PSCredential Created {pw}"); // Establish new remote ps session @@ -59,29 +64,123 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi runSpace.Open(); _logger.LogTrace("Workspace opened"); - using var _ = new PowerShellCertRequest(config.CertificateStoreDetails.ClientMachine, config.CertificateStoreDetails.StorePath, runSpace); + // NEW + var ps = PowerShell.Create(); + ps.Runspace = runSpace; - // Build INF file and create CSR - string CSRFilename = _.AddNewCertificate(config); + string CSR = string.Empty; - // Sign CSR in Keyfactor - X509Certificate2 myCert = submitReenrollment.Invoke(CSRFilename); + var subjectText = config.JobProperties["subjectText"]; + var providerName = config.JobProperties["ProviderName"]; + var keyType = config.JobProperties["keyType"]; + var keySize = config.JobProperties["keySize"]; + var SAN = config.JobProperties["SAN"]; + + // Create the script file + ps.AddScript("$infFilename = New-TemporaryFile"); + ps.AddScript("$csrFilename = New-TemporaryFile"); + + ps.AddScript("if (Test-Path $csrFilename) { Remove-Item $csrFilename }"); + + ps.AddScript($"Set-Content $infFilename [NewRequest]"); + ps.AddScript($"Add-Content $infFilename 'Subject = \"{subjectText}\"'"); + ps.AddScript($"Add-Content $infFilename 'ProviderName = \"{providerName}\"'"); + ps.AddScript($"Add-Content $infFilename 'MachineKeySet = True'"); + ps.AddScript($"Add-Content $infFilename 'HashAlgorithm = SHA256'"); + ps.AddScript($"Add-Content $infFilename 'KeyAlgorithm = {keyType}'"); + ps.AddScript($"Add-Content $infFilename 'KeyLength={keySize}'"); + ps.AddScript($"Add-Content $infFilename 'KeySpec = 0'"); + + ps.AddScript($"Add-Content $infFilename '[Extensions]'"); + ps.AddScript(@"Add-Content $infFilename '2.5.29.17 = ""{text}""'"); - // Accept the signed cert - //x509object as encoded string to send back to powershell - //X509Certificate2 myCert = X509Certificate2.CreateFromCertFile("Myfile"); - _.AcceptCertificate(myCert.GetRawCertDataString()); + // Todo: Parse SAN by '&' and add the below entry for each DSN + foreach (string s in SAN.ToString().Split("&")) + { + ps.AddScript($"Add-Content $infFilename '_continue_ = \"{s + "&"}\"'"); + } - runSpace.Close(); + // Execute the -new command + ps.AddScript($"certreq -new -q $infFilename $csrFilename"); + + Collection results = ps.Invoke(); + ps.Commands.Clear(); + + try + { + ps.AddScript($"$CSR = Get-Content $csrFilename"); + results = ps.Invoke(); + } + catch (Exception e) + { + var psError = ps.Streams.Error.ReadAll().Aggregate(String.Empty, (current, error) => current + error.ErrorDetails.Message); + throw new PowerShellCertException($"Error creating CSR File. {psError}"); + } + finally + { + ps.Commands.Clear(); + + // Delete the temp files + ps.AddScript("if (Test-Path $infFilename) { Remove-Item -Path $infFilename }"); + ps.AddScript("if (Test-Path $csrFilename) { Remove-Item -Path $csrFilename }"); + results = ps.Invoke(); + } + + // Get the byte array + var CSRContent = ps.Runspace.SessionStateProxy.GetVariable("CSR").ToString(); + + // Sign CSR in Keyfactor + X509Certificate2 myCert = submitReenrollment.Invoke(CSRContent); + + if (myCert != null) + { + // Get the cert data into string format + string csrData = Convert.ToBase64String(myCert.RawData, Base64FormattingOptions.InsertLineBreaks); + + // Write out the cert file + StringBuilder sb = new StringBuilder(); + sb.AppendLine("-----BEGIN CERTIFICATE-----"); + sb.AppendLine(csrData); + sb.AppendLine("-----END CERTIFICATE-----"); + + ps.AddScript("$cerFilename = New-TemporaryFile"); + ps.AddScript($"Set-Content $cerFilename '{sb}'"); + + results = ps.Invoke(); + ps.Commands.Clear(); + + // Accept the signed cert + ps.AddScript("certreq -accept $cerFilename"); + ps.Invoke(); + ps.Commands.Clear(); + + // Delete the temp files + ps.AddScript("if (Test-Path $infFilename) { Remove-Item -Path $infFilename }"); + ps.AddScript("if (Test-Path $csrFilename) { Remove-Item -Path $csrFilename }"); + ps.AddScript("if (Test-Path $cerFilename) { Remove-Item -Path $cerFilename }"); + results = ps.Invoke(); + + ps.Commands.Clear(); + runSpace.Close(); + + // Bind the certificate to IIS + var iisManager = new IISManager(config); + return iisManager.ReEnrollCertificate(myCert); + } + else + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "The ReEnrollment job was unable to sign te CSR. Please check the formatting of the SAN and other ReEnrollment properties." + }; + } - // Bind the certificate to IIS - var iisManager = new IISManager(); - return iisManager.ReEnrollCertificate(config); - } catch (Exception ex) { - var failureMessage = $"Add job failed for Site '{config.CertificateStoreDetails.StorePath}' on server '{config.CertificateStoreDetails.ClientMachine}' with error: '{LogHandler.FlattenException(ex)}'"; + var failureMessage = $"ReEnrollment job failed for Site '{config.CertificateStoreDetails.StorePath}' on server '{config.CertificateStoreDetails.ClientMachine}' with error: '{LogHandler.FlattenException(ex)}'"; _logger.LogWarning(failureMessage); return new JobResult diff --git a/IISU/PowerShellCertRequest.cs b/IISU/PowerShellCertRequest.cs index 5c963b6..056b75c 100644 --- a/IISU/PowerShellCertRequest.cs +++ b/IISU/PowerShellCertRequest.cs @@ -1,4 +1,5 @@ using Keyfactor.Orchestrators.Extensions; +using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; @@ -18,14 +19,15 @@ internal class PowerShellCertRequest : IDisposable private PowerShell ps { get; set; } - public PowerShellCertRequest(string serverName, string storePath, Runspace runspace) + public PowerShellCertRequest(WSManConnectionInfo connectionInfo, string serverName, string storePath) //, Runspace runspace) { ServerName = serverName; StorePath = storePath; - RunSpace = runspace; + //RunSpace = runspace; ps = PowerShell.Create(); - ps.Runspace = runspace; + ps.Runspace = RunspaceFactory.CreateRunspace(connectionInfo); // = runspace; + ps.Runspace.Open(); } /// @@ -34,50 +36,68 @@ public PowerShellCertRequest(string serverName, string storePath, Runspace runsp /// Unsigned CSR Filename public string AddNewCertificate(ReenrollmentJobConfiguration config) { - // Define the variables sent from config argument - // Todo: Set the values that come from the Reenrollment object - string subject = "CN=Bobs Test Win Cert"; - string providerName = "Microsoft Enhanced RSA and AES Cryptographic Provider"; - string machineKeySet = "true"; - string certificateTemplate = "ExportableWebServer"; - string SAN = "SAN=\"dns=www.bobs.com\"&dns=bobs.com"; - - // Create the script file - ps.AddScript("$infFilename = New-TemporaryFile"); - ps.AddScript("$csrFilename = New-TemporaryFile"); - - ps.AddScript("if (Test-Path $csrFilename) { Remove-Item $csrFilename }"); - - ps.AddScript($"Set-Content $infFilename [NewRequest]"); - ps.AddScript($"Add-Content $infFilename 'Subject = \"{subject}\"'"); - ps.AddScript($"Add-Content $infFilename 'ProviderName = \"{providerName}\"'"); - ps.AddScript($"Add-Content $infFilename 'MachineKeySet = {machineKeySet}'"); - - ps.AddScript($"Add-Content $infFilename [RequestAttributes]"); - ps.AddScript($"Add-Content $infFilename 'CertificateTemplate = {certificateTemplate}'"); - ps.AddScript($"Add-Content $infFilename 'SAN = \"{SAN}\"'"); - - // Execute the -new command - ps.AddScript($"certreq -new -q $infFilename $csrFilename"); - ps.AddScript($"$CSR = Get-Content $csrFilename"); - - // Get the returned results back from Powershell - Collection results = ps.Invoke(); - - if (ps.HadErrors) - { - var psError = ps.Streams.Error.ReadAll().Aggregate(String.Empty, (current, error) => current + error.ErrorDetails.Message); - throw new PowerShellCertException($"Error creating CSR File. {psError}"); - } - - // Get the byte array - var CSRArray = ps.Runspace.SessionStateProxy.PSVariable.GetValue("CSR"); string CSR = string.Empty; - foreach(object o in (IEnumerable)(CSRArray)) - { - CSR += o.ToString() + "\n"; - } + + var subjectText = config.JobProperties["subjectText"]; + var providerName = config.JobProperties["ProviderName"]; + //var keyType = config.JobProperties["keyType"]; + //var keySize = config.JobProperties["keySize"]; + var SAN = config.JobProperties["SAN"]; + + try + { + // Create the script file + ps.AddScript("$infFilename = New-TemporaryFile"); + ps.AddScript("$csrFilename = New-TemporaryFile"); + + ps.AddScript("if (Test-Path $csrFilename) { Remove-Item $csrFilename }"); + + //Collection results = ps.Invoke(); + + ps.AddScript($"Set-Content $infFilename [NewRequest]"); + ps.AddScript($"Add-Content $infFilename 'Subject = \"{subjectText}\"'"); + ps.AddScript($"Add-Content $infFilename 'ProviderName = \"{providerName}\"'"); + ps.AddScript($"Add-Content $infFilename 'MachineKeySet = True"); + ps.AddScript($"Add-Content $infFilename 'KeySpec = 0"); + + //results = ps.Invoke(); + + ps.AddScript($"Add-Content $infFilename [RequestAttributes]"); + ps.AddScript($"Add-Content $infFilename 'SAN = \"{SAN}\"'"); + + //results = ps.Invoke(); + + // Execute the -new command + ps.AddScript($"certreq -new -q $infFilename $csrFilename"); + Collection results = ps.Invoke(); + + ps.AddScript($"$CSR = Get-Content $csrFilename"); + + // Get the returned results back from Powershell + // Collection results = ps.Invoke(); + results = ps.Invoke(); + + if (ps.HadErrors) + { + var psError = ps.Streams.Error.ReadAll().Aggregate(String.Empty, (current, error) => current + error.ErrorDetails.Message); + throw new PowerShellCertException($"Error creating CSR File. {psError}"); + } + + // Get the byte array + var CSRArray = ps.Runspace.SessionStateProxy.PSVariable.GetValue("CSR"); + + foreach (object o in (IEnumerable)(CSRArray)) + { + CSR += o.ToString() + "\n"; + } + return CSR; + + } + catch (Exception ex) + { + throw ex; + } } public void SubmitCertificate() From fa12bf8ecc8666b8f1cedf54b7de7eaf9614c1cd Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Mon, 14 Nov 2022 14:55:52 +0000 Subject: [PATCH 14/17] Update generated README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9dc8962..828329d 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,15 @@ The IIS Orchestrator treats the certificates bound (actively in use) on a Micros ## About the Keyfactor Universal Orchestrator Capability -This repository contains a Universal Orchestrator Extension which is a plugin to the Keyfactor Universal Orchestrator. Within the Keyfactor Platform, Orchestrators are used to manage “certificate stores” — collections of certificates and roots of trust that are found within and used by various applications. +This repository contains a Universal Orchestrator Capability which is a plugin to the Keyfactor Universal Orchestrator. Within the Keyfactor Platform, Orchestrators are used to manage “certificate stores” — collections of certificates and roots of trust that are found within and used by various applications. The Universal Orchestrator is part of the Keyfactor software distribution and is available via the Keyfactor customer portal. For general instructions on installing Capabilities, see the “Keyfactor Command Orchestrator Installation and Configuration Guide” section of the Keyfactor documentation. For configuration details of this specific Capability, see below in this readme. The Universal Orchestrator is the successor to the Windows Orchestrator. This Capability plugin only works with the Universal Orchestrator and does not work with the Windows Orchestrator. + + + --- @@ -19,6 +22,8 @@ The Universal Orchestrator is the successor to the Windows Orchestrator. This Ca ## Platform Specific Notes +The minimum version of the Universal Orchestrator Framework needed to run this version of the extension is + The Keyfactor Universal Orchestrator may be installed on either Windows or Linux based platforms. The certificate operations supported by a capability may vary based what platform the capability is installed on. The table below indicates what capabilities are supported based on which platform the encompassing Universal Orchestrator is running. | Operation | Win | Linux | |-----|-----|------| @@ -33,6 +38,7 @@ The Keyfactor Universal Orchestrator may be installed on either Windows or Linux --- + **IIS Orchestrator Configuration** **Overview** From 8d9a6043ed75fd503fe4b3d3fe36ec91bc8578ab Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 21 Nov 2022 15:40:21 -0600 Subject: [PATCH 15/17] Added additional logging for debugging purposes. Updated the ReadMe.MD to reflect the new reEnrollment functionality. --- IISU.sln | 21 ++++++++------------- IISU/Jobs/ReEnrollment.cs | 27 ++++++++++++++++++++------- images/ReEnrollment1.png | Bin 0 -> 24937 bytes images/ReEnrollment1a.png | Bin 0 -> 9228 bytes images/ReEnrollment1b.png | Bin 0 -> 15240 bytes images/Screen1.png | Bin 0 -> 25047 bytes images/Screen2.png | Bin 0 -> 23033 bytes integration-manifest.json | 2 +- readme_source.md | 7 ++++++- 9 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 images/ReEnrollment1.png create mode 100644 images/ReEnrollment1a.png create mode 100644 images/ReEnrollment1b.png create mode 100644 images/Screen1.png create mode 100644 images/Screen2.png diff --git a/IISU.sln b/IISU.sln index 8c6a7ed..65b071d 100644 --- a/IISU.sln +++ b/IISU.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30717.126 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISU", "IISU\IISU.csproj", "{33FBC5A1-3466-4F10-B9A6-7186F804A65A}" EndProject @@ -10,21 +10,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CHANGELOG.md = CHANGELOG.md integration-manifest.json = integration-manifest.json .github\workflows\keyfactor-extension-release.yml = .github\workflows\keyfactor-extension-release.yml - README.md = README.md - README.md.tpl = README.md.tpl + readme_source.md = readme_source.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{6302034E-DF8C-4B65-AC36-CED24C068999}" ProjectSection(SolutionItems) = preProject - Images\Image1.png = Images\Image1.png - Images\Image2.png = Images\Image2.png - Images\Image3.png = Images\Image3.png - Images\Image4.png = Images\Image4.png - Images\Image5.png = Images\Image5.png - Images\Image6.png = Images\Image6.png - Images\Image7.png = Images\Image7.png - Images\Image8.png = Images\Image8.png - Images\Image9.png = Images\Image9.png + images\ReEnrollment1.png = images\ReEnrollment1.png + images\ReEnrollment1a.png = images\ReEnrollment1a.png + images\ReEnrollment1b.png = images\ReEnrollment1b.png + images\Screen1.png = images\Screen1.png + images\Screen2.png = images\Screen2.png EndProjectSection EndProject Global diff --git a/IISU/Jobs/ReEnrollment.cs b/IISU/Jobs/ReEnrollment.cs index ec3415c..eba5534 100644 --- a/IISU/Jobs/ReEnrollment.cs +++ b/IISU/Jobs/ReEnrollment.cs @@ -64,7 +64,6 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi runSpace.Open(); _logger.LogTrace("Workspace opened"); - // NEW var ps = PowerShell.Create(); ps.Runspace = runSpace; @@ -75,7 +74,10 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi var keyType = config.JobProperties["keyType"]; var keySize = config.JobProperties["keySize"]; var SAN = config.JobProperties["SAN"]; - + + // If the provider name is null, default it to the Microsoft CA + if (providerName == null) providerName = "Microsoft Strong Cryptographic Provider"; + // Create the script file ps.AddScript("$infFilename = New-TemporaryFile"); ps.AddScript("$csrFilename = New-TemporaryFile"); @@ -99,17 +101,20 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi { ps.AddScript($"Add-Content $infFilename '_continue_ = \"{s + "&"}\"'"); } - + // Execute the -new command ps.AddScript($"certreq -new -q $infFilename $csrFilename"); - + _logger.LogTrace("Attempting to create the CSR by Invoking the script."); Collection results = ps.Invoke(); + _logger.LogTrace("Completed the attempt in creating the CSR."); ps.Commands.Clear(); try { ps.AddScript($"$CSR = Get-Content $csrFilename"); + _logger.LogTrace("Attempting to get the contents of the CSR file."); results = ps.Invoke(); + _logger.LogTrace("Completet getting the CSR Contents."); } catch (Exception e) { @@ -123,20 +128,24 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi // Delete the temp files ps.AddScript("if (Test-Path $infFilename) { Remove-Item -Path $infFilename }"); ps.AddScript("if (Test-Path $csrFilename) { Remove-Item -Path $csrFilename }"); + _logger.LogTrace("Attempt to delete the temporary files."); results = ps.Invoke(); } // Get the byte array var CSRContent = ps.Runspace.SessionStateProxy.GetVariable("CSR").ToString(); - + // Sign CSR in Keyfactor + _logger.LogTrace("Get the signed CSR from KF."); X509Certificate2 myCert = submitReenrollment.Invoke(CSRContent); if (myCert != null) { // Get the cert data into string format string csrData = Convert.ToBase64String(myCert.RawData, Base64FormattingOptions.InsertLineBreaks); - + + _logger.LogTrace("Creating the text version of the certificate."); + // Write out the cert file StringBuilder sb = new StringBuilder(); sb.AppendLine("-----BEGIN CERTIFICATE-----"); @@ -150,20 +159,24 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi ps.Commands.Clear(); // Accept the signed cert + _logger.LogTrace("Attempting to accept or bind the certificate to the HSM."); ps.AddScript("certreq -accept $cerFilename"); ps.Invoke(); + _logger.LogTrace("Successfully bind the certificate to the HSM."); ps.Commands.Clear(); // Delete the temp files ps.AddScript("if (Test-Path $infFilename) { Remove-Item -Path $infFilename }"); ps.AddScript("if (Test-Path $csrFilename) { Remove-Item -Path $csrFilename }"); ps.AddScript("if (Test-Path $cerFilename) { Remove-Item -Path $cerFilename }"); + _logger.LogTrace("Removing temporary files."); results = ps.Invoke(); ps.Commands.Clear(); runSpace.Close(); // Bind the certificate to IIS + _logger.LogTrace("Binding the certificate to IIS."); var iisManager = new IISManager(config); return iisManager.ReEnrollCertificate(myCert); } @@ -173,7 +186,7 @@ private JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submi { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, - FailureMessage = "The ReEnrollment job was unable to sign te CSR. Please check the formatting of the SAN and other ReEnrollment properties." + FailureMessage = "The ReEnrollment job was unable to sign the CSR. Please check the formatting of the SAN and other ReEnrollment properties." }; } diff --git a/images/ReEnrollment1.png b/images/ReEnrollment1.png new file mode 100644 index 0000000000000000000000000000000000000000..2778ca501fc1bb2611d65f581f7019c2e4207237 GIT binary patch literal 24937 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 G8~+4BLVZ^N literal 0 HcmV?d00001 diff --git a/images/ReEnrollment1a.png b/images/ReEnrollment1a.png new file mode 100644 index 0000000000000000000000000000000000000000..ddcaec4217340b8e0668f408b441108e3417506a GIT binary patch literal 9228 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 M7PjUkrii$|0dup&&j0`b literal 0 HcmV?d00001 diff --git a/integration-manifest.json b/integration-manifest.json index 76f6d7a..dc6cc1e 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -12,7 +12,7 @@ "supportsDiscovery": false, "supportsManagementAdd": true, "supportsManagementRemove": true, - "supportsReenrollment": false, + "supportsReenrollment": true, "supportsInventory": true, "platformSupport": "Unused" }, diff --git a/readme_source.md b/readme_source.md index c5a3300..00ef8c4 100644 --- a/readme_source.md +++ b/readme_source.md @@ -7,7 +7,7 @@ The "Personal" (My) and "Web Hosting" Stores are supported. Only certificates that are bound to an IIS web site are managed. Unbound certificates are ignored. -This agent implements three job types – Inventory, Management Add, and Management Remove. Below are the steps necessary to configure this AnyAgent. +This agent implements four job types – Inventory, Management Add, Remove and ReEnrollment. Below are the steps necessary to configure this AnyAgent. WinRM is used to remotely manage the certificate stores and IIS bindings. WinRM must be properly configured to allow the server running the orchestrator to manage the server running IIS. @@ -71,6 +71,8 @@ This section must be configured with binding fields. The parameters will be popu - 1 - SNI Enabled - 2 - Non SNI Binding - 3 - SNI Binding +- **Prover Name** - Optional. To get a list of Crypto Providers, open PowerShell and issue the 'certutil -csplist' command. If no Provider Name is provided, the 'Microsoft Strong Cryptographic Provider' will be used. +- **SAN** - Required. The SAN must have one entry that matches the Subject Name when using ReEnrollment. Multiple SANs maybe chained together using '&'. Example: dns=www.mysite.com&dns=www.mysite2.com. Parameter Name|Parameter Type|Default Value|Required ---|---|---|--- @@ -80,6 +82,8 @@ Host Name |String||No Site Name |String|Default Web Site|Yes Sni Flag |String|0 - No SNI|No Protocol |Multiple Choice|https|Yes +Provider Name |String||No +SAN |String||Yes ![](images/screen1-c.gif) @@ -122,6 +126,7 @@ Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot 10 |Renew Single Cert on Same Site Same Binding Settings Different Hostname Different Certs|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding2.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on only one binding because the other binding does not match thrumbprint|True|![](images/TestCase10Binding1.gif)![](images/TestCase10Binding2.gif) 11 |Renew Same Cert on Same Site Same Binding Settings Different IPs|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.160`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase11Binding1.gif)![](images/TestCase11Binding2.gif) 12 |Renew Same Cert on Same Site Same Binding Settings Different Ports|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 543
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase12Binding1.gif)![](images/TestCase12Binding2.gif) +13 |ReEnrollment to Fortanix HSM|**Subject Name:** cn=www.mysite.com
**Port:** 433
**IP Address:**`*`
**Host Name:** mysite.command.local
**Site Name:**Default Web Site
**Sni Flag:** 0 - No SNI
**Protocol:** https
**Provider Name:** Fortanix KMS CNG Provider
**SAN:** dns=www.mysite.com&dns=mynewsite.com|Cert will be generated with keys stored in Fortanix HSM and the cert will be bound to the supplied site.|true|![](images/ReEnrollment1a.png)![](images/ReEnrollment1b.png) From f149921611e578b4f0305be7681262d4f3f556e7 Mon Sep 17 00:00:00 2001 From: Michael Henderson Date: Mon, 21 Nov 2022 14:40:18 -0800 Subject: [PATCH 16/17] Add token for readme builds --- .github/workflows/keyfactor-starter-workflow.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 6e9ef2d..72f4d2d 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -18,6 +18,9 @@ jobs: call-generate-readme-workflow: if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: Keyfactor/actions/.github/workflows/generate-readme.yml@main + secrets: + token: ${{ secrets.APPROVE_README_PUSH }} + call-update-catalog-workflow: if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' From 24d9af9dd9be1498ef02e13d1bb9489be13bbc3f Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Mon, 21 Nov 2022 22:40:57 +0000 Subject: [PATCH 17/17] Update generated README --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 828329d..78e2a1d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The Keyfactor Universal Orchestrator may be installed on either Windows or Linux |Supports Management Remove|✓ | | |Supports Create Store| | | |Supports Discovery| | | -|Supports Renrollment| | | +|Supports Renrollment|✓ | | |Supports Inventory|✓ | | @@ -48,7 +48,7 @@ The "Personal" (My) and "Web Hosting" Stores are supported. Only certificates that are bound to an IIS web site are managed. Unbound certificates are ignored. -This agent implements three job types – Inventory, Management Add, and Management Remove. Below are the steps necessary to configure this AnyAgent. +This agent implements four job types – Inventory, Management Add, Remove and ReEnrollment. Below are the steps necessary to configure this AnyAgent. WinRM is used to remotely manage the certificate stores and IIS bindings. WinRM must be properly configured to allow the server running the orchestrator to manage the server running IIS. @@ -112,6 +112,8 @@ This section must be configured with binding fields. The parameters will be popu - 1 - SNI Enabled - 2 - Non SNI Binding - 3 - SNI Binding +- **Prover Name** - Optional. To get a list of Crypto Providers, open PowerShell and issue the 'certutil -csplist' command. If no Provider Name is provided, the 'Microsoft Strong Cryptographic Provider' will be used. +- **SAN** - Required. The SAN must have one entry that matches the Subject Name when using ReEnrollment. Multiple SANs maybe chained together using '&'. Example: dns=www.mysite.com&dns=www.mysite2.com. Parameter Name|Parameter Type|Default Value|Required ---|---|---|--- @@ -121,6 +123,8 @@ Host Name |String||No Site Name |String|Default Web Site|Yes Sni Flag |String|0 - No SNI|No Protocol |Multiple Choice|https|Yes +Provider Name |String||No +SAN |String||Yes ![](images/screen1-c.gif) @@ -163,6 +167,7 @@ Case Number|Case Name|Enrollment Params|Expected Results|Passed|Screenshot 10 |Renew Single Cert on Same Site Same Binding Settings Different Hostname Different Certs|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`*`
**Host Name:** www.firstsitebinding2.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on only one binding because the other binding does not match thrumbprint|True|![](images/TestCase10Binding1.gif)![](images/TestCase10Binding2.gif) 11 |Renew Same Cert on Same Site Same Binding Settings Different IPs|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.160`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase11Binding1.gif)![](images/TestCase11Binding2.gif) 12 |Renew Same Cert on Same Site Same Binding Settings Different Ports|`BINDING 1`
**Site Name:** FirstSite
**Port:** 443
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https
`BINDING 2`
**Site Name:** FirstSite
**Port:** 543
**IP Address:**`192.168.58.162`
**Host Name:** www.firstsitebinding1.com
**Sni Flag:** 1 - SNI Enabled
**Protocol:** https|Cert will be renewed on both bindings because it has the same thrumbprint|True|![](images/TestCase12Binding1.gif)![](images/TestCase12Binding2.gif) +13 |ReEnrollment to Fortanix HSM|**Subject Name:** cn=www.mysite.com
**Port:** 433
**IP Address:**`*`
**Host Name:** mysite.command.local
**Site Name:**Default Web Site
**Sni Flag:** 0 - No SNI
**Protocol:** https
**Provider Name:** Fortanix KMS CNG Provider
**SAN:** dns=www.mysite.com&dns=mynewsite.com|Cert will be generated with keys stored in Fortanix HSM and the cert will be bound to the supplied site.|true|![](images/ReEnrollment1a.png)![](images/ReEnrollment1b.png)