Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
873b8e0
Added PAM resolution for Server Username and Server Password
joevanwanzeeleKF Nov 26, 2025
1268ed1
Update generated docs
Nov 26, 2025
404c59a
Merge branch 'release-1.0' into pam_support_#79006
joevanwanzeeleKF Nov 26, 2025
d133d62
Update generated docs
Nov 26, 2025
9b2a733
cleaned up manifest
joevanwanzeeleKF Nov 26, 2025
b11f7df
Merge branch 'pam_support_#79006' of https://github.com/Keyfactor/vmw…
joevanwanzeeleKF Nov 26, 2025
02dcc31
Create keyfactor-release-workflow.yml
joevanwanzeeleKF Nov 26, 2025
ddb5c7c
Delete .github/workflows/keyfactor-starter-workflow.yml
joevanwanzeeleKF Nov 26, 2025
977dcc8
added additional logging for troubleshooting
joevanwanzeeleKF Dec 1, 2025
d57dc5f
replaced newtonsoft with system.text.json; additional logging
joevanwanzeeleKF Dec 8, 2025
cb261c1
Merge branch 'pam_support_#79006' of https://github.com/Keyfactor/vmw…
joevanwanzeeleKF Dec 8, 2025
141f9a6
updated nuget packages
joevanwanzeeleKF Dec 8, 2025
a044b27
added IPamResolver to job constructors.
joevanwanzeeleKF Dec 10, 2025
29ba6dc
fixed issue where logging was called before logger was initialized.
joevanwanzeeleKF Dec 10, 2025
0f40d53
casing username and password to string for PAM resolution.. additiona…
joevanwanzeeleKF Dec 11, 2025
c9a2fcc
using the custom fields for server username and server password rathe…
joevanwanzeeleKF Dec 19, 2025
6263511
Added missing field in ssl cert response model. Marked fields nullab…
joevanwanzeeleKF Jan 5, 2026
12d01e8
added null check and logging for trusted root chain
joevanwanzeeleKF Jan 8, 2026
602bc34
Updated fields used for PAM username/password resolution
joevanwanzeeleKF Feb 2, 2026
c2a43ad
updated changelog
joevanwanzeeleKF Feb 2, 2026
eaa9cb9
Merge branch 'release-1.0' into bugfix_#81594
joevanwanzeeleKF Feb 2, 2026
2d9287e
trimming whitespace chars from beginning of trusted root
joevanwanzeeleKF Feb 19, 2026
ffd7ffb
Merge branch 'bugfix_#81594' of https://github.com/Keyfactor/vmware-v…
joevanwanzeeleKF Feb 19, 2026
c24acb4
assigning the trimmed value
joevanwanzeeleKF Feb 19, 2026
cb10ef8
added unit tests, improved PEM parsing, logging improvements
joevanwanzeeleKF Apr 3, 2026
b82a50b
Update generated docs
Apr 3, 2026
227e8da
updated changelog
joevanwanzeeleKF Apr 3, 2026
637d688
Merge branch 'pem_parse_updates_#84849' of https://github.com/Keyfact…
joevanwanzeeleKF Apr 3, 2026
f8e77dc
Merge branch 'release-1.0' into pem_parse_updates_#84849
joevanwanzeeleKF Apr 3, 2026
2dc12f5
removed unnecessary folder from repository
joevanwanzeeleKF Apr 3, 2026
ccb4e2e
Merge branch 'pem_parse_updates_#84849' of https://github.com/Keyfact…
joevanwanzeeleKF Apr 3, 2026
65b44e0
Update generated docs
Apr 3, 2026
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
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
- 1.0.3
- Improved PEM certificate parsing, unit tests, logging improvements

- 1.0.2
* Added/fixed PAM support for the Server Username and Server Password fields
- Added/fixed PAM support for the Server Username and Server Password fields

