Skip to content
Open
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
3.0.0
* As of this version of the extension, SANs will be handled through the ODKG Enrollment page in Command, and will no longer use the SAN Entry Parameter. This version, we are removing the Entry Parameter "SAN" from the integration-manifest.json, but will still support previous versions of Command in the event the SAN Entry Parameter is passed. The next major version (4.0) will remove all support for the SAN Entry Parameter.
* Added WinADFS Store Type for rotating certificates in ADFS environments. Please note, only the service-communications certificate is rotated throughout your farm.
* Internal only: 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
* Fixed the SNI/SSL flag when binding the certificate to allow for extended SSL flags
* Added SSL Flag validation to make sure the bit flag is correct. These are the current SSL Flags (NOTE: Values greater than 4 are only supported in IIS 10 version 1809 and higher. The default value is 0):
* 0 No SNI
* 1 Use SNI
* 2 Use Centralized SSL certificate store.
* 4 Disable HTTP/2.
* 8 Disable OCSP Stapling.
* 16 Disable QUIC.
* 32 Disable TLS 1.3 over TCP.
* 64 Disable Legacy TLS.

2.6.4
* Fixed an issue with SSL Flags greater than 3 were not being applied correctly to newer IIS servers.
* Fixed an issue when formatting private RSA keys when connecting using the ssh protocol.
* When using ssh protocol in containers, the SQL ACL on private keys was not being updating correctly. This has been fixed.
* Updated documentation to indicate that the username and password fields on the Cert Store are automatically added by Command.

2.6.3
* Fixed re-enrollment or ODKG 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.
Expand Down
78 changes: 78 additions & 0 deletions IISU/Certificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@

// 021225 rcp 2.6.0 Cleaned up and verified code

// Ignore Spelling: Keyfactor

using Keyfactor.Logging;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;

namespace Keyfactor.Extensions.Orchestrator.WindowsCertStore
{
Expand Down Expand Up @@ -52,6 +57,79 @@ public static List<T> DeserializeCertificates<T>(string jsonResults)
return new List<T> { singleObject };
}
}

public static string WriteCertificateToTempPfx(string certificateContents)
{
if (string.IsNullOrWhiteSpace(certificateContents))
throw new ArgumentException("Certificate contents cannot be null or empty.", nameof(certificateContents));

try
{
// Decode the Base64 string into bytes
byte[] certBytes = Convert.FromBase64String(certificateContents);

// Create a unique temporary directory
string tempDirectory = Path.Combine(Path.GetTempPath(), "CertTemp");
Directory.CreateDirectory(tempDirectory);

// Create a unique filename
string fileName = $"cert_{Guid.NewGuid():N}.pfx";
string filePath = Path.Combine(tempDirectory, fileName);

// Write the bytes to the .pfx file
File.WriteAllBytes(filePath, certBytes);

// Return the path to the newly created file
return filePath;
}
catch (FormatException)
{
throw new InvalidDataException("The provided certificate contents are not a valid Base64 string.");
}
catch (Exception ex)
{
throw new IOException($"Failed to write certificate to temp PFX file: {ex.Message}", ex);
}
}

public static void CleanupTempCertificate(string pfxFilePath)
{
ILogger logger = LogHandler.GetClassLogger<Certificate>();

if (string.IsNullOrWhiteSpace(pfxFilePath))
return;

try
{
if (File.Exists(pfxFilePath))
{
File.Delete(pfxFilePath);
}

string? parentDir = Path.GetDirectoryName(pfxFilePath);
if (!string.IsNullOrEmpty(parentDir) && Directory.Exists(parentDir))
{
// Delete the directory if it's empty
if (Directory.GetFiles(parentDir).Length == 0 &&
Directory.GetDirectories(parentDir).Length == 0)
{
Directory.Delete(parentDir);
}
}
Comment on lines +110 to +118
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (!string.IsNullOrEmpty(parentDir) && Directory.Exists(parentDir))
{
// Delete the directory if it's empty
if (Directory.GetFiles(parentDir).Length == 0 &&
Directory.GetDirectories(parentDir).Length == 0)
{
Directory.Delete(parentDir);
}
}
if (!string.IsNullOrEmpty(parentDir) &&
Directory.Exists(parentDir) &&
Directory.GetFiles(parentDir).Length == 0 &&
Directory.GetDirectories(parentDir).Length == 0)
{
// Delete the directory if it's empty
Directory.Delete(parentDir);
}

Copilot uses AI. Check for mistakes.
}
catch (IOException ioEx)
{
logger.LogWarning($"Warning: Could not delete temporary file or folder: {ioEx.Message}");
}
catch (UnauthorizedAccessException uaEx)
{
logger.LogWarning($"Warning: Access denied when cleaning up temp file: {uaEx.Message}");
}
catch (Exception ex)
{
logger.LogWarning($"Warning: Unexpected error during cleanup: {ex.Message}");
}
}
}
}
}
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;
}
}
}
Loading