diff --git a/src/Renci.SshNet/AgentAuthenticationMethod.cs b/src/Renci.SshNet/AgentAuthenticationMethod.cs new file mode 100644 index 000000000..17df72918 --- /dev/null +++ b/src/Renci.SshNet/AgentAuthenticationMethod.cs @@ -0,0 +1,228 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +using System; +using System.Linq; +using System.Text; +using System.Threading; +using Renci.SshNet.Common; +using Renci.SshNet.Messages; +using Renci.SshNet.Messages.Authentication; + +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) { + Protocol = protocol; + } + + /// + /// Authenticates the specified session. + /// + /// The session to authenticate. + /// + public override AuthenticationResult Authenticate (Session session) { + if (Protocol == null) + return AuthenticationResult.Failure; + + session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessReceived; + session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; + session.UserAuthenticationPublicKeyReceived += Session_UserAuthenticationPublicKeyReceived; + + session.RegisterMessage ("SSH_MSG_USERAUTH_PK_OK"); + + try { + foreach (var identity in Protocol.GetIdentities ()) { + _authenticationCompleted.Reset (); + _isSignatureRequired = false; + + var message = new RequestMessagePublicKey (ServiceName.Connection, + Username, + identity.Type, + identity.Blob); + + // Send public key authentication request + session.SendMessage (message); + + session.WaitOnHandle (_authenticationCompleted); + + if (_isSignatureRequired) { + _authenticationCompleted.Reset (); + + var signatureMessage = new RequestMessagePublicKey (ServiceName.Connection, + 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 (_authenticationCompleted); + + if (_authenticationResult == AuthenticationResult.Success) { + break; + } + } + return _authenticationResult; + } finally { + session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessReceived; + session.UserAuthenticationFailureReceived -= Session_UserAuthenticationFailureReceived; + session.UserAuthenticationPublicKeyReceived -= Session_UserAuthenticationPublicKeyReceived; + session.UnRegisterMessage ("SSH_MSG_USERAUTH_PK_OK"); + } + } + + 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) + _authenticationResult = AuthenticationResult.PartialSuccess; + else + _authenticationResult = AuthenticationResult.Failure; + + // Copy allowed authentication methods + AllowedAuthentications = e.Message.AllowedAuthentications; + + _authenticationCompleted.Set (); + } + + private void Session_UserAuthenticationPublicKeyReceived (object sender, MessageEventArgs e) { + 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 (!_isDisposed) + return; + + if (disposing) { + // If disposing equals true, dispose all managed + // and unmanaged resources. + if (disposing) { + var authenticationCompleted = _authenticationCompleted; + // Dispose managed resources. + if (this._authenticationCompleted != null) { + _authenticationCompleted = null; + authenticationCompleted.Dispose (); + } + } + + // 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 readonly RequestMessagePublicKey _message; + + private byte[] _sessionId; + private readonly byte[] _serviceName; + private readonly byte[] _authenticationMethod; + + protected override int BufferCapacity { + get { + var capacity = base.BufferCapacity; + capacity += 4; // SessionId length + capacity += _sessionId.Length; // SessionId + capacity += 1; // Authentication Message Code + capacity += 4; // UserName length + capacity += _message.Username.Length; // UserName + capacity += 4; // ServiceName length + capacity += _serviceName.Length; // ServiceName + capacity += 4; // AuthenticationMethod length + capacity += _authenticationMethod.Length; // AuthenticationMethod + capacity += 1; // TRUE + capacity += 4; // PublicKeyAlgorithmName length + capacity += _message.PublicKeyAlgorithmName.Length; // PublicKeyAlgorithmName + capacity += 4; // PublicKeyData length + capacity += _message.PublicKeyData.Length; // PublicKeyData + return capacity; + } + } + + public SignatureData (RequestMessagePublicKey message, byte[] sessionId) { + _message = message; + _sessionId = sessionId; + _serviceName = Ascii.GetBytes ("ssh-connection"); + _authenticationMethod = Ascii.GetBytes ("publickey"); + } + + protected override void LoadData () { + throw new System.NotImplementedException (); + } + + protected override void SaveData () { + WriteBinaryString (_sessionId); + Write ((byte) RequestMessage.AuthenticationMessageCode); + WriteBinaryString (_message.Username); + WriteBinaryString (_serviceName); + WriteBinaryString (_authenticationMethod); + Write ((byte) 1); // TRUE + WriteBinaryString (_message.PublicKeyAlgorithmName); + WriteBinaryString (_message.PublicKeyData); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Renci.SshNet/AgentConnectionInfo.cs b/src/Renci.SshNet/AgentConnectionInfo.cs new file mode 100644 index 000000000..196fad6cb --- /dev/null +++ b/src/Renci.SshNet/AgentConnectionInfo.cs @@ -0,0 +1,164 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +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 + } +} +#endif \ No newline at end of file diff --git a/src/Renci.SshNet/COPYDATASTRUCT.cs b/src/Renci.SshNet/COPYDATASTRUCT.cs new file mode 100644 index 000000000..f25d1c9d0 --- /dev/null +++ b/src/Renci.SshNet/COPYDATASTRUCT.cs @@ -0,0 +1,13 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +using System; +using System.Runtime.InteropServices; + +namespace Renci.SshNet.Pageant { + [StructLayout (LayoutKind.Sequential)] + internal struct COPYDATASTRUCT { + public IntPtr dwData; + public int cbData; + public IntPtr lpData; + } +} +#endif \ No newline at end of file diff --git a/src/Renci.SshNet/IAgentProtocol.cs b/src/Renci.SshNet/IAgentProtocol.cs new file mode 100644 index 000000000..e1357fa45 --- /dev/null +++ b/src/Renci.SshNet/IAgentProtocol.cs @@ -0,0 +1,24 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +using System.Collections.Generic; + +namespace Renci.SshNet { + /// + /// + /// + public interface IAgentProtocol { + /// + /// + /// + /// + IEnumerable GetIdentities (); + + /// + /// + /// + /// + /// + /// + byte[] SignData (IdentityReference identity, byte[] data); + } +} +#endif \ 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..987eb6c68 --- /dev/null +++ b/src/Renci.SshNet/IdentityReference.cs @@ -0,0 +1,36 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +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; + } + + } +} +#endif \ No newline at end of file diff --git a/src/Renci.SshNet/NativeMethods.cs b/src/Renci.SshNet/NativeMethods.cs new file mode 100644 index 000000000..df4fc88e3 --- /dev/null +++ b/src/Renci.SshNet/NativeMethods.cs @@ -0,0 +1,15 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +using System; +using System.Runtime.InteropServices; + +namespace Renci.SshNet.Pageant { + internal class NativeMethods { + [DllImport ("user32.dll")] + public static extern IntPtr SendMessage (IntPtr hWnd, uint dwMsg, IntPtr wParam, IntPtr 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); + } +} +#endif \ No newline at end of file diff --git a/src/Renci.SshNet/PageantProtocol.cs b/src/Renci.SshNet/PageantProtocol.cs new file mode 100644 index 000000000..42b768844 --- /dev/null +++ b/src/Renci.SshNet/PageantProtocol.cs @@ -0,0 +1,200 @@ +#if NETFRAMEWORK && !NET20 && !NET35 +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Net; +using System.Runtime.InteropServices; +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 long AGENT_COPYDATA_ID = 0x804e50ba; + private const int AGENT_COPYDATA_ID_INT = unchecked ((int) AGENT_COPYDATA_ID); + + 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)) { + var security = mmFile.GetAccessControl (); + security.SetOwner (System.Security.Principal.WindowsIdentity.GetCurrent ().User); + mmFile.SetAccessControl (security); + using (var accessor = mmFile.CreateViewAccessor ()) { + + accessor.Write (0, IPAddress.NetworkToHostOrder (AGENT_MAX_MSGLEN - 4)); + accessor.Write (4, SSH2_AGENTC_REQUEST_IDENTITIES); + + COPYDATASTRUCT copyData = new COPYDATASTRUCT (); + if (IntPtr.Size == 4) { + copyData.dwData = new IntPtr (unchecked ((int) AGENT_COPYDATA_ID)); + } else { + copyData.dwData = new IntPtr (AGENT_COPYDATA_ID); + } + copyData.cbData = mmFileName.Length + 1; + copyData.lpData = Marshal.StringToCoTaskMemAnsi (mmFileName); + IntPtr copyDataPtr = Marshal.AllocHGlobal (Marshal.SizeOf (copyData)); + Marshal.StructureToPtr (copyData, copyDataPtr, false); + IntPtr resultPtr = NativeMethods.SendMessage (hWnd, WM_COPYDATA, IntPtr.Zero, copyDataPtr); + Marshal.FreeHGlobal (copyData.lpData); + Marshal.FreeHGlobal (copyDataPtr); + + if (resultPtr == 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, blob[3]); // 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); + + COPYDATASTRUCT copyData = new COPYDATASTRUCT (); + if (IntPtr.Size == 4) { + copyData.dwData = new IntPtr (unchecked ((int) AGENT_COPYDATA_ID)); + } else { + copyData.dwData = new IntPtr (AGENT_COPYDATA_ID); + } + copyData.cbData = mmFileName.Length + 1; + copyData.lpData = Marshal.StringToCoTaskMemAnsi (mmFileName); + IntPtr copyDataPtr = Marshal.AllocHGlobal (Marshal.SizeOf (copyData)); + Marshal.StructureToPtr (copyData, copyDataPtr, false); + IntPtr resultPtr = NativeMethods.SendMessage (hWnd, WM_COPYDATA, IntPtr.Zero, copyDataPtr); + Marshal.FreeHGlobal (copyData.lpData); + Marshal.FreeHGlobal (copyDataPtr); + + if (resultPtr == 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 + } +} +#endif \ No newline at end of file