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
19 changes: 13 additions & 6 deletions .github/workflows/keyfactor-starter-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ on:

jobs:
call-starter-workflow:
uses: keyfactor/actions/.github/workflows/starter.yml@3.1.2-rc.0
uses: keyfactor/actions/.github/workflows/starter.yml@v4
with:
command_token_url: ${{ vars.COMMAND_TOKEN_URL }} # Only required for doctool generated screenshots
command_hostname: ${{ vars.COMMAND_HOSTNAME }} # Only required for doctool generated screenshots
command_base_api_path: ${{ vars.COMMAND_API_PATH }} # Only required for doctool generated screenshots
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
scan_token: ${{ secrets.SAST_TOKEN }}
token: ${{ secrets.V2BUILDTOKEN}} # REQUIRED
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} # Only required for golang builds
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} # Only required for golang builds
scan_token: ${{ secrets.SAST_TOKEN }} # REQUIRED
entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} # Only required for doctool generated screenshots
entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} # Only required for doctool generated screenshots
command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} # Only required for doctool generated screenshots
command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} # Only required for doctool generated screenshots
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
3.0.0
* Add StorePassword as an option to allow clients to encrypt private keys with passwords in situations where their Citrix settings require a password protected key
* Add optional custom field to set timeout for login

2.2.1
* Add ServerUsername and ServerPassword to the integration-manifest.json to add both fields to the README documentation.

Expand Down
10 changes: 2 additions & 8 deletions CitrixAdcOrchestratorJobExtension.sln
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31229.75
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keyfactor.Extensions.Orchestrator.CitricAdc", "CitrixAdcOrchestratorJobExtension\Keyfactor.Extensions.Orchestrator.CitricAdc.csproj", "{2B3106BF-A1B4-4BCC-9650-597B576E14D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CitrixAdcTestConsole", "CitrixAdcTestConsole\CitrixAdcTestConsole.csproj", "{28D3BFB3-1484-4A4A-9BC1-3D20255943FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -17,10 +15,6 @@ Global
{2B3106BF-A1B4-4BCC-9650-597B576E14D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B3106BF-A1B4-4BCC-9650-597B576E14D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B3106BF-A1B4-4BCC-9650-597B576E14D0}.Release|Any CPU.Build.0 = Release|Any CPU
{28D3BFB3-1484-4A4A-9BC1-3D20255943FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28D3BFB3-1484-4A4A-9BC1-3D20255943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28D3BFB3-1484-4A4A-9BC1-3D20255943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28D3BFB3-1484-4A4A-9BC1-3D20255943FD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
88 changes: 47 additions & 41 deletions CitrixAdcOrchestratorJobExtension/CitrixAdcStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Xml.Linq;
using com.citrix.netscaler.nitro.exception;
using com.citrix.netscaler.nitro.resource.Base;
using com.citrix.netscaler.nitro.resource.config.ssl;
Expand All @@ -28,18 +26,22 @@
using com.citrix.netscaler.nitro.util;
using Keyfactor.Logging;
using Keyfactor.Orchestrators.Extensions;
using Keyfactor.PKI.CryptographicObjects.Formatters;
using Keyfactor.PKI.PEM;
using Keyfactor.PKI.PrivateKeys;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Keyfactor.Orchestrators.Common.Enums;

namespace Keyfactor.Extensions.Orchestrator.CitricAdc
{
// ReSharper disable once InconsistentNaming
internal class CitrixAdcStore
{
private const uint Timeout = 3600;
private const string DefaultTimeout = "3600";
public static readonly string StoreType = "CitrixAdc";

private readonly string _clientMachine;
Expand All @@ -50,6 +52,7 @@ internal class CitrixAdcStore
public readonly string StorePath;
private readonly string _username;
private readonly bool _useSsl;
private uint _timeout;

private nitro_service _nss;

Expand All @@ -60,6 +63,8 @@ public CitrixAdcStore(InventoryJobConfiguration config, string serverUserName, s
Logger = LogHandler.GetClassLogger<CitrixAdcStore>();
Logger.MethodEntry(LogLevel.Debug);

SetTimeout(JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties.ToString()));

_clientMachine = config.CertificateStoreDetails.ClientMachine;
StorePath = StripTrailingSlash(config.CertificateStoreDetails.StorePath);
var o = new systemfile_args();
Expand Down Expand Up @@ -93,6 +98,8 @@ public CitrixAdcStore(ManagementJobConfiguration config, string serverUserName,
Logger = LogHandler.GetClassLogger<CitrixAdcStore>();
Logger.MethodEntry(LogLevel.Debug);

SetTimeout(JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties.ToString()));

_clientMachine = config.CertificateStoreDetails.ClientMachine;
StorePath = StripTrailingSlash(config.CertificateStoreDetails.StorePath);
_useSsl = config.UseSSL;
Expand Down Expand Up @@ -125,11 +132,14 @@ public CitrixAdcStore(ManagementJobConfiguration config, string serverUserName,
public void Login()
{
Logger.MethodEntry(LogLevel.Debug);
Logger.LogTrace($"Timeout Value Used: {_timeout}");
_nss ??= new nitro_service(_clientMachine, _useSsl ? "https" : "http");
_nss.set_timeout(_timeout);

base_response response = null;
try
{
response = _nss.login(_username, _password, Timeout);
response = _nss.login(_username, _password, _timeout);
Logger.LogDebug($"Login Response: {JsonConvert.SerializeObject(response)}");
}
catch (Exception ex)
Expand Down Expand Up @@ -287,7 +297,7 @@ public string FindKeyPairByCertPath(string certPath)
}
}

public void UpdateKeyPair(string keyPairName, string certFileName, string keyFileName)
public void UpdateKeyPair(string keyPairName, string certFileName, string keyFileName, string keyPassword)
{
Logger.MethodEntry(LogLevel.Debug);

Expand All @@ -302,8 +312,8 @@ public void UpdateKeyPair(string keyPairName, string certFileName, string keyFil
key = keyFileName,
inform = "PEM",
nodomaincheck = true,
passplain = "0",
password = false
passplain = keyPassword,
password = keyPassword == null ? null : false
};

var filters = new filtervalue[1];
Expand All @@ -328,8 +338,9 @@ public void UpdateKeyPair(string keyPairName, string certFileName, string keyFil
}
catch (nitro_exception ne)
{
Logger.LogError($"Exception occured while trying to add or update {keyPairName}. {LogHandler.FlattenException(ne)}");
throw;
string error = $"Exception occured while trying to add or update {keyPairName}.";
Logger.LogError(error + LogHandler.FlattenException(ne));
throw new Exception(error, ne);
}

Logger.MethodExit(LogLevel.Debug);
Expand Down Expand Up @@ -478,47 +489,33 @@ public void LinkToIssuer(string cert, string privateKeyPassword, string keyPairN
Logger.MethodExit(LogLevel.Debug);
}

private (string, string) GetPemFromPfx(byte[] pfxBytes, char[] pfxPassword)
private (string, string) GetPemFromPfx(byte[] pfxBytes, char[] pfxPassword, string storePassword)
{
Logger.MethodEntry(LogLevel.Debug);

try
{
var p = new Pkcs12Store(new MemoryStream(pfxBytes), pfxPassword);
Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder();
Pkcs12Store store = storeBuilder.Build();
store.Load(new MemoryStream(pfxBytes), pfxPassword);

// Extract private key
var memoryStream = new MemoryStream();
TextWriter streamWriter = new StreamWriter(memoryStream);
var pemWriter = new PemWriter(streamWriter);
var alias = store.Aliases.Cast<string>().SingleOrDefault(p => store.IsKeyEntry(p));

var alias = p.Aliases.Cast<string>().SingleOrDefault(a => p.IsKeyEntry(a));
Logger.LogTrace($"alias: {alias}");
X509CertificateEntry[] chainEntries = store.GetCertificateChain(alias);
Org.BouncyCastle.X509.X509Certificate endCertificate = chainEntries[0].Certificate;

var publicKey = p.GetCertificate(alias).Certificate.GetPublicKey();
if (p.GetKey(alias) == null) throw new Exception($"Unable to get the key for alias: {alias}");
var privateKey = p.GetKey(alias).Key;
var keyPair = new AsymmetricCipherKeyPair(publicKey, privateKey);
AsymmetricKeyParameter privateKey = store.GetKey(alias).Key;
PrivateKeyConverter keyConverter = PrivateKeyConverterFactory.FromBCPrivateKeyAndCert(privateKey, endCertificate);

pemWriter.WriteObject(keyPair.Private);
streamWriter.Flush();
var privateKeyString = Encoding.ASCII.GetString(memoryStream.GetBuffer()).Trim().Replace("\r", "")
.Replace("\0", "");
memoryStream.Close();
streamWriter.Close();
string pemString = CryptographicObjectFormatter.PEM.Format(endCertificate, false);
string keyString = string.Empty;

// Extract server certificate
var certStart = "-----BEGIN CERTIFICATE-----\n";
var certEnd = "\n-----END CERTIFICATE-----";

string Pemify(string ss)
{
return ss.Length <= 64 ? ss : ss.Substring(0, 64) + "\n" + Pemify(ss.Substring(64));
}
if (string.IsNullOrEmpty(storePassword))
keyString = PemUtilities.DERToPEM(keyConverter.ToPkcs8BlobUnencrypted(), Keyfactor.PKI.PEM.PemUtilities.PemObjectType.PrivateKey);
else
keyString = CryptographicObjectFormatter.PEM.Format(keyConverter, storePassword);

var certPem =
certStart + Pemify(Convert.ToBase64String(p.GetCertificate(alias).Certificate.GetEncoded())) +
certEnd;
return (certPem, privateKeyString);
return (pemString, keyString);
}
catch (Exception e)
{
Expand Down Expand Up @@ -611,14 +608,14 @@ private systemfile GetSystemFile(string fileName)
}
}

public (systemfile pemFile, systemfile privateKeyFile) UploadCertificate(string contents, string pwd,
public (systemfile pemFile, systemfile privateKeyFile) UploadCertificate(string contents, string certTempPassword, string storePassword,
string alias, bool overwrite)
{
Logger.MethodEntry(LogLevel.Debug);

try
{
var (certificate, privateKey) = GetPemFromPfx(Convert.FromBase64String(contents), pwd.ToCharArray());
var (certificate, privateKey) = GetPemFromPfx(Convert.FromBase64String(contents), certTempPassword.ToCharArray(), storePassword);

//upload certificate and key
systemfile certificateFile = UploadFile(alias, certificate, true, 0);
Expand All @@ -637,6 +634,15 @@ private systemfile GetSystemFile(string fileName)
}
}

private void SetTimeout(dynamic properties)
{
if (!UInt32.TryParse((properties.timeout == null || string.IsNullOrEmpty(properties.timeout.Value) ? DefaultTimeout : properties.timeout.Value), out _timeout))
{
Logger.LogWarning($"Invalid Custom Field 'timeout' value {properties.timeout.Value}. Value must be an integer. Will use default value of {DefaultTimeout.ToString()}");
_timeout = Convert.ToUInt32(DefaultTimeout);
}
}

private systemfile UploadFile(string alias, string contents, bool isCertificate, int fileNameSuffix)
{
Logger.LogDebug("Entering UploadFile() Method...");
Expand Down
4 changes: 3 additions & 1 deletion CitrixAdcOrchestratorJobExtension/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
using Keyfactor.Orchestrators.Extensions.Interfaces;

using com.citrix.netscaler.nitro.resource.config.ssl;
using Newtonsoft.Json;
using Keyfactor.Orchestrators.Common.Enums;

namespace Keyfactor.Extensions.Orchestrator.CitricAdc
{
Expand Down Expand Up @@ -49,10 +51,10 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn
_logger.LogDebug($"Client Machine: {jobConfiguration.CertificateStoreDetails.ClientMachine}");
_logger.LogDebug($"UseSSL: {jobConfiguration.UseSSL}");
_logger.LogDebug($"StorePath: {jobConfiguration.CertificateStoreDetails.StorePath}");

ServerPassword = ResolvePamField("ServerPassword", jobConfiguration.ServerPassword);
ServerUserName = ResolvePamField("ServerUserName", jobConfiguration.ServerUsername);


_logger.LogDebug("Entering ProcessJob");
CitrixAdcStore store = new CitrixAdcStore(jobConfiguration, ServerUserName, ServerPassword);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@

<PropertyGroup>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Keyfactor.Common" Version="2.3.6" />
<PackageReference Include="Keyfactor.Logging" Version="1.1.1" />
<PackageReference Include="Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="0.7.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
<PackageReference Include="Keyfactor.PKI" Version="8.2.2" />
<PackageReference Include="System.Text.Encodings.Web" Version="6.0.0" />

<None Update="manifest.json">
Expand Down
21 changes: 12 additions & 9 deletions CitrixAdcOrchestratorJobExtension/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class Management : IManagementJobExtension

private string ServerPassword { get; set; }

private string StorePassword { get; set; }

public Management(IPAMSecretResolver resolver)
{
this.resolver = resolver;
Expand All @@ -61,6 +63,10 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration)

ServerPassword = ResolvePamField("ServerPassword", jobConfiguration.ServerPassword);
ServerUserName = ResolvePamField("ServerUserName", jobConfiguration.ServerUsername);
StorePassword = ResolvePamField("StorePassword", jobConfiguration.CertificateStoreDetails.StorePassword);

dynamic properties = JsonConvert.DeserializeObject(jobConfiguration.CertificateStoreDetails.Properties.ToString());
var linkToIssuer = properties.linkToIssuer == null || string.IsNullOrEmpty(properties.linkToIssuer.Value) ? false : Convert.ToBoolean(properties.linkToIssuer.Value);

ApplicationSettings.Initialize(this.GetType().Assembly.Location);

Expand Down Expand Up @@ -89,9 +95,6 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration)
var virtualServerName = (string)jobConfiguration.JobProperties["virtualServerName"];
var sniCert = (string)jobConfiguration.JobProperties["sniCert"];

dynamic properties = JsonConvert.DeserializeObject(jobConfiguration.CertificateStoreDetails.Properties.ToString());
var linkToIssuer = properties.linkToIssuer == null || string.IsNullOrEmpty(properties.linkToIssuer.Value) ? false : Convert.ToBoolean(properties.linkToIssuer.Value);

_logger.LogTrace($"alias: {jobConfiguration.JobCertificate.Alias} virtualServerName {virtualServerName}");

if (!aliasExists)
Expand Down Expand Up @@ -126,7 +129,7 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration)
}
}

PerformAdd(store, jobConfiguration.JobCertificate, virtualServerNames,
PerformAdd(store, jobConfiguration.JobCertificate, StorePassword, virtualServerNames,
aliasExists, jobConfiguration.Overwrite, sniCerts, linkToIssuer);

if (ApplicationSettings.AutoSaveConfig)
Expand Down Expand Up @@ -162,7 +165,7 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration)
{
Result = OrchestratorJobStatusJobResult.Warning,
JobHistoryId = jobConfiguration.JobHistoryId,
FailureMessage = ex.Message
FailureMessage = LogHandler.FlattenException(ex, true)
};
}
catch (Exception ex)
Expand All @@ -172,7 +175,7 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration)
{
Result = OrchestratorJobStatusJobResult.Failure,
JobHistoryId = jobConfiguration.JobHistoryId,
FailureMessage = ex.Message
FailureMessage = LogHandler.FlattenException(ex, true)
};
}

Expand All @@ -191,15 +194,15 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration)
return result;
}

private void PerformAdd(CitrixAdcStore store, ManagementJobCertificate cert,
private void PerformAdd(CitrixAdcStore store, ManagementJobCertificate cert, string storePassword,
List<string> virtualServerNames, bool aliasExists, bool overwrite, List<bool> sniCerts, bool linkToIssuer)
{
_logger.MethodEntry(LogLevel.Debug);

_logger.LogDebug("Updating keyPair");

var (pemFile, privateKeyFile) = store.UploadCertificate(cert.Contents, cert.PrivateKeyPassword, cert.Alias, overwrite);
store.UpdateKeyPair(cert.Alias, pemFile.filename, privateKeyFile.filename);
var (pemFile, privateKeyFile) = store.UploadCertificate(cert.Contents, cert.PrivateKeyPassword, storePassword, cert.Alias, overwrite);
store.UpdateKeyPair(cert.Alias, pemFile.filename, privateKeyFile.filename, storePassword);

_logger.LogDebug("Updating cert bindings");
//update cert bindings
Expand Down
1 change: 0 additions & 1 deletion CitrixAdcTestConsole/CitrixAdcTestConsole.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Moq.AutoMock" Version="3.4.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
</ItemGroup>
Expand Down
Binary file not shown.
Binary file not shown.
Loading
Loading