diff --git a/src/Renci.SshNet.Benchmarks/Common/HostKeyEventArgsBenchmarks.cs b/src/Renci.SshNet.Benchmarks/Common/HostKeyEventArgsBenchmarks.cs new file mode 100644 index 000000000..54900d046 --- /dev/null +++ b/src/Renci.SshNet.Benchmarks/Common/HostKeyEventArgsBenchmarks.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Attributes; + +using Renci.SshNet.Benchmarks.Security.Cryptography.Ciphers; +using Renci.SshNet.Common; +using Renci.SshNet.Security; + +namespace Renci.SshNet.Benchmarks.Common +{ + [MemoryDiagnoser] + [ShortRunJob] + public class HostKeyEventArgsBenchmarks + { + private readonly KeyHostAlgorithm _keyHostAlgorithm; + + public HostKeyEventArgsBenchmarks() + { + _keyHostAlgorithm = GetKeyHostAlgorithm(); + } + private static KeyHostAlgorithm GetKeyHostAlgorithm() + { + using (var s = typeof(RsaCipherBenchmarks).Assembly.GetManifestResourceStream("Renci.SshNet.Benchmarks.Data.Key.RSA.txt")) + { + var privateKey = new PrivateKeyFile(s); + return (KeyHostAlgorithm) privateKey.HostKeyAlgorithms.First(); + } + } + + [Benchmark()] + public HostKeyEventArgs Constructor() + { + return new HostKeyEventArgs(_keyHostAlgorithm); + } + + [Benchmark()] + public (string, string) CalculateFingerPrintSHA256AndMD5() + { + var test = new HostKeyEventArgs(_keyHostAlgorithm); + + return (test.FingerPrintSHA256, test.FingerPrintMD5); + } + + [Benchmark()] + public string CalculateFingerPrintSHA256() + { + var test = new HostKeyEventArgs(_keyHostAlgorithm); + + return test.FingerPrintSHA256; + } + + [Benchmark()] + public byte[] CalculateFingerPrint() + { + var test = new HostKeyEventArgs(_keyHostAlgorithm); + + return test.FingerPrint; + } + + [Benchmark()] + public string CalculateFingerPrintMD5() + { + var test = new HostKeyEventArgs(_keyHostAlgorithm); + + return test.FingerPrintSHA256; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs index 5401e6b6b..2f94ba7ff 100644 --- a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs +++ b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs @@ -380,6 +380,53 @@ public void Common_HostKeyValidation_Success() Assert.IsTrue(hostValidationSuccessful); } + [TestMethod] + public void Common_HostKeyValidationSHA256_Success() + { + var hostValidationSuccessful = false; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => + { + if (e.FingerPrintSHA256 == "9fa6vbz64gimzsGZ/xZi3aaYE1o7E96iU2NjcfQNGwI") + { + hostValidationSuccessful = e.CanTrust; + } + else + { + e.CanTrust = false; + } + }; + client.Connect(); + } + + Assert.IsTrue(hostValidationSuccessful); + } + + [TestMethod] + public void Common_HostKeyValidationMD5_Success() + { + var hostValidationSuccessful = false; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => + { + if (e.FingerPrintMD5 == "3d:90:d8:0d:d5:e0:b6:13:42:7c:78:1e:19:a3:99:2b") + { + hostValidationSuccessful = e.CanTrust; + } + else + { + e.CanTrust = false; + } + }; + client.Connect(); + } + + Assert.IsTrue(hostValidationSuccessful); + } /// /// Verifies whether we handle a disconnect initiated by the SSH server (through a SSH_MSG_DISCONNECT message). /// diff --git a/src/Renci.SshNet.IntegrationTests/Dockerfile b/src/Renci.SshNet.IntegrationTests/Dockerfile index 160ea6f29..19ef6e19e 100644 --- a/src/Renci.SshNet.IntegrationTests/Dockerfile +++ b/src/Renci.SshNet.IntegrationTests/Dockerfile @@ -14,6 +14,8 @@ RUN apk update && apk upgrade --no-cache && \ chmod 400 /etc/ssh/ssh*key && \ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ sed -i 's/#LogLevel\s*INFO/LogLevel DEBUG3/' /etc/ssh/sshd_config && \ + # Set the default RSA key + echo 'HostKey /etc/ssh/ssh_host_rsa_key' >> /etc/ssh/sshd_config && \ chmod 646 /etc/ssh/sshd_config && \ # install and configure sudo apk add --no-cache sudo && \ @@ -45,4 +47,4 @@ RUN apk update && apk upgrade --no-cache && \ EXPOSE 22 22 -ENTRYPOINT ["/opt/sshnet/start.sh"] \ No newline at end of file +ENTRYPOINT ["/opt/sshnet/start.sh"] diff --git a/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs b/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs index 28001655a..39ff85d7b 100644 --- a/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs @@ -56,6 +56,8 @@ public void HostKeyEventArgsConstructorTest_VerifyMD5() Assert.IsTrue(new byte[] { 0x92, 0xea, 0x54, 0xa1, 0x01, 0xf9, 0x95, 0x9c, 0x71, 0xd9, 0xbb, 0x51, 0xb2, 0x55, 0xf8, 0xd9 }.SequenceEqual(target.FingerPrint)); + Assert.AreEqual("92:ea:54:a1:01:f9:95:9c:71:d9:bb:51:b2:55:f8:d9", target.FingerPrintMD5); + } /// diff --git a/src/Renci.SshNet/Common/HostKeyEventArgs.cs b/src/Renci.SshNet/Common/HostKeyEventArgs.cs index ddf49d0ac..993b6d645 100644 --- a/src/Renci.SshNet/Common/HostKeyEventArgs.cs +++ b/src/Renci.SshNet/Common/HostKeyEventArgs.cs @@ -1,4 +1,5 @@ using System; + using Renci.SshNet.Abstractions; using Renci.SshNet.Security; @@ -9,6 +10,10 @@ namespace Renci.SshNet.Common /// public class HostKeyEventArgs : EventArgs { + private readonly Lazy _lazyFingerPrint; + private readonly Lazy _lazyFingerPrintSHA256; + private readonly Lazy _lazyFingerPrintMD5; + /// /// Gets or sets a value indicating whether host key can be trusted. /// @@ -33,15 +38,42 @@ public class HostKeyEventArgs : EventArgs /// /// MD5 fingerprint as byte array. /// - public byte[] FingerPrint { get; private set; } + public byte[] FingerPrint + { + get + { + return _lazyFingerPrint.Value; + } + } /// - /// Gets the SHA256 fingerprint. + /// Gets the SHA256 fingerprint of the host key in the same format as the ssh command, + /// i.e. non-padded base64, but without the SHA256: prefix. /// + /// ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og /// /// Base64 encoded SHA256 fingerprint with padding (equals sign) removed. /// - public string FingerPrintSHA256 { get; private set; } + public string FingerPrintSHA256 + { + get + { + return _lazyFingerPrintSHA256.Value; + } + } + + /// + /// Gets the MD5 fingerprint of the host key in the same format as the ssh command, + /// i.e. hexadecimal bytes separated by colons, but without the MD5: prefix. + /// + /// 97:70:33:82:fd:29:3a:73:39:af:6a:07:ad:f8:80:49 + public string FingerPrintMD5 + { + get + { + return _lazyFingerPrintMD5.Value; + } + } /// /// Gets the length of the key in bits. @@ -61,16 +93,21 @@ public HostKeyEventArgs(KeyHostAlgorithm host) HostKey = host.Data; HostKeyName = host.Name; KeyLength = host.Key.KeyLength; - - using (var md5 = CryptoAbstraction.CreateMD5()) + + _lazyFingerPrint = new Lazy(() => { - FingerPrint = md5.ComputeHash(host.Data); - } + using var md5 = CryptoAbstraction.CreateMD5(); + return md5.ComputeHash(HostKey); + }); - using (var sha256 = CryptoAbstraction.CreateSHA256()) + _lazyFingerPrintSHA256 = new Lazy(() => { - FingerPrintSHA256 = Convert.ToBase64String(sha256.ComputeHash(host.Data)).Replace("=", ""); - } + using var sha256 = CryptoAbstraction.CreateSHA256(); + return Convert.ToBase64String(sha256.ComputeHash(HostKey)).Replace("=", ""); + }); + + _lazyFingerPrintMD5 = new Lazy(() => + BitConverter.ToString(FingerPrint).Replace("-", ":").ToLowerInvariant()); } } }