- 1.0.0
* Initial release
- Initial release
12 changes: 6 additions & 6 deletions VmwareVcenterOrchestrator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ VisualStudioVersion = 17.7.34221.43
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VmwareVcenterOrchestrator", "VmwareVcenterOrchestrator\VmwareVcenterOrchestrator.csproj", "{3645725A-2C84-4536-9A04-4F4CEDF30B21}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VmwareVcenterOrchestratorTest", "VmwareVcenterOrchestratorTest\VmwareVcenterOrchestratorTest.csproj", "{A1671009-5C03-4A81-84F8-4353928D7A57}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{53F1F754-B49A-4E83-9DF4-546399B5B89B}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
Expand All @@ -15,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
vcenter.md = vcenter.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VmwareVcenterOrchestratorTests", "VmwareVcenterOrchestratorTests\VmwareVcenterOrchestratorTests.csproj", "{9FE79B18-C912-4086-9C14-C9761566389F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -26,10 +26,10 @@ Global
{3645725A-2C84-4536-9A04-4F4CEDF30B21}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{3645725A-2C84-4536-9A04-4F4CEDF30B21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3645725A-2C84-4536-9A04-4F4CEDF30B21}.Release|Any CPU.Build.0 = Release|Any CPU
{A1671009-5C03-4A81-84F8-4353928D7A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1671009-5C03-4A81-84F8-4353928D7A57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1671009-5C03-4A81-84F8-4353928D7A57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1671009-5C03-4A81-84F8-4353928D7A57}.Release|Any CPU.Build.0 = Release|Any CPU
{9FE79B18-C912-4086-9C14-C9761566389F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FE79B18-C912-4086-9C14-C9761566389F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FE79B18-C912-4086-9C14-C9761566389F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FE79B18-C912-4086-9C14-C9761566389F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
5 changes: 3 additions & 2 deletions VmwareVcenterOrchestrator/Client/VmwareVcenterClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
VcenterClient.DefaultRequestHeaders.Add("vmware-api-session-id", apiKey);
}

private async Task<string?> GetApiToken(string username, string password)

Check warning on line 49 in VmwareVcenterOrchestrator/Client/VmwareVcenterClient.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.
{
var credentials = username + ":" + password;
var encodedCredentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(credentials));
Expand Down Expand Up @@ -125,7 +125,7 @@
var trustedRoots = new List<VCenterTrustedRootChainsSummary>();
string responseContent;

_logger.LogDebug("Calling GET on vcenter endpoint for trusted root chain");
_logger.LogDebug($"Calling GET on vcenter endpoint {TRUSTEDROOTENDPOINT} for trusted root chain");
try
{
var response = await VcenterClient.GetAsync(TRUSTEDROOTENDPOINT);
Expand All @@ -143,6 +143,7 @@
_logger.LogError($"There was an error retrieving the trusted root chains: {LogHandler.FlattenException(ex)}");
throw;
}
_logger.LogTrace($"raw response content: {responseContent}");
trustedRoots = JsonSerializer.Deserialize<List<VCenterTrustedRootChainsSummary>>(responseContent);
var chains = trustedRoots.Select(tr => tr.chain).ToList();

Expand All @@ -163,7 +164,7 @@
}

