Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ab#55979 #73

Merged
merged 26 commits into from
Feb 21, 2025
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
v2.10.0
- Added support for Eliptical Curve (EC) private keys for RFPEM.
- For Linux hosted certificate stores, added ability to inherit file permissions and ownership when creating new stores by modifying default behavior when config.json and certificate store permissions/ownership settings are left empty.
- Added new optional custom field to store type definitions - IncludePortInSPN - which will set this option when creating remote Powershell connections.
- Added new optional custom field to store type definitions - FileTransferProtocol - which will act as a store level override to the config.json setting.
- Fixed documentation error in Discovery section
- Added RemoveRootCertificate custom field to integration-manifest.json. This option was previously added in v2.8.0 but never added to the integration-manifest.json.

v2.9.1
- Bug Fix: Use AES encryption when creating PKCS12 files instead of less secure defaults

Expand Down
70 changes: 58 additions & 12 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion RemoteFile/ApplicationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum FileTransferProtocolEnum
Both
}

private const string DEFAULT_LINUX_PERMISSION_SETTING = "600";
private const string DEFAULT_LINUX_PERMISSION_SETTING = "";
private const string DEFAULT_OWNER_SETTING = "";
private const string DEFAULT_SUDO_IMPERSONATION_SETTING = "";

Expand Down
172 changes: 118 additions & 54 deletions RemoteFile/ImplementedStoreTypes/PEM/PEMCertificateStoreSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,32 @@

using Microsoft.Extensions.Logging;

using Org.BouncyCastle.Math;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
using System.Security.Cryptography;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Asn1.X9;

