Merge pull request #2285 from tecxx/develop-orig

support extraction of SSH private keys from external cred prov
This commit is contained in:
Dimitrij
2022-09-07 13:59:21 +01:00
committed by GitHub
9 changed files with 296 additions and 35 deletions

View File

@@ -1,6 +1,11 @@
using Microsoft.Win32;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using SecretServerAuthentication.DSS;
using SecretServerRestClient.DSS;
using System.Security.Cryptography;
namespace ExternalConnectors.DSS
{
@@ -114,25 +119,22 @@ namespace ExternalConnectors.DSS
}
}
private static void FetchSecret(int secretID, out string secretUsername, out string secretPassword, out string secretDomain)
private static SecretsServiceClient ConstructSecretsServiceClient()
{
string baseURL = SSConnectionData.ssUrl;
SecretModel secret;
if (SSConnectionData.ssSSO)
{
// REQUIRES IIS CONFIG! https://docs.thycotic.com/ss/11.0.0/api-scripting/webservice-iwa-powershell
var handler = new HttpClientHandler() { UseDefaultCredentials = true };
using (var httpClient = new HttpClient(handler))
var httpClient = new HttpClient(handler);
{
// Call REST API:
var client = new SecretsServiceClient($"{baseURL}/winauthwebservices/api", httpClient);
secret = client.GetSecretAsync(false, true, secretID, null).Result;
return new SecretsServiceClient($"{baseURL}/winauthwebservices/api", httpClient);
}
}
else
{
using (var httpClient = new HttpClient())
var httpClient = new HttpClient();
{
var token = GetToken();
@@ -140,15 +142,22 @@ namespace ExternalConnectors.DSS
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Call REST API:
var client = new SecretsServiceClient($"{baseURL}/api", httpClient);
secret = client.GetSecretAsync(false, true, secretID, null).Result;
return new SecretsServiceClient($"{baseURL}/api", httpClient);
}
}
}
private static void FetchSecret(int secretID, out string secretUsername, out string secretPassword, out string secretDomain, out string privatekey)
{
var client = ConstructSecretsServiceClient();
SecretModel secret = client.GetSecretAsync(false, true, secretID, null).Result;
// clear return variables
secretDomain = "";
secretUsername = "";
secretPassword = "";
privatekey = "";
string privatekeypassphrase = "";
// parse data and extract what we need
foreach (var item in secret.Items)
@@ -159,10 +168,91 @@ namespace ExternalConnectors.DSS
secretUsername = item.ItemValue;
else if (item.FieldName.ToLower().Equals("password"))
secretPassword = item.ItemValue;
else if (item.FieldName.ToLower().Equals("private key"))
{
client.ReadResponseNoJSONConvert = true;
privatekey = client.GetFieldAsync(false, false, secretID, "private-key").Result;
client.ReadResponseNoJSONConvert = false;
}
else if (item.FieldName.ToLower().Equals("private key passphrase"))
privatekeypassphrase = item.ItemValue;
}
// need to decode the private key?
if (!string.IsNullOrEmpty(privatekeypassphrase))
{
try
{
var key = DecodePrivateKey(privatekey, privatekeypassphrase);
privatekey = key;
}
catch(Exception)
{
}
}
// conversion to putty format necessary?
if (!string.IsNullOrEmpty(privatekey) && !privatekey.StartsWith("PuTTY-User-Key-File-2"))
{
try
{
RSACryptoServiceProvider key = ImportPrivateKey(privatekey);
privatekey = PuttyKeyFileGenerator.ToPuttyPrivateKey(key);
}
catch (Exception)
{
}
}
}
#region PUTTY KEY HANDLING
// decode rsa private key with encryption password
private static string DecodePrivateKey(string encryptedPrivateKey, string password)
{
TextReader textReader = new StringReader(encryptedPrivateKey);
PemReader pemReader = new PemReader(textReader, new PasswordFinder(password));
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
TextWriter textWriter = new StringWriter();
var pemWriter = new PemWriter(textWriter);
pemWriter.WriteObject(keyPair.Private);
pemWriter.Writer.Flush();
return ""+textWriter.ToString();
}
private class PasswordFinder : IPasswordFinder
{
private string password;
public PasswordFinder(string password)
{
this.password = password;
}
public char[] GetPassword()
{
return password.ToCharArray();
}
}
// read private key pem string to rsacryptoserviceprovider
public static RSACryptoServiceProvider ImportPrivateKey(string pem)
{
PemReader pr = new PemReader(new StringReader(pem));
AsymmetricCipherKeyPair KeyPair = (AsymmetricCipherKeyPair)pr.ReadObject();
RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)KeyPair.Private);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParams);
return rsa;
}
#endregion
#region TOKEN
private static string GetToken()
{
// if there is no token, fetch a fresh one
@@ -233,11 +323,11 @@ namespace ExternalConnectors.DSS
return tokenResult;
}
}
#endregion
// input must be the secret id to fetch
public static void FetchSecretFromServer(string input, out string username, out string password, out string domain)
public static void FetchSecretFromServer(string input, out string username, out string password, out string domain, out string privatekey)
{
// get secret id
int secretID = Int32.Parse(input);
@@ -246,7 +336,7 @@ namespace ExternalConnectors.DSS
SSConnectionData.Init();
// get the secret
FetchSecret(secretID, out username, out password, out domain);
FetchSecret(secretID, out username, out password, out domain, out privatekey);
}
}
}

