diff --git a/src/Renci.SshNet.Tests/Classes/KnownHostStoreTests.cs b/src/Renci.SshNet.Tests/Classes/KnownHostStoreTests.cs
new file mode 100644
index 000000000..021acaec0
--- /dev/null
+++ b/src/Renci.SshNet.Tests/Classes/KnownHostStoreTests.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+ [TestClass]
+ public class KnownHostStoreTests
+ {
+ private static readonly byte[] ExampleKey = new byte[]
+ {
+ 0x00, 0x11, 0x22, 0x33, 0x44,
+ 0x55, 0x66, 0x77, 0x88, 0x99,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
+ 0xff
+ };
+
+ private const string ExampleHost = "some.domain.com";
+ private const string ExampleSubDomain = "*.domain.com";
+ private const string ExampleKeyType = "rsa";
+
+ [TestMethod]
+ public void TestKnownHostStoreFindsHostIfMatch()
+ {
+ KnownHostStore hostStore = new KnownHostStore();
+ hostStore.AddHost(ExampleHost, 22, ExampleKeyType, ExampleKey, false);
+ Assert.IsTrue(hostStore.Knows(ExampleHost, ExampleKeyType, ExampleKey, 22));
+ }
+
+ [TestMethod]
+ public void TestKnownHostStoreFindsHashedHostIfMatch()
+ {
+ KnownHostStore hostStore = new KnownHostStore();
+ hostStore.AddHost(ExampleHost, 22, ExampleKeyType, ExampleKey, true);
+ Assert.IsTrue(hostStore.Knows(ExampleHost, ExampleKeyType, ExampleKey, 22));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(RevokedKeyException), "Did not throw an exception upon finding a revoked key in the store")]
+ public void TestKnownHostStoreRejectsKeyIfRevoked()
+ {
+ KnownHostStore hostStore = new KnownHostStore();
+ hostStore.AddHost(ExampleHost, 22, ExampleKeyType, ExampleKey, false);
+ hostStore.AddHost(ExampleSubDomain, 22, ExampleKeyType, ExampleKey, false, "@revoked");
+
+ hostStore.Knows(ExampleHost, ExampleKeyType, ExampleKey, 22);
+ }
+ }
+}
diff --git a/src/Renci.SshNet.Tests/Classes/KnownHostTest.cs b/src/Renci.SshNet.Tests/Classes/KnownHostTest.cs
new file mode 100644
index 000000000..05572be5a
--- /dev/null
+++ b/src/Renci.SshNet.Tests/Classes/KnownHostTest.cs
@@ -0,0 +1,428 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Renci.SshNet.Security.Cryptography;
+using System;
+using System.Text;
+
+namespace Renci.SshNet.Tests.Classes
+{
+ [TestClass]
+ public class KnownHostTest
+ {
+ private const string ExampleHost1 = "example.com";
+ private const string ExampleHost2 = "test.web.net";
+ private const string ExampleAlgo = "some-key-algorithm";
+
+ private static readonly byte[] ExampleKey = new byte[]
+ {
+ 0x00, 0x11, 0x22, 0x33, 0x44,
+ 0x55, 0x66, 0x77, 0x88, 0x99,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
+ 0xff
+ };
+ private static readonly string Base64ExampleKey = Convert.ToBase64String(ExampleKey);
+
+ private static readonly byte[] ExampleSalt = new byte[]
+ {
+ 0x00, 0x01, 0x02, 0x03, 0x04,
+ 0x05, 0x06, 0x07, 0x08, 0x09,
+ 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+ 0x0f, 0x10, 0x11, 0x12, 0x13
+ };
+ private static readonly string Base64ExampleSalt = Convert.ToBase64String(ExampleSalt);
+ private const string ExampleHostHashed = "nnUK16ANsXd3hL31YfAkGOluSjU=";
+
+ [TestMethod]
+ public void KnownHostCanParsePlaintextHostEntry()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostCanParseHashedHostEntry()
+ {
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", Base64ExampleSalt, ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsTrue(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedHashedHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostCanParsePlaintextHostEntryWithTrailingComments()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2} nonagon infinity opens the door", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ }
+
+ [TestMethod]
+ public void KnownHostCanParseHashedHostEntryWithTrailingComments()
+ {
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3} wait for the answer to open the door", Base64ExampleSalt, ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsTrue(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ }
+
+ [TestMethod]
+ public void KnownHostRejectsInvalidHost()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.DoesNoMatch, parsedHost.MatchesPubKey("MaliciousHost.evil", ExampleAlgo, ExampleKey, 22));
+ }
+
+ [TestMethod]
+ public void KnownHostRejectsInvalidKeyAlgo()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.DoesNoMatch, parsedHost.MatchesPubKey(ExampleHost1, "failingAlgo", ExampleKey, 22));
+ }
+
+ [TestMethod]
+ public void KnownHostRejectsInvalidKey()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.DoesNoMatch, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, new byte[]{0x00}, 22));
+ }
+
+ [TestMethod]
+ public void KnownHostRejectsInvalidPort()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.DoesNoMatch, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 9001));
+ }
+
+ [TestMethod]
+ public void KnownHostCanParsePlaintextHostEntryWithNonstandardPort()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("[{0}:9001] {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.DoesNoMatch, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 9001));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostCanParsePlaintextHostEntryWithBeginningGlob()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("*.com {0} {1}", ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostCanParsePlaintextHostEntryWithEndingGlob()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("example.* {0} {1}", ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostCanParsePlaintextHostEntryWithMultipleGlobs()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("ex*le.* {0} {1}", ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostCanParseMultipleHostPatterns()
+ {
+
+ string expectedPlaintextHostEntry =
+ string.Format("{0},{1} {2} {3}", ExampleHost1, ExampleHost2, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost2, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostSupportsExcludedPatterns()
+ {
+
+ string expectedPlaintextHostEntry =
+ string.Format("example.*,!*.com {0} {1}", ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.DoesNoMatch, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey("example.net", ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostSupportsKeyRevocationForPlaintextHost()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("@revoked {0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.KeyRevoked, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostSupportsKeyRevocationForHashedHost()
+ {
+ string expectedHashedHostEntry =
+ string.Format("@revoked |1|{0}|{1} {2} {3}", Base64ExampleSalt, ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsTrue(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.KeyRevoked, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedHashedHostEntry, parsedHost.ToString());
+ }
+
+
+ [TestMethod]
+ public void KnownHostRecognizesCertAuthForPlaintextHost()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("@cert-authority {0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsTrue(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.InvalidSignature, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedPlaintextHostEntry, parsedHost.ToString());
+ }
+
+
+ [TestMethod]
+ public void KnownHostRecognizesCertAuthForHashedHost()
+ {
+ string expectedHashedHostEntry =
+ string.Format("@cert-authority |1|{0}|{1} {2} {3}", Base64ExampleSalt, ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsTrue(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.InvalidSignature, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedHashedHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostcanParseHasedHostEntryWithNonstandardPort()
+ {
+ string hostWithNonStandardPort = string.Format("[{0}:9001]", ExampleHost1);
+ HMACSHA1 hmac = new HMACSHA1(ExampleSalt);
+ byte[] hashedHost = hmac.ComputeHash(Encoding.ASCII.GetBytes(hostWithNonStandardPort));
+
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", Base64ExampleSalt, Convert.ToBase64String(hashedHost), ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsTrue(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 9001));
+ Assert.AreEqual(expectedHashedHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostcanParseHasedHostEntryWithRedundantPort()
+ {
+ string hostWithNonStandardPort = string.Format("[{0}:22]", ExampleHost1);
+ HMACSHA1 hmac = new HMACSHA1(ExampleSalt);
+ byte[] hashedHost = hmac.ComputeHash(Encoding.ASCII.GetBytes(hostWithNonStandardPort));
+
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", Base64ExampleSalt, Convert.ToBase64String(hashedHost), ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsTrue(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ Assert.AreEqual(KnownHost.HostValidationResponse.Matches, parsedHost.MatchesPubKey(ExampleHost1, ExampleAlgo, ExampleKey, 22));
+ Assert.AreEqual(expectedHashedHostEntry, parsedHost.ToString());
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseMalformedKey()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1} {2}", ExampleHost1, ExampleAlgo, "This_Key_Not_Base_64");
+
+ KnownHost parsedHost;
+
+ Assert.IsFalse(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsIfSectionMissing()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("{0} {1}", ExampleHost1, ExampleAlgo);
+
+ KnownHost parsedHost;
+
+ Assert.IsFalse(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseEmptyLine()
+ {
+ string expectedPlaintextHostEntry = String.Empty;
+
+ KnownHost parsedHost;
+
+ Assert.IsFalse(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseCommentLine()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("#{0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsFalse(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseLineWithOperandAndMissingSecion()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("@cert-authority {0} {1}", ExampleHost1, ExampleAlgo);
+
+ KnownHost parsedHost;
+
+ Assert.IsFalse(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseInvalidOperand()
+ {
+ string expectedPlaintextHostEntry =
+ string.Format("@fail-parsing {0} {1} {2}", ExampleHost1, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+
+ Assert.IsFalse(KnownHost.TryParse(expectedPlaintextHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseInvalidHashType()
+ {
+ string expectedHashedHostEntry =
+ string.Format("|2|{0}|{1} {2} {3}", Base64ExampleSalt, ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsFalse(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseMalformedSalt()
+ {
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", "not_base_64", ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsFalse(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseMalformedHash()
+ {
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", Base64ExampleSalt, "not_base_64", ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsFalse(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ }
+
+
+ [TestMethod]
+ public void KnownHostFailsToParseIfHashSectionMissing()
+ {
+ string expectedHashedHostEntry =
+ string.Format("|1|{0} {1} {2}", Base64ExampleSalt, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsFalse(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ }
+
+
+ [TestMethod]
+ public void KnownHostFailsToParseSaltTooShort()
+ {
+ byte[] shortSalt = new byte[19];
+
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", Convert.ToBase64String(shortSalt), ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsFalse(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ }
+
+ [TestMethod]
+ public void KnownHostFailsToParseSaltTooLong()
+ {
+ byte[] longSalt = new byte[21];
+
+ string expectedHashedHostEntry =
+ string.Format("|1|{0}|{1} {2} {3}", Convert.ToBase64String(longSalt), ExampleHostHashed, ExampleAlgo, Base64ExampleKey);
+
+ KnownHost parsedHost;
+ Assert.IsFalse(KnownHost.TryParse(expectedHashedHostEntry, out parsedHost));
+ }
+ }
+}
diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
index aee9c21b4..230e8cbfc 100644
--- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
+++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
@@ -224,6 +224,8 @@
+
+
diff --git a/src/Renci.SshNet/Common/RevokedKeyException.cs b/src/Renci.SshNet/Common/RevokedKeyException.cs
new file mode 100644
index 000000000..c10312dcb
--- /dev/null
+++ b/src/Renci.SshNet/Common/RevokedKeyException.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+#if FEATURE_BINARY_SERIALIZATION
+using System.Runtime.Serialization;
+#endif // FEATURE_BINARY_SERIALIZATION
+
+namespace Renci.SshNet.Common
+{
+ ///
+ /// The exception that is thrown when SSH exception occurs.
+ ///
+#if FEATURE_BINARY_SERIALIZATION
+ [Serializable]
+#endif // FEATURE_BINARY_SERIALIZATION
+ public class RevokedKeyException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RevokedKeyException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message.
+ public RevokedKeyException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message.
+ /// The inner.
+ public RevokedKeyException(string message, Exception inner)
+ : base(message, inner)
+ {
+ }
+
+#if FEATURE_BINARY_SERIALIZATION
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ /// The parameter is null.
+ /// The class name is null or is zero (0).
+ protected RevokedKeyException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+#endif // FEATURE_BINARY_SERIALIZATION
+ }
+}
diff --git a/src/Renci.SshNet/KnownHost.cs b/src/Renci.SshNet/KnownHost.cs
new file mode 100644
index 000000000..65c3015df
--- /dev/null
+++ b/src/Renci.SshNet/KnownHost.cs
@@ -0,0 +1,305 @@
+using Renci.SshNet.Security.Cryptography;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Renci.SshNet
+{
+ ///
+ /// Represents a host with a known public key
+ ///
+ internal class KnownHost
+ {
+ //If present at the start of a known_hosts entry, signifies that the hostname is sha1 hashed
+ private const string HashedHostSignifier = "|1|";
+
+ private const UInt16 DefaultSshPortNumber = 22;
+ private const string DefaultSshPortString = "22";
+
+ private const string KeyRevokeMarker = "@revoked";
+ private const string KeyCertAuthMarker = "@cert-authority";
+
+ private readonly bool _isHashed;
+ private readonly List> _plaintextHostPatterns;
+ private readonly byte[] _hashedHostName;
+ private readonly byte[] _hashSalt;
+ private readonly byte[] _pubKey;
+ private readonly string _keyType;
+ private readonly HostPubkeyMarker _hostMarker;
+
+ public enum HostValidationResponse
+ {
+ Matches,
+ DoesNoMatch,
+ KeyRevoked,
+ InvalidSignature,
+ ValidSignature
+ }
+
+ private enum HostPubkeyMarker
+ {
+ None,
+ Revoke,
+ CertAuthority,
+ Error
+ }
+
+ private static HostPubkeyMarker ParseSpecialOperand(string toParse)
+ {
+ switch (toParse)
+ {
+ case KeyRevokeMarker:
+ return HostPubkeyMarker.Revoke;
+ case KeyCertAuthMarker:
+ return HostPubkeyMarker.CertAuthority;
+ default:
+ return HostPubkeyMarker.Error;
+ }
+ }
+
+ private KnownHost(string hostName, string keyType, string base64Pubkey, HostPubkeyMarker marker)
+ {
+ _isHashed = hostName.StartsWith("|");
+
+ if (_isHashed)
+ {
+ if (!hostName.StartsWith(HashedHostSignifier))
+ throw new FormatException("The hashed section was not properly composed");
+
+ string[] splitHashedSection = hostName.Split('|');
+ if (splitHashedSection.Length != 4)
+ throw new FormatException("The hashed section was not properly composed");
+
+ _hashSalt = Convert.FromBase64String(splitHashedSection[2]);
+
+ if (_hashSalt.Length != 20)
+ throw new ArgumentException("The salt must be exacly 20 bytes");
+
+ _hashedHostName = Convert.FromBase64String(splitHashedSection[3]);
+ }
+ else
+ {
+ _plaintextHostPatterns = GetPatternList(hostName);
+ if (_plaintextHostPatterns.Count == 0)
+ throw new FormatException("No hostname patterns given");
+ }
+
+ _keyType = keyType;
+ _pubKey = Convert.FromBase64String(base64Pubkey);
+ _hostMarker = marker;
+ }
+
+ ///
+ /// Attempts to parse a line from an openssh known_hosts file
+ ///
+ ///
+ ///
+ /// true if the host line could be parsed. false if an error occurred
+ public static bool TryParse(string hostLine, out KnownHost host)
+ {
+ try
+ {
+ return UnsafeTryParse(hostLine, out host);
+ }
+ catch (FormatException)
+ {
+ host = null;
+ return false;
+ }
+ catch (ArgumentException)
+ {
+ host = null;
+ return false;
+ }
+ }
+
+ private static bool UnsafeTryParse(string hostLine, out KnownHost host)
+ {
+ host = null;
+ if (string.IsNullOrEmpty(hostLine) || hostLine.StartsWith("#"))
+ return false;
+
+ bool hasSpecialPrefix = hostLine.StartsWith("@");
+
+ string[] sectionedLine = hostLine.Split(' ');
+ if (hasSpecialPrefix && sectionedLine.Length < 4)
+ return false;
+ else if (sectionedLine.Length < 3)
+ return false;
+
+ HostPubkeyMarker marker =
+ hasSpecialPrefix ? ParseSpecialOperand(sectionedLine[0]) : HostPubkeyMarker.None;
+ if (marker == HostPubkeyMarker.Error)
+ return false;
+
+ int sectionOffset = hasSpecialPrefix ? 1 : 0;
+ string hostNameSecion = sectionedLine[sectionOffset];
+ string keyTypeSection = sectionedLine[1 + sectionOffset];
+ string pubKeySection = sectionedLine[2 + sectionOffset];
+
+ host = new KnownHost(hostNameSecion, keyTypeSection, pubKeySection, marker);
+
+ return true;
+ }
+
+ ///
+ /// Compare the given host information with this KnownHost
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Whether or not the given hostname-keytype-pubkey combination is a match to this KnownHost
+ public HostValidationResponse MatchesPubKey(string hostname, string keyType, byte[] pubKey, UInt16 portNumber)
+ {
+ if (!ValidateHostName(hostname, portNumber))
+ return HostValidationResponse.DoesNoMatch;
+
+ if (_hostMarker == HostPubkeyMarker.CertAuthority)
+ return ValidateKeySignature(hostname, keyType, pubKey);
+
+ if (_keyType != keyType || !_pubKey.SequenceEqual(pubKey))
+ return HostValidationResponse.DoesNoMatch;
+
+ switch (_hostMarker)
+ {
+ case HostPubkeyMarker.Revoke:
+ return HostValidationResponse.KeyRevoked;
+ default:
+ return HostValidationResponse.Matches;
+ }
+ }
+
+ private HostValidationResponse ValidateKeySignature(string hostname, string keyType, byte[] keyToCheck)
+ {
+ //TODO: Return HostValidationResponse.ValidSignature if the key is signed by this Certificate Authority
+ return HostValidationResponse.InvalidSignature;
+ }
+
+ ///
+ /// Writes this KnownHost as an openssh known_hosts formatted line
+ ///
+ ///
+ public override string ToString()
+ {
+ string hostnameSection = _isHashed
+ ? string.Format("{0}{1}|{2}", HashedHostSignifier, Convert.ToBase64String(_hashSalt), Convert.ToBase64String(_hashedHostName))
+ : string.Join("," , _plaintextHostPatterns.Select(x => x.Item1));
+
+ string specialPrefix = string.Empty;
+ switch (_hostMarker)
+ {
+ case HostPubkeyMarker.CertAuthority:
+ specialPrefix = KeyCertAuthMarker + " ";
+ break;
+ case HostPubkeyMarker.Revoke:
+ specialPrefix = KeyRevokeMarker + " ";
+ break;
+ }
+
+ return string.Format("{0}{1} {2} {3}",specialPrefix, hostnameSection, _keyType, Convert.ToBase64String(_pubKey));
+ }
+
+ private bool ValidateHostName(string host, UInt16 port)
+ {
+ if (_isHashed)
+ {
+ //Corner case with hashed hosts: If the default port is specified, then the hash could either be just the hostname,
+ //or hostname with port
+ if (port == DefaultSshPortNumber && ValidateHashedHostName(host))
+ return true;
+ return ValidateHashedHostName(string.Format("[{0}:{1}]", host, port));
+ }
+ return ValidatePlaintextHostName(string.Format("[{0}:{1}]", host, port));
+ }
+
+ private bool ValidateHashedHostName(string hostAndPort)
+ {
+ HMACSHA1 hmac = new HMACSHA1(_hashSalt);
+ byte[] hashToCompare = hmac.ComputeHash(Encoding.ASCII.GetBytes(hostAndPort));
+
+ return _hashedHostName.SequenceEqual(hashToCompare);
+ }
+
+ private bool ValidatePlaintextHostName(string hostAndPort)
+ {
+ bool foundAtLeastOneMatch = false;
+ foreach (Tuple possibleMatch in _plaintextHostPatterns)
+ {
+ bool negateMatch = possibleMatch.Item1.StartsWith("!");
+ bool foundMatch = possibleMatch.Item2.IsMatch(hostAndPort);
+ if (foundMatch && negateMatch)
+ return false;
+
+ foundAtLeastOneMatch = foundAtLeastOneMatch || foundMatch;
+ }
+
+ return foundAtLeastOneMatch;
+ }
+
+ private List> GetPatternList(string unsplitHostPatterns)
+ {
+ List> patternList = new List>();
+ foreach (string s in unsplitHostPatterns.Split(','))
+ {
+ Regex toAdd;
+ if(!TryGetRegexFromPlaintextHostPattern(s, out toAdd))
+ continue;
+ patternList.Add(new Tuple(s, toAdd));
+ }
+
+ return patternList;
+ }
+
+ private static bool TryGetRegexFromPlaintextHostPattern(string pattern, out Regex matchingExpression)
+ {
+ matchingExpression = null;
+
+ StringBuilder regexBuilder = new StringBuilder();
+
+ string strippedHostPattern;
+ if(!TryValidateAndFormatHostPattern(pattern, out strippedHostPattern))
+ return false;
+
+ //split by *, regex escape the split sections, replace * with regex equivalent (.*)
+ string[] sectionedPattern = strippedHostPattern.Split('*');
+ regexBuilder.Append(string.Join(".*", sectionedPattern.Select(Regex.Escape)));
+
+
+ matchingExpression = new Regex(regexBuilder.ToString());
+ return true;
+ }
+
+ private static bool TryValidateAndFormatHostPattern(string input, out string hostPattern)
+ {
+ hostPattern = null;
+
+ bool squareBraceOpens = input.StartsWith("[");
+ bool squareBraceCloses = input.EndsWith("]");
+
+ //Opening/Closing braces must be matched
+ if (squareBraceOpens ^ squareBraceCloses)
+ return false;
+
+ if(squareBraceOpens)
+ {
+ //Patterns surrounded by braces must specify port
+ if (!input.Contains(":"))
+ return false;
+ hostPattern = input;
+ }
+ else
+ {
+ hostPattern = string.Format("[{0}:{1}]", input, DefaultSshPortString);
+ }
+
+ //Strip the negation operand if present
+ if (hostPattern.StartsWith("[!"))
+ hostPattern = hostPattern.Remove(1, 1);
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Renci.SshNet/KnownHostStore.cs b/src/Renci.SshNet/KnownHostStore.cs
new file mode 100644
index 000000000..78fc995ad
--- /dev/null
+++ b/src/Renci.SshNet/KnownHostStore.cs
@@ -0,0 +1,154 @@
+using System;
+using Renci.SshNet.Abstractions;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Renci.SshNet.Common;
+using Renci.SshNet.Security.Cryptography;
+
+namespace Renci.SshNet
+{
+ ///
+ /// A class to save and store the public keys of connected hosts. Capable
+ /// of reading and creating files compatible with openssh's known_hosts
+ /// format. For more information on known_hosts files, read the
+ /// man 8 sshd section on known_hosts
+ ///
+ public class KnownHostStore
+ {
+ private readonly List _knownHosts;
+
+ ///
+ /// Construct an empty KnownHostStore
+ ///
+ public KnownHostStore()
+ {
+ _knownHosts = new List();
+ }
+
+ ///
+ /// Construct a KnownHostStore from an openssh known_hosts file
+ ///
+ /// The path of the file to read
+ public KnownHostStore(string knownHostsFile)
+ {
+ _knownHosts = new List();
+ ImportKnownHostsFromFile(knownHostsFile);
+ }
+
+ ///
+ /// Fill this store with elements from an openssh known_hosts file
+ ///
+ /// The path of the file to read
+ public void ImportKnownHostsFromFile(string filePath)
+ {
+ using (var fStream = File.OpenText(filePath))
+ {
+ while (!fStream.EndOfStream)
+ {
+ KnownHost createdHost;
+ if (KnownHost.TryParse(fStream.ReadLine(), out createdHost))
+ _knownHosts.Add(createdHost);
+ }
+ }
+ }
+
+ ///
+ /// Add a host to this store.
+ ///
+ /// The name of the host to add
+ ///
+ /// The algorithm of the public key
+ /// The public key of the host to add
+ /// Whether the hostname should be stored as a SHA1 hash
+ /// An optional @ prefixed string that corresponds to a marker for this host. See the man 8 sshd section about known_hosts for valid markers
+ /// Thrown if the given arguments cannor be parsed into a valid host entry
+ public void AddHost(string hostname, UInt16 portNumber, string keyType, byte[] pubKey, bool storeHostnameHashed, string marker = "")
+ {
+ string hostNameWithPort = string.Format("[{0}:{1}]", hostname, portNumber);
+ string hostnameSection;
+
+ if (!string.IsNullOrEmpty(marker))
+ {
+ if(!marker.StartsWith("@"))
+ throw new FormatException("The given host marker must be prefixed with \'@\'. See the \'man 8 sshd\' section about known_hosts for more details on markers");
+ marker = string.Format("{0} ", marker);
+ }
+
+ if (storeHostnameHashed)
+ {
+ byte[] salt = new byte[20];
+ CryptoAbstraction.GenerateRandom(salt);
+
+ HMACSHA1 hmac = new HMACSHA1(salt);
+ byte[] hash = hmac.ComputeHash(Encoding.ASCII.GetBytes(hostNameWithPort));
+
+ hostnameSection = string.Format("|1|{0}|{1}", Convert.ToBase64String(salt), Convert.ToBase64String(hash));
+ }
+ else
+ {
+ hostnameSection = hostNameWithPort;
+ }
+
+ string hostToParse = string.Format("{3}{0} {1} {2}", hostnameSection, keyType, Convert.ToBase64String(pubKey), marker);
+
+ KnownHost newHost;
+ if(!KnownHost.TryParse(hostToParse, out newHost))
+ throw new FormatException("Malformed input: Failed to create entry. If you specified a marker, ensure it is valid (see the \'man 8 sshd\' section on known_hosts for more details on markers)");
+
+ _knownHosts.Add(newHost);
+ }
+
+ ///
+ /// Removes all hosts from this store
+ ///
+ public void ClearHosts()
+ {
+ _knownHosts.Clear();
+ }
+
+ ///
+ /// Writes all hosts in this store to an openssh known_hosts formatted file
+ ///
+ /// The path of the file to be written
+ public void ExportKnownHostsToFile(string outFile)
+ {
+ using (var fStream = File.OpenWrite(outFile))
+ {
+ foreach (KnownHost host in _knownHosts)
+ {
+ byte[] hostLine = Encoding.ASCII.GetBytes(host.ToString() + Environment.NewLine);
+ fStream.Write(hostLine, 0, hostLine.Length);
+ }
+ }
+ }
+
+ ///
+ /// Look for a host in this store with the given hostname and public key
+ ///
+ /// The name of the host to search for
+ /// The algorithm of the public key
+ /// The public key of the host to search for
+ /// The port number
+ /// If the given key is marked as revoked
+ /// Whether a host corresponding to the given parameters exists in this store
+ public bool Knows(string hostname, string keyType, byte[] pubKey, UInt16 port)
+ {
+ bool foundMatch = false;
+
+ foreach (KnownHost host in _knownHosts)
+ {
+ KnownHost.HostValidationResponse typeOfMatch = host.MatchesPubKey(hostname, keyType, pubKey, port);
+
+ if(typeOfMatch == KnownHost.HostValidationResponse.KeyRevoked)
+ throw new RevokedKeyException("The given host-pubkey pair is marked as revoked");
+
+ foundMatch = foundMatch || (typeOfMatch == KnownHost.HostValidationResponse.Matches);
+ }
+
+ return foundMatch;
+ }
+ }
+}
diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj
index 0598d18ce..40d2e8bb5 100644
--- a/src/Renci.SshNet/Renci.SshNet.csproj
+++ b/src/Renci.SshNet/Renci.SshNet.csproj
@@ -114,6 +114,7 @@
Code
+
Code
@@ -159,6 +160,8 @@
+
+