Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
3.0.0
* Removing Entry Parameter "SAN" from integration-manifest.json, but still supporting previous versions of Command in the event the SAN Entry Parameter is passed. SAN's are now supported via ODKG enrollment page. Next major version will remove all support for the SAN Entry Parameter.
* Added Integration Tests to aid in future development and testing.
* Improved messaging in the event an Entry Parameter is missing (or does not meet the casing requirements)
* Fixed the SNI/SSL flag being returned during inventory, now returns extended SSL flags
Expand Down
49 changes: 47 additions & 2 deletions IISU/ClientPSCertStoreReEnrollment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore
{
internal class ClientPSCertStoreReEnrollment
public class ClientPSCertStoreReEnrollment
{
private readonly ILogger _logger;
private readonly IPAMSecretResolver _resolver;
Expand All @@ -44,6 +44,12 @@ internal class ClientPSCertStoreReEnrollment
private Collection<PSObject>? _results;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

// Empty constructor for testing purposes
public ClientPSCertStoreReEnrollment()
{
_logger = LogHandler.GetClassLogger(typeof(ClientPSCertStoreReEnrollment));
}

public ClientPSCertStoreReEnrollment(ILogger logger, IPAMSecretResolver resolver)
{
_logger = logger;
Expand All @@ -65,7 +71,11 @@ public JobResult PerformReEnrollment(ReenrollmentJobConfiguration config, Submit
var subjectText = config.JobProperties["subjectText"] as string;
var providerName = config.JobProperties["ProviderName"] as string;
var keyType = config.JobProperties["keyType"] as string;
var SAN = config.JobProperties["SAN"] as string;

// Prior to Version 3.0, SANs were passed using config.JobProperties.
// Now they are passed as a config parameter, but we will check both to maintain backward compatibility.
// Version 3.0 and greater will default to the new SANs parameter.
var SAN = ResolveSANString(config);

int keySize = 0;
if (config.JobProperties["keySize"] is not null && int.TryParse(config.JobProperties["keySize"].ToString(), out int size))
Expand Down Expand Up @@ -373,5 +383,40 @@ private string ImportCertificate(byte[] certificateRawData, string storeName)
}
}

public string ResolveSANString(ReenrollmentJobConfiguration config)
{
if (config == null)
throw new ArgumentNullException(nameof(config));

string sourceUsed;
string sanValue = string.Empty;

if (config.SANs != null && config.SANs.Count > 0)
{
var builder = new SANBuilder(config.SANs);
sanValue = builder.BuildSanString();
sourceUsed = "config.SANs (preferred)";
}
else if (config.JobProperties != null &&
config.JobProperties.TryGetValue("SAN", out object legacySanValue) &&
!string.IsNullOrWhiteSpace(legacySanValue.ToString()))
{
sanValue = legacySanValue.ToString().Trim();
sourceUsed = "config.JobProperties[\"SAN\"] (legacy)";
}
else
{
sanValue = string.Empty;
sourceUsed = "none (no SANs provided)";
}

_logger.LogTrace($"[SAN Resolver] Source used: {sourceUsed}");
if (!string.IsNullOrEmpty(sanValue))
_logger.LogTrace($"[SAN Resolver] Value: {sanValue}");
else
_logger.LogTrace("[SAN Resolver] No SAN values found.");

return sanValue;
}
}
}
73 changes: 73 additions & 0 deletions IISU/SANBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore
{
public class SANBuilder
{
public Dictionary<string, string[]> SANs { get; set; } = new Dictionary<string, string[]>();
public SANBuilder(Dictionary<string, string[]> sans)
{
SANs = sans ?? throw new ArgumentNullException(nameof(sans));
}

public string BuildSanString()
{
if (SANs == null || SANs.Count == 0)
return string.Empty;

var parts = new List<string>();

foreach (var entry in SANs)
{
string key = NormalizeSanKey(entry.Key);
if (entry.Value == null) continue;

parts.AddRange(
entry.Value
.Where(v => !string.IsNullOrWhiteSpace(v))
.Select(v => $"{key}={v.Trim()}")
);
}

return string.Join("&", parts);
}

/// <summary>
/// Normalize SAN type keys to RFC-compliant names.
/// </summary>
private static string NormalizeSanKey(string key)
{
return key.Trim().ToLower() switch
{
"dns" => "dns",
"ip" or "ip4" or "ip6" => "ipaddress",
"email" or "rfc822" => "email",
"uri" => "uri",
"upn" => "upn",
_ => key.ToLower() // fallback
};
}

public override string ToString()
{
if (SANs == null || SANs.Count == 0)
return "No SANs defined.";

var lines = new List<string>();
foreach (var entry in SANs)
{
string key = NormalizeSanKey(entry.Key);
string joined = entry.Value != null && entry.Value.Length > 0
? string.Join(", ", entry.Value)
: "(none)";
lines.Add($"{key.ToUpper()}: {joined}");
}

return string.Join(Environment.NewLine, lines);
}
}
}
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,6 @@ 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 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 <san_type>=<san_value> 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:

Expand Down Expand Up @@ -420,7 +419,6 @@ the Keyfactor Command Portal
| 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 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 <san_type>=<san_value> 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:

Expand Down Expand Up @@ -543,7 +541,6 @@ the Keyfactor Command Portal
| ---- | ------------ | ---- | ------------- | ----------------------- | ---------------- | ----------------- | ------------------- | ----------- |
| 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 | 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 <san_type>=<san_value> 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:

Expand Down
96 changes: 96 additions & 0 deletions WindowsCertStore.UnitTests/SANsUnitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Keyfactor.Extensions.Orchestrator.WindowsCertStore;
using Keyfactor.Orchestrators.Extensions;
using System.Security.Permissions;