View File

@@ -72886,6 +72886,9 @@ namespace SecretServerRestClient.DSS
}
public bool ReadResponseAsString { get; set; }
// RR 2022-09-97
public bool ReadResponseNoJSONConvert { get; set; }
// RR END
protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
{
@@ -72894,6 +72897,14 @@ namespace SecretServerRestClient.DSS
return new ObjectResponseResult<T>(default(T), string.Empty);
}
// RR 2022-09-97
if (ReadResponseNoJSONConvert)
{
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new ObjectResponseResult<T>((T)(object)responseText, responseText); // not sure if this is best practice, but it works.
}
// RR END
if (ReadResponseAsString)
{
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

View File

@@ -14,6 +14,7 @@
<PackageReference Include="AWSSDK.Core" Version="3.7.12.15" />
<PackageReference Include="AWSSDK.EC2" Version="3.7.79.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,109 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace ExternalConnectors;
public class PuttyKeyFileGenerator
{
private const int prefixSize = 4;
private const int paddedPrefixSize = prefixSize + 1;
private const int lineLength = 64;
private const string keyType = "ssh-rsa";
private const string encryptionType = "none";
// source from
// https://gist.github.com/canton7/5670788?permalink_comment_id=3240331
// https://gist.github.com/bosima/ee6630d30b533c7d7b2743a849e9b9d0
public static string ToPuttyPrivateKey(RSACryptoServiceProvider cryptoServiceProvider, string Comment = "imported-openssh-key")
{
var publicParameters = cryptoServiceProvider.ExportParameters(false);
byte[] publicBuffer = new byte[3 + keyType.Length + GetPrefixSize(publicParameters.Exponent) + publicParameters.Exponent.Length +
GetPrefixSize(publicParameters.Modulus) + publicParameters.Modulus.Length + 1];
using (var bw = new BinaryWriter(new MemoryStream(publicBuffer)))
{
bw.Write(new byte[] { 0x00, 0x00, 0x00 });
bw.Write(keyType);
PutPrefixed(bw, publicParameters.Exponent, CheckIsNeddPadding(publicParameters.Exponent));
PutPrefixed(bw, publicParameters.Modulus, CheckIsNeddPadding(publicParameters.Modulus));
}
var publicBlob = System.Convert.ToBase64String(publicBuffer);
var privateParameters = cryptoServiceProvider.ExportParameters(true);
byte[] privateBuffer = new byte[paddedPrefixSize + privateParameters.D.Length + paddedPrefixSize + privateParameters.P.Length + paddedPrefixSize + privateParameters.Q.Length + paddedPrefixSize + privateParameters.InverseQ.Length];
using (var bw = new BinaryWriter(new MemoryStream(privateBuffer)))
{
PutPrefixed(bw, privateParameters.D, true);
PutPrefixed(bw, privateParameters.P, true);
PutPrefixed(bw, privateParameters.Q, true);
PutPrefixed(bw, privateParameters.InverseQ, true);
}
var privateBlob = System.Convert.ToBase64String(privateBuffer);
HMACSHA1 hmacsha1 = new HMACSHA1(new SHA1CryptoServiceProvider().ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key")));
//byte[] bytesToHash = new byte[4 + 7 + 4 + 4 + 4 + Comment.Length + 4 + publicBuffer.Length + 4 + privateBuffer.Length];
byte[] bytesToHash = new byte[prefixSize + keyType.Length + prefixSize + encryptionType.Length + prefixSize + Comment.Length +
prefixSize + publicBuffer.Length + prefixSize + privateBuffer.Length];
using (var bw = new BinaryWriter(new MemoryStream(bytesToHash)))
{
PutPrefixed(bw, Encoding.ASCII.GetBytes("ssh-rsa"));
PutPrefixed(bw, Encoding.ASCII.GetBytes("none"));
PutPrefixed(bw, Encoding.ASCII.GetBytes(Comment));
PutPrefixed(bw, publicBuffer);
PutPrefixed(bw, privateBuffer);
}
var hash = string.Join("", hmacsha1.ComputeHash(bytesToHash).Select(x => string.Format("{0:x2}", x)));
var sb = new StringBuilder();
sb.AppendLine("PuTTY-User-Key-File-2: " + keyType);
sb.AppendLine("Encryption: " + encryptionType);
sb.AppendLine("Comment: " + Comment);
var publicLines = SpliceText(publicBlob, lineLength);
sb.AppendLine("Public-Lines: " + publicLines.Length);
foreach (var line in publicLines)
{
sb.AppendLine(line);
}
var privateLines = SpliceText(privateBlob, lineLength);
sb.AppendLine("Private-Lines: " + privateLines.Length);
foreach (var line in privateLines)
{
sb.AppendLine(line);
}
sb.AppendLine("Private-MAC: " + hash);
return sb.ToString();
}
private static void PutPrefixed(BinaryWriter bw, byte[] bytes, bool addLeadingNull = false)
{
bw.Write(BitConverter.GetBytes(bytes.Length + (addLeadingNull ? 1 : 0)).Reverse().ToArray());
if (addLeadingNull)
bw.Write(new byte[] { 0x00 });
bw.Write(bytes);
}
private static string[] SpliceText(string text, int lineLength)
{
return Regex.Matches(text, ".{1," + lineLength + "}").Cast<Match>().Select(m => m.Value).ToArray();
}
private static int GetPrefixSize(byte[] bytes)
{
return CheckIsNeddPadding(bytes) ? paddedPrefixSize : prefixSize;
}
private static bool CheckIsNeddPadding(byte[] bytes)
{
// 128 == 10000000
// This means that the number of bits can be divided by 8.
// According to the algorithm in putty, you need to add a padding.
return bytes[0] >= 128;
}
}

View File

@@ -12,6 +12,7 @@ using System.Windows.Forms;
using mRemoteNG.Properties;
using mRemoteNG.Resources.Language;
using Connection;
using System.IO;
// ReSharper disable ArrangeAccessorOwnerBody
@@ -57,6 +58,8 @@ namespace mRemoteNG.Connection.Protocol
public override bool Connect()
{
string optionalTemporaryPrivateKeyPath = ""; // path to ppk file instead of password. only temporary (extracted from credential vault).
try
{
_isPuttyNg = PuttyTypeDetector.GetPuttyType() == PuttyTypeDetector.PuttyType.PuttyNg;
@@ -85,14 +88,22 @@ namespace mRemoteNG.Connection.Protocol
var password = InterfaceControl.Info?.Password ?? "";
var domain = InterfaceControl.Info?.Domain ?? "";
var UserViaAPI = InterfaceControl.Info?.UserViaAPI ?? "";
string privatekey = "";
// access secret server api if necessary
if (InterfaceControl.Info.ExternalCredentialProvider == ExternalCredentialProvider.DelineaSecretServer)
{
try
{
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer($"{UserViaAPI}", out username, out password, out domain);
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer($"{UserViaAPI}", out username, out password, out domain, out privatekey);
if (!string.IsNullOrEmpty(privatekey))
{
optionalTemporaryPrivateKeyPath = Path.GetTempFileName();
File.WriteAllText(optionalTemporaryPrivateKeyPath, privatekey);
FileInfo fileInfo = new FileInfo(optionalTemporaryPrivateKeyPath);
fileInfo.Attributes = FileAttributes.Temporary;
}
}
catch (Exception ex)
{
@@ -111,22 +122,26 @@ namespace mRemoteNG.Connection.Protocol
username = Properties.OptionsCredentialsPage.Default.DefaultUsername;
break;
case "custom":
try
if (Properties.OptionsCredentialsPage.Default.ExternalCredentialProviderDefault == ExternalCredentialProvider.DelineaSecretServer)
{
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer(
"SSAPI:" + Properties.OptionsCredentialsPage.Default.UserViaAPDefault, out username, out password,
out domain);
}
catch (Exception ex)
{
Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0);
try
{
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer(
$"{Properties.OptionsCredentialsPage.Default.UserViaAPIDefault}", out username, out password, out domain, out privatekey);
}
catch (Exception ex)
{
Event_ErrorOccured(this, "Secret Server Interface Error: " + ex.Message, 0);
}
}
break;
}
}
if (string.IsNullOrEmpty(password))
if (string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(optionalTemporaryPrivateKeyPath))
{
if (Properties.OptionsCredentialsPage.Default.EmptyCredentials == "custom")
{
@@ -149,6 +164,13 @@ namespace mRemoteNG.Connection.Protocol
arguments.Add("-pw", password);
}
}
// use private key if specified
if (!string.IsNullOrEmpty(optionalTemporaryPrivateKeyPath))
{
arguments.Add("-i", optionalTemporaryPrivateKeyPath);
}
}
arguments.Add("-P", InterfaceControl.Info.Port.ToString());
@@ -219,6 +241,15 @@ namespace mRemoteNG.Connection.Protocol
Runtime.MessageCollector.AddMessage(MessageClass.ErrorMsg, Language.ConnectionFailed + Environment.NewLine + ex.Message);
return false;
}
finally
{
// make sure to remove the private key file
if (!string.IsNullOrEmpty(optionalTemporaryPrivateKeyPath))
{
System.Threading.Thread.Sleep(500);
System.IO.File.Delete(optionalTemporaryPrivateKeyPath);
}
}
}
public override void Focus()

