From 3b23d11c8a2b0ba3e4d6a44a2c9c23d3614cef21 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Sun, 24 Sep 2017 11:35:27 +0200 Subject: [PATCH 01/12] Mark ChannelSession sealed, and added doc for SendChannelOpenMessage(). --- src/Renci.SshNet/Channels/ChannelSession.cs | 52 +++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Renci.SshNet/Channels/ChannelSession.cs b/src/Renci.SshNet/Channels/ChannelSession.cs index 3295c5a5..38e13096 100644 --- a/src/Renci.SshNet/Channels/ChannelSession.cs +++ b/src/Renci.SshNet/Channels/ChannelSession.cs @@ -10,7 +10,7 @@ namespace Renci.SshNet.Channels /// /// Implements Session SSH channel. /// - internal class ChannelSession : ClientChannel, IChannelSession + internal sealed class ChannelSession : ClientChannel, IChannelSession { /// /// Counts failed channel open attempts @@ -62,7 +62,7 @@ namespace Renci.SshNet.Channels /// /// Opens the channel. /// - public virtual void Open() + public void Open() { // Try to open channel several times while (!IsOpen && _failedOpenAttempts < ConnectionInfo.RetryAttempts) @@ -356,19 +356,53 @@ namespace Renci.SshNet.Channels /// /// Sends the channel open message. /// - protected void SendChannelOpenMessage() + /// The client is not connected. + /// The operation timed out. + /// The size of the packet exceeds the maximum size defined by the protocol. + /// + /// + /// When a session semaphore for this instance has not yet been obtained by this or any other thread, + /// the thread will block until such a semaphore is available and send a + /// to the remote host. + /// + /// + /// Note that the session semaphore is released in any of the following cases: + /// + /// + /// A is received for the channel being opened. + /// + /// + /// The remote host does not respond to the within the configured . + /// + /// + /// The remote host closes the channel. + /// + /// + /// The is disposed. + /// + /// + /// A socket error occurs sending a message to the remote host. + /// + /// + /// + /// + /// If the session semaphore was already obtained for this instance (and not released), then this method + /// immediately returns control to the caller. This should only happen when another thread has obtain the + /// session semaphore and already sent the , but the remote host did not + /// confirmed or rejected attempt to open the channel. + /// + /// + private void SendChannelOpenMessage() { // do not allow open to be ChannelOpenMessage to be sent again until we've // had a response on the previous attempt for the current channel if (Interlocked.CompareExchange(ref _sessionSemaphoreObtained, 1, 0) == 0) { SessionSemaphore.Wait(); - SendMessage( - new ChannelOpenMessage( - LocalChannelNumber, - LocalWindowSize, - LocalPacketSize, - new SessionChannelOpenInfo())); + SendMessage(new ChannelOpenMessage(LocalChannelNumber, + LocalWindowSize, + LocalPacketSize, + new SessionChannelOpenInfo())); } } From b5d07629e8a07bf652a57d2fb54853dd34163f0a Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Sun, 24 Sep 2017 11:49:41 +0200 Subject: [PATCH 02/12] Document exception for SendMessage. --- src/Renci.SshNet/Channels/ClientChannel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Renci.SshNet/Channels/ClientChannel.cs b/src/Renci.SshNet/Channels/ClientChannel.cs index a33c3bc7..844c1866 100644 --- a/src/Renci.SshNet/Channels/ClientChannel.cs +++ b/src/Renci.SshNet/Channels/ClientChannel.cs @@ -52,6 +52,9 @@ namespace Renci.SshNet.Channels /// Send message to open a channel. /// /// Message to send + /// The client is not connected. + /// The operation timed out. + /// The size of the packet exceeds the maximum size defined by the protocol. protected void SendMessage(ChannelOpenMessage message) { Session.SendMessage(message); From 79c0f90db3038e7530bf80cfca729b6cce87097d Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Tue, 26 Sep 2017 21:57:37 +0200 Subject: [PATCH 03/12] Modify our DH kex to generate a private component that is twice the size of the hash that a given algorithm produces, with a minimum of 1024 bits. Fixes issue #304. Avoid using TryParse in diffie-hellman-group1-sha1 and diffie-hellman-group14-sha1. Improve test coverage. --- ...KeyExchangeDiffieHellmanGroup14Sha1Test.cs | 53 +++++++++++++++- .../KeyExchangeDiffieHellmanGroup1Sha1Test.cs | 61 +++++++++---------- src/Renci.SshNet/Renci.SshNet.csproj | 1 + .../Security/KeyExchangeDiffieHellman.cs | 26 +++++--- .../KeyExchangeDiffieHellmanGroup14Sha1.cs | 37 +++++++++-- .../KeyExchangeDiffieHellmanGroup1Sha1.cs | 23 +++++-- ...yExchangeDiffieHellmanGroupExchangeSha1.cs | 13 +++- ...xchangeDiffieHellmanGroupExchangeSha256.cs | 13 +++- ...changeDiffieHellmanGroupExchangeShaBase.cs | 5 +- .../KeyExchangeDiffieHellmanGroupSha1.cs | 14 ++++- 10 files changed, 185 insertions(+), 61 deletions(-) diff --git a/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup14Sha1Test.cs b/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup14Sha1Test.cs index f4fcb91f..4f0ef37a 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup14Sha1Test.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup14Sha1Test.cs @@ -1,13 +1,60 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Security; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Security { - /// - /// Represents "diffie-hellman-group14-sha1" algorithm implementation. - /// [TestClass] public class KeyExchangeDiffieHellmanGroup14Sha1Test : TestBase { + private static readonly byte[] SecondOkleyGroup = + { + 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, + 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, + 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, + 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, + 0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, + 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, + 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, + 0x7c, 0x4b, 0x1f, 0xe6, 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, + 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, 0x98, 0xda, 0x48, 0x36, + 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, + 0x20, 0x85, 0x52, 0xbb, 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, + 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, 0xf1, 0x74, 0x6c, 0x08, + 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, + 0xec, 0x07, 0xa2, 0x8f, 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, + 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, 0x39, 0x95, 0x49, 0x7c, + 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xac, 0xaa, 0x68, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff + }; + + private KeyExchangeDiffieHellmanGroup14Sha1 _group14; + + protected override void OnInit() + { + base.OnInit(); + + _group14 = new KeyExchangeDiffieHellmanGroup14Sha1(); + } + + [TestMethod] + public void GroupPrimeShouldBeSecondOakleyGroup() + { + var bytes = _group14.GroupPrime.ToByteArray().Reverse(); + Assert.IsTrue(SecondOkleyGroup.IsEqualTo(bytes)); + } + + [TestMethod] + public void NameShouldBeDiffieHellmanGroup14Sha1() + { + Assert.AreEqual("diffie-hellman-group14-sha1", _group14.Name); + } } } \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup1Sha1Test.cs b/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup1Sha1Test.cs index 150f27c8..9cbaaba2 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup1Sha1Test.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/KeyExchangeDiffieHellmanGroup1Sha1Test.cs @@ -5,48 +5,47 @@ using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Security { - /// - ///This is a test class for KeyExchangeDiffieHellmanGroup1Sha1Test and is intended - ///to contain all KeyExchangeDiffieHellmanGroup1Sha1Test Unit Tests - /// [TestClass] public class KeyExchangeDiffieHellmanGroup1Sha1Test : TestBase { - /// - ///A test for KeyExchangeDiffieHellmanGroup1Sha1 Constructor - /// - [TestMethod] - [Ignore] // placeholder for actual test - public void KeyExchangeDiffieHellmanGroup1Sha1ConstructorTest() + private static readonly byte[] SecondOkleyGroup = + { + 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, + 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, + 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, + 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, + 0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, + 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, + 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, + 0x7c, 0x4b, 0x1f, 0xe6, 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + }; + + private KeyExchangeDiffieHellmanGroup1Sha1 _group1; + + protected override void OnInit() { - KeyExchangeDiffieHellmanGroup1Sha1 target = new KeyExchangeDiffieHellmanGroup1Sha1(); - Assert.Inconclusive("TODO: Implement code to verify target"); + base.OnInit(); + + _group1 = new KeyExchangeDiffieHellmanGroup1Sha1(); } - /// - ///A test for GroupPrime - /// [TestMethod] - [Ignore] // placeholder for actual test - public void GroupPrimeTest() + public void GroupPrimeShouldBeSecondOakleyGroup() { - KeyExchangeDiffieHellmanGroup1Sha1 target = new KeyExchangeDiffieHellmanGroup1Sha1(); // TODO: Initialize to an appropriate value - BigInteger actual; - actual = target.GroupPrime; - Assert.Inconclusive("Verify the correctness of this test method."); + var bytes = _group1.GroupPrime.ToByteArray().Reverse(); + Assert.IsTrue(SecondOkleyGroup.IsEqualTo(bytes)); + + SecondOkleyGroup.Reverse().DebugPrint(); } - /// - ///A test for Name - /// [TestMethod] - [Ignore] // placeholder for actual test - public void NameTest() + public void NameShouldBeDiffieHellmanGroup1Sha1() { - KeyExchangeDiffieHellmanGroup1Sha1 target = new KeyExchangeDiffieHellmanGroup1Sha1(); // TODO: Initialize to an appropriate value - string actual; - actual = target.Name; - Assert.Inconclusive("Verify the correctness of this test method."); + Assert.AreEqual("diffie-hellman-group1-sha1", _group1.Name); } } -} +} \ No newline at end of file diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index 7dd4c67f..0598d18c 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -47,6 +47,7 @@ + diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs index 7ade9379..fa53d36a 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs @@ -8,7 +8,7 @@ namespace Renci.SshNet.Security /// /// Represents base class for Diffie Hellman key exchange algorithm /// - public abstract class KeyExchangeDiffieHellman : KeyExchange + internal abstract class KeyExchangeDiffieHellman : KeyExchange { /// /// Specifies key exchange group number. @@ -43,7 +43,7 @@ namespace Renci.SshNet.Security /// /// Specifies random generated number. /// - protected BigInteger _randomValue; + protected BigInteger _privateExponent; /// /// Specifies host key data. @@ -55,6 +55,14 @@ namespace Renci.SshNet.Security /// protected byte[] _signature; + /// + /// Gets the size, in bits, of the computed hash code. + /// + /// + /// The size, in bits, of the computed hash code. + /// + protected abstract int HashSize { get; } + /// /// Validates the exchange hash. /// @@ -102,14 +110,16 @@ namespace Renci.SshNet.Security if (_prime.IsZero) throw new ArgumentNullException("_prime"); - var bitLength = _prime.BitLength; + // generate private component that is twice the hash size (RFC 4419) with a minimum + // of 1024 bits (whatever is less) + var privateComponentSize = Math.Max(HashSize * 2, 1024); do { - _randomValue = BigInteger.Random(bitLength); - - _clientExchangeValue = BigInteger.ModPow(_group, _randomValue, _prime); - + // create private component + _privateExponent = BigInteger.Random(privateComponentSize); + // generate public component + _clientExchangeValue = BigInteger.ModPow(_group, _privateExponent, _prime); } while (_clientExchangeValue < 1 || _clientExchangeValue > (_prime - 1)); } @@ -123,7 +133,7 @@ namespace Renci.SshNet.Security { _serverExchangeValue = serverExchangeValue; _hostKey = hostKey; - SharedKey = BigInteger.ModPow(serverExchangeValue, _randomValue, _prime); + SharedKey = BigInteger.ModPow(serverExchangeValue, _privateExponent, _prime); _signature = signature; } } diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs index ad714f30..ec7a237d 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs @@ -1,5 +1,4 @@ using Renci.SshNet.Common; -using System.Globalization; namespace Renci.SshNet.Security { @@ -8,7 +7,35 @@ namespace Renci.SshNet.Security /// internal class KeyExchangeDiffieHellmanGroup14Sha1 : KeyExchangeDiffieHellmanGroupSha1 { - private const string SecondOkleyGroup = "00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF"; + /// + /// https://tools.ietf.org/html/rfc2409#section-6.2 + /// + private static readonly byte[] SecondOkleyGroupReversed = + { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x68, 0xaa, 0xac, 0x8a, + 0x5a, 0x8e, 0x72, 0x15, 0x10, 0x05, 0xfa, 0x98, 0x18, 0x26, 0xd2, 0x15, + 0xe5, 0x6a, 0x95, 0xea, 0x7c, 0x49, 0x95, 0x39, 0x18, 0x17, 0x58, 0x95, + 0xf6, 0xcb, 0x2b, 0xde, 0xc9, 0x52, 0x4c, 0x6f, 0xf0, 0x5d, 0xc5, 0xb5, + 0x8f, 0xa2, 0x07, 0xec, 0xa2, 0x83, 0x27, 0x9b, 0x03, 0x86, 0x0e, 0x18, + 0x2c, 0x77, 0x9e, 0xe3, 0x3b, 0xce, 0x36, 0x2e, 0x46, 0x5e, 0x90, 0x32, + 0x7c, 0x21, 0x18, 0xca, 0x08, 0x6c, 0x74, 0xf1, 0x04, 0x98, 0xbc, 0x4a, + 0x4e, 0x35, 0x0c, 0x67, 0x6d, 0x96, 0x96, 0x70, 0x07, 0x29, 0xd5, 0x9e, + 0xbb, 0x52, 0x85, 0x20, 0x56, 0xf3, 0x62, 0x1c, 0x96, 0xad, 0xa3, 0xdc, + 0x23, 0x5d, 0x65, 0x83, 0x5f, 0xcf, 0x24, 0xfd, 0xa8, 0x3f, 0x16, 0x69, + 0x9a, 0xd3, 0x55, 0x1c, 0x36, 0x48, 0xda, 0x98, 0x05, 0xbf, 0x63, 0xa1, + 0xb8, 0x7c, 0x00, 0xc2, 0x3d, 0x5b, 0xe4, 0xec, 0x51, 0x66, 0x28, 0x49, + 0xe6, 0x1f, 0x4b, 0x7c, 0x11, 0x24, 0x9f, 0xae, 0xa5, 0x9f, 0x89, 0x5a, + 0xfb, 0x6b, 0x38, 0xee, 0xed, 0xb7, 0x06, 0xf4, 0xb6, 0x5c, 0xff, 0x0b, + 0x6b, 0xed, 0x37, 0xa6, 0xe9, 0x42, 0x4c, 0xf4, 0xc6, 0x7e, 0x5e, 0x62, + 0x76, 0xb5, 0x85, 0xe4, 0x45, 0xc2, 0x51, 0x6d, 0x6d, 0x35, 0xe1, 0x4f, + 0x37, 0x14, 0x5f, 0xf2, 0x6d, 0x0a, 0x2b, 0x30, 0x1b, 0x43, 0x3a, 0xcd, + 0xb3, 0x19, 0x95, 0xef, 0xdd, 0x04, 0x34, 0x8e, 0x79, 0x08, 0x4a, 0x51, + 0x22, 0x9b, 0x13, 0x3b, 0xa6, 0xbe, 0x0b, 0x02, 0x74, 0xcc, 0x67, 0x8a, + 0x08, 0x4e, 0x02, 0x29, 0xd1, 0x1c, 0xdc, 0x80, 0x8b, 0x62, 0xc6, 0xc4, + 0x34, 0xc2, 0x68, 0x21, 0xa2, 0xda, 0x0f, 0xc9, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0x00 + }; /// /// Gets algorithm name. @@ -28,10 +55,8 @@ namespace Renci.SshNet.Security { get { - BigInteger prime; - BigInteger.TryParse(SecondOkleyGroup, NumberStyles.AllowHexSpecifier, NumberFormatInfo.CurrentInfo, out prime); - return prime; + return new BigInteger(SecondOkleyGroupReversed); } } } -} +} \ No newline at end of file diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs index 65e05ad1..3a8de7f4 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs @@ -1,14 +1,27 @@ using Renci.SshNet.Common; -using System.Globalization; namespace Renci.SshNet.Security { /// /// Represents "diffie-hellman-group1-sha1" algorithm implementation. /// - public class KeyExchangeDiffieHellmanGroup1Sha1 : KeyExchangeDiffieHellmanGroupSha1 + internal class KeyExchangeDiffieHellmanGroup1Sha1 : KeyExchangeDiffieHellmanGroupSha1 { - private const string SecondOkleyGroup = @"00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF"; + private static readonly byte[] SecondOkleyGroupReversed = + { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0x53, 0xe6, 0xec, + 0x51, 0x66, 0x28, 0x49, 0xe6, 0x1f, 0x4b, 0x7c, 0x11, 0x24, 0x9f, 0xae, + 0xa5, 0x9f, 0x89, 0x5a, 0xfb, 0x6b, 0x38, 0xee, 0xed, 0xb7, 0x06, 0xf4, + 0xb6, 0x5c, 0xff, 0x0b, 0x6b, 0xed, 0x37, 0xa6, 0xe9, 0x42, 0x4c, 0xf4, + 0xc6, 0x7e, 0x5e, 0x62, 0x76, 0xb5, 0x85, 0xe4, 0x45, 0xc2, 0x51, 0x6d, + 0x6d, 0x35, 0xe1, 0x4f, 0x37, 0x14, 0x5f, 0xf2, 0x6d, 0x0a, 0x2b, 0x30, + 0x1b, 0x43, 0x3a, 0xcd, 0xb3, 0x19, 0x95, 0xef, 0xdd, 0x04, 0x34, 0x8e, + 0x79, 0x08, 0x4a, 0x51, 0x22, 0x9b, 0x13, 0x3b, 0xa6, 0xbe, 0x0b, 0x02, + 0x74, 0xcc, 0x67, 0x8a, 0x08, 0x4e, 0x02, 0x29, 0xd1, 0x1c, 0xdc, 0x80, + 0x8b, 0x62, 0xc6, 0xc4, 0x34, 0xc2, 0x68, 0x21, 0xa2, 0xda, 0x0f, 0xc9, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00 + }; /// /// Gets algorithm name. @@ -28,9 +41,7 @@ namespace Renci.SshNet.Security { get { - BigInteger prime; - BigInteger.TryParse(SecondOkleyGroup, NumberStyles.AllowHexSpecifier, NumberFormatInfo.CurrentInfo, out prime); - return prime; + return new BigInteger(SecondOkleyGroupReversed); } } } diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs index d7bcff03..fc1a18a6 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs @@ -3,7 +3,7 @@ /// /// Represents "diffie-hellman-group-exchange-sha1" algorithm implementation. /// - public class KeyExchangeDiffieHellmanGroupExchangeSha1 : KeyExchangeDiffieHellmanGroupExchangeShaBase + internal class KeyExchangeDiffieHellmanGroupExchangeSha1 : KeyExchangeDiffieHellmanGroupExchangeShaBase { /// /// Gets algorithm name. @@ -12,5 +12,16 @@ { get { return "diffie-hellman-group-exchange-sha1"; } } + + /// + /// Gets the size, in bits, of the computed hash code. + /// + /// + /// The size, in bits, of the computed hash code. + /// + protected override int HashSize + { + get { return 160; } + } } } diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs index 0792e397..dca2de71 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs @@ -5,7 +5,7 @@ namespace Renci.SshNet.Security /// /// Represents "diffie-hellman-group-exchange-sha256" algorithm implementation. /// - public class KeyExchangeDiffieHellmanGroupExchangeSha256 : KeyExchangeDiffieHellmanGroupExchangeShaBase + internal class KeyExchangeDiffieHellmanGroupExchangeSha256 : KeyExchangeDiffieHellmanGroupExchangeShaBase { /// /// Gets algorithm name. @@ -15,6 +15,17 @@ namespace Renci.SshNet.Security get { return "diffie-hellman-group-exchange-sha256"; } } + /// + /// Gets the size, in bits, of the computed hash code. + /// + /// + /// The size, in bits, of the computed hash code. + /// + protected override int HashSize + { + get { return 256; } + } + /// /// Hashes the specified data bytes. /// diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeShaBase.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeShaBase.cs index 6b77c2d9..2ccdf731 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeShaBase.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupExchangeShaBase.cs @@ -1,12 +1,11 @@ -using Renci.SshNet.Messages; -using Renci.SshNet.Messages.Transport; +using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security { /// /// Base class for "diffie-hellman-group-exchange" algorithms. /// - public abstract class KeyExchangeDiffieHellmanGroupExchangeShaBase : KeyExchangeDiffieHellman + internal abstract class KeyExchangeDiffieHellmanGroupExchangeShaBase : KeyExchangeDiffieHellman { private const int MinimumGroupSize = 1024; private const int PreferredGroupSize = 1024; diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupSha1.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupSha1.cs index 638c44bd..eb2a0cb1 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupSha1.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellmanGroupSha1.cs @@ -1,6 +1,5 @@ using System; using Renci.SshNet.Common; -using Renci.SshNet.Messages; using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security @@ -8,7 +7,7 @@ namespace Renci.SshNet.Security /// /// Represents "diffie-hellman-group1-sha1" algorithm implementation. /// - public abstract class KeyExchangeDiffieHellmanGroupSha1 : KeyExchangeDiffieHellman + internal abstract class KeyExchangeDiffieHellmanGroupSha1 : KeyExchangeDiffieHellman { /// /// Gets the group prime. @@ -18,6 +17,17 @@ namespace Renci.SshNet.Security /// public abstract BigInteger GroupPrime { get; } + /// + /// Gets the size, in bits, of the computed hash code. + /// + /// + /// The size, in bits, of the computed hash code. + /// + protected override int HashSize + { + get { return 160; } + } + /// /// Calculates key exchange hash value. /// From 1eac7d89da7fe6b1c8265d48a6562ff82ecf68da Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 27 Sep 2017 21:38:30 +0200 Subject: [PATCH 04/12] Fix typos. --- src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs index fa53d36a..29223a2e 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs @@ -110,14 +110,14 @@ namespace Renci.SshNet.Security if (_prime.IsZero) throw new ArgumentNullException("_prime"); - // generate private component that is twice the hash size (RFC 4419) with a minimum + // generate private exponent that is twice the hash size (RFC 4419) with a minimum // of 1024 bits (whatever is less) - var privateComponentSize = Math.Max(HashSize * 2, 1024); + var privateExponentSize = Math.Max(HashSize * 2, 1024); do { // create private component - _privateExponent = BigInteger.Random(privateComponentSize); + _privateExponent = BigInteger.Random(privateExponentSize); // generate public component _clientExchangeValue = BigInteger.ModPow(_group, _privateExponent, _prime); } while (_clientExchangeValue < 1 || _clientExchangeValue > (_prime - 1)); From a6f5d3580a8add0633de58571b229715347b8239 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 27 Sep 2017 21:46:05 +0200 Subject: [PATCH 05/12] Fix warnings. --- src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs index 37e9a0ed..884bedd1 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs @@ -18,7 +18,7 @@ namespace Renci.SshNet.Tests.Classes [TestMethod] public void AuthenticateShouldThrowArgumentNullExceptionWhenConnectionInfoIsNull() { - IConnectionInfoInternal connectionInfo = null; + const IConnectionInfoInternal connectionInfo = null; var session = new Mock(MockBehavior.Strict).Object; try @@ -37,7 +37,7 @@ namespace Renci.SshNet.Tests.Classes public void AuthenticateShouldThrowArgumentNullExceptionWhenSessionIsNull() { var connectionInfo = new Mock(MockBehavior.Strict).Object; - ISession session = null; + const ISession session = null; try { From 69fb3be4fa27c01d737a21c2835479685745453d Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Sat, 30 Sep 2017 21:42:56 +0200 Subject: [PATCH 06/12] Signal the closed wait handle to avoid a deadlock when a subscriber to the Closed event in turn closes or disposes the channel. --- ...hannelEofReceived_DisposeInEventHandler.cs | 136 ++++++++++++++++++ .../Renci.SshNet.Tests.csproj | 1 + src/Renci.SshNet/Channels/Channel.cs | 13 +- 3 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/Channels/ChannelSessionTest_Dispose_SessionIsConnectedAndChannelIsOpen_ChannelCloseAndChannelEofReceived_DisposeInEventHandler.cs diff --git a/src/Renci.SshNet.Tests/Classes/Channels/ChannelSessionTest_Dispose_SessionIsConnectedAndChannelIsOpen_ChannelCloseAndChannelEofReceived_DisposeInEventHandler.cs b/src/Renci.SshNet.Tests/Classes/Channels/ChannelSessionTest_Dispose_SessionIsConnectedAndChannelIsOpen_ChannelCloseAndChannelEofReceived_DisposeInEventHandler.cs new file mode 100644 index 00000000..35e70ac3 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Channels/ChannelSessionTest_Dispose_SessionIsConnectedAndChannelIsOpen_ChannelCloseAndChannelEofReceived_DisposeInEventHandler.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Channels; +using Renci.SshNet.Common; +using Renci.SshNet.Messages.Connection; +using Renci.SshNet.Tests.Common; + +namespace Renci.SshNet.Tests.Classes.Channels +{ + [TestClass] + public class ChannelSessionTest_Dispose_SessionIsConnectedAndChannelIsOpen_ChannelCloseAndChannelEofReceived_DisposeInEventHandler + { + private Mock _sessionMock; + private uint _localChannelNumber; + private uint _localWindowSize; + private uint _localPacketSize; + private uint _remoteChannelNumber; + private uint _remoteWindowSize; + private uint _remotePacketSize; + private IList _channelClosedRegister; + private List _channelExceptionRegister; + private ChannelSession _channel; + private Mock _connectionInfoMock; + private MockSequence _sequence; + private SemaphoreLight _sessionSemaphore; + private int _initialSessionSemaphoreCount; + + [TestInitialize] + public void Initialize() + { + Arrange(); + Act(); + } + + private void Arrange() + { + var random = new Random(); + _localChannelNumber = (uint)random.Next(0, int.MaxValue); + _localWindowSize = (uint)random.Next(0, int.MaxValue); + _localPacketSize = (uint)random.Next(0, int.MaxValue); + _remoteChannelNumber = (uint)random.Next(0, int.MaxValue); + _remoteWindowSize = (uint)random.Next(0, int.MaxValue); + _remotePacketSize = (uint)random.Next(0, int.MaxValue); + _channelClosedRegister = new List(); + _channelExceptionRegister = new List(); + _initialSessionSemaphoreCount = random.Next(10, 20); + _sessionSemaphore = new SemaphoreLight(_initialSessionSemaphoreCount); + + _sessionMock = new Mock(MockBehavior.Strict); + _connectionInfoMock = new Mock(MockBehavior.Strict); + + _sequence = new MockSequence(); + _sessionMock.InSequence(_sequence).Setup(p => p.ConnectionInfo).Returns(_connectionInfoMock.Object); + _connectionInfoMock.InSequence(_sequence).Setup(p => p.RetryAttempts).Returns(1); + _sessionMock.Setup(p => p.SessionSemaphore).Returns(_sessionSemaphore); + _sessionMock.InSequence(_sequence) + .Setup( + p => + p.SendMessage( + It.Is( + m => + m.LocalChannelNumber == _localChannelNumber && + m.InitialWindowSize == _localWindowSize && m.MaximumPacketSize == _localPacketSize && + m.Info is SessionChannelOpenInfo))); + _sessionMock.InSequence(_sequence) + .Setup(p => p.WaitOnHandle(It.IsNotNull())) + .Callback( + w => + { + _sessionMock.Raise( + s => s.ChannelOpenConfirmationReceived += null, + new MessageEventArgs( + new ChannelOpenConfirmationMessage( + _localChannelNumber, + _remoteWindowSize, + _remotePacketSize, + _remoteChannelNumber))); + w.WaitOne(); + }); + _sessionMock.InSequence(_sequence).Setup(p => p.IsConnected).Returns(true); + _sessionMock.InSequence(_sequence) + .Setup(p => p.TrySendMessage(It.Is(c => c.LocalChannelNumber == _remoteChannelNumber))) + .Returns(true); + _sessionMock.InSequence(_sequence) + .Setup(s => s.WaitOnHandle(It.IsNotNull())) + .Callback(w => w.WaitOne()); + + _channel = new ChannelSession(_sessionMock.Object, _localChannelNumber, _localWindowSize, _localPacketSize); + _channel.Closed += (sender, args) => + { + _channelClosedRegister.Add(args); + _channel.Dispose(); + }; + _channel.Exception += (sender, args) => _channelExceptionRegister.Add(args); + _channel.Open(); + + _sessionMock.Raise(p => p.ChannelEofReceived += null, + new MessageEventArgs(new ChannelEofMessage(_localChannelNumber))); + _sessionMock.Raise(p => p.ChannelCloseReceived += null, + new MessageEventArgs(new ChannelCloseMessage(_localChannelNumber))); + } + + private void Act() + { + _channel.Dispose(); + } + + [TestMethod] + public void CurrentCountOfSessionSemaphoreShouldBeEqualToInitialCount() + { + Assert.AreEqual(_initialSessionSemaphoreCount, _sessionSemaphore.CurrentCount); + } + + [TestMethod] + public void ExceptionShouldNeverHaveFired() + { + Assert.AreEqual(0, _channelExceptionRegister.Count, _channelExceptionRegister.AsString()); + } + + [TestMethod] + public void ClosedEventShouldHaveFiredOnce() + { + Assert.AreEqual(1, _channelClosedRegister.Count); + Assert.AreEqual(_localChannelNumber, _channelClosedRegister[0].ChannelNumber); + } + + [TestMethod] + public void IsOpenShouldReturnFalse() + { + Assert.IsFalse(_channel.IsOpen); + } + } +} diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index b34d1bf6..456fdcff 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -94,6 +94,7 @@ + diff --git a/src/Renci.SshNet/Channels/Channel.cs b/src/Renci.SshNet/Channels/Channel.cs index 818f6959..a5c449bf 100644 --- a/src/Renci.SshNet/Channels/Channel.cs +++ b/src/Renci.SshNet/Channels/Channel.cs @@ -400,6 +400,14 @@ namespace Renci.SshNet.Channels { _closeMessageReceived = true; + // signal that SSH_MSG_CHANNEL_CLOSE message was received from server + // we need to signal this before firing the Closed event, as a subscriber + // may very well react to the Closed event by closing or disposing the + // channel which in turn will wait for this handle to be signaled + var channelClosedWaitHandle = _channelClosedWaitHandle; + if (channelClosedWaitHandle != null) + channelClosedWaitHandle.Set(); + // raise event signaling that the server has closed its end of the channel var closed = Closed; if (closed != null) @@ -407,11 +415,6 @@ namespace Renci.SshNet.Channels closed(this, new ChannelEventArgs(LocalChannelNumber)); } - // signal that SSH_MSG_CHANNEL_CLOSE message was received from server - var channelClosedWaitHandle = _channelClosedWaitHandle; - if (channelClosedWaitHandle != null) - channelClosedWaitHandle.Set(); - // close the channel Close(); } From e04e0129cd5e4e53b99fb6daae47a8c44bff50e2 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Mon, 2 Oct 2017 22:10:20 +0200 Subject: [PATCH 07/12] Added partialSuccessLimit argument to ClientAuthentication ctor. --- src/Renci.SshNet/ClientAuthentication.cs | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Renci.SshNet/ClientAuthentication.cs b/src/Renci.SshNet/ClientAuthentication.cs index 2efadc5a..4b734b4c 100644 --- a/src/Renci.SshNet/ClientAuthentication.cs +++ b/src/Renci.SshNet/ClientAuthentication.cs @@ -6,6 +6,34 @@ namespace Renci.SshNet { internal class ClientAuthentication : IClientAuthentication { + private readonly int _partialSuccessLimit; + + /// + /// Initializes a new instance. + /// + /// The number of times an authentication attempt with any given can result in before it is disregarded. + /// is less than one. + public ClientAuthentication(int partialSuccessLimit) + { + if (partialSuccessLimit < 1) + throw new ArgumentOutOfRangeException("partialSuccessLimit", "Cannot be less than one."); + + _partialSuccessLimit = partialSuccessLimit; + } + + /// + /// Gets the number of times an authentication attempt with any given can + /// result in before it is disregarded. + /// + /// + /// The number of times an authentication attempt with any given can result + /// in before it is disregarded. + /// + internal int PartialSuccessLimit + { + get { return _partialSuccessLimit; } + } + public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session) { if (connectionInfo == null) From 9e5ed683fedd54f39121cba4b3bf0b4e83d99fda Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 4 Oct 2017 22:04:06 +0200 Subject: [PATCH 08/12] Remove empty line. --- ...actoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs index d6d0c9cd..ee3db7cb 100644 --- a/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs +++ b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs @@ -90,6 +90,5 @@ namespace Renci.SshNet.Tests.Classes Assert.IsNotNull(_actual); Assert.AreSame(_sftpFileReaderMock.Object, _actual); } - } } From 14b654d0ab321f801a3e3f6f6e3ed524c56f583b Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 4 Oct 2017 22:07:04 +0200 Subject: [PATCH 09/12] Introduce a partial success limit. Added first batch of tests. --- .../Classes/ClientAuthenticationTest.cs | 57 ++++- .../Classes/ClientAuthenticationTestBase.cs | 19 +- ...e_SingleList_AuthenticationMethodFailed.cs | 43 +++- ...eList_AuthenticationMethodNotConfigured.cs | 16 +- ...ticationsHaveReachedPartialSuccessLimit.cs | 14 + ...lowedAuthenticationsAfterPartialSuccess.cs | 17 +- ...achedFollowedByFailureInAlternateBranch.cs | 175 +++++++++++++ ...chedFollowedByFailureInAlternateBranch2.cs | 202 +++++++++++++++ ...mitReachedFollowedByFailureInSameBranch.cs | 144 +++++++++++ ...mitReachedFollowedBySuccessInSameBranch.cs | 121 +++++++++ ...stponePartialAccessAuthenticationMethod.cs | 61 +++-- ...lowedAuthenticationsAfterPartialSuccess.cs | 17 +- ...ultiList_SkipFailedAuthenticationMethod.cs | 17 +- ...llowedAuthenticationAfterPartialSuccess.cs | 48 +++- ...rtialSuccess_PartialSuccessLimitReached.cs | 112 ++++++++ ...eFactoryTest_CreateClientAuthentication.cs | 42 +++ .../Renci.SshNet.Tests.csproj | 7 + src/Renci.SshNet/ClientAuthentication.cs | 241 ++++++++++++------ src/Renci.SshNet/ServiceFactory.cs | 8 +- 19 files changed, 1218 insertions(+), 143 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs index 884bedd1..a5969d35 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs @@ -12,9 +12,64 @@ namespace Renci.SshNet.Tests.Classes [TestInitialize] public void Init() { - _clientAuthentication = new ClientAuthentication(); + _clientAuthentication = new ClientAuthentication(1); } + [TestMethod] + public void Ctor_PartialSuccessLimit_Zero() + { + const int partialSuccessLimit = 0; + + try + { + new ClientAuthentication(partialSuccessLimit); + Assert.Fail(); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message); + Assert.AreEqual("partialSuccessLimit", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_PartialSuccessLimit_Negative() + { + var partialSuccessLimit = new Random().Next(int.MinValue, -1); + + try + { + new ClientAuthentication(partialSuccessLimit); + Assert.Fail(); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message); + Assert.AreEqual("partialSuccessLimit", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_PartialSuccessLimit_One() + { + const int partialSuccessLimit = 1; + + var clientAuthentication = new ClientAuthentication(partialSuccessLimit); + Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit); + } + + [TestMethod] + public void Ctor_PartialSuccessLimit_MaxValue() + { + const int partialSuccessLimit = int.MaxValue; + + var clientAuthentication = new ClientAuthentication(partialSuccessLimit); + Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit); + } + + [TestMethod] public void AuthenticateShouldThrowArgumentNullExceptionWhenConnectionInfoIsNull() { diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs index 3ff76d18..853114af 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs @@ -13,7 +13,8 @@ namespace Renci.SshNet.Tests.Classes internal Mock PasswordAuthenticationMethodMock { get; private set; } internal Mock PublicKeyAuthenticationMethodMock { get; private set; } internal Mock KeyboardInteractiveAuthenticationMethodMock { get; private set; } - internal ClientAuthentication ClientAuthentication { get; private set; } + + protected abstract void SetupData(); protected void CreateMocks() { @@ -27,18 +28,20 @@ namespace Renci.SshNet.Tests.Classes protected abstract void SetupMocks(); + protected virtual void Arrange() + { + SetupData(); + CreateMocks(); + SetupMocks(); + } + protected abstract void Act(); - protected override void OnInit() + protected sealed override void OnInit() { base.OnInit(); - // Arrange - CreateMocks(); - SetupMocks(); - ClientAuthentication = new ClientAuthentication(); - - // Act + Arrange(); Act(); } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs index a3b939d9..66e931f8 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs @@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; private SshAuthenticationException _actualException; + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -21,23 +28,26 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - PublicKeyAuthenticationMethodMock.Object, - PasswordAuthenticationMethodMock.Object - }); NoneAuthenticationMethodMock.InSequence(seq) - .Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password" }); + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); // obtain name for inclusion in SshAuthenticationException PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); @@ -46,11 +56,18 @@ namespace Renci.SshNet.Tests.Classes SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { try { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); Assert.Fail(); } catch (SshAuthenticationException ex) diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs index b87a3409..9a8921be 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs @@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotSupported : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; private SshAuthenticationException _actualException; + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -39,11 +46,18 @@ namespace Renci.SshNet.Tests.Classes SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { try { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); Assert.Fail(); } catch (SshAuthenticationException ex) diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs new file mode 100644 index 00000000..79797bf0 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs @@ -0,0 +1,14 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit + { + [TestMethod] + public void Test() + { + Assert.Fail(); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs index 9ef8e98b..552f47bd 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -47,9 +55,16 @@ namespace Renci.SshNet.Tests.Classes SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs new file mode 100644 index 00000000..964fec71 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +-------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=FAIL) + /// | | + /// password | + /// (3=PS) | + /// | | + /// password | + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +------------+ + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + /* 1 */ + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object, + KeyboardInteractiveAuthenticationMethodMock.Object, + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey", "keyboard-interactive"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 2 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial"); + + /* 6 */ + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive-failure"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Permission denied (keyboard-interactive-failure).", _actualException.Message); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs new file mode 100644 index 00000000..f61b735f --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +-------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=PS) + /// | | | + /// password | +-----------+ + /// (3=PS) | | | + /// | | password publickey + /// password | (7=SKIP) (8=FAIL) + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +------------+ + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2 : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + /* 1 */ + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object, + KeyboardInteractiveAuthenticationMethodMock.Object, + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "publickey", "keyboard-interactive" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 2 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + /* 6 */ + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 7: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + /* 8 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Permission denied (publickey).", _actualException.Message); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs new file mode 100644 index 00000000..0358c731 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and authentication methods, and partial + /// success limit is set to 2. + /// + /// Authentication proceeds as follows: + /// + /// 1 x * Client performs 'none' authentication attempt. + /// * Server responds with 'failure', and 'password' allowed authentication method. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods. + /// + /// 1 x * Client performs 'publickey' authentication attempt. + /// * Server responds with 'failure'. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods. + /// + /// 1 x * Client performs 'keyboard-interactive' authentication attempt. + /// * Server responds with 'failure'. + /// + /// Since the server only ever allowed the 'password' authentication method, there are no + /// authentication methods left to try after reaching the partial success limit for 'password' + /// and as such authentication fails. + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey-failure"); + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"keyboard-interactive"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive-failure"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Permission denied (keyboard-interactive-failure).", _actualException.Message); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs new file mode 100644 index 00000000..35288a78 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and authentication methods, and partial + /// success limit is set to 2. + /// + /// Authentication proceeds as follows: + /// + /// 1 x * Client performs 'none' authentication attempt. + /// * Server responds with 'failure', and 'password' allowed authentication method. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods. + /// + /// 1 x * Client performs 'publickey' authentication attempt. + /// * Server responds with 'failure'. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods. + /// + /// 1 x * Client performs 'keyboard-interactive' authentication attempt. + /// * Server responds with 'success'. + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey-failure"); + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "keyboard-interactive" }); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs index 7112ca95..dd7c14c3 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 3; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -16,42 +24,59 @@ namespace Renci.SshNet.Tests.Classes SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) - .Returns(NoneAuthenticationMethodMock.Object); + .Returns(NoneAuthenticationMethodMock.Object); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - KeyboardInteractiveAuthenticationMethodMock.Object, - PasswordAuthenticationMethodMock.Object, - PublicKeyAuthenticationMethodMock.Object - }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password" }); KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.PartialSuccess); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password", "publickey" }); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Failure); - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs index 90476047..7cfd76ee 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -53,9 +61,16 @@ namespace Renci.SshNet.Tests.Classes SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs index 1d409500..4ca447d6 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -47,9 +55,16 @@ namespace Renci.SshNet.Tests.Classes SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs index 5327ea33..4aff4ab3 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -20,34 +28,48 @@ namespace Renci.SshNet.Tests.Classes NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - PublicKeyAuthenticationMethodMock.Object, - PasswordAuthenticationMethodMock.Object - }); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password" }); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.PartialSuccess); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password" }); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs new file mode 100644 index 00000000..005968a1 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// ConnectionInfo provides 'password' and 'publickey' authentication methods, and partial success limit is + /// set to 3. + /// + /// Authentication proceeds as follows: + /// + /// 1 x * Client performs 'none' authentication attempt. + /// * Server responds with 'failure', and 'password' allowed authentication method. + /// + /// 3 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'password' allowed authentication method + /// + /// Since the server only ever allowed the 'password' authentication method, there are no + /// authentication methods left to try after reaching the partial success limit for 'password' + /// and as such authentication fails. + /// + [TestClass] + public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 3; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + for (var i = 0; i < _partialSuccessLimit; i++) + { + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + } + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + // used to construct exception message + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("x_password_x"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Reached authentication attempt limit for method (x_password_x).",_actualException.Message); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs new file mode 100644 index 00000000..87448833 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs @@ -0,0 +1,42 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ServiceFactoryTest_CreateClientAuthentication + { + private ServiceFactory _serviceFactory; + private IClientAuthentication _actual; + + private void Arrange() + { + _serviceFactory = new ServiceFactory(); + } + + [TestInitialize] + public void Initialize() + { + Arrange(); + Act(); + } + + private void Act() + { + _actual = _serviceFactory.CreateClientAuthentication(); + } + + [TestMethod] + public void CreateClientAuthenticationShouldNotReturnNull() + { + Assert.IsNotNull(_actual); + } + + [TestMethod] + public void ClientAuthenticationShouldHavePartialSuccessLimitOf5() + { + var clientAuthentication = _actual as ClientAuthentication; + Assert.IsNotNull(clientAuthentication); + Assert.AreEqual(5, clientAuthentication.PartialSuccessLimit); + } + } +} diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index 456fdcff..393a110e 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -139,11 +139,17 @@ + + + + + + @@ -263,6 +269,7 @@ + diff --git a/src/Renci.SshNet/ClientAuthentication.cs b/src/Renci.SshNet/ClientAuthentication.cs index 4b734b4c..f0b31f87 100644 --- a/src/Renci.SshNet/ClientAuthentication.cs +++ b/src/Renci.SshNet/ClientAuthentication.cs @@ -34,6 +34,12 @@ namespace Renci.SshNet get { return _partialSuccessLimit; } } + /// + /// Attempts to authentication for a given using the + /// of the specified . + /// + /// A to use for authenticating. + /// The for which to perform authentication. public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session) { if (connectionInfo == null) @@ -72,10 +78,10 @@ namespace Renci.SshNet } } - private static bool TryAuthenticate(ISession session, - AuthenticationState authenticationState, - string[] allowedAuthenticationMethods, - ref SshAuthenticationException authenticationException) + private bool TryAuthenticate(ISession session, + AuthenticationState authenticationState, + string[] allowedAuthenticationMethods, + ref SshAuthenticationException authenticationException) { if (allowedAuthenticationMethods.Length == 0) { @@ -86,40 +92,39 @@ namespace Renci.SshNet // we want to try authentication methods in the order in which they were // passed in the ctor, not the order in which the SSH server returns // the allowed authentication methods - var matchingAuthenticationMethods = GetAllowedAuthenticationMethodsThatAreSupported(authenticationState, allowedAuthenticationMethods); + var matchingAuthenticationMethods = authenticationState.GetSupportedAuthenticationMethods(allowedAuthenticationMethods); if (matchingAuthenticationMethods.Count == 0) { - authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", string.Join(",", allowedAuthenticationMethods))); + authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", + string.Join(",", allowedAuthenticationMethods))); return false; } - foreach (var authenticationMethod in GetOrderedAuthenticationMethods(authenticationState, matchingAuthenticationMethods)) + foreach (var authenticationMethod in authenticationState.GetActiveAuthenticationMethods(matchingAuthenticationMethods)) { - if (authenticationState.FailedAuthenticationMethods.Contains(authenticationMethod)) - continue; - - // when the authentication method was previously executed, then skip the authentication - // method as long as there's another authentication method to try; this is done to avoid - // a stack overflow for servers that do not update the list of allowed authentication + // guard against a stack overlow for servers that do not update the list of allowed authentication // methods after a partial success - - if (!authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod)) + if (authenticationState.GetPartialSuccessCount(authenticationMethod) >= _partialSuccessLimit) { - // update state to reflect previosuly executed authentication methods - authenticationState.ExecutedAuthenticationMethods.Add(authenticationMethod); + // TODO Get list of all authentication methods that have reached the partial success limit? + + authenticationException = new SshAuthenticationException(string.Format("Reached authentication attempt limit for method ({0}).", + authenticationMethod.Name)); + continue; } var authenticationResult = authenticationMethod.Authenticate(session); switch (authenticationResult) { case AuthenticationResult.PartialSuccess: + authenticationState.RecordPartialSuccess(authenticationMethod); if (TryAuthenticate(session, authenticationState, authenticationMethod.AllowedAuthentications, ref authenticationException)) { authenticationResult = AuthenticationResult.Success; } break; case AuthenticationResult.Failure: - authenticationState.FailedAuthenticationMethods.Add(authenticationMethod); + authenticationState.RecordFailure(authenticationMethod); authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name)); break; case AuthenticationResult.Success: @@ -134,85 +139,151 @@ namespace Renci.SshNet return false; } - private static List GetAllowedAuthenticationMethodsThatAreSupported(AuthenticationState authenticationState, - string[] allowedAuthenticationMethods) - { - var result = new List(); - - foreach (var supportedAuthenticationMethod in authenticationState.SupportedAuthenticationMethods) - { - var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name; - - for (var i = 0; i < allowedAuthenticationMethods.Length; i++) - { - if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod) - { - result.Add(supportedAuthenticationMethod); - break; - } - } - } - - return result; - } - - private static IEnumerable GetOrderedAuthenticationMethods(AuthenticationState authenticationState, List matchingAuthenticationMethods) - { - var skippedAuthenticationMethods = new List(); - - for (var i = 0; i < matchingAuthenticationMethods.Count; i++) - { - var authenticationMethod = matchingAuthenticationMethods[i]; - - if (authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod)) - { - skippedAuthenticationMethods.Add(authenticationMethod); - continue; - } - - yield return authenticationMethod; - } - - foreach (var authenticationMethod in skippedAuthenticationMethods) - yield return authenticationMethod; - } - private class AuthenticationState { private readonly IList _supportedAuthenticationMethods; + /// + /// Records if a given has been tried, and how many times this resulted + /// in . + /// + /// + /// When there's no entry for a given , then it was never tried. + /// + private readonly Dictionary _authenticationMethodPartialSuccessRegister; + + /// + /// Holds the list of authentications methods that failed. + /// + private readonly List _failedAuthenticationMethods; + public AuthenticationState(IList supportedAuthenticationMethods) { _supportedAuthenticationMethods = supportedAuthenticationMethods; - ExecutedAuthenticationMethods = new List(); - FailedAuthenticationMethods = new List(); + _failedAuthenticationMethods = new List(); + _authenticationMethodPartialSuccessRegister = new Dictionary(); } /// - /// Gets the list of authentication methods that were previously executed. + /// Records a authentication attempt for the specified + /// . /// - /// - /// The list of authentication methods that were previously executed. - /// - public IList ExecutedAuthenticationMethods { get; private set; } - - /// - /// Gets the list of authentications methods that failed. - /// - /// - /// The list of authentications methods that failed. - /// - public IList FailedAuthenticationMethods { get; private set; } - - /// - /// Gets the list of supported authentication methods. - /// - /// - /// The list of supported authentication methods. - /// - public IList SupportedAuthenticationMethods + /// An for which to record the result of an authentication attempt. + public void RecordFailure(IAuthenticationMethod authenticationMethod) { - get { return _supportedAuthenticationMethods; } + _failedAuthenticationMethods.Add(authenticationMethod); + } + + /// + /// Records a authentication attempt for the specified + /// . + /// + /// An for which to record the result of an authentication attempt. + public void RecordPartialSuccess(IAuthenticationMethod authenticationMethod) + { + int partialSuccessCount; + if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount)) + { + _authenticationMethodPartialSuccessRegister[authenticationMethod] = ++partialSuccessCount; + } + else + { + _authenticationMethodPartialSuccessRegister.Add(authenticationMethod, 1); + } + } + + /// + /// Returns the number of times an authentication attempt with the specified + /// has resulted in . + /// + /// An . + /// + /// The number of times an authentication attempt with the specified + /// has resulted in . + /// + public int GetPartialSuccessCount(IAuthenticationMethod authenticationMethod) + { + int partialSuccessCount; + if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount)) + { + return partialSuccessCount; + } + return 0; + } + + /// + /// Returns a list of supported authentication methods that match one of the specified allowed authentication + /// methods. + /// + /// A list of allowed authentication methods. + /// + /// A list of supported authentication methods that match one of the specified allowed authentication methods. + /// + /// + /// The authentication methods are returned in the order in which they were specified in the list that was + /// used to initialize the current instance. + /// + public List GetSupportedAuthenticationMethods(string[] allowedAuthenticationMethods) + { + var result = new List(); + + foreach (var supportedAuthenticationMethod in _supportedAuthenticationMethods) + { + var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name; + + for (var i = 0; i < allowedAuthenticationMethods.Length; i++) + { + if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod) + { + result.Add(supportedAuthenticationMethod); + break; + } + } + } + + return result; + } + + /// + /// Returns the authentication methods from the specified list that have not yet failed. + /// + /// A list of authentication methods. + /// + /// The authentication methods from that have not yet failed. + /// + /// + /// + /// This method first returns the authentication methods that have not yet been executed, and only then + /// returns those for which an authentication attempt resulted in a . + /// + /// + /// Any that has failed is skipped. + /// + /// + public IEnumerable GetActiveAuthenticationMethods(List matchingAuthenticationMethods) + { + var skippedAuthenticationMethods = new List(); + + for (var i = 0; i < matchingAuthenticationMethods.Count; i++) + { + var authenticationMethod = matchingAuthenticationMethods[i]; + + // skip authentication methods that have already failed + if (_failedAuthenticationMethods.Contains(authenticationMethod)) + continue; + + // delay use of authentication methods that had a PartialSuccess result + if (_authenticationMethodPartialSuccessRegister.ContainsKey(authenticationMethod)) + { + skippedAuthenticationMethods.Add(authenticationMethod); + continue; + } + + yield return authenticationMethod; + } + + foreach (var authenticationMethod in skippedAuthenticationMethods) + yield return authenticationMethod; } } } diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs index 5da31a85..9d7e8310 100644 --- a/src/Renci.SshNet/ServiceFactory.cs +++ b/src/Renci.SshNet/ServiceFactory.cs @@ -15,6 +15,12 @@ namespace Renci.SshNet /// internal partial class ServiceFactory : IServiceFactory { + /// + /// Defines the number of times an authentication attempt with any given + /// can result in before it is disregarded. + /// + private static int PartialSuccessLimit = 5; + /// /// Creates a . /// @@ -23,7 +29,7 @@ namespace Renci.SshNet /// public IClientAuthentication CreateClientAuthentication() { - return new ClientAuthentication(); + return new ClientAuthentication(PartialSuccessLimit); } /// From c2e1de507de42d236173692ae36a11c9405a43e9 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Thu, 5 Oct 2017 21:44:32 +0200 Subject: [PATCH 10/12] Improve tests for partial success limit. --- ...ticationsHaveReachedPartialSuccessLimit.cs | 190 ++++++++++++++++++ ...eList_AuthenticationMethodNotConfigured.cs | 19 +- ...ticationsHaveReachedPartialSuccessLimit.cs | 14 -- ...lowedAuthenticationsAfterPartialSuccess.cs | 41 +++- ...achedFollowedByFailureInAlternateBranch.cs | 30 +-- ...chedFollowedByFailureInAlternateBranch2.cs | 2 +- ...mitReachedFollowedByFailureInSameBranch.cs | 61 +++--- ...achedFollowedBySuccessInAlternateBranch.cs | 185 +++++++++++++++++ ...mitReachedFollowedBySuccessInSameBranch.cs | 59 ++++-- ...llowedAuthenticationAfterPartialSuccess.cs | 27 +++ ...rtialSuccess_PartialSuccessLimitReached.cs | 92 ++++++--- .../Renci.SshNet.Tests.csproj | 3 +- 12 files changed, 610 insertions(+), 113 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs new file mode 100644 index 00000000..bcf93a91 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs @@ -0,0 +1,190 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o publickey + /// o password + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +------------------------+------------------------+ + /// | | | + /// password ◄--\ publickey keyboard-interactive + /// (7=SKIP) | (2=PS) + /// | | + /// | password + /// | (3=PS) + /// | | + /// | password + /// | (4=PS) + /// | | + /// | publickey + /// | (5=PS) + /// | | + /// \---- publickey + /// (6=SKIP) + /// + [TestClass] + public class ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + /* 1 */ + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey", "keyboard-interactive"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 2 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 5 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 6: Record partial success limit reached exception, and skip password authentication method */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey-partial1"); + + /* 7: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnPasswordAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PasswordAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + + [TestMethod] + public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Reached authentication attempt limit for method (password-partial1).", _actualException.Message); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs index 9a8921be..72a9da49 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs @@ -28,16 +28,17 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - PublicKeyAuthenticationMethodMock.Object - }); NoneAuthenticationMethodMock.InSequence(seq) - .Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password" }); + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs deleted file mode 100644 index 79797bf0..00000000 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Renci.SshNet.Tests.Classes -{ - [TestClass] - public class ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit - { - [TestMethod] - public void Test() - { - Assert.Fail(); - } - } -} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs index 552f47bd..0f21f0bf 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs @@ -4,6 +4,25 @@ using Moq; namespace Renci.SshNet.Tests.Classes { + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 1 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// +------------------------------+ + /// | | + /// publickey password + /// (2=PARTIAL) + /// *----------------------* + /// | | + /// keyboard-interactive publickey + /// (3=SUCCESS) + /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase { @@ -26,6 +45,8 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); + /* 1 */ + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) @@ -35,20 +56,34 @@ namespace Renci.SshNet.Tests.Classes PublicKeyAuthenticationMethodMock.Object, KeyboardInteractiveAuthenticationMethodMock.Object, }); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "publickey", "password" }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey", "password"}); + + /* Enumerate supported authentication methods */ + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications) - .Returns(new[] { "keyboard-interactive", "publickey" }); + .Returns(new[] {"keyboard-interactive", "publickey"}); + + /* Enumerate supported authentication methods */ + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + /* 3 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs index 964fec71..f4c49023 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs @@ -11,24 +11,24 @@ namespace Renci.SshNet.Tests.Classes /// o publickey /// o keyboard-interactive /// * Partial success limit is 2 - /// + /// * Scenario: /// none /// (1=FAIL) /// | - /// +-------------------+ - /// | | - /// publickey keyboard-interactive - /// (2=PS) ^ (6=FAIL) - /// | | - /// password | - /// (3=PS) | - /// | | - /// password | - /// (4=PS) | - /// | | - /// password | - /// (5=SKIP) | - /// +------------+ + /// +------------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=FAIL) + /// | | + /// password | + /// (3=PS) | + /// | | + /// password | + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +---------------+ /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch : ClientAuthenticationTestBase diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs index f61b735f..e8c0e474 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs @@ -145,7 +145,7 @@ namespace Renci.SshNet.Tests.Classes PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Name) - .Returns("password-partial1"); + .Returns("password-partial2"); /* 8 */ diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs index 0358c731..be985a70 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs @@ -6,29 +6,25 @@ using Renci.SshNet.Common; namespace Renci.SshNet.Tests.Classes { /// - /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and authentication methods, and partial - /// success limit is set to 2. - /// - /// Authentication proceeds as follows: - /// - /// 1 x * Client performs 'none' authentication attempt. - /// * Server responds with 'failure', and 'password' allowed authentication method. - /// - /// 1 x * Client performs 'password' authentication attempt. - /// * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods. - /// - /// 1 x * Client performs 'publickey' authentication attempt. - /// * Server responds with 'failure'. - /// - /// 1 x * Client performs 'password' authentication attempt. - /// * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods. - /// - /// 1 x * Client performs 'keyboard-interactive' authentication attempt. - /// * Server responds with 'failure'. - /// - /// Since the server only ever allowed the 'password' authentication method, there are no - /// authentication methods left to try after reaching the partial success limit for 'password' - /// and as such authentication fails. + /// * ConnectionInfo provides the following authentication methods (in order): + /// o keyboard-interactive + /// o password + /// o publickey + /// * Partial success limit is 2 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// +------------------------------+ + /// | | + /// password publickey + /// (4=PARTIAL) (3=FAILURE) + /// | + /// keyboard-interactive + /// (5=FAILURE) /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch : ClientAuthenticationTestBase @@ -53,6 +49,8 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); + /* 1 */ + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); ConnectionInfoMock.InSequence(seq) @@ -67,9 +65,14 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + /* Enumerate supported authentication methods */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); @@ -77,9 +80,14 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password", "publickey"}); + /* Enumerate supported authentication methods */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + /* 3 */ + PublicKeyAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); @@ -87,6 +95,8 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.Name) .Returns("publickey-failure"); + /* 4 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); @@ -94,9 +104,14 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] {"keyboard-interactive"}); + /* Enumerate supported authentication methods */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + /* 5 */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs new file mode 100644 index 00000000..8c6413ef --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +-------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=PS) + /// | | | + /// password | +-----------+ + /// (3=PS) | | | + /// | | password publickey + /// password | (7=SKIP) (8=SUCCESS) + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +------------+ + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + /* 1 */ + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object, + KeyboardInteractiveAuthenticationMethodMock.Object, + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey", "keyboard-interactive"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 2 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + /* 6 */ + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 7: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial2"); + + /* 8 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs index 35288a78..228ad23f 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs @@ -5,25 +5,25 @@ using Moq; namespace Renci.SshNet.Tests.Classes { /// - /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and authentication methods, and partial - /// success limit is set to 2. - /// - /// Authentication proceeds as follows: - /// - /// 1 x * Client performs 'none' authentication attempt. - /// * Server responds with 'failure', and 'password' allowed authentication method. - /// - /// 1 x * Client performs 'password' authentication attempt. - /// * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods. - /// - /// 1 x * Client performs 'publickey' authentication attempt. - /// * Server responds with 'failure'. - /// - /// 1 x * Client performs 'password' authentication attempt. - /// * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods. - /// - /// 1 x * Client performs 'keyboard-interactive' authentication attempt. - /// * Server responds with 'success'. + /// * ConnectionInfo provides the following authentication methods (in order): + /// o keyboard-interactive + /// o password + /// o publickey + /// * Partial success limit is 2 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// +------------------------------+ + /// | | + /// password publickey + /// (4=PARTIAL) (3=FAILURE) + /// | + /// keyboard-interactive + /// (5=FAILURE) /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch : ClientAuthenticationTestBase @@ -47,6 +47,8 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); + /* 1 */ + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); ConnectionInfoMock.InSequence(seq) @@ -61,9 +63,14 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + /* Enumerate supported authentication methods */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); @@ -71,9 +78,14 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password", "publickey"}); + /* Enumerate supported authentication methods */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + /* 3 */ + PublicKeyAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); @@ -81,16 +93,23 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.Name) .Returns("publickey-failure"); + /* 4 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.AllowedAuthentications) - .Returns(new[] { "keyboard-interactive" }); + .Returns(new[] {"keyboard-interactive"}); + + /* Enumerate supported authentication methods */ KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + /* 5 */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Success); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs index 4aff4ab3..e3f8ff3f 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs @@ -4,6 +4,22 @@ using Moq; namespace Renci.SshNet.Tests.Classes { + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o keyboard-interactive + /// o password + /// o publickey + /// * Partial success limit is 2 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// password + /// (3=SUCCESS) + /// [TestClass] public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess : ClientAuthenticationTestBase { @@ -26,6 +42,8 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); + /* 1 */ + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); ConnectionInfoMock.InSequence(seq) @@ -39,18 +57,27 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + /* Enumerate supported authentication methods */ + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + /* 3 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Success); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs index 005968a1..74261dc1 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs @@ -6,20 +6,25 @@ using Renci.SshNet.Common; namespace Renci.SshNet.Tests.Classes { /// - /// ConnectionInfo provides 'password' and 'publickey' authentication methods, and partial success limit is - /// set to 3. - /// - /// Authentication proceeds as follows: - /// - /// 1 x * Client performs 'none' authentication attempt. - /// * Server responds with 'failure', and 'password' allowed authentication method. - /// - /// 3 x * Client performs 'password' authentication attempt. - /// * Server responds with 'partial success', and 'password' allowed authentication method - /// - /// Since the server only ever allowed the 'password' authentication method, there are no - /// authentication methods left to try after reaching the partial success limit for 'password' - /// and as such authentication fails. + /// * ConnectionInfo provides the following authentication methods (in order): + /// o publickey + /// o password + /// * Partial success limit is 3 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// password + /// (3=PARTIAL) + /// | + /// password + /// (4=PARTIAL) + /// | + /// password + /// (5=SKIP) /// [TestClass] public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached : ClientAuthenticationTestBase @@ -44,6 +49,7 @@ namespace Renci.SshNet.Tests.Classes ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); + /* 1 */ NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); ConnectionInfoMock.InSequence(seq) @@ -57,23 +63,55 @@ namespace Renci.SshNet.Tests.Classes .Setup(p => p.AllowedAuthentications) .Returns(new[] { "password" }); - for (var i = 0; i < _partialSuccessLimit; i++) - { - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - - PasswordAuthenticationMethodMock.InSequence(seq) - .Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.PartialSuccess); - PasswordAuthenticationMethodMock.InSequence(seq) - .Setup(p => p.AllowedAuthentications) - .Returns(new[] {"password"}); - } + /* Enumerate supported authentication methods */ PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - // used to construct exception message + /* 2 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("x_password_x"); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index 393a110e..61df8cdc 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -139,11 +139,12 @@ - + + From 292c0e46b2de9260814754b3e12e619c4c27361e Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Sat, 7 Oct 2017 09:30:12 +0200 Subject: [PATCH 11/12] Sync .NET 3.5 test project. --- .../Renci.SshNet.Tests.NET35.csproj | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj b/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj index 7ae4ac5d..33c9b1c2 100644 --- a/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj +++ b/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj @@ -213,9 +213,15 @@ Classes\CipherInfoTest.cs + + Classes\ClientAuthenticationTest.cs + Classes\ClientAuthenticationTestBase.cs + + Classes\ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs + Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs @@ -225,6 +231,21 @@ Classes\ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs + + Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs + + + Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs + + + Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs + + + Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs + + + Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs + Classes\ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs @@ -234,6 +255,9 @@ Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs + + Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs + Classes\CommandAsyncResultTest.cs @@ -1668,7 +1692,7 @@ - +