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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
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 valid bit flags for the version of Windows:
### Windows Server 2012 R2 / Windows 8.1 and earlier (IIS 8.5):
* 0 No SNI
* 1 Use SNI
* 2 Use Centralized SSL certificate store.

### Windows Server 2016 (IIS 10.0):
* 0 No SNI
* 1 Use SNI
* 4 Disable HTTP/2.

### Windows Server 2019 (IIS 10.0.17763)
* 0 No SNI
* 1 Use SNI
* 4 Disable HTTP/2.
* 8 Disable OCSP Stapling.

### Windows Server 2022+ (IIS 10.0.20348+)
* 0 No SNI
* 1 Use SNI
* 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.
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 @@
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);

Check warning on line 109 in IISU/Certificate.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 109 in IISU/Certificate.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 109 in IISU/Certificate.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 109 in IISU/Certificate.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 109 in IISU/Certificate.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 109 in IISU/Certificate.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
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);
}
}
}
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
Loading