View File

@@ -409,14 +409,15 @@ namespace mRemoteNG.Connection.Protocol.RDP
string gwu = connectionInfo.RDGatewayUsername;
string gwp = connectionInfo.RDGatewayPassword;
string gwd = connectionInfo.RDGatewayDomain;
string pkey = "";
// access secret server api if necessary
if (InterfaceControl.Info.RDGatewayExternalCredentialProvider == ExternalCredentialProvider.DelineaSecretServer)
{
try
{
string idviaapi = InterfaceControl.Info.RDGatewayUserViaAPI;
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer($"{idviaapi}", out gwu, out gwp, out gwd);
string RDGUserViaAPI = InterfaceControl.Info.RDGatewayUserViaAPI;
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer($"{RDGUserViaAPI}", out gwu, out gwp, out gwd, out pkey);
}
catch (Exception ex)
{
@@ -494,13 +495,14 @@ namespace mRemoteNG.Connection.Protocol.RDP
var password = connectionInfo?.Password ?? "";
var domain = connectionInfo?.Domain ?? "";
var UserViaAPI = connectionInfo?.UserViaAPI ?? "";
string pkey = "";
// access secret server api if necessary
if (InterfaceControl.Info.ExternalCredentialProvider == ExternalCredentialProvider.DelineaSecretServer)
{
try
{
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer($"{UserViaAPI}", out userName, out password, out domain);
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer($"{UserViaAPI}", out userName, out password, out domain, out pkey);
}
catch (Exception ex)
{
@@ -522,7 +524,7 @@ namespace mRemoteNG.Connection.Protocol.RDP
case "custom":
try
{
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer("SSAPI:" + Properties.OptionsCredentialsPage.Default.UserViaAPDefault, out userName, out password, out domain);
ExternalConnectors.DSS.SecretServerInterface.FetchSecretFromServer(Properties.OptionsCredentialsPage.Default.UserViaAPIDefault, out userName, out password, out domain, out pkey);
_rdpClient.UserName = userName;
}
catch (Exception ex)

View File

@@ -2200,10 +2200,10 @@ Nightly Channel includes Alphas, Betas &amp; Release Candidates.</value>
<comment>https://docs.microsoft.com/en-us/windows/win32/termserv/imstscsecuredsettings-workdir</comment>
</data>
<data name="OpeningCommand" xml:space="preserve">
<value>TODO</value>
<value>OpeningCommand TODO</value>
</data>
<data name="PropertyDescriptionOpeningCommand" xml:space="preserve">
<value>TODO</value>
<value>Description of OpeningCommand TODO</value>
</data>
<data name="RedirectDrives" xml:space="preserve">
<value>Disk Drives</value>

View File

@@ -8,6 +8,8 @@
// </auto-generated>
//------------------------------------------------------------------------------
using Connection;
namespace mRemoteNG.Properties {
@@ -58,16 +60,31 @@ namespace mRemoteNG.Properties {
this["DefaultDomain"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string UserViaAPDefault {
public ExternalCredentialProvider ExternalCredentialProviderDefault
{
get
{
return ((ExternalCredentialProvider)(this["ExternalCredentialProviderDefault"]));
}
set
{
this["ExternalCredentialProviderDefault"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string UserViaAPIDefault {
get {
return ((string)(this["UserViaAPDefault"]));
return ((string)(this["UserViaAPIDefault"]));
}
set {
this["UserViaAPDefault"] = value;
this["UserViaAPIDefault"] = value;
}
}

View File

@@ -54,7 +54,7 @@ namespace mRemoteNG.UI.Forms.OptionsPages
txtCredentialsPassword.Text =
cryptographyProvider.Decrypt(Properties.OptionsCredentialsPage.Default.DefaultPassword, Runtime.EncryptionKey);
txtCredentialsDomain.Text = Properties.OptionsCredentialsPage.Default.DefaultDomain;
txtCredentialsUserViaAPI.Text = Properties.OptionsCredentialsPage.Default.UserViaAPDefault;
txtCredentialsUserViaAPI.Text = Properties.OptionsCredentialsPage.Default.UserViaAPIDefault;
}
public override void SaveSettings()
@@ -77,7 +77,7 @@ namespace mRemoteNG.UI.Forms.OptionsPages
Properties.OptionsCredentialsPage.Default.DefaultPassword =
cryptographyProvider.Encrypt(txtCredentialsPassword.Text, Runtime.EncryptionKey);
Properties.OptionsCredentialsPage.Default.DefaultDomain = txtCredentialsDomain.Text;
Properties.OptionsCredentialsPage.Default.UserViaAPDefault = txtCredentialsUserViaAPI.Text;
Properties.OptionsCredentialsPage.Default.UserViaAPIDefault = txtCredentialsUserViaAPI.Text;
}
private void radCredentialsCustom_CheckedChanged(object sender, EventArgs e)