namespace WindowsCertStore.UnitTests
{
public class SANsUnitTests
{

private ClientPSCertStoreReEnrollment enrollment = new ClientPSCertStoreReEnrollment();

[Fact]
public void Test_SANs()
{
// Arrange
var sans = new Dictionary<string, string[]>
{
{ "dns", new[] { "example.com", "www.example.com" } },
{ "ip", new[] { "192.168.1.1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334" } },
{ "email", new[] { "myemail@company.com" } },
{ "uri", new[] { "http://mycompany.com" } },
{ "upn", new[] { "myusername@company.com" } }
};

// Act
var sanBuilder = new Keyfactor.Extensions.Orchestrator.WindowsCertStore.SANBuilder(sans);
string sanString = sanBuilder.BuildSanString();
string sanToString = sanBuilder.ToString();

// Assert
Assert.Equal("dns=example.com&dns=www.example.com&ipaddress=192.168.1.1&ipaddress=2001:0db8:85a3:0000:0000:8a2e:0370:7334&email=myemail@company.com&uri=http://mycompany.com&upn=myusername@company.com", sanString);
Assert.Contains("DNS: example.com, www.example.com", sanToString);
}
[Fact]
public void ResolveSanString_PrefersConfigSANs_WhenBothSourcesExist()
{
// Arrange
var config = new ReenrollmentJobConfiguration
{
JobProperties = new Dictionary<string, object>
{
{ "SAN", "dns=legacy.example.com&dns=old.example.com" }
},
SANs = new Dictionary<string, string[]>
{
{ "dns", new[] { "example.com", "www.example.com" } },
{ "ip", new[] { "192.168.1.1" } },
{ "email", new[] { "user@mycompany.com" } }
}
};

// Act
string result = enrollment.ResolveSANString(config);

// Assert
Assert.Contains("dns=example.com", result);
Assert.Contains("dns=www.example.com", result);
Assert.Contains("ipaddress=192.168.1.1", result);
Assert.Contains("email=user@mycompany.com", result);
Assert.DoesNotContain("legacy.example.com", result); // ensure legacy ignored
}

[Fact]
public void ResolveSanString_UsesLegacySAN_WhenConfigSANsMissing()
{
// Arrange
var config = new ReenrollmentJobConfiguration
{
JobProperties = new Dictionary<string, object>
{
{ "SAN", "dns=legacy.example.com&dns=old.example.com" }
},
SANs = new Dictionary<string, string[]>()
};

// Act
string result = enrollment.ResolveSANString(config);

// Assert
Assert.Equal("dns=legacy.example.com&dns=old.example.com", result);
}

[Fact]
public void ResolveSanString_ReturnsEmpty_WhenNoSANsProvided()
{
// Arrange
var config = new ReenrollmentJobConfiguration();

// Act
string result = enrollment.ResolveSANString(config);

// Assert
Assert.Equal(string.Empty, result);
}
}
}
27 changes: 27 additions & 0 deletions WindowsCertStore.UnitTests/WindowsCertStore.UnitTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\IISU\WindowsCertStore.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions WindowsCertStore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{60C10F
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsCertStore.IntegrationTests", "WindowsCertStore.IntegrationTests\WindowsCertStore.IntegrationTests.csproj", "{74FDA232-BC6D-428F-BC28-C5BF228F04ED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsCertStore.UnitTests", "WindowsCertStore.UnitTests\WindowsCertStore.UnitTests.csproj", "{84DD1D36-2D35-4483-836F-98EFC44CA132}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -74,6 +76,14 @@ Global
{74FDA232-BC6D-428F-BC28-C5BF228F04ED}.Release|Any CPU.Build.0 = Release|Any CPU
{74FDA232-BC6D-428F-BC28-C5BF228F04ED}.Release|x64.ActiveCfg = Release|Any CPU
{74FDA232-BC6D-428F-BC28-C5BF228F04ED}.Release|x64.Build.0 = Release|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Debug|x64.ActiveCfg = Debug|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Debug|x64.Build.0 = Debug|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Release|Any CPU.Build.0 = Release|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Release|x64.ActiveCfg = Release|Any CPU
{84DD1D36-2D35-4483-836F-98EFC44CA132}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
49 changes: 2 additions & 47 deletions integration-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,6 @@
"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",
"Type": "String",
"RequiredWhen": {
"HasPrivateKey": false,
"OnAdd": false,
"OnRemove": false,
"OnReenrollment": true
},
"DependsOn": "",
"DefaultValue": "",
"Options": "",
"Description": "String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of <san_type>=<san_value> 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."
}
],
"PasswordOptions": {
Expand Down Expand Up @@ -320,22 +305,7 @@
"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",
"Type": "String",
"RequiredWhen": {
"HasPrivateKey": false,
"OnAdd": false,
"OnRemove": false,
"OnReenrollment": true
},
"DependsOn": "",
"DefaultValue": "",
"Options": "",
"Description": "String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of <san_type>=<san_value> 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."
}
}
],
"PasswordOptions": {
"EntrySupported": false,
Expand Down Expand Up @@ -455,22 +425,7 @@
"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",
"Type": "String",
"RequiredWhen": {
"HasPrivateKey": false,
"OnAdd": false,
"OnRemove": false,
"OnReenrollment": true
},
"DependsOn": "",
"DefaultValue": "",
"Options": "",
"Description": "String value specifying the Subject Alternative Name (SAN) to be used when performing reenrollment jobs. Format as a list of <san_type>=<san_value> entries separated by ampersands; Example: 'dns=www.example.com&dns=www.example2.com' for multiple SANs."
}
}
],
"PasswordOptions": {
"EntrySupported": false,
Expand Down
Loading