From c0c5207f05752bf7c2e6f43f93f89fe32c498d21 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Tue, 8 Jul 2025 04:52:54 +0000 Subject: [PATCH 1/7] Update generated docs --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3a5e085..6be61f2 100644 --- a/README.md +++ b/README.md @@ -593,8 +593,8 @@ The Windows Certificate Universal Orchestrator extension implements 3 Certificat Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "Windows Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -685,8 +685,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "IIS Bound Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the IIS certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -777,8 +777,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "WinSql" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the SQL Server Certificate Store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | From 56127b19de8fc0c2a13cd7fec78cbdb22f7ad1a6 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Mon, 7 Jul 2025 23:54:18 -0500 Subject: [PATCH 2/7] #ab73289 Resolved issues parsing Distinguished Name subject string and properly quotes the RDN values containing escaped commas. --- CHANGELOG.md | 3 + IISU/PowerShellScripts/WinCertScripts.ps1 | 109 +++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6b284..0c4d0d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +2.6.3 +* Fixed reenrollment job when RDN Components contained escaped commas + 2.6.2 * Fixed error when attempting to connect to remote computer using UO service account * Fixed error when connecting to remote computer using HTTPS; was defaulting to HTTP diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index a77dccf..423cb0a 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -1166,6 +1166,9 @@ function New-CSREnrollment { # Validate the Crypto Service Provider Validate-CryptoProvider -ProviderName $ProviderName + # Parse Subject for any escaped commas + $parsedSubject = Parse-DNSubject $SubjectText + # Build the SAN entries if provided $sanContent = "" if ($SAN) { @@ -1184,7 +1187,7 @@ $($sanDirectives -join "`n") Signature=`"$`Windows NT$`" [NewRequest] -Subject = "$SubjectText" +Subject = "$parsedSubject" ProviderName = "$ProviderName" MachineKeySet = True HashAlgorithm = SHA256 @@ -1402,4 +1405,108 @@ function Validate-CryptoProvider { } Write-Verbose "Crypto Service Provider '$ProviderName' is valid." +} + +function Parse-DNSubject { + <# + .SYNOPSIS + Parses a Distinguished Name (DN) subject string and properly quotes RDN values containing escaped commas. + + .DESCRIPTION + This function takes a DN subject string and parses the Relative Distinguished Name (RDN) components, + adding proper quotes around values that contain escaped commas and escaping quotes for use in + PowerShell here-strings. Only RDN values with escaped commas get quoted. + + .PARAMETER Subject + The DN subject string to parse (e.g., "CN=Keyfactor,O=Keyfactor\, Inc") + + .EXAMPLE + Parse-DNSubject -Subject "CN=Keyfactor,O=Keyfactor\, Inc" + Returns: CN=Keyfactor,O=""Keyfactor, Inc"" + + .EXAMPLE + Parse-DNSubject -Subject "CN=Test User,O=Company\, LLC,OU=IT Department\, Security" + Returns: CN=Test User,O=""Company, LLC"",OU=""IT Department, Security"" + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Subject + ) + + # Initialize variables + $parsedComponents = @() + $currentComponent = "" + $i = 0 + + # Convert string to character array for easier parsing + $chars = $Subject.ToCharArray() + + while ($i -lt $chars.Length) { + $char = $chars[$i] + + # Check if we hit a comma + if ($char -eq ',') { + # Look back to see if it's escaped + $isEscaped = $false + if ($i -gt 0 -and $chars[$i-1] -eq '\') { + $isEscaped = $true + } + + if ($isEscaped) { + # This is an escaped comma, add it to current component + $currentComponent += $char + } else { + # This is a separator comma, finish current component + if ($currentComponent.Trim() -ne "") { + $parsedComponents += $currentComponent.Trim() + $currentComponent = "" + } + } + } else { + # Regular character, add to current component + $currentComponent += $char + } + + $i++ + } + + # Add the last component + if ($currentComponent.Trim() -ne "") { + $parsedComponents += $currentComponent.Trim() + } + + # Process each component to add quotes where needed + $processedComponents = @() + + foreach ($component in $parsedComponents) { + # Split on first equals sign to get attribute and value + $equalIndex = $component.IndexOf('=') + if ($equalIndex -gt 0) { + $attribute = $component.Substring(0, $equalIndex).Trim() + $value = $component.Substring($equalIndex + 1).Trim() + + # Clean up escaped commas first + $cleanValue = $value -replace '\\,', ',' + + # Check if original value had escaped commas (needs quotes) + if ($value -match '\\,') { + # This RDN value had escaped commas, so wrap in doubled quotes and escape quotes + $escapedValue = $cleanValue -replace '"', '""' + $processedComponents += "$attribute=`"`"$escapedValue`"`"" + } else { + # No escaped commas, keep as simple value but escape any existing quotes + $escapedValue = $cleanValue -replace '"', '""' + $processedComponents += "$attribute=$escapedValue" + } + } else { + # Invalid component format, keep as is + $processedComponents += $component + } + } + + # Join components back together (no outer quotes needed since it goes in PowerShell string) + $subjectString = ($processedComponents -join ',') + return $subjectString } \ No newline at end of file From 85234586a12fbcf8e366ee3cb67c329861de6a84 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Tue, 8 Jul 2025 04:55:56 +0000 Subject: [PATCH 3/7] Update generated docs --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3a5e085..6be61f2 100644 --- a/README.md +++ b/README.md @@ -593,8 +593,8 @@ The Windows Certificate Universal Orchestrator extension implements 3 Certificat Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "Windows Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -685,8 +685,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "IIS Bound Certificate" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the IIS certificate store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | @@ -777,8 +777,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- | ----------- | + | Attribute | Description | + | --------- |---------------------------------------------------------| | Category | Select "WinSql" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | Hostname of the Windows Server containing the SQL Server Certificate Store to be managed. If this value is a hostname, a WinRM session will be established using the credentials specified in the Server Username and Server Password fields. For more information, see [Client Machine](#note-regarding-client-machine). | From 8d0d83f491c482cf1ee2b9386e03e4f58304dda0 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Tue, 5 Aug 2025 15:59:03 -0700 Subject: [PATCH 4/7] #ab64674 - Added logic to remove old certs when not bound to any IIS sites. --- CHANGELOG.md | 1 + IISU/ImplementedStoreTypes/WinIIS/Inventory.cs | 2 +- IISU/ImplementedStoreTypes/WinIIS/Management.cs | 10 ++++++++++ IISU/PowerShellScripts/WinCertScripts.ps1 | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c4d0d2..4d93681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 2.6.3 * Fixed reenrollment job when RDN Components contained escaped commas +* Updated renewal job for IIS Certs to delete the old cert if not bound or used by other web sites. 2.6.2 * Fixed error when attempting to connect to remote computer using UO service account diff --git a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs index af45bdd..65c1b68 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Inventory.cs @@ -95,7 +95,7 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = "" + FailureMessage = $"Inventory completed returning {inventoryItems.Count} Items." }; } diff --git a/IISU/ImplementedStoreTypes/WinIIS/Management.cs b/IISU/ImplementedStoreTypes/WinIIS/Management.cs index bbd19c1..97989f7 100644 --- a/IISU/ImplementedStoreTypes/WinIIS/Management.cs +++ b/IISU/ImplementedStoreTypes/WinIIS/Management.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Management.Automation; using Keyfactor.Extensions.Orchestrator.WindowsCertStore.Models; using Keyfactor.Logging; @@ -89,6 +90,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) string protocol = jobProperties?.WinRmProtocol; string port = jobProperties?.WinRmPort; bool includePortInSPN = (bool)jobProperties?.SpnPortFlag; + string alias = config.JobCertificate.Alias.Split(':').FirstOrDefault() ?? string.Empty; // Thumbprint is first part of the alias _psHelper = new(protocol, port, includePortInSPN, _clientMachineName, serverUserName, serverPassword); @@ -171,6 +173,14 @@ public JobResult ProcessJob(ManagementJobConfiguration config) psResult = OrchestratorJobStatusJobResult.Unknown; } + // Only is the binding returns successful, check of original cert is still bound to any site, if not remove it from the store + if (psResult == OrchestratorJobStatusJobResult.Success && !string.IsNullOrEmpty(alias)) + { + _logger.LogTrace("Attempting to remove original certificate from store if it is no longer bound to any site."); + RemoveIISCertificate(alias); + _logger.LogTrace("Returned from removing cert if not used."); + } + complete = new JobResult { Result = psResult, diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index 423cb0a..f7bf47b 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -818,7 +818,7 @@ function Remove-KFIISCertificateIfUnused { if ($bindings.Count -gt 0) { Write-Warning "The certificate with thumbprint $thumbprint is still used by the following bindings:" - $bindings | Format-Table -AutoSize + $bindings | Format-Table -AutoSize | Out-String | Write-Warning return } From 62d684658b50e487545401af2e1843ce2d1d88fb Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Tue, 5 Aug 2025 17:58:52 -0700 Subject: [PATCH 5/7] Update CSP Documentation --- docsource/content.md | 20 ++++++++++++++ integration-manifest.json | 58 +++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index 39d1793..4e4cf0f 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -82,6 +82,26 @@ For customers wishing to use something other than the local administrator accoun - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. +### Using Crypto Service Providers (CSP) +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. + +The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: + + certutil -csplist + +When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. + +When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. + +Each CSP only supports certain key types and algorithms. + +Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: +|CSP Name|Supports RSA?|Supports ECC?| +|---|---|---| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| + ## Client Machine Instructions Prior to version 2.6, this extension would only run in the Windows environment. Version 2.6 and greater is capable of running on Linux, however, only the SSH protocol is supported. diff --git a/integration-manifest.json b/integration-manifest.json index 13088fd..ef70981 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -115,7 +115,7 @@ "DependsOn": "", "DefaultValue": "", "Options": "", - "Description": "Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server." + "Description": "Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers'" }, { "Name": "SAN", @@ -306,21 +306,21 @@ "Options": "https,http", "Description": "Multiple choice value specifying the protocol to bind to. Example: 'https' for secure communication." }, - { - "Name": "ProviderName", - "DisplayName": "Crypto Provider Name", - "Type": "String", - "RequiredWhen": { - "HasPrivateKey": false, - "OnAdd": false, - "OnRemove": false, - "OnReenrollment": false - }, - "DependsOn": "", - "DefaultValue": "", - "Options": "", - "Description": "Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server." + { + "Name": "ProviderName", + "DisplayName": "Crypto Provider Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false }, + "DependsOn": "", + "DefaultValue": "", + "Options": "", + "Description": "Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers'" + }, { "Name": "SAN", "DisplayName": "SAN", @@ -441,21 +441,21 @@ }, "Description": "String value specifying the SQL Server instance name to bind the certificate to. Example: 'MSSQLServer' for the default instance or 'Instance1' for a named instance." }, - { - "Name": "ProviderName", - "DisplayName": "Crypto Provider Name", - "Type": "String", - "RequiredWhen": { - "HasPrivateKey": false, - "OnAdd": false, - "OnRemove": false, - "OnReenrollment": false - }, - "DependsOn": "", - "DefaultValue": "", - "Options": "", - "Description": "Optional string value specifying the name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing private keys. Example: 'Microsoft Strong Cryptographic Provider'." + { + "Name": "ProviderName", + "DisplayName": "Crypto Provider Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false }, + "DependsOn": "", + "DefaultValue": "", + "Options": "", + "Description": "Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers'" + }, { "Name": "SAN", "DisplayName": "SAN", From 5c593e92f6f4d3e033debac52c12961b73a47b09 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 6 Aug 2025 01:01:24 +0000 Subject: [PATCH 6/7] Update generated docs --- README.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6be61f2..c61806f 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,9 @@ The Windows Certificate Universal Orchestrator extension implements 3 Certificat This integration is compatible with Keyfactor Universal Orchestrator version 10.1 and later. ## Support -The Windows Certificate Universal Orchestrator extension If you have a support issue, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. +The Windows Certificate Universal Orchestrator extension is supported by Keyfactor. If you require support for any issues or have feature request, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. -> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. +> If you want to contribute bug fixes or additional enhancements, use the **[Pull requests](../../pulls)** tab. ## Requirements & Prerequisites @@ -135,6 +135,26 @@ For customers wishing to use something other than the local administrator accoun - Access any Cryptographic Service Provider (CSP) referenced in re-enrollment jobs. - Read and Write values in the registry (HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server) when performing SQL Server certificate binding. +### Using Crypto Service Providers (CSP) +When adding or reenrolling certificates, you may specify an optional CSP to be used when generating and storing the private keys. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. + +The list of installed cryptographic providers can be obtained by running the PowerShell command on the target server: + + certutil -csplist + +When performing a ReEnrollment or On Device Key Generation (ODKG) job, if no CSP is specified, a default value of 'Microsoft Strong Cryptographic Provider' will be used. + +When performing an Add job, if no CSP is specified, the machine's default CSP will be used, in most cases this could be the 'Microsoft Enhanced Cryptographic Provider v1.0' provider. + +Each CSP only supports certain key types and algorithms. + +Below is a brief summary of the CSPs and their support for RSA and ECC algorithms: +|CSP Name|Supports RSA?|Supports ECC?| +|---|---|---| +|Microsoft RSA SChannel Cryptographic Provider |✅|❌| +|Microsoft Software Key Storage Provider |✅|✅| +|Microsoft Enhanced Cryptographic Provider |✅|❌| + ## Certificate Store Types @@ -257,7 +277,7 @@ the Keyfactor Command Portal | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | | ---- | ------------ | ---- | ------------- | ----------------------- | ---------------- | ----------------- | ------------------- | ----------- | - | ProviderName | Crypto Provider Name | Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | + | ProviderName | Crypto Provider Name | Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | SAN | SAN | String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of = entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs. Can be made optional if RFC 2818 is disabled on the CA. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | ✅ Checked | The Entry Parameters tab should look like this: @@ -392,7 +412,7 @@ the Keyfactor Command Portal | SiteName | IIS Site Name | String value specifying the name of the IIS web site to bind the certificate to. Example: 'Default Web Site' or any custom site name such as 'MyWebsite'. | String | Default Web Site | 🔲 Unchecked | ✅ Checked | ✅ Checked | ✅ Checked | | SniFlag | SSL Flags | A 128-Bit Flag that determines what type of SSL settings you wish to use. The default is 0, meaning No SNI. For more information, check IIS documentation for the appropriate bit setting.) | String | 0 | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | Protocol | Protocol | Multiple choice value specifying the protocol to bind to. Example: 'https' for secure communication. | MultipleChoice | https | 🔲 Unchecked | ✅ Checked | ✅ Checked | ✅ Checked | - | ProviderName | Crypto Provider Name | Name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing the private keys. If not specified, defaults to 'Microsoft Strong Cryptographic Provider'. This value would typically be specified when leveraging a Hardware Security Module (HSM). The specified cryptographic provider must be available on the target server being managed. The list of installed cryptographic providers can be obtained by running 'certutil -csplist' on the target Server. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | + | ProviderName | Crypto Provider Name | Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | SAN | SAN | String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of = entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs. Can be made optional if RFC 2818 is disabled on the CA. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | ✅ Checked | The Entry Parameters tab should look like this: @@ -515,7 +535,7 @@ the Keyfactor Command Portal | Name | Display Name | Description | Type | Default Value | Entry has a private key | Adding an entry | Removing an entry | Reenrolling an entry | | ---- | ------------ | ---- | ------------- | ----------------------- | ---------------- | ----------------- | ------------------- | ----------- | | InstanceName | Instance Name | String value specifying the SQL Server instance name to bind the certificate to. Example: 'MSSQLServer' for the default instance or 'Instance1' for a named instance. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | - | ProviderName | Crypto Provider Name | Optional string value specifying the name of the Windows cryptographic provider to use during reenrollment jobs when generating and storing private keys. Example: 'Microsoft Strong Cryptographic Provider'. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | + | ProviderName | Crypto Provider Name | Name of the Windows cryptographic service provider to use when generating and storing private keys. For more information, refer to the section 'Using Crypto Service Providers' | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | | SAN | SAN | String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of = entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs. | String | | 🔲 Unchecked | 🔲 Unchecked | 🔲 Unchecked | ✅ Checked | The Entry Parameters tab should look like this: From 4d7369179a86210240b01ecf8f4250a5032f7614 Mon Sep 17 00:00:00 2001 From: Bob Pokorny Date: Fri, 15 Aug 2025 13:11:35 -0700 Subject: [PATCH 7/7] #ab74699 - Fixed an error with complex PFX passwords --- CHANGELOG.md | 2 + IISU/PowerShellScripts/WinCertScripts.ps1 | 227 +++++++++++----------- 2 files changed, 112 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d93681..643c8a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ 2.6.3 * Fixed reenrollment job when RDN Components contained escaped commas * Updated renewal job for IIS Certs to delete the old cert if not bound or used by other web sites. +* Improved Inventory reporting of CSP when cert uses newer CNG Keys +* Fixed an issue with complex PFX passwords that contained special characters such as '@' or '$', etc. 2.6.2 * Fixed error when attempting to connect to remote computer using UO service account diff --git a/IISU/PowerShellScripts/WinCertScripts.ps1 b/IISU/PowerShellScripts/WinCertScripts.ps1 index f7bf47b..10d6d70 100644 --- a/IISU/PowerShellScripts/WinCertScripts.ps1 +++ b/IISU/PowerShellScripts/WinCertScripts.ps1 @@ -1,4 +1,9 @@ -# Set preferences globally at the script level +# Update notes: +# 8/12/25 Updated functions to manage IIS bindings and certificates +# Updated script to read CSPs correctly using newer CNG Keys +# Fix an error with complex PFX passwords having irregular characters + +# Set preferences globally at the script level $DebugPreference = "Continue" $VerbosePreference = "Continue" $InformationPreference = "Continue" @@ -225,136 +230,80 @@ function Add-KFCertificateToStore{ # Execute certutil based on whether a private key password was supplied try { - # Build certutil command to import the certificate with exportable private key and CSP - $command = "certutil -f -p `"$PrivateKeyPassword`" -csp `"$CryptoServiceProvider`" -importpfx $StoreName `"$tempPfx`"" - $traceCommand = "certutil -f -p `"************`" -csp `"$CryptoServiceProvider`" -importpfx $StoreName `"$tempPfx`"" - - Write-Verbose "Running: $traceCommand" - $output = Invoke-Expression $command + # Start building certutil arguments + $arguments = @('-f') - if ($LASTEXITCODE -ne 0) { - throw "certutil failed with code $LASTEXITCODE. `nOutput: $output `nMake sure there is no cryptographic mismatch and the CSP supports the imported PFX.`n" + if ($PrivateKeyPassword) { + Write-Verbose "Has a private key" + $arguments += '-p' + $arguments += $PrivateKeyPassword } - # Get latest cert with private key in the store - $store = "Cert:\LocalMachine\$StoreName" - $cert = Get-ChildItem -Path $store | Where-Object { $_.HasPrivateKey } | Sort-Object NotBefore -Descending | Select-Object -First 1 - - if ($cert) { - Write-Information "Certificate imported successfully with Thumbprint: $($cert.Thumbprint)" - return $cert.Thumbprint - } else { - throw "Import succeeded, but no certificate with a private key was found in $store" + if ($CryptoServiceProvider) { + Write-Verbose "Has a CryptoServiceProvider: $CryptoServiceProvider" + $arguments += '-csp' + $arguments += $CryptoServiceProvider } - } catch { - Write-Error "ERROR: $_" - } finally { - if (Test-Path $tempPfx) { - #Remove-Item $tempPfx -Force - } - } - - } else { - $bytes = [System.Convert]::FromBase64String($Base64Cert) - $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" - Write-Information "Store '$StoreName' is open." - $certStore.Open(5) - - $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $bytes, $PrivateKeyPassword, 18 <# Persist, Machine #> - $certStore.Add($cert) - $certStore.Close(); - Write-Information "Store '$StoreName' is closed." - - # Get the thumbprint so it can be returned to the calling function - $thumbprint = $cert.Thumbprint - Write-Information "The thumbprint '$thumbprint' was created." - } + $arguments += '-importpfx' + $arguments += $StoreName + $arguments += $tempPfx - Write-Host "Certificate added successfully to $StoreName." - return $thumbprint - } catch { - Write-Error "An error occurred: $_" - return $null - } -} + # Quote any arguments with spaces + $argLine = ($arguments | ForEach-Object { + if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } + }) -join ' ' -function Add-KFCertificateToStoreNEW{ - param ( - [Parameter(Mandatory = $true)] - [string]$Base64Cert, - - [Parameter(Mandatory = $false)] - [string]$PrivateKeyPassword, - - [Parameter(Mandatory = $true)] - [string]$StoreName, - - [Parameter(Mandatory = $false)] - [string]$CryptoServiceProvider - ) + write-Verbose "Running certutil with arguments: $argLine" - try { - Write-Information "Entering PowerShell Script Add-KFCertificate" - Write-Verbose "Add-KFCertificateToStore - Received: StoreName: '$StoreName', CryptoServiceProvider: '$CryptoServiceProvider', Base64Cert: '$Base64Cert'" + # Setup process execution + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "certutil.exe" + $processInfo.Arguments = $argLine.Trim() + $processInfo.RedirectStandardOutput = $true + $processInfo.RedirectStandardError = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true - $thumbprint = $null + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo - if ($CryptoServiceProvider) - { - # Test to see if CSP exists - if(-not (Test-CryptoServiceProvider -CSPName $CryptoServiceProvider)) - { - Write-Information "INFO: The CSP $CryptoServiceProvider was not found on the system." - Write-Warning "WARN: CSP $CryptoServiceProvider was not found on the system." - return - } + $process.Start() | Out-Null - Write-Information "Adding certificate with the CSP '$CryptoServiceProvider'" + $stdOut = $process.StandardOutput.ReadToEnd() + $stdErr = $process.StandardError.ReadToEnd() - # Convert Base64 PFX to bytes and save to temp file - $tempPfxPath = [System.IO.Path]::GetTempFileName() + ".pfx" - [System.IO.File]::WriteAllBytes($tempPfxPath, [Convert]::FromBase64String($Base64Cert)) + $process.WaitForExit() - try { - # Load the PFX into a PKCS12 object - $pfx = New-Object -ComObject X509Enrollment.CX509Enrollment - $pfx.InitializeImport(1, [System.IO.File]::ReadAllText($tempPfxPath), $PrivateKeyPassword) - - # Create new private key with desired CSP - $privateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey - $privateKey.ProviderName = $CryptoServiceProvider - $privateKey.Length = [int]2048 - $privateKey.KeySpec = 1 # AT_KEYEXCHANGE - $privateKey.ExportPolicy = 1 # AllowExport - $privateKey.MachineContext = $true - $privateKey.Create() - - # Associate private key with enrollment - $pfx.InstallResponse(2, "", 0, $null) - - Write-Host "Certificate imported successfully using CSP: $CryptoServiceProvider" - - # The most recently added cert (with private key) should be the new one - $latest = $certsBefore | Where-Object { $_.HasPrivateKey } | Sort-Object NotBefore -Descending | Select-Object -First 1 - - if ($latest) { - Write-Information "Certificate imported successfully with thumbprint: $($latest.Thumbprint)" - return $latest.Thumbprint - } else { - throw "Certificate installed but no cert with private key was found in store '$StoreName'." + if ($process.ExitCode -ne 0) { + throw "certutil failed with code $($process.ExitCode). Output:`n$stdOut`nError:`n$stdErr" } + # Retrieve thumbprint of the newly imported cert + try { + $cert = Get-ChildItem -Path "Cert:\LocalMachine\$StoreName" | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + if ($cert) { + Write-Information "Imported certificate thumbprint: $($cert.Thumbprint)" + return $cert.Thumbprint + } else { + Write-Warning "Could not retrieve the imported certificate." + return $null + } + } + catch { + Write-Warning "Failed to retrieve thumbprint: $_" + return $null + } } catch { - # Handle any errors and log the exception message - Write-Error "Error during certificate import: $_" - return "Error: $_" + Write-Error "ERROR: $_" } finally { - # Ensure the temporary file is deleted - if (Test-Path $tempFileName) { - Remove-Item $tempFileName -Force + if (Test-Path $tempPfx) { + #Remove-Item $tempPfx -Force } } + } else { $bytes = [System.Convert]::FromBase64String($Base64Cert) $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, "LocalMachine" @@ -378,6 +327,7 @@ function Add-KFCertificateToStoreNEW{ return $null } } + function Remove-KFCertificateFromStore { param ( [string]$Thumbprint, @@ -464,13 +414,17 @@ function New-KFIISSiteBinding { return $result } + Write-Verbose "No binding conflicts found for SiteName: '$SiteName', IPAddress: '$IPAddress', Port: $Port, HostName: '$Hostname'" $searchBindings = "${IPAddress}:${Port}:${Hostname}" $hasIISDrive = Ensure-IISDrive Write-Verbose "IIS Drive is available: $hasIISDrive" if ($hasIISDrive) { - Import-Module WebAdministration + + Write-Verbose "IIS Drive is available, using WebAdministration module." + + $null = Import-Module WebAdministration $sitePath = "IIS:\Sites\$SiteName" if (-not (Test-Path $sitePath)) { $msg = "Site '$SiteName' not found in IIS drive." @@ -480,7 +434,7 @@ function New-KFIISSiteBinding { $site = Get-Item $sitePath $httpsBindings = $site.Bindings.Collection | Where-Object { $_.bindingInformation -eq $searchBindings -and $_.protocol -eq "https" - } + } foreach ($binding in $httpsBindings) { try { @@ -520,6 +474,8 @@ function New-KFIISSiteBinding { } } else { # SERVERMANAGER FALLBACK + Write-Verbose "IIS Drive is not available, using ServerManager fallback." + Add-Type -Path "$env:windir\System32\inetsrv\Microsoft.Web.Administration.dll" $iis = New-Object Microsoft.Web.Administration.ServerManager $site = $iis.Sites[$SiteName] @@ -578,7 +534,7 @@ function CheckExistingBindings { $conflicts = @() if (Ensure-IISDrive) { - Import-Module WebAdministration + $null = Import-Module WebAdministration Get-Website | Where-Object { $_.Name -ne $TargetSiteName } | ForEach-Object { $siteName = $_.Name @@ -647,7 +603,7 @@ function CheckExistingBindingsORIG { ) if (Ensure-IISDrive) { - Import-Module WebAdministration + $null = Import-Module WebAdministration $conflict = $false @@ -710,7 +666,7 @@ function Ensure-IISDrive { # Try to import the WebAdministration module if not already loaded if (-not (Get-Module -Name WebAdministration)) { try { - Import-Module WebAdministration -ErrorAction Stop + $null = Import-Module WebAdministration -ErrorAction Stop } catch { Write-Warning "WebAdministration module could not be imported. IIS:\ drive will not be available." @@ -1336,6 +1292,43 @@ function Get-CertificateCSP { [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) + # Check if the certificate has a private key + if (-not $Cert.HasPrivateKey) { + Write-Warning "Certificate does not have a private key associated with it" + return $null + } + + $privateKey = $Cert.PrivateKey + if ($privateKey) { + # For older .NET Framework + $cspKeyContainerInfo = $privateKey.CspKeyContainerInfo + + if ($cspKeyContainerInfo) { + return $cspKeyContainerInfo.ProviderName + } + } + + try { + $key = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) + if ($key -and $key.GetType().Name -eq "RSACng") { + $cngKey = $key.Key + + return $cngKey.Provider.Provider + } + } + catch { + Write-Warning "CNG key detection failed: $($_.Exception.Message)" + return $null + } +} + +# Function that takes an x509 certificate object and returns the csp +function Get-CertificateCSPOLD { + param ( + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert + ) + # Check if the certificate has a private key if ($Cert -and $Cert.HasPrivateKey) { try {