From 34719699c2d46d389df95080707c8622d9143e82 Mon Sep 17 00:00:00 2001 From: Martin Dimov Date: Tue, 2 Aug 2016 22:45:16 +0200 Subject: [PATCH] Pageant support (migrated from 2014) --- src/Renci.SshNet/AgentAuthenticationMethod.cs | 223 ++++++++++++++++++ src/Renci.SshNet/AgentConnectionInfo.cs | 193 +++++++++++++++ src/Renci.SshNet/IAgentProtocol.cs | 24 ++ src/Renci.SshNet/IdentityReference.cs | 37 +++ src/Renci.SshNet/Pageant/COPYDATASTRUCT.cs | 22 ++ src/Renci.SshNet/Pageant/NativeMethods.cs | 17 ++ src/Renci.SshNet/Pageant/PageantProtocol.cs | 199 ++++++++++++++++ src/Renci.SshNet/Renci.SshNet.csproj | 7 + 8 files changed, 722 insertions(+) create mode 100644 src/Renci.SshNet/AgentAuthenticationMethod.cs create mode 100644 src/Renci.SshNet/AgentConnectionInfo.cs create mode 100644 src/Renci.SshNet/IAgentProtocol.cs create mode 100644 src/Renci.SshNet/IdentityReference.cs create mode 100644 src/Renci.SshNet/Pageant/COPYDATASTRUCT.cs create mode 100644 src/Renci.SshNet/Pageant/NativeMethods.cs create mode 100644 src/Renci.SshNet/Pageant/PageantProtocol.cs diff --git a/src/Renci.SshNet/AgentAuthenticationMethod.cs b/src/Renci.SshNet/AgentAuthenticationMethod.cs new file mode 100644 index 000000000..f97f1253d --- /dev/null +++ b/src/Renci.SshNet/AgentAuthenticationMethod.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using Renci.SshNet.Messages.Authentication; +using Renci.SshNet.Messages; +using Renci.SshNet.Common; +using System.Threading; +using System.Text; + +namespace Renci.SshNet +{ + /// + /// Provides functionality to perform private key authentication. + /// + public class AgentAuthenticationMethod : AuthenticationMethod, IDisposable + { + private AuthenticationResult _authenticationResult = AuthenticationResult.Failure; + + private EventWaitHandle _authenticationCompleted = new ManualResetEvent(false); + + private bool _isSignatureRequired; + + /// + /// Gets authentication method name + /// + public override string Name + { + get { return "publickey"; } + } + + /// + /// + /// + public IAgentProtocol Protocol { get; private set; } + + + /// + /// Initializes a new instance of the class. + /// + /// The username. + /// The key files. + /// is whitespace or null. + public AgentAuthenticationMethod(string username, IAgentProtocol protocol) + : base(username) + { + this.Protocol = protocol; + } + + /// + /// Authenticates the specified session. + /// + /// The session to authenticate. + /// + public override AuthenticationResult Authenticate(Session session) + { + if (this.Protocol == null) + return AuthenticationResult.Failure; + + session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived; + session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; + session.MessageReceived += Session_MessageReceived; + + session.RegisterMessage("SSH_MSG_USERAUTH_PK_OK"); + + foreach (var identity in this.Protocol.GetIdentities()) + { + this._authenticationCompleted.Reset(); + this._isSignatureRequired = false; + + var message = new RequestMessagePublicKey(ServiceName.Connection, this.Username, identity.Type, identity.Blob); + + + // Send public key authentication request + session.SendMessage(message); + + session.WaitOnHandle(this._authenticationCompleted); + + if (this._isSignatureRequired) + { + this._authenticationCompleted.Reset(); + + var signatureMessage = new RequestMessagePublicKey(ServiceName.Connection, this.Username, identity.Type, identity.Blob); + + var signatureData = new SignatureData(message, session.SessionId).GetBytes(); + + signatureMessage.Signature = this.Protocol.SignData(identity, signatureData); + + // Send public key authentication request with signature + session.SendMessage(signatureMessage); + } + + session.WaitOnHandle(this._authenticationCompleted); + + if (this._authenticationResult == AuthenticationResult.Success) + { + break; + } + } + + session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived; + session.UserAuthenticationFailureReceived -= Session_UserAuthenticationFailureReceived; + session.MessageReceived -= Session_MessageReceived; + + session.UnRegisterMessage("SSH_MSG_USERAUTH_PK_OK"); + + return this._authenticationResult; + } + + private void Session_UserAuthenticationSuccessReceived(object sender, MessageEventArgs e) + { + this._authenticationResult = AuthenticationResult.Success; + + this._authenticationCompleted.Set(); + } + + private void Session_UserAuthenticationFailureReceived(object sender, MessageEventArgs e) + { + if (e.Message.PartialSuccess) + this._authenticationResult = AuthenticationResult.PartialSuccess; + else + this._authenticationResult = AuthenticationResult.Failure; + + // Copy allowed authentication methods + this.AllowedAuthentications = e.Message.AllowedAuthentications.ToList(); + + this._authenticationCompleted.Set(); + } + + private void Session_MessageReceived(object sender, MessageEventArgs e) + { + var publicKeyMessage = e.Message as PublicKeyMessage; + if (publicKeyMessage != null) + { + this._isSignatureRequired = true; + this._authenticationCompleted.Set(); + } + } + + #region IDisposable Members + + private bool isDisposed = false; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (!this.isDisposed) + { + // If disposing equals true, dispose all managed + // and unmanaged resources. + if (disposing) + { + // Dispose managed resources. + if (this._authenticationCompleted != null) + { + this._authenticationCompleted.Dispose(); + this._authenticationCompleted = null; + } + } + + // Note disposing has been done. + isDisposed = true; + } + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~AgentAuthenticationMethod() + { + // Do not re-create Dispose clean-up code here. + // Calling Dispose(false) is optimal in terms of + // readability and maintainability. + Dispose(false); + } + + #endregion + + private class SignatureData : SshData + { + private RequestMessagePublicKey _message; + + private byte[] _sessionId; + + public SignatureData(RequestMessagePublicKey message, byte[] sessionId) + { + this._message = message; + this._sessionId = sessionId; + } + + protected override void LoadData() + { + throw new System.NotImplementedException(); + } + + protected override void SaveData() + { + this.WriteBinaryString(this._sessionId); + this.Write((byte)50); + this.WriteBinaryString(this._message.Username); + this.Write("ssh-connection", SshData.Ascii); + this.Write("publickey", SshData.Ascii); + this.Write((byte)1); + this.WriteBinaryString(this._message.PublicKeyAlgorithmName); + this.WriteBinaryString(this._message.PublicKeyData); + } + } + + } +} diff --git a/src/Renci.SshNet/AgentConnectionInfo.cs b/src/Renci.SshNet/AgentConnectionInfo.cs new file mode 100644 index 000000000..34c0f67ed --- /dev/null +++ b/src/Renci.SshNet/AgentConnectionInfo.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections.ObjectModel; + +namespace Renci.SshNet +{ + /// + /// Provides connection information when private key authentication method is used + /// + public class AgentConnectionInfo : ConnectionInfo, IDisposable + { + /// + /// Gets the key files used for authentication. + /// + public IAgentProtocol Protocol { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection username. + /// Connection key files. + public AgentConnectionInfo(string host, string username, IAgentProtocol protocol) + : this(host, 22, username, ProxyTypes.None, string.Empty, 0, string.Empty, string.Empty, protocol) + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection port. + /// Connection username. + /// Connection key files. + public AgentConnectionInfo(string host, int port, string username, IAgentProtocol protocol) + : this(host, port, username, ProxyTypes.None, string.Empty, 0, string.Empty, string.Empty, protocol) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// The port. + /// Connection username. + /// Type of the proxy. + /// The proxy host. + /// The proxy port. + /// The key files. + public AgentConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, IAgentProtocol protocol) + : this(host, port, username, proxyType, proxyHost, proxyPort, string.Empty, string.Empty, protocol) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// The port. + /// Connection username. + /// Type of the proxy. + /// The proxy host. + /// The proxy port. + /// The proxy username. + /// The key files. + public AgentConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, IAgentProtocol protocol) + : this(host, port, username, proxyType, proxyHost, proxyPort, proxyUsername, string.Empty, protocol) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection username. + /// Type of the proxy. + /// The proxy host. + /// The proxy port. + /// The key files. + public AgentConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, IAgentProtocol protocol) + : this(host, 22, username, proxyType, proxyHost, proxyPort, string.Empty, string.Empty, protocol) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection username. + /// Type of the proxy. + /// The proxy host. + /// The proxy port. + /// The proxy username. + /// The key files. + public AgentConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, IAgentProtocol protocol) + : this(host, 22, username, proxyType, proxyHost, proxyPort, proxyUsername, string.Empty, protocol) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection username. + /// Type of the proxy. + /// The proxy host. + /// The proxy port. + /// The proxy username. + /// The proxy password. + /// The key files. + public AgentConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, string proxyPassword, IAgentProtocol protocol) + : this(host, 22, username, proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword, protocol) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// The port. + /// Connection username. + /// Type of the proxy. + /// The proxy host. + /// The proxy port. + /// The proxy username. + /// The proxy password. + /// The key files. + public AgentConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, string proxyPassword, IAgentProtocol protocol) + : base(host, port, username, proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword, new AgentAuthenticationMethod(username,protocol)) + { + this.Protocol = protocol; + } + + #region IDisposable Members + + private bool isDisposed = false; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (!this.isDisposed) + { + // If disposing equals true, dispose all managed + // and unmanaged resources. + if (disposing) + { + // Dispose managed resources. + if (this.AuthenticationMethods != null) + { + foreach (var authenticationMethods in this.AuthenticationMethods.OfType()) + { + authenticationMethods.Dispose(); + } + } + } + + // Note disposing has been done. + isDisposed = true; + } + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~AgentConnectionInfo() + { + // Do not re-create Dispose clean-up code here. + // Calling Dispose(false) is optimal in terms of + // readability and maintainability. + Dispose(false); + } + + #endregion + } +} diff --git a/src/Renci.SshNet/IAgentProtocol.cs b/src/Renci.SshNet/IAgentProtocol.cs new file mode 100644 index 000000000..fc87a0b4e --- /dev/null +++ b/src/Renci.SshNet/IAgentProtocol.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Renci.SshNet +{ + /// + /// + /// + public interface IAgentProtocol + { + /// + /// + /// + /// + IEnumerable GetIdentities(); + + /// + /// + /// + /// + /// + /// + byte[] SignData(IdentityReference identity, byte[] data); + } +} \ No newline at end of file diff --git a/src/Renci.SshNet/IdentityReference.cs b/src/Renci.SshNet/IdentityReference.cs new file mode 100644 index 000000000..f2ef58dcd --- /dev/null +++ b/src/Renci.SshNet/IdentityReference.cs @@ -0,0 +1,37 @@ +namespace Renci.SshNet +{ + /// + /// + /// + public class IdentityReference + { + /// + /// + /// + public string Type { get; private set; } + + /// + /// + /// + public byte[] Blob { get; private set; } + + /// + /// + /// + public string Comment { get; private set; } + + /// + /// + /// + /// + /// + /// + public IdentityReference(string type,byte[] blob,string comment ) + { + this.Type = type; + this.Blob = blob; + this.Comment = comment; + } + + } +} \ No newline at end of file diff --git a/src/Renci.SshNet/Pageant/COPYDATASTRUCT.cs b/src/Renci.SshNet/Pageant/COPYDATASTRUCT.cs new file mode 100644 index 000000000..75444c215 --- /dev/null +++ b/src/Renci.SshNet/Pageant/COPYDATASTRUCT.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace Renci.SshNet.Pageant +{ + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct COPYDATASTRUCT + { + public COPYDATASTRUCT(int dwData, string lpData) + { + this.dwData = dwData; + this.lpData = lpData; + cbData = lpData.Length + 1; + } + + + private readonly int dwData; + + private readonly int cbData; + + [MarshalAs(UnmanagedType.LPStr)] private readonly string lpData; + } +} \ No newline at end of file diff --git a/src/Renci.SshNet/Pageant/NativeMethods.cs b/src/Renci.SshNet/Pageant/NativeMethods.cs new file mode 100644 index 000000000..64be2d2ae --- /dev/null +++ b/src/Renci.SshNet/Pageant/NativeMethods.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Renci.SshNet.Pageant +{ + internal class NativeMethods + { + [DllImport("user32.dll", EntryPoint = "SendMessageA", CallingConvention = CallingConvention.StdCall, + ExactSpelling = true)] + public static extern IntPtr SendMessage(IntPtr hWnd, int dwMsg, IntPtr wParam, ref COPYDATASTRUCT lParam); + + [DllImportAttribute("user32.dll", EntryPoint = "FindWindowA", CallingConvention = CallingConvention.Winapi, + ExactSpelling = true)] + public static extern IntPtr FindWindow([MarshalAsAttribute(UnmanagedType.LPStr)] string lpClassName, + [MarshalAsAttribute(UnmanagedType.LPStr)] string lpWindowName); + } +} \ No newline at end of file diff --git a/src/Renci.SshNet/Pageant/PageantProtocol.cs b/src/Renci.SshNet/Pageant/PageantProtocol.cs new file mode 100644 index 000000000..6574d811a --- /dev/null +++ b/src/Renci.SshNet/Pageant/PageantProtocol.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Net; +using System.Text; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Pageant +{ + /// + /// + /// + public class PageantProtocol:IAgentProtocol + { + + #region Constants + + private const int WM_COPYDATA = 0x004A; + + private const int AGENT_COPYDATA_ID = unchecked((int)0x804e50ba); + + private const int AGENT_MAX_MSGLEN = 8192; + + /// + /// + /// + public const byte SSH2_AGENTC_REQUEST_IDENTITIES = 11; + + /// + /// + /// + public const byte SSH2_AGENT_IDENTITIES_ANSWER = 12; + + /// + /// + /// + public const byte SSH2_AGENTC_SIGN_REQUEST = 13; + + /// + /// + /// + public const byte SSH2_AGENT_SIGN_RESPONSE = 14; + + #endregion + + /// + /// + /// + public static bool IsRunning + { + get + { + var hWnd = NativeMethods.FindWindow("Pageant", "Pageant"); + + return hWnd != IntPtr.Zero; + } + } + + + /// + /// + /// + public PageantProtocol() + { + var hWnd = NativeMethods.FindWindow("Pageant", "Pageant"); + + if (hWnd == IntPtr.Zero) + { + throw new SshException("Pageant not running"); + } + + } + + + #region Implementation of IAgentProtocol + + IEnumerable IAgentProtocol.GetIdentities() + { + var hWnd = NativeMethods.FindWindow("Pageant", "Pageant"); + + if (hWnd == IntPtr.Zero) + { + yield break; + } + + string mmFileName = Path.GetRandomFileName(); + + using (var mmFile = MemoryMappedFile.CreateNew(mmFileName, AGENT_MAX_MSGLEN)) + { + using (var accessor = mmFile.CreateViewAccessor()) + { + var security = mmFile.GetAccessControl(); + security.SetOwner(System.Security.Principal.WindowsIdentity.GetCurrent().User); + mmFile.SetAccessControl(security); + + accessor.Write(0, IPAddress.NetworkToHostOrder(AGENT_MAX_MSGLEN - 4)); + accessor.Write(4, SSH2_AGENTC_REQUEST_IDENTITIES); + + + var copy = new COPYDATASTRUCT(AGENT_COPYDATA_ID, mmFileName); + + if (NativeMethods.SendMessage(hWnd, WM_COPYDATA, IntPtr.Zero, ref copy) == IntPtr.Zero) + { + yield break; + } + + if (accessor.ReadByte(4) != SSH2_AGENT_IDENTITIES_ANSWER) + { + yield break; + } + + int numberOfIdentities = IPAddress.HostToNetworkOrder(accessor.ReadInt32(5)); + + + if (numberOfIdentities == 0) + { + yield break; + } + + int position = 9; + for (int i = 0; i < numberOfIdentities; i++) + { + int blobSize = IPAddress.HostToNetworkOrder(accessor.ReadInt32(position)); + position += 4; + + var blob = new byte[blobSize]; + + accessor.ReadArray(position, blob, 0, blobSize); + position += blobSize; + int commnetLenght = IPAddress.HostToNetworkOrder(accessor.ReadInt32(position)); + position += 4; + var commentChars = new byte[commnetLenght]; + accessor.ReadArray(position, commentChars, 0, commnetLenght); + position += commnetLenght; + + string comment = Encoding.ASCII.GetString(commentChars); + string type = Encoding.ASCII.GetString(blob, 4, 7);// needs more testing kind of hack + + yield return new IdentityReference(type,blob,comment); + + } + } + + } + } + + byte[] IAgentProtocol.SignData(IdentityReference identity, byte[] data) + { + var hWnd = NativeMethods.FindWindow("Pageant", "Pageant"); + + if (hWnd == IntPtr.Zero) + { + return new byte[0]; + } + + string mmFileName = Path.GetRandomFileName(); + + using (var mmFile = MemoryMappedFile.CreateNew(mmFileName, AGENT_MAX_MSGLEN)) + { + using (var accessor = mmFile.CreateViewAccessor()) + { + var security = mmFile.GetAccessControl(); + security.SetOwner(System.Security.Principal.WindowsIdentity.GetCurrent().User); + mmFile.SetAccessControl(security); + + accessor.Write(0, IPAddress.NetworkToHostOrder(AGENT_MAX_MSGLEN - 4)); + accessor.Write(4, SSH2_AGENTC_SIGN_REQUEST); + accessor.Write(5, IPAddress.NetworkToHostOrder(identity.Blob.Length)); + accessor.WriteArray(9, identity.Blob, 0, identity.Blob.Length); + accessor.Write(9 + identity.Blob.Length, IPAddress.NetworkToHostOrder(data.Length)); + accessor.WriteArray(13 + identity.Blob.Length, data, 0, data.Length); + + + + var copy = new COPYDATASTRUCT(AGENT_COPYDATA_ID, mmFileName); + + if (NativeMethods.SendMessage(hWnd, WM_COPYDATA, IntPtr.Zero, ref copy) == IntPtr.Zero) + { + return new byte[0]; + } + + if (accessor.ReadByte(4) != SSH2_AGENT_SIGN_RESPONSE) + { + return new byte[0]; + } + + int size = IPAddress.HostToNetworkOrder(accessor.ReadInt32(5)); + var ret = new byte[size]; + accessor.ReadArray(9, ret, 0, size); + return ret; + } + } + } + + #endregion + } +} diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index 8e704bc69..071f841aa 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -57,6 +57,8 @@ + + Code @@ -147,9 +149,11 @@ Code + + @@ -160,6 +164,9 @@ + + +