var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogTrace($"serialized response: {responseContent}");
_logger.LogTrace($"raw response: {responseContent}");
_logger.LogTrace("deserializing...");
var trustedRootInfo = JsonSerializer.Deserialize<VCenterTrustedRootChainsInfo>(responseContent);
_logger.LogTrace($"deserialized chain: {trustedRootInfo.cert_chain?.cert_chain}");
Expand Down
57 changes: 45 additions & 12 deletions VmwareVcenterOrchestrator/Jobs/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,56 @@ public IEnumerable<CurrentInventoryItem> FormatSslCert(VCenterTlsCertInfo sslCer
public CurrentInventoryItem FormatTrustedRoot(VCenterTrustedRootChainsInfo trustedRootInfo)
{
_logger.MethodEntry();
_logger.LogTrace($"trusted root chain: {String.Join(",", trustedRootInfo?.cert_chain?.cert_chain)}");
_logger.LogTrace($"trusted root chain: {String.Join(",", trustedRootInfo?.cert_chain?.cert_chain ?? Enumerable.Empty<string>())}");

// Guard: null input, missing cert_chain object, or empty chain list.
// The original condition used nullable-propagation logic that evaluated
// to false (not true) when cert_chain was null, causing a NullReferenceException
// on the very next line. Replaced with explicit null checks.
if (trustedRootInfo == null
|| trustedRootInfo.cert_chain == null
|| trustedRootInfo.cert_chain.cert_chain == null
|| !trustedRootInfo.cert_chain.cert_chain.Any())
{
_logger.LogTrace("no entries found.");
return null;
}

// if trusted root is null, or there are no entries, return null
if (trustedRootInfo == null || (!trustedRootInfo?.cert_chain?.cert_chain.Any() ?? false)) {
var rootCert = trustedRootInfo.cert_chain.cert_chain[0];

var headerStartIndex = rootCert.IndexOf(X509Certificate2Extensions.CERTIFICATE_HEADER_PEM);
if (headerStartIndex == -1)
{
_logger.LogTrace("no PEM header found");
return null;
}
//Format the retrieved trusted root chain certificate
//Remove attached X509 CRL Cert if it exists
var index = trustedRootInfo.cert_chain.cert_chain[0].IndexOf(X509Certificate2Extensions.CERTIFICATE_FOOTER_PEM);
var trustedRootCert = string.Empty;
if (index >= 0)

var footerStartIndex = rootCert.IndexOf(X509Certificate2Extensions.CERTIFICATE_FOOTER_PEM);
if (footerStartIndex == -1)
{
trustedRootCert = trustedRootInfo.cert_chain.cert_chain[0].Substring(0, index);
_logger.LogTrace("no PEM footer found");
return null;
}
trustedRootCert = trustedRootCert.Trim('\n', '\r'); // remove any leading or trailing hidden chars

var pkcs12CertBytes = Convert.FromBase64String(trustedRootCert.TrimStart(X509Certificate2Extensions.CERTIFICATE_HEADER_PEM.ToCharArray()));
// Extract the raw base64 body: everything between the end of the header
// and the start of the footer. Then strip ALL whitespace so that
// Convert.FromBase64String receives a clean string regardless of:
// - line wrap width (64-char, 76-char, or no wrapping)
// - line endings (LF, CRLF, or mixed)
// - leading/trailing spaces or blank lines in the body
// - bag-attribute blocks before the header (skipped by headerStartIndex)
var bodyStartIndex = headerStartIndex + X509Certificate2Extensions.CERTIFICATE_HEADER_PEM.Length;
var certContent = rootCert.Substring(bodyStartIndex, footerStartIndex - bodyStartIndex);
certContent = new string(certContent.Where(c => !char.IsWhiteSpace(c)).ToArray());

_logger.LogTrace("extracted cert content to be base64 decoded:");
_logger.LogTrace(certContent);

var pkcs12CertBytes = Convert.FromBase64String(certContent);

_logger.LogTrace($"successfully decoded into a byte array of length {pkcs12CertBytes.Length}");

_logger.LogTrace($"creating new x509 certificate from certificate byte array");
var certificate = new X509Certificate2(pkcs12CertBytes);

// Create new inventory item for the certificate
Expand All @@ -126,7 +159,7 @@ public CurrentInventoryItem FormatTrustedRoot(VCenterTrustedRootChainsInfo trust
PrivateKeyEntry = false,
ItemStatus = OrchestratorInventoryItemStatus.Unknown,
UseChainLevel = true,
Certificates = certList
Certificates = certList,
};
return inventoryItem;
}
Expand Down
8 changes: 3 additions & 5 deletions VmwareVcenterOrchestrator/Jobs/VmwareVcenterJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Keyfactor.Extensions.Orchestrator.VmwareVcenterOrchestrator.Jobs
public abstract class VmwareVcenterJob : IOrchestratorJobExtension
{
public string ExtensionName => "Vcenter";
internal protected ILogger _logger { get; set; }
public ILogger _logger { get; set; }

protected VmwareVcenterClient VcenterClient { get; private set; }

Expand All @@ -40,8 +40,8 @@ protected void Initialize(InventoryJobConfiguration config)

VcenterProperties properties = JsonConvert.DeserializeObject<VcenterProperties>(config.CertificateStoreDetails?.Properties);

_logger.LogTrace($"server username: {config.ServerUsername}");
_logger.LogTrace($"server password: {config.ServerPassword}");
//_logger.LogTrace($"server username: {config.ServerUsername}");
//_logger.LogTrace($"server password: {config.ServerPassword}");
_logger.LogTrace($"PamSecretResolver is {(PamSecretResolver == null ? "" : "not")} null");

string ClientMachine = config.CertificateStoreDetails?.ClientMachine;
Expand All @@ -61,8 +61,6 @@ protected void Initialize(ManagementJobConfiguration config)
VcenterProperties properties = JsonConvert.DeserializeObject<VcenterProperties>(config.CertificateStoreDetails?.Properties);

string ClientMachine = config.CertificateStoreDetails?.ClientMachine;
_logger.LogTrace($"server username: {config.ServerUsername}");
_logger.LogTrace($"server password: {config.ServerPassword}");
string Username = PamUtilities.ResolvePAMField(PamSecretResolver, _logger, "Server Username", config.ServerUsername);
string Password = PamUtilities.ResolvePAMField(PamSecretResolver, _logger, "Server Password", config.ServerPassword);

Expand Down
28 changes: 21 additions & 7 deletions VmwareVcenterOrchestrator/X509Certificate2Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
{
public static class X509Certificate2Extensions
{
public static string CERTIFICATE_HEADER_PEM => "-----BEGIN CERTIFICATE-----\n";
public static string CERTIFICATE_FOOTER_PEM => "\n-----END CERTIFICATE-----";
public static string CERTIFICATE_HEADER_PEM => "-----BEGIN CERTIFICATE-----";
public static string CERTIFICATE_FOOTER_PEM => "-----END CERTIFICATE-----";

public static string PRIVATE_KEY_HEADER_PEM => "-----BEGIN PRIVATE KEY-----\n";
public static string PRIVATE_KEY_FOOTER_PEM => "\n-----END PRIVATE KEY-----";
public static string PRIVATE_KEY_HEADER_PEM => "-----BEGIN PRIVATE KEY-----";
public static string PRIVATE_KEY_FOOTER_PEM => "-----END PRIVATE KEY-----";

public static X509Certificate2? RootCACert(this X509Certificate2 cert)

Check warning on line 24 in VmwareVcenterOrchestrator/X509Certificate2Extensions.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.
{
var chain = new X509Chain();
chain.Build(cert);
Expand All @@ -44,7 +44,7 @@
{
//cert.pem
// Convert the certificate to PEM format
string certificatePem = $"{CERTIFICATE_HEADER_PEM}{Convert.ToBase64String(cert.Export(X509ContentType.Cert))}{CERTIFICATE_FOOTER_PEM}";
string certificatePem = $"{CERTIFICATE_HEADER_PEM}\n{Convert.ToBase64String(cert.Export(X509ContentType.Cert))}\n{CERTIFICATE_FOOTER_PEM}";

// Convert the private key to PEM format
string privateKeyPem = cert.ExportPrivateKeyToPem();
Expand Down Expand Up @@ -76,7 +76,7 @@
var rootChainPem = string.Empty;

for (var i = 1; i < elementCount; i++) {
rootChainPem += $"{CERTIFICATE_HEADER_PEM}{Convert.ToBase64String(certChain.ChainElements[i].Certificate.Export(X509ContentType.Cert))}{CERTIFICATE_FOOTER_PEM}";
rootChainPem += $"{CERTIFICATE_HEADER_PEM}\n{Convert.ToBase64String(certChain.ChainElements[i].Certificate.Export(X509ContentType.Cert))}\n{CERTIFICATE_FOOTER_PEM}";
}
_logger.LogTrace($"root chain = {rootChainPem}");

Expand All @@ -88,9 +88,23 @@
var privateKey = certificate.GetRSAPrivateKey();
if (privateKey == null) return string.Empty;

// On Windows, GetRSAPrivateKey() returns an RSACng whose CNG key may only have
// AllowExport set (permits encrypted PKCS#12 export), not AllowPlaintextExport
// (required for raw PKCS#8 export via ExportPkcs8PrivateKey). This override
// sets AllowPlaintextExport before calling ExportPkcs8PrivateKey so the method
// works regardless of how the certificate was originally loaded.
if (privateKey is System.Security.Cryptography.RSACng rsaCng)
{
rsaCng.Key.SetProperty(
new System.Security.Cryptography.CngProperty(
"Export Policy",
BitConverter.GetBytes((int)System.Security.Cryptography.CngExportPolicies.AllowPlaintextExport),
System.Security.Cryptography.CngPropertyOptions.None));
}

var pkcs8privatekey = privateKey.ExportPkcs8PrivateKey();
var pem = Convert.ToBase64String(pkcs8privatekey);
return $"{PRIVATE_KEY_HEADER_PEM}{pem}{PRIVATE_KEY_FOOTER_PEM}";
return $"{PRIVATE_KEY_HEADER_PEM}\n{pem}\n{PRIVATE_KEY_FOOTER_PEM}";
}
}
}
Loading
Loading