namespace Keyfactor.Extensions.Orchestrator.RemoteFile.PEM
{
class PEMCertificateStoreSerializer : ICertificateStoreSerializer
{
string[] PrivateKeyDelimetersPkcs8 = new string[] { "-----BEGIN PRIVATE KEY-----", "-----BEGIN ENCRYPTED PRIVATE KEY-----" };
string[] PrivateKeyDelimetersPkcs1 = new string[] { "-----BEGIN RSA PRIVATE KEY-----" };
string[] PrivateKeyDelimetersRSA = new string[] { "-----BEGIN RSA PRIVATE KEY-----" };
string[] PrivateKeyDelimetersEC = new string[] { "-----BEGIN EC PRIVATE KEY-----" };
string CertDelimBeg = "-----BEGIN CERTIFICATE-----";
string CertDelimEnd = "-----END CERTIFICATE-----";

private enum PrivateKeyTypeEnum
{
EC,
RSA,
PKCS8
}

private bool IsTrustStore { get; set; }
private bool IncludesChain { get; set; }
private string SeparatePrivateKeyFilePath { get; set; }
Expand Down Expand Up @@ -68,11 +79,8 @@ public Pkcs12Store DeserializeRemoteCertificateStore(byte[] storeContentBytes, s
}
else
{
bool isRSAPrivateKey = false;
AsymmetricKeyEntry keyEntry = GetPrivateKey(storeContents, storePassword ?? string.Empty, remoteHandler, out isRSAPrivateKey);

if (isRSAPrivateKey && !string.IsNullOrEmpty(storePassword))
throw new RemoteFileException($"Certificate store with an RSA Private Key cannot contain a store password. Invalid store format not supported.");
PrivateKeyTypeEnum privateKeyType;
AsymmetricKeyEntry keyEntry = GetPrivateKey(storeContents, storePassword ?? string.Empty, remoteHandler, out privateKeyType);

store.SetKeyEntry(CertificateConverterFactory.FromBouncyCastleCertificate(certificates[0].Certificate).ToX509Certificate2().Thumbprint, keyEntry, certificates);
}
Expand Down Expand Up @@ -112,16 +120,14 @@ public List<SerializedStoreInfo> SerializeRemoteCertificateStore(Pkcs12Store cer
else
{
string storeContents = Encoding.ASCII.GetString(remoteHandler.DownloadCertificateFile(storePath + storeFileName));
bool isRSAPrivateKey = false;
try
{
GetPrivateKey(storeContents, storePassword, remoteHandler, out isRSAPrivateKey);
}
catch (RemoteFileException) { }

if (isRSAPrivateKey && !string.IsNullOrEmpty(storePassword))
throw new RemoteFileException($"Certificate store with an RSA Private Key cannot contain a store password. Invalid store format not supported.");
string begDelim;
string privateKeyContents = String.IsNullOrEmpty(SeparatePrivateKeyFilePath) ? storeContents : Encoding.ASCII.GetString(remoteHandler.DownloadCertificateFile(SeparatePrivateKeyFilePath));
PrivateKeyTypeEnum privateKeyType = GetPrivateKeyType(privateKeyContents, out begDelim);

if (!string.IsNullOrEmpty(storePassword) && privateKeyType != PrivateKeyTypeEnum.PKCS8)
throw new RemoteFileException("Error retrieving private key. Certificate store password cannot have a non empty value if the private key is in PKCS#1 format (BEGIN [RSA|EC] PRIVATE KEY)");

bool keyEntryProcessed = false;
foreach (string alias in certificateStore.Aliases)
{
Expand All @@ -137,10 +143,16 @@ public List<SerializedStoreInfo> SerializeRemoteCertificateStore(Pkcs12Store cer
CertificateConverter certConverter = CertificateConverterFactory.FromBouncyCastleCertificate(chainEntries[0].Certificate);

AsymmetricKeyParameter privateKey = certificateStore.GetKey(alias).Key;
X509CertificateEntry[] certEntries = certificateStore.GetCertificateChain(alias);
AsymmetricKeyParameter publicKey = certEntries[0].Certificate.GetPublicKey();
AsymmetricKeyParameter publicKey = chainEntries[0].Certificate.GetPublicKey();

if (isRSAPrivateKey)
if (privateKeyType == PrivateKeyTypeEnum.PKCS8)
{
PrivateKeyConverter keyConverter = PrivateKeyConverterFactory.FromBCKeyPair(privateKey, publicKey, false);

byte[] privateKeyBytes = string.IsNullOrEmpty(storePassword) ? keyConverter.ToPkcs8BlobUnencrypted() : keyConverter.ToPkcs8Blob(storePassword);
keyString = PemUtilities.DERToPEM(privateKeyBytes, string.IsNullOrEmpty(storePassword) ? PemUtilities.PemObjectType.PrivateKey : PemUtilities.PemObjectType.EncryptedPrivateKey);
}
else
{
TextWriter textWriter = new StringWriter();
PemWriter pemWriter = new PemWriter(textWriter);
Expand All @@ -149,13 +161,6 @@ public List<SerializedStoreInfo> SerializeRemoteCertificateStore(Pkcs12Store cer

keyString = textWriter.ToString();
}
else
{
PrivateKeyConverter keyConverter = PrivateKeyConverterFactory.FromBCKeyPair(privateKey, publicKey, false);

byte[] privateKeyBytes = string.IsNullOrEmpty(storePassword) ? keyConverter.ToPkcs8BlobUnencrypted() : keyConverter.ToPkcs8Blob(storePassword);
keyString = PemUtilities.DERToPEM(privateKeyBytes, string.IsNullOrEmpty(storePassword) ? PemUtilities.PemObjectType.PrivateKey : PemUtilities.PemObjectType.EncryptedPrivateKey);
}

pemString = certConverter.ToPEM(true);
if (string.IsNullOrEmpty(SeparatePrivateKeyFilePath))
Expand Down Expand Up @@ -230,61 +235,120 @@ private X509CertificateEntry[] GetCertificates(string certificates)
return certificateEntries.ToArray();
}

private AsymmetricKeyEntry GetPrivateKey(string storeContents, string storePassword, IRemoteHandler remoteHandler, out bool isRSA)
private AsymmetricKeyEntry GetPrivateKey(string storeContents, string storePassword, IRemoteHandler remoteHandler, out PrivateKeyTypeEnum privateKeyType)
{
logger.MethodEntry(LogLevel.Debug);

AsymmetricKeyEntry keyEntry = null;

if (!String.IsNullOrEmpty(SeparatePrivateKeyFilePath))
{
storeContents = Encoding.ASCII.GetString(remoteHandler.DownloadCertificateFile(SeparatePrivateKeyFilePath));
}

isRSA = false;
foreach (string begDelim in PrivateKeyDelimetersPkcs1)
string begDelim = string.Empty;
privateKeyType = GetPrivateKeyType(storeContents, out begDelim);

string privateKey = string.Empty;
string endDelim = begDelim.Replace("BEGIN", "END");

int keyStart = storeContents.IndexOf(begDelim);
if (keyStart == -1)
throw new RemoteFileException("Invalid private key: No beginning private key delimiter found.");

int keyLength = storeContents.IndexOf(endDelim) + endDelim.Length - keyStart;
if (keyLength == -1)
throw new RemoteFileException("Invalid private key: No ending private key delimiter found.");

privateKey = storeContents.Substring(keyStart, keyLength).Replace(begDelim, string.Empty).Replace(endDelim, string.Empty);

if (string.IsNullOrEmpty(privateKey))
throw new RemoteFileException("Invalid private key: No private key or invalid private key format found.");

PrivateKeyConverter c = null;
int bytesRead;
switch (privateKeyType)
{
case PrivateKeyTypeEnum.PKCS8:
c = PrivateKeyConverterFactory.FromPkcs8Blob(Convert.FromBase64String(privateKey), storePassword);
keyEntry = new AsymmetricKeyEntry(c.ToBCPrivateKey());
break;
case PrivateKeyTypeEnum.RSA:
RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out bytesRead);
c = PrivateKeyConverterFactory.FromNetPrivateKey(rsa, false);
keyEntry = new AsymmetricKeyEntry(c.ToBCPrivateKey());
break;
case PrivateKeyTypeEnum.EC:
ECDiffieHellman ec = ECDiffieHellman.Create();
keyEntry = new AsymmetricKeyEntry(this.ToBCPrivateKey(ec));
break;
}

logger.MethodExit(LogLevel.Debug);

return keyEntry;
}

private PrivateKeyTypeEnum GetPrivateKeyType(string storeContents, out string privateKeyBegDelim)
{
foreach (string begDelim in PrivateKeyDelimetersPkcs8)
{
if (string.IsNullOrEmpty(storeContents) || storeContents.Contains(begDelim))
{
privateKeyBegDelim = begDelim;
return PrivateKeyTypeEnum.PKCS8;
}
}

foreach (string begDelim in PrivateKeyDelimetersRSA)
{
if (storeContents.Contains(begDelim))
{
isRSA = true;
break;
privateKeyBegDelim = begDelim;
return PrivateKeyTypeEnum.RSA;
}
}

string privateKey = string.Empty;
foreach (string begDelim in isRSA ? PrivateKeyDelimetersPkcs1 : PrivateKeyDelimetersPkcs8)
foreach (string begDelim in PrivateKeyDelimetersEC)
{
string endDelim = begDelim.Replace("BEGIN", "END");
if (storeContents.Contains(begDelim))
{
privateKeyBegDelim = begDelim;
return PrivateKeyTypeEnum.EC;
}
}

int keyStart = storeContents.IndexOf(begDelim);
if (keyStart == -1)
continue;
int keyLength = storeContents.IndexOf(endDelim) + endDelim.Length - keyStart;
if (keyLength == -1)
throw new RemoteFileException("Invalid private key: No ending private key delimiter found.");
throw new RemoteFileException("Invalid or unsupported Private Key format.");
}

privateKey = storeContents.Substring(keyStart, keyLength).Replace(begDelim, string.Empty).Replace(endDelim, string.Empty);
private AsymmetricKeyParameter ToBCPrivateKey(ECDiffieHellman ecdh)
{
// Export the key as ECParameters
ECParameters parameters = ecdh.ExportParameters(true);

break;
}
// Convert the parameters to a BouncyCastle ECCurve
var curve2 = ECNamedCurveTable.GetByName(parameters.Curve.Oid.FriendlyName);
var curve = ECNamedCurveTable.GetByOid(new Org.BouncyCastle.Asn1.DerObjectIdentifier(parameters.Curve.Oid.Value));
if (curve == null)
throw new RemoteFileException("Error converting to BouncyCastle private key - Unsupported curve");

if (string.IsNullOrEmpty(privateKey))
throw new RemoteFileException("Invalid private key: No private key or invalid private key format found.");
// Convert the parameters to BigInteger
var q = curve.Curve.CreatePoint(
new BigInteger(1, parameters.Q.X),
new BigInteger(1, parameters.Q.Y));

PrivateKeyConverter c;
if (isRSA)
if (parameters.D != null)
{
RSA rsa = RSA.Create();
int bytesRead;
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out bytesRead);
c = PrivateKeyConverterFactory.FromNetPrivateKey(rsa, false);
// Create private key parameter
return new ECPrivateKeyParameters(
new BigInteger(1, parameters.D),
new ECDomainParameters(curve));
}
else
{
c = PrivateKeyConverterFactory.FromPkcs8Blob(Convert.FromBase64String(privateKey), storePassword);
throw new RemoteFileException("Error converting to BouncyCastle private key - Invalid parameter.");
}

logger.MethodExit(LogLevel.Debug);

return new AsymmetricKeyEntry(c.ToBCPrivateKey());
}
}
}
11 changes: 10 additions & 1 deletion RemoteFile/InventoryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,17 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
string sudoImpersonatedUser = properties.SudoImpersonatedUser == null || string.IsNullOrEmpty(properties.SudoImpersonatedUser.Value) ?
ApplicationSettings.DefaultSudoImpersonatedUser :
properties.SudoImpersonatedUser.Value;
bool includePortInSPN = properties.IncludePortInSPN == null || string.IsNullOrEmpty(properties.IncludePortInSPN.Value) ?
false :
Convert.ToBoolean(properties.IncludePortInSPN.Value);

certificateStore = new RemoteCertificateStore(config.CertificateStoreDetails.ClientMachine, userName, userPassword, config.CertificateStoreDetails.StorePath, storePassword, config.JobProperties);
ApplicationSettings.FileTransferProtocolEnum fileTransferProtocol = ApplicationSettings.FileTransferProtocol;
if (properties.FileTransferProtocol != null && !string.IsNullOrEmpty(properties.FileTransferProtocol.Value))
{
Enum.TryParse(properties.FileTransferProtocol.Value, out fileTransferProtocol);
}

certificateStore = new RemoteCertificateStore(config.CertificateStoreDetails.ClientMachine, userName, userPassword, config.CertificateStoreDetails.StorePath, storePassword, fileTransferProtocol, includePortInSPN);
certificateStore.Initialize(sudoImpersonatedUser);
certificateStore.LoadCertificateStore(certificateStoreSerializer, true);

Expand Down
13 changes: 11 additions & 2 deletions RemoteFile/ManagementBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,17 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
bool removeRootCertificate = properties.RemoveRootCertificate == null || string.IsNullOrEmpty(properties.RemoveRootCertificate.Value) ?
false :
Convert.ToBoolean(properties.RemoveRootCertificate.Value);
bool includePortInSPN = properties.IncludePortInSPN == null || string.IsNullOrEmpty(properties.IncludePortInSPN.Value) ?
false :
Convert.ToBoolean(properties.IncludePortInSPN.Value);

ApplicationSettings.FileTransferProtocolEnum fileTransferProtocol = ApplicationSettings.FileTransferProtocol;
if (properties.FileTransferProtocol != null && !string.IsNullOrEmpty(properties.FileTransferProtocol.Value))
{
Enum.TryParse(properties.FileTransferProtocol.Value, out fileTransferProtocol);
}

certificateStore = new RemoteCertificateStore(config.CertificateStoreDetails.ClientMachine, userName, userPassword, config.CertificateStoreDetails.StorePath, storePassword, config.JobProperties);
certificateStore = new RemoteCertificateStore(config.CertificateStoreDetails.ClientMachine, userName, userPassword, config.CertificateStoreDetails.StorePath, storePassword, fileTransferProtocol, includePortInSPN);
certificateStore.Initialize(sudoImpersonatedUser);

PathFile storePathFile = RemoteCertificateStore.SplitStorePathFile(config.CertificateStoreDetails.StorePath);
Expand Down Expand Up @@ -135,7 +144,7 @@ private void CreateStore(ICertificateStoreSerializer certificateStoreSerializer,
ApplicationSettings.DefaultOwnerOnStoreCreation :
properties.LinuxFileOwnerOnStoreCreation.Value;

certificateStore.CreateCertificateStore(certificateStoreSerializer, config.CertificateStoreDetails.StorePath, linuxFilePermissions, string.IsNullOrEmpty(linuxFileOwner) ? config.ServerUsername : linuxFileOwner);
certificateStore.CreateCertificateStore(certificateStoreSerializer, config.CertificateStoreDetails.StorePath, linuxFilePermissions, linuxFileOwner);
}
}
}
Loading
Loading