diff --git a/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs b/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs index 64676a0d2..af64ff62a 100644 --- a/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs +++ b/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs @@ -20,9 +20,7 @@ public static void Reset(this RemoteSshdConfig remoteSshdConfig) .ClearCiphers() .ClearKeyExchangeAlgorithms() .ClearHostKeyAlgorithms() - .AddHostKeyAlgorithm(HostKeyAlgorithm.SshRsa) .ClearPublicKeyAcceptedAlgorithms() - .AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.SshRsa) .WithUsePAM(true) .Update() .Restart(); diff --git a/src/Renci.SshNet.IntegrationTests/Dockerfile b/src/Renci.SshNet.IntegrationTests/Dockerfile index 811d51543..160ea6f29 100644 --- a/src/Renci.SshNet.IntegrationTests/Dockerfile +++ b/src/Renci.SshNet.IntegrationTests/Dockerfile @@ -14,7 +14,6 @@ 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 && \ - echo 'PubkeyAcceptedAlgorithms ssh-rsa' >> /etc/ssh/sshd_config && \ chmod 646 /etc/ssh/sshd_config && \ # install and configure sudo apk add --no-cache sudo && \ diff --git a/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs b/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs index 3732f1e22..7f177a6f4 100644 --- a/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs +++ b/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs @@ -22,63 +22,46 @@ public void TearDown() { _remoteSshdConfig?.Reset(); } - + [TestMethod] [Ignore] // No longer supported in recent versions of OpenSSH + // TODO: We should be able to enable some legacy settings to make it work + // https://www.openssh.com/legacy.html e.g. PubkeyAcceptedKeyTypes / HostbasedAcceptedKeyTypes ? public void SshDsa() { - _remoteSshdConfig.ClearHostKeyAlgorithms() - .AddHostKeyAlgorithm(HostKeyAlgorithm.SshDsa) - .ClearHostKeyFiles() - .AddHostKeyFile(HostKeyFile.Dsa.FilePath) - .Update() - .Restart(); - - HostKeyEventArgs hostKeyEventsArgs = null; - - using (var client = new SshClient(_connectionInfoFactory.Create())) - { - client.HostKeyReceived += (sender, e) => hostKeyEventsArgs = e; - client.Connect(); - client.Disconnect(); - } - - Assert.IsNotNull(hostKeyEventsArgs); - Assert.AreEqual(HostKeyFile.Dsa.KeyName, hostKeyEventsArgs.HostKeyName); - Assert.AreEqual(1024, hostKeyEventsArgs.KeyLength); - Assert.IsTrue(hostKeyEventsArgs.FingerPrint.SequenceEqual(HostKeyFile.Dsa.FingerPrint)); + DoTest(HostKeyAlgorithm.SshDsa, HostKeyFile.Dsa, 1024); } [TestMethod] public void SshRsa() { - _remoteSshdConfig.ClearHostKeyAlgorithms() - .AddHostKeyAlgorithm(HostKeyAlgorithm.SshRsa) - .Update() - .Restart(); - - HostKeyEventArgs hostKeyEventsArgs = null; + DoTest(HostKeyAlgorithm.SshRsa, HostKeyFile.Rsa, 3072); + } - using (var client = new SshClient(_connectionInfoFactory.Create())) - { - client.HostKeyReceived += (sender, e) => hostKeyEventsArgs = e; - client.Connect(); - client.Disconnect(); - } + [TestMethod] + public void SshRsaSha256() + { + DoTest(HostKeyAlgorithm.RsaSha2256, HostKeyFile.Rsa, 3072); + } - Assert.IsNotNull(hostKeyEventsArgs); - Assert.AreEqual(HostKeyFile.Rsa.KeyName, hostKeyEventsArgs.HostKeyName); - Assert.AreEqual(3072, hostKeyEventsArgs.KeyLength); - Assert.IsTrue(hostKeyEventsArgs.FingerPrint.SequenceEqual(HostKeyFile.Rsa.FingerPrint)); + [TestMethod] + public void SshRsaSha512() + { + DoTest(HostKeyAlgorithm.RsaSha2512, HostKeyFile.Rsa, 3072); } [TestMethod] public void SshEd25519() + { + DoTest(HostKeyAlgorithm.SshEd25519, HostKeyFile.Ed25519, 256); + } + + private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile, int keyLength) { _remoteSshdConfig.ClearHostKeyAlgorithms() - .AddHostKeyAlgorithm(HostKeyAlgorithm.SshEd25519) + .AddHostKeyAlgorithm(hostKeyAlgorithm) .ClearHostKeyFiles() - .AddHostKeyFile(HostKeyFile.Ed25519.FilePath) + .AddHostKeyFile(hostKeyFile.FilePath) .Update() .Restart(); @@ -92,14 +75,9 @@ public void SshEd25519() } Assert.IsNotNull(hostKeyEventsArgs); - Assert.AreEqual(HostKeyFile.Ed25519.KeyName, hostKeyEventsArgs.HostKeyName); - Assert.AreEqual(256, hostKeyEventsArgs.KeyLength); - Assert.IsTrue(hostKeyEventsArgs.FingerPrint.SequenceEqual(HostKeyFile.Ed25519.FingerPrint)); - } - - private void Client_HostKeyReceived(object sender, HostKeyEventArgs e) - { - throw new NotImplementedException(); + Assert.AreEqual(hostKeyAlgorithm.Name, hostKeyEventsArgs.HostKeyName); + Assert.AreEqual(keyLength, hostKeyEventsArgs.KeyLength); + CollectionAssert.AreEqual(hostKeyFile.FingerPrint, hostKeyEventsArgs.FingerPrint); } } } diff --git a/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs b/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs index 83313f4a8..308f2ab3f 100644 --- a/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs +++ b/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs @@ -5,7 +5,7 @@ namespace Renci.SshNet.IntegrationTests { [TestClass] public class PrivateKeyAuthenticationTests : TestBase - { + { private IConnectionInfoFactory _connectionInfoFactory; private RemoteSshdConfig _remoteSshdConfig; @@ -23,43 +23,64 @@ public void TearDown() } [TestMethod] - public void Ecdsa256() + [Ignore] // No longer supported in recent versions of OpenSSH + // TODO: We should be able to enable some legacy settings to make it work + // https://www.openssh.com/legacy.html e.g. PubkeyAcceptedKeyTypes / HostbasedAcceptedKeyTypes ? + public void SshDsa() { - _remoteSshdConfig.AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.EcdsaSha2Nistp256) - .Update() - .Restart(); + DoTest(PublicKeyAlgorithm.SshDss, "id_dsa"); + } - var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod("key_ecdsa_256_openssh")); + [TestMethod] + public void SshRsa() + { + DoTest(PublicKeyAlgorithm.SshRsa, "id_rsa"); + } - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - } + [TestMethod] + public void SshRsaSha256() + { + DoTest(PublicKeyAlgorithm.RsaSha2256, "id_rsa"); } [TestMethod] - public void Ecdsa384() + public void SshRsaSha512() { - _remoteSshdConfig.AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.EcdsaSha2Nistp384) - .Update() - .Restart(); + DoTest(PublicKeyAlgorithm.RsaSha2512, "id_rsa"); + } - var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod("key_ecdsa_384_openssh")); + [TestMethod] + public void Ecdsa256() + { + DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp256, "key_ecdsa_256_openssh"); + } - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - } + [TestMethod] + public void Ecdsa384() + { + DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp384, "key_ecdsa_384_openssh"); } [TestMethod] - public void EcdsaA521() + public void Ecdsa521() + { + DoTest(PublicKeyAlgorithm.EcdsaSha2Nistp521, "key_ecdsa_521_openssh"); + } + + [TestMethod] + public void Ed25519() + { + DoTest(PublicKeyAlgorithm.SshEd25519, "key_ed25519_openssh"); + } + + private void DoTest(PublicKeyAlgorithm publicKeyAlgorithm, string keyResource) { - _remoteSshdConfig.AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.EcdsaSha2Nistp521) + _remoteSshdConfig.ClearPublicKeyAcceptedAlgorithms() + .AddPublicKeyAcceptedAlgorithms(publicKeyAlgorithm) .Update() .Restart(); - var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod("key_ecdsa_521_openssh")); + var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod(keyResource)); using (var client = new SshClient(connectionInfo)) { diff --git a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index ae3505f10..db411f361 100644 --- a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Renci.SshNet.IntegrationTests/user/sshnet/authorized_keys b/src/Renci.SshNet.IntegrationTests/user/sshnet/authorized_keys index 484a0bdcc..8f0372d84 100644 --- a/src/Renci.SshNet.IntegrationTests/user/sshnet/authorized_keys +++ b/src/Renci.SshNet.IntegrationTests/user/sshnet/authorized_keys @@ -1,4 +1,5 @@ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4N0T6VopnwPSYQUw0waQNhBjz0DYFQwvkv4OwWYSf//fJF3M6bH42Tn2J+IlQ+4/fCFnE3m99seV5T1myEj7fsupNteY2sKFGXENLGtAD/76FM0iBmXx76xlSTyZSSmNDIRU4xUR23cfc+al84F5mO2lEk+5Zr3Qn5JUpucBfis4vtu0sMDgZ4w1d0tcuXkT/MEJn2iX2cnxbSy5qNAPHu7b+LEfXBv2OrMDqPrx/X6QREgi3w5RxL5kz7bvitWsIwIvb3ST2ARAArBwb8pEyp2A/w5p22rkQtL+3ibZ8fkmpgn33f31AZPgtM++iJPBmPKFjArcWEJ9fIVB/6DAj ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzzrPpItEjNG7tU0DpJJ4pkI01E9d6K61OKTVPdFQSyGCdMj9XdP93lC6sJA+9/ahvf5F3gWEKxUJL2CKUiFWw= ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLSsu/HNKiaALhQ26UDv+N0AFdMb26fMVrOKe866CGu6ajSf9HUOhJFdjhseihB2rTalMPr8MrcXNLufii4mL8u4l9fUQXFgwnM/ZpiVPSs6C+8i4u/ZDg7Nx2NXybNIgQ== -ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACB4WgRgGBRo6Uk+cRgg8tJPCbEtGURRWlUA7PDDerXR+P9O6mm3L99Etxsyh5XNYqXyaMNtH5c51ooMajrFwcayAHIhPPb8X3CsTwEfIUQ96aDyHQMotbRfnkn6uefeUTRrSNcqeAndUtVyAqBdqbsq2mgJYXHrz2NUKlPFYgauQi+WQ== \ No newline at end of file +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACB4WgRgGBRo6Uk+cRgg8tJPCbEtGURRWlUA7PDDerXR+P9O6mm3L99Etxsyh5XNYqXyaMNtH5c51ooMajrFwcayAHIhPPb8X3CsTwEfIUQ96aDyHQMotbRfnkn6uefeUTRrSNcqeAndUtVyAqBdqbsq2mgJYXHrz2NUKlPFYgauQi+WQ== +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAkNGPVOTuzuKTgGfHcve2MRj57yXhmZgkUyi9RpmJrl diff --git a/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs b/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs index b80967113..7dc9f309c 100644 --- a/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs +++ b/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs @@ -211,8 +211,11 @@ public void SaveTo(TextWriter writer) writer.WriteLine("MACs " + string.Join(",", MessageAuthenticationCodeAlgorithms.Select(c => c.Name).ToArray())); } - writer.WriteLine("PubkeyAcceptedAlgorithms " + string.Join(",", PublicKeyAcceptedAlgorithms.Select(c => c.Name).ToArray())); - + if (PublicKeyAcceptedAlgorithms.Count > 0) + { + writer.WriteLine("PubkeyAcceptedAlgorithms " + string.Join(",", PublicKeyAcceptedAlgorithms.Select(c => c.Name).ToArray())); + } + foreach (var match in Matches) { _matchFormatter.Format(match, writer); diff --git a/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs b/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs index 516e3623f..e7ee53b90 100644 --- a/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Common/HostKeyEventArgsTest.cs @@ -42,7 +42,7 @@ public void HostKeyEventArgsConstructorTest() 0xa0, 0x23, 0xaf, 0xff, 0x9c, 0x0f, 0x8c, 0x83, 0x7c, 0xf8, 0xe1, 0x8e, 0x32, 0x8e, 0x61, 0xfc, 0x5b, 0xbd, 0xd4, 0x46, 0xe1 }.SequenceEqual(target.HostKey)); - Assert.AreEqual("ssh-rsa", target.HostKeyName); + Assert.AreEqual("rsa-sha2-512", target.HostKeyName); Assert.AreEqual(2048, target.KeyLength); } diff --git a/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs b/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs index 4ca6e9544..c684f0be7 100644 --- a/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs +++ b/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs @@ -1,8 +1,11 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Common; +using Renci.SshNet.Security; using Renci.SshNet.Tests.Common; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; namespace Renci.SshNet.Tests.Classes { @@ -144,7 +147,7 @@ public void Test_PrivateKey_RSA() { using (var stream = GetData("Key.RSA.txt")) { - _ = new PrivateKeyFile(stream); + TestRsaKeyFile(new PrivateKeyFile(stream)); } } @@ -166,7 +169,7 @@ public void Test_PrivateKey_SSH2_RSA() { using (var stream = GetData("Key.SSH2.RSA.txt")) { - _ = new PrivateKeyFile(stream); + TestRsaKeyFile(new PrivateKeyFile(stream)); } } @@ -188,7 +191,7 @@ public void Test_PrivateKey_SSH2_Encrypted_RSA_DES_CBC() { using (var stream = GetData("Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt")) { - _ = new PrivateKeyFile(stream, "12345"); + TestRsaKeyFile(new PrivateKeyFile(stream, "12345")); } } @@ -262,7 +265,7 @@ public void Test_PrivateKey_RSA_DES_CBC() { using (var stream = GetData("Key.RSA.Encrypted.Des.CBC.12345.txt")) { - _ = new PrivateKeyFile(stream, "12345"); + TestRsaKeyFile(new PrivateKeyFile(stream, "12345")); } } @@ -284,7 +287,7 @@ public void Test_PrivateKey_RSA_AES_128_CBC() { using (var stream = GetData("Key.RSA.Encrypted.Aes.128.CBC.12345.txt")) { - _ = new PrivateKeyFile(stream, "12345"); + TestRsaKeyFile(new PrivateKeyFile(stream, "12345")); } } @@ -295,7 +298,7 @@ public void Test_PrivateKey_RSA_AES_192_CBC() { using (var stream = GetData("Key.RSA.Encrypted.Aes.192.CBC.12345.txt")) { - _ = new PrivateKeyFile(stream, "12345"); + TestRsaKeyFile(new PrivateKeyFile(stream, "12345")); } } @@ -306,7 +309,7 @@ public void Test_PrivateKey_RSA_AES_256_CBC() { using (var stream = GetData("Key.RSA.Encrypted.Aes.256.CBC.12345.txt")) { - _ = new PrivateKeyFile(stream, "12345"); + TestRsaKeyFile(new PrivateKeyFile(stream, "12345")); } } @@ -317,7 +320,7 @@ public void Test_PrivateKey_RSA_DES_EDE3_CFB() { using (var stream = GetData("Key.RSA.Encrypted.Des.Ede3.CFB.1234567890.txt")) { - _ = new PrivateKeyFile(stream, "1234567890"); + TestRsaKeyFile(new PrivateKeyFile(stream, "1234567890")); } } @@ -576,7 +579,7 @@ public void Test_PrivateKey_OPENSSH_RSA() { using (var stream = GetData("Key.OPENSSH.RSA.txt")) { - _ = new PrivateKeyFile(stream); + TestRsaKeyFile(new PrivateKeyFile(stream)); } } @@ -587,7 +590,7 @@ public void Test_PrivateKey_OPENSSH_RSA_ENCRYPTED() { using (var stream = GetData("Key.OPENSSH.RSA.Encrypted.txt")) { - _ = new PrivateKeyFile(stream, "12345"); + TestRsaKeyFile(new PrivateKeyFile(stream, "12345")); } } @@ -678,5 +681,18 @@ private string GetTempFileName() File.Delete(tempFile); return tempFile; } + + private static void TestRsaKeyFile(PrivateKeyFile rsaPrivateKeyFile) + { + Assert.AreEqual(3, rsaPrivateKeyFile.HostAlgorithms.Count); + + List algorithms = rsaPrivateKeyFile.HostAlgorithms.Cast().ToList(); + + Assert.AreEqual("rsa-sha2-512", algorithms[0].Name); + Assert.AreEqual("rsa-sha2-256", algorithms[1].Name); + Assert.AreEqual("ssh-rsa", algorithms[2].Name); + + Assert.AreSame(algorithms[0], rsaPrivateKeyFile.HostKey); + } } } diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/RsaDigitalSignatureTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/RsaDigitalSignatureTest.cs index 93a76bbee..0c5c7eb02 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/RsaDigitalSignatureTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/RsaDigitalSignatureTest.cs @@ -1,4 +1,9 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Security.Cryptography; +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + using Renci.SshNet.Security; using Renci.SshNet.Security.Cryptography; using Renci.SshNet.Tests.Common; @@ -11,17 +16,156 @@ namespace Renci.SshNet.Tests.Classes.Security.Cryptography [TestClass] public class RsaDigitalSignatureTest : TestBase { + [TestMethod] + public void Sha1_SignAndVerify() + { + byte[] data = Encoding.UTF8.GetBytes("hello world"); + + RsaKey rsaKey = GetRsaKey(); + + var digitalSignature = new RsaDigitalSignature(rsaKey); // Verify SHA-1 is the default + + byte[] signedBytes = digitalSignature.Sign(data); + + byte[] expectedSignedBytes = new byte[] + { + // echo -n 'hello world' | openssl dgst -sha1 -sign Key.RSA.txt -out test.signed + 0x41, 0x50, 0x12, 0x14, 0xd3, 0x7c, 0xe0, 0x40, 0x50, 0x65, 0xfb, 0x33, 0xd9, 0x17, 0x89, 0xbf, + 0xb2, 0x4b, 0x85, 0x15, 0xbf, 0x9e, 0x57, 0x3b, 0x01, 0x15, 0x2b, 0x99, 0xfa, 0x62, 0x9b, 0x2a, + 0x05, 0xa0, 0x73, 0xc7, 0xb7, 0x5b, 0xd9, 0x01, 0xaa, 0x56, 0x73, 0x95, 0x13, 0x41, 0x33, 0x0d, + 0x7f, 0x83, 0x8a, 0x60, 0x4d, 0x19, 0xdc, 0x9b, 0xba, 0x8e, 0x61, 0xed, 0xd0, 0x8a, 0x3e, 0x38, + 0x71, 0xee, 0x34, 0xc3, 0x55, 0x0f, 0x55, 0x65, 0x89, 0xbb, 0x3e, 0x41, 0xee, 0xdf, 0xf5, 0x2f, + 0xab, 0x9e, 0x89, 0x37, 0x68, 0x1f, 0x9f, 0x38, 0x00, 0x81, 0x29, 0x93, 0xeb, 0x61, 0x37, 0xad, + 0x8d, 0x35, 0xf1, 0x3d, 0x4b, 0x9b, 0x99, 0x74, 0x7b, 0xeb, 0xf4, 0xfb, 0x76, 0xb4, 0xb6, 0xb4, + 0x09, 0x33, 0x5c, 0xfa, 0x6a, 0xad, 0x1e, 0xed, 0x1c, 0xe1, 0xb4, 0x4d, 0xf2, 0xa5, 0xc3, 0x64, + 0x9a, 0x45, 0x81, 0xee, 0x1b, 0xa6, 0x1d, 0x01, 0x3c, 0x4d, 0xb5, 0x62, 0x9e, 0xff, 0x8e, 0xff, + 0x6c, 0x18, 0xed, 0xe9, 0x8e, 0x03, 0x2c, 0xc5, 0x94, 0x81, 0xca, 0x8b, 0x18, 0x3f, 0x25, 0xcd, + 0xe5, 0x42, 0x49, 0x43, 0x23, 0x1f, 0xdc, 0x3f, 0xa2, 0x43, 0xbc, 0xbd, 0x42, 0xf5, 0x60, 0xfb, + 0x01, 0xd3, 0x67, 0x0d, 0x8d, 0x85, 0x7b, 0x51, 0x14, 0xec, 0x26, 0x53, 0x00, 0x61, 0x25, 0x16, + 0x19, 0x10, 0x3c, 0x86, 0x16, 0x59, 0x84, 0x08, 0xd1, 0xf9, 0x1e, 0x05, 0x88, 0xbd, 0x4a, 0x01, + 0x43, 0x4e, 0xec, 0x76, 0x0b, 0xd7, 0x2c, 0xe9, 0x98, 0xb1, 0x4c, 0x0a, 0x13, 0xc6, 0x95, 0xf9, + 0x8f, 0x95, 0x5c, 0x98, 0x4c, 0x8f, 0x97, 0x4a, 0xad, 0x0d, 0xfe, 0x84, 0xf0, 0x56, 0xc3, 0x29, + 0x73, 0x75, 0x55, 0x3c, 0xd9, 0x5e, 0x5b, 0x6f, 0xf9, 0x81, 0xbc, 0xbc, 0x50, 0x75, 0x7d, 0xa8 + }; + + CollectionAssert.AreEqual(expectedSignedBytes, signedBytes); + + // Also verify RsaKey uses SHA-1 by default + CollectionAssert.AreEqual(expectedSignedBytes, rsaKey.Sign(data)); + + // The following fails due to the _isPrivate decision in RsaCipher.Transform. Is that really correct? + //Assert.IsTrue(digitalSignature.Verify(data, signedBytes)); + + // 'Workaround': use a key with no private key information + var digitalSignaturePublic = new RsaDigitalSignature(new RsaKey() + { + Public = rsaKey.Public + }); + Assert.IsTrue(digitalSignaturePublic.Verify(data, signedBytes)); + } - /// - ///A test for RsaDigitalSignature Constructor - /// [TestMethod] - [Ignore] // placeholder for actual test - public void RsaDigitalSignatureConstructorTest() + public void Sha256_SignAndVerify() { - RsaKey rsaKey = null; // TODO: Initialize to an appropriate value - RsaDigitalSignature target = new RsaDigitalSignature(rsaKey); - Assert.Inconclusive("TODO: Implement code to verify target"); + byte[] data = Encoding.UTF8.GetBytes("hello world"); + + RsaKey rsaKey = GetRsaKey(); + + var digitalSignature = new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256); + + byte[] signedBytes = digitalSignature.Sign(data); + + CollectionAssert.AreEqual(new byte[] + { + // echo -n 'hello world' | openssl dgst -sha256 -sign Key.RSA.txt -out test.signed + 0x2e, 0xef, 0x01, 0x49, 0x5c, 0x66, 0x37, 0x56, 0xc2, 0xfb, 0x7b, 0xfa, 0x80, 0x2f, 0xdb, 0xaa, + 0x0d, 0x15, 0xd9, 0x8d, 0xa9, 0xad, 0x81, 0x4f, 0x09, 0x2e, 0x53, 0x9e, 0xce, 0x5d, 0x68, 0x07, + 0xae, 0xb9, 0xc0, 0x45, 0xfa, 0x30, 0xd0, 0xf7, 0xd6, 0xa6, 0x8d, 0x19, 0x24, 0x3a, 0xea, 0x91, + 0x3e, 0xa2, 0x4a, 0x42, 0x2e, 0x21, 0xf1, 0x48, 0x57, 0xca, 0x2b, 0x6c, 0x9f, 0x79, 0x54, 0x91, + 0x3e, 0x3a, 0x4d, 0xd1, 0x70, 0x87, 0x3d, 0xbe, 0x22, 0x97, 0xc9, 0xb0, 0x02, 0xf0, 0xa2, 0xae, + 0x7a, 0xbb, 0x8b, 0xaf, 0xc0, 0x3b, 0xab, 0x71, 0xe8, 0x29, 0x1c, 0x18, 0x88, 0xca, 0x74, 0x1b, + 0x34, 0x4f, 0xd1, 0x83, 0x39, 0x6e, 0x8f, 0x69, 0x3d, 0x7e, 0xef, 0xef, 0x57, 0x7c, 0xff, 0x21, + 0x9c, 0x10, 0x2b, 0xd1, 0x4f, 0x26, 0xbe, 0xaa, 0xd2, 0xd9, 0x03, 0x14, 0x75, 0x97, 0x11, 0xaf, + 0xf0, 0x28, 0xf2, 0xd3, 0x07, 0x79, 0x5b, 0x27, 0xdc, 0x97, 0xd8, 0xce, 0x4e, 0x78, 0x89, 0x16, + 0x91, 0x2a, 0xb2, 0x47, 0x53, 0x94, 0xe9, 0xa1, 0x15, 0x98, 0x29, 0x0c, 0xa1, 0xf5, 0xe2, 0x8e, + 0x11, 0xdc, 0x0c, 0x1c, 0x10, 0xa4, 0xf2, 0x46, 0x5c, 0x78, 0x0c, 0xc1, 0x4a, 0x65, 0x21, 0x8a, + 0x2e, 0x32, 0x6c, 0x72, 0x06, 0xf9, 0x7f, 0xa1, 0x6c, 0x2e, 0x13, 0x06, 0x41, 0xaa, 0x23, 0xdd, + 0xc8, 0x1c, 0x61, 0xb6, 0x96, 0x87, 0xc4, 0x84, 0xc8, 0x61, 0xec, 0x4e, 0xdd, 0x49, 0x9e, 0x4f, + 0x0d, 0x8c, 0xf1, 0x7f, 0xf2, 0x6c, 0x73, 0x5a, 0xa6, 0x3b, 0xbf, 0x4e, 0xba, 0x57, 0x6b, 0xb3, + 0x1e, 0x6c, 0x57, 0x76, 0x87, 0x9f, 0xb4, 0x3b, 0xcb, 0xcd, 0xe5, 0x10, 0x7a, 0x4c, 0xeb, 0xc0, + 0xc4, 0xc3, 0x75, 0x51, 0x5f, 0xb7, 0x7c, 0xbc, 0x55, 0x8d, 0x05, 0xc7, 0xed, 0xc7, 0x52, 0x4a + }, signedBytes); + + + // The following fails due to the _isPrivate decision in RsaCipher.Transform. Is that really correct? + //Assert.IsTrue(digitalSignature.Verify(data, signedBytes)); + + // 'Workaround': use a key with no private key information + var digitalSignaturePublic = new RsaDigitalSignature(new RsaKey() + { + Public = rsaKey.Public + }, HashAlgorithmName.SHA256); + Assert.IsTrue(digitalSignaturePublic.Verify(data, signedBytes)); + } + + [TestMethod] + public void Sha512_SignAndVerify() + { + byte[] data = Encoding.UTF8.GetBytes("hello world"); + + RsaKey rsaKey = GetRsaKey(); + + var digitalSignature = new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512); + + byte[] signedBytes = digitalSignature.Sign(data); + + CollectionAssert.AreEqual(new byte[] + { + // echo -n 'hello world' | openssl dgst -sha512 -sign Key.RSA.txt -out test.signed + 0x69, 0x70, 0xb5, 0x9f, 0x32, 0x86, 0x3b, 0xae, 0xc0, 0x79, 0x6e, 0xdb, 0x35, 0xd5, 0xa6, 0x22, + 0xcd, 0x2b, 0x4b, 0xd2, 0x68, 0x1a, 0x65, 0x41, 0xa6, 0xd9, 0x20, 0x54, 0x31, 0x9a, 0xb1, 0x44, + 0x6e, 0x8f, 0x56, 0x4b, 0xfc, 0x27, 0x7f, 0x3f, 0xe7, 0x47, 0xcb, 0x78, 0x03, 0x05, 0x79, 0x8a, + 0x16, 0x7b, 0x12, 0x01, 0x3a, 0xa2, 0xd5, 0x0d, 0x2b, 0x16, 0x38, 0xef, 0x84, 0x6b, 0xd7, 0x19, + 0xeb, 0xac, 0x54, 0x01, 0x9d, 0xa6, 0x80, 0x74, 0x43, 0xa8, 0x6e, 0x5e, 0x33, 0x05, 0x06, 0x1d, + 0x6d, 0xfe, 0x32, 0x4f, 0xe3, 0xcb, 0x3e, 0x2d, 0x4e, 0xe1, 0x47, 0x03, 0x69, 0xb4, 0x59, 0x80, + 0x59, 0x05, 0x15, 0xa0, 0x11, 0x34, 0x47, 0x58, 0xd7, 0x93, 0x2d, 0x40, 0xf2, 0x2c, 0x37, 0x48, + 0x6b, 0x3c, 0xd3, 0x03, 0x09, 0x32, 0x74, 0xa0, 0x2d, 0x33, 0x11, 0x99, 0x10, 0xb4, 0x09, 0x31, + 0xec, 0xa3, 0x2c, 0x63, 0xba, 0x50, 0xd1, 0x02, 0x45, 0xae, 0xb5, 0x75, 0x7e, 0xfa, 0xfc, 0x06, + 0xb6, 0x6a, 0xb2, 0xa1, 0x73, 0x14, 0xa5, 0xaa, 0x17, 0x88, 0x03, 0x19, 0x14, 0x9b, 0xe1, 0x10, + 0xf8, 0x2f, 0x73, 0x01, 0xc7, 0x8d, 0x37, 0xef, 0x98, 0x69, 0xc2, 0xe2, 0x7a, 0x11, 0xd5, 0xb8, + 0xc9, 0x35, 0x45, 0xcb, 0x56, 0x4b, 0x92, 0x4a, 0xe0, 0x4c, 0xd6, 0x82, 0xae, 0xad, 0x5b, 0xe9, + 0x40, 0x7e, 0x2a, 0x48, 0x7d, 0x57, 0xc5, 0xfd, 0xe9, 0x98, 0xe0, 0xbb, 0x09, 0xa1, 0xf5, 0x48, + 0x45, 0xcb, 0xee, 0xb9, 0x99, 0x81, 0x44, 0x15, 0x2e, 0x50, 0x39, 0x64, 0x58, 0x4c, 0x34, 0x86, + 0xf8, 0x81, 0x9e, 0x1d, 0xb6, 0x97, 0xe0, 0xce, 0x16, 0xca, 0x20, 0x46, 0xe9, 0x49, 0x8f, 0xe6, + 0xa0, 0x23, 0x08, 0x80, 0xa6, 0x37, 0x70, 0x06, 0xcc, 0x8f, 0xf4, 0xa0, 0x74, 0x53, 0x26, 0x38 + }, signedBytes); + + // The following fails due to the _isPrivate decision in RsaCipher.Transform. Is that really correct? + //Assert.IsTrue(digitalSignature.Verify(data, signedBytes)); + + // 'Workaround': use a key with no private key information + var digitalSignaturePublic = new RsaDigitalSignature(new RsaKey() + { + Public = rsaKey.Public + }, HashAlgorithmName.SHA512); + Assert.IsTrue(digitalSignaturePublic.Verify(data, signedBytes)); + } + + [TestMethod] + public void Constructor_InvalidHashAlgorithm_ThrowsArgumentException() + { + ArgumentException exception = Assert.ThrowsException( + () => new RsaDigitalSignature(new RsaKey(), new HashAlgorithmName("invalid"))); + + Assert.AreEqual("hashAlgorithmName", exception.ParamName); + } + + private static RsaKey GetRsaKey() + { + using (var stream = GetData("Key.RSA.txt")) + { + return (RsaKey) ((KeyHostAlgorithm) new PrivateKeyFile(stream).HostKey).Key; + } } /// @@ -37,4 +181,4 @@ public void DisposeTest() Assert.Inconclusive("A method that does not return a value cannot be verified."); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/Security/KeyAlgorithmTest.cs b/src/Renci.SshNet.Tests/Classes/Security/KeyAlgorithmTest.cs new file mode 100644 index 000000000..6f92362b4 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Security/KeyAlgorithmTest.cs @@ -0,0 +1,215 @@ +using System.Security.Cryptography; +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Renci.SshNet.Security; +using Renci.SshNet.Security.Cryptography; +using Renci.SshNet.Tests.Common; + +namespace Renci.SshNet.Tests.Classes.Security +{ + [TestClass] + public class KeyHostAlgorithmTest : TestBase + { + [TestMethod] + public void NoSuppliedDigitalSignature_PropertyIsKeyDigitalSignature() + { + RsaKey rsaKey = GetRsaKey(); + + KeyHostAlgorithm keyHostAlgorithm = new KeyHostAlgorithm("ssh-rsa", rsaKey); + + Assert.AreEqual("ssh-rsa", keyHostAlgorithm.Name); + Assert.AreSame(rsaKey, keyHostAlgorithm.Key); + Assert.AreSame(rsaKey.DigitalSignature, keyHostAlgorithm.DigitalSignature); + } + + [TestMethod] + public void SuppliedDigitalSignature_PropertyIsSuppliedDigitalSignature() + { + RsaKey rsaKey = GetRsaKey(); + RsaDigitalSignature rsaDigitalSignature = new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256); + KeyHostAlgorithm keyHostAlgorithm = new KeyHostAlgorithm("rsa-sha2-256", rsaKey, rsaDigitalSignature); + + Assert.AreEqual("rsa-sha2-256", keyHostAlgorithm.Name); + Assert.AreSame(rsaKey, keyHostAlgorithm.Key); + Assert.AreSame(rsaDigitalSignature, keyHostAlgorithm.DigitalSignature); + } + + [TestMethod] + public void RsaPublicKeyDataDoesNotDependOnSignatureAlgorithm() + { + TestRsaPublicKeyData("ssh-rsa", HashAlgorithmName.SHA1); + TestRsaPublicKeyData("rsa-sha2-256", HashAlgorithmName.SHA256); + } + + private void TestRsaPublicKeyData(string signatureIdentifier, HashAlgorithmName hashAlgorithmName) + { + RsaKey key = GetRsaKey(); + KeyHostAlgorithm keyAlgorithm = new KeyHostAlgorithm(signatureIdentifier, key, new RsaDigitalSignature(key, hashAlgorithmName)); + + CollectionAssert.AreEqual(GetRsaPublicKeyBytes(), keyAlgorithm.Data); + } + + [TestMethod] + public void SshRsa_SignAndVerify() + { + byte[] data = Encoding.UTF8.GetBytes("hello world"); + + RsaKey key = GetRsaKey(); + KeyHostAlgorithm keyAlgorithm = new KeyHostAlgorithm("ssh-rsa", key); + + byte[] expectedEncodedSignatureBytes = new byte[] + { + 0, 0, 0, 7, // byte count of "ssh-rsa" + (byte)'s', (byte)'s', (byte)'h', (byte)'-', (byte)'r', (byte)'s', (byte)'a', // ssh-rsa + 0, 0, 1, 0, // byte count of signature (=256) + + // echo -n 'hello world' | openssl dgst -sha1 -sign Key.RSA.txt -out test.signed + 0x41, 0x50, 0x12, 0x14, 0xd3, 0x7c, 0xe0, 0x40, 0x50, 0x65, 0xfb, 0x33, 0xd9, 0x17, 0x89, 0xbf, + 0xb2, 0x4b, 0x85, 0x15, 0xbf, 0x9e, 0x57, 0x3b, 0x01, 0x15, 0x2b, 0x99, 0xfa, 0x62, 0x9b, 0x2a, + 0x05, 0xa0, 0x73, 0xc7, 0xb7, 0x5b, 0xd9, 0x01, 0xaa, 0x56, 0x73, 0x95, 0x13, 0x41, 0x33, 0x0d, + 0x7f, 0x83, 0x8a, 0x60, 0x4d, 0x19, 0xdc, 0x9b, 0xba, 0x8e, 0x61, 0xed, 0xd0, 0x8a, 0x3e, 0x38, + 0x71, 0xee, 0x34, 0xc3, 0x55, 0x0f, 0x55, 0x65, 0x89, 0xbb, 0x3e, 0x41, 0xee, 0xdf, 0xf5, 0x2f, + 0xab, 0x9e, 0x89, 0x37, 0x68, 0x1f, 0x9f, 0x38, 0x00, 0x81, 0x29, 0x93, 0xeb, 0x61, 0x37, 0xad, + 0x8d, 0x35, 0xf1, 0x3d, 0x4b, 0x9b, 0x99, 0x74, 0x7b, 0xeb, 0xf4, 0xfb, 0x76, 0xb4, 0xb6, 0xb4, + 0x09, 0x33, 0x5c, 0xfa, 0x6a, 0xad, 0x1e, 0xed, 0x1c, 0xe1, 0xb4, 0x4d, 0xf2, 0xa5, 0xc3, 0x64, + 0x9a, 0x45, 0x81, 0xee, 0x1b, 0xa6, 0x1d, 0x01, 0x3c, 0x4d, 0xb5, 0x62, 0x9e, 0xff, 0x8e, 0xff, + 0x6c, 0x18, 0xed, 0xe9, 0x8e, 0x03, 0x2c, 0xc5, 0x94, 0x81, 0xca, 0x8b, 0x18, 0x3f, 0x25, 0xcd, + 0xe5, 0x42, 0x49, 0x43, 0x23, 0x1f, 0xdc, 0x3f, 0xa2, 0x43, 0xbc, 0xbd, 0x42, 0xf5, 0x60, 0xfb, + 0x01, 0xd3, 0x67, 0x0d, 0x8d, 0x85, 0x7b, 0x51, 0x14, 0xec, 0x26, 0x53, 0x00, 0x61, 0x25, 0x16, + 0x19, 0x10, 0x3c, 0x86, 0x16, 0x59, 0x84, 0x08, 0xd1, 0xf9, 0x1e, 0x05, 0x88, 0xbd, 0x4a, 0x01, + 0x43, 0x4e, 0xec, 0x76, 0x0b, 0xd7, 0x2c, 0xe9, 0x98, 0xb1, 0x4c, 0x0a, 0x13, 0xc6, 0x95, 0xf9, + 0x8f, 0x95, 0x5c, 0x98, 0x4c, 0x8f, 0x97, 0x4a, 0xad, 0x0d, 0xfe, 0x84, 0xf0, 0x56, 0xc3, 0x29, + 0x73, 0x75, 0x55, 0x3c, 0xd9, 0x5e, 0x5b, 0x6f, 0xf9, 0x81, 0xbc, 0xbc, 0x50, 0x75, 0x7d, 0xa8 + }; + + CollectionAssert.AreEqual(expectedEncodedSignatureBytes, keyAlgorithm.Sign(data)); + + keyAlgorithm = new KeyHostAlgorithm("ssh-rsa", new RsaKey(), GetRsaPublicKeyBytes()); + Assert.IsTrue(keyAlgorithm.VerifySignature(data, expectedEncodedSignatureBytes)); + } + + [TestMethod] + public void RsaSha256_SignAndVerify() + { + byte[] data = Encoding.UTF8.GetBytes("hello world"); + + RsaKey key = GetRsaKey(); + KeyHostAlgorithm keyAlgorithm = new KeyHostAlgorithm("rsa-sha2-256", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); + + byte[] expectedEncodedSignatureBytes = new byte[] + { + 0, 0, 0, 12, // byte count of "rsa-sha2-256" + (byte)'r', (byte)'s', (byte)'a', (byte)'-', (byte)'s', (byte)'h', (byte)'a', (byte)'2', + (byte)'-', (byte)'2', (byte)'5', (byte)'6', + 0, 0, 1, 0, // byte count of signature (=256) + + // echo -n 'hello world' | openssl dgst -sha256 -sign Key.RSA.txt -out test.signed + 0x2e, 0xef, 0x01, 0x49, 0x5c, 0x66, 0x37, 0x56, 0xc2, 0xfb, 0x7b, 0xfa, 0x80, 0x2f, 0xdb, 0xaa, + 0x0d, 0x15, 0xd9, 0x8d, 0xa9, 0xad, 0x81, 0x4f, 0x09, 0x2e, 0x53, 0x9e, 0xce, 0x5d, 0x68, 0x07, + 0xae, 0xb9, 0xc0, 0x45, 0xfa, 0x30, 0xd0, 0xf7, 0xd6, 0xa6, 0x8d, 0x19, 0x24, 0x3a, 0xea, 0x91, + 0x3e, 0xa2, 0x4a, 0x42, 0x2e, 0x21, 0xf1, 0x48, 0x57, 0xca, 0x2b, 0x6c, 0x9f, 0x79, 0x54, 0x91, + 0x3e, 0x3a, 0x4d, 0xd1, 0x70, 0x87, 0x3d, 0xbe, 0x22, 0x97, 0xc9, 0xb0, 0x02, 0xf0, 0xa2, 0xae, + 0x7a, 0xbb, 0x8b, 0xaf, 0xc0, 0x3b, 0xab, 0x71, 0xe8, 0x29, 0x1c, 0x18, 0x88, 0xca, 0x74, 0x1b, + 0x34, 0x4f, 0xd1, 0x83, 0x39, 0x6e, 0x8f, 0x69, 0x3d, 0x7e, 0xef, 0xef, 0x57, 0x7c, 0xff, 0x21, + 0x9c, 0x10, 0x2b, 0xd1, 0x4f, 0x26, 0xbe, 0xaa, 0xd2, 0xd9, 0x03, 0x14, 0x75, 0x97, 0x11, 0xaf, + 0xf0, 0x28, 0xf2, 0xd3, 0x07, 0x79, 0x5b, 0x27, 0xdc, 0x97, 0xd8, 0xce, 0x4e, 0x78, 0x89, 0x16, + 0x91, 0x2a, 0xb2, 0x47, 0x53, 0x94, 0xe9, 0xa1, 0x15, 0x98, 0x29, 0x0c, 0xa1, 0xf5, 0xe2, 0x8e, + 0x11, 0xdc, 0x0c, 0x1c, 0x10, 0xa4, 0xf2, 0x46, 0x5c, 0x78, 0x0c, 0xc1, 0x4a, 0x65, 0x21, 0x8a, + 0x2e, 0x32, 0x6c, 0x72, 0x06, 0xf9, 0x7f, 0xa1, 0x6c, 0x2e, 0x13, 0x06, 0x41, 0xaa, 0x23, 0xdd, + 0xc8, 0x1c, 0x61, 0xb6, 0x96, 0x87, 0xc4, 0x84, 0xc8, 0x61, 0xec, 0x4e, 0xdd, 0x49, 0x9e, 0x4f, + 0x0d, 0x8c, 0xf1, 0x7f, 0xf2, 0x6c, 0x73, 0x5a, 0xa6, 0x3b, 0xbf, 0x4e, 0xba, 0x57, 0x6b, 0xb3, + 0x1e, 0x6c, 0x57, 0x76, 0x87, 0x9f, 0xb4, 0x3b, 0xcb, 0xcd, 0xe5, 0x10, 0x7a, 0x4c, 0xeb, 0xc0, + 0xc4, 0xc3, 0x75, 0x51, 0x5f, 0xb7, 0x7c, 0xbc, 0x55, 0x8d, 0x05, 0xc7, 0xed, 0xc7, 0x52, 0x4a + }; + + CollectionAssert.AreEqual(expectedEncodedSignatureBytes, keyAlgorithm.Sign(data)); + + key = new RsaKey(); + keyAlgorithm = new KeyHostAlgorithm("rsa-sha2-256", key, GetRsaPublicKeyBytes(), new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); + Assert.IsTrue(keyAlgorithm.VerifySignature(data, expectedEncodedSignatureBytes)); + } + + [TestMethod] + public void RsaSha512_SignAndVerify() + { + byte[] data = Encoding.UTF8.GetBytes("hello world"); + + RsaKey key = GetRsaKey(); + KeyHostAlgorithm keyAlgorithm = new KeyHostAlgorithm("rsa-sha2-512", key, new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); + + byte[] expectedEncodedSignatureBytes = new byte[] + { + 0, 0, 0, 12, // byte count of "rsa-sha2-512" + (byte)'r', (byte)'s', (byte)'a', (byte)'-', (byte)'s', (byte)'h', (byte)'a', (byte)'2', + (byte)'-', (byte)'5', (byte)'1', (byte)'2', + 0, 0, 1, 0, // byte count of signature (=256) + + // echo -n 'hello world' | openssl dgst -sha512 -sign Key.RSA.txt -out test.signed + 0x69, 0x70, 0xb5, 0x9f, 0x32, 0x86, 0x3b, 0xae, 0xc0, 0x79, 0x6e, 0xdb, 0x35, 0xd5, 0xa6, 0x22, + 0xcd, 0x2b, 0x4b, 0xd2, 0x68, 0x1a, 0x65, 0x41, 0xa6, 0xd9, 0x20, 0x54, 0x31, 0x9a, 0xb1, 0x44, + 0x6e, 0x8f, 0x56, 0x4b, 0xfc, 0x27, 0x7f, 0x3f, 0xe7, 0x47, 0xcb, 0x78, 0x03, 0x05, 0x79, 0x8a, + 0x16, 0x7b, 0x12, 0x01, 0x3a, 0xa2, 0xd5, 0x0d, 0x2b, 0x16, 0x38, 0xef, 0x84, 0x6b, 0xd7, 0x19, + 0xeb, 0xac, 0x54, 0x01, 0x9d, 0xa6, 0x80, 0x74, 0x43, 0xa8, 0x6e, 0x5e, 0x33, 0x05, 0x06, 0x1d, + 0x6d, 0xfe, 0x32, 0x4f, 0xe3, 0xcb, 0x3e, 0x2d, 0x4e, 0xe1, 0x47, 0x03, 0x69, 0xb4, 0x59, 0x80, + 0x59, 0x05, 0x15, 0xa0, 0x11, 0x34, 0x47, 0x58, 0xd7, 0x93, 0x2d, 0x40, 0xf2, 0x2c, 0x37, 0x48, + 0x6b, 0x3c, 0xd3, 0x03, 0x09, 0x32, 0x74, 0xa0, 0x2d, 0x33, 0x11, 0x99, 0x10, 0xb4, 0x09, 0x31, + 0xec, 0xa3, 0x2c, 0x63, 0xba, 0x50, 0xd1, 0x02, 0x45, 0xae, 0xb5, 0x75, 0x7e, 0xfa, 0xfc, 0x06, + 0xb6, 0x6a, 0xb2, 0xa1, 0x73, 0x14, 0xa5, 0xaa, 0x17, 0x88, 0x03, 0x19, 0x14, 0x9b, 0xe1, 0x10, + 0xf8, 0x2f, 0x73, 0x01, 0xc7, 0x8d, 0x37, 0xef, 0x98, 0x69, 0xc2, 0xe2, 0x7a, 0x11, 0xd5, 0xb8, + 0xc9, 0x35, 0x45, 0xcb, 0x56, 0x4b, 0x92, 0x4a, 0xe0, 0x4c, 0xd6, 0x82, 0xae, 0xad, 0x5b, 0xe9, + 0x40, 0x7e, 0x2a, 0x48, 0x7d, 0x57, 0xc5, 0xfd, 0xe9, 0x98, 0xe0, 0xbb, 0x09, 0xa1, 0xf5, 0x48, + 0x45, 0xcb, 0xee, 0xb9, 0x99, 0x81, 0x44, 0x15, 0x2e, 0x50, 0x39, 0x64, 0x58, 0x4c, 0x34, 0x86, + 0xf8, 0x81, 0x9e, 0x1d, 0xb6, 0x97, 0xe0, 0xce, 0x16, 0xca, 0x20, 0x46, 0xe9, 0x49, 0x8f, 0xe6, + 0xa0, 0x23, 0x08, 0x80, 0xa6, 0x37, 0x70, 0x06, 0xcc, 0x8f, 0xf4, 0xa0, 0x74, 0x53, 0x26, 0x38 + }; + + CollectionAssert.AreEqual(expectedEncodedSignatureBytes, keyAlgorithm.Sign(data)); + + key = new RsaKey(); + keyAlgorithm = new KeyHostAlgorithm("rsa-sha2-512", key, GetRsaPublicKeyBytes(), new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); + Assert.IsTrue(keyAlgorithm.VerifySignature(data, expectedEncodedSignatureBytes)); + } + + private static RsaKey GetRsaKey() + { + using (var stream = GetData("Key.RSA.txt")) + { + return (RsaKey) ((KeyHostAlgorithm) new PrivateKeyFile(stream).HostKey).Key; + } + } + + private static byte[] GetRsaPublicKeyBytes() + { + return new byte[] + { + 0, 0, 0, 7, // byte count of "ssh-rsa" + (byte)'s', (byte)'s', (byte)'h', (byte)'-', (byte)'r', (byte)'s', (byte)'a', // ssh-rsa + 0, 0, 0, 1, // byte count of exponent + 35, // exponent + 0, 0, 1, 1, // byte count of modulus (=257) + + // openssl rsa -in Key.RSA.txt -text + 0x00, 0xb9, 0x3b, 0x57, 0x9f, 0xe0, 0x5a, 0xb5, 0x7d, 0x68, 0x26, 0xeb, 0xe1, 0xa9, 0xf2, + 0x59, 0xc3, 0x98, 0xdc, 0xfe, 0x97, 0x08, 0xc4, 0x95, 0x0f, 0x9a, 0xea, 0x05, 0x08, 0x7d, + 0xfe, 0x6d, 0x77, 0xca, 0x04, 0x9f, 0xfd, 0xe2, 0x2c, 0x4d, 0x11, 0x3c, 0xd9, 0x05, 0xab, + 0x32, 0xbd, 0x3f, 0xe8, 0xcd, 0xba, 0x00, 0x6c, 0x21, 0xb7, 0xa9, 0xc2, 0x4e, 0x63, 0x17, + 0xf6, 0x04, 0x47, 0x93, 0x00, 0x85, 0xde, 0xd6, 0x32, 0xc0, 0xa1, 0x37, 0x75, 0x18, 0xa0, + 0xb0, 0x32, 0xf6, 0x4e, 0xca, 0x39, 0xec, 0x3c, 0xdf, 0x79, 0xfe, 0x50, 0xa1, 0xc1, 0xf7, + 0x67, 0x05, 0xb3, 0x33, 0xa5, 0x96, 0x13, 0x19, 0xfa, 0x14, 0xca, 0x55, 0xe6, 0x7b, 0xf9, + 0xb3, 0x8e, 0x32, 0xee, 0xfc, 0x9d, 0x2a, 0x5e, 0x04, 0x79, 0x97, 0x29, 0x3d, 0x1c, 0x54, + 0xfe, 0xc7, 0x96, 0x04, 0xb5, 0x19, 0x7c, 0x55, 0x21, 0xe2, 0x0e, 0x42, 0xca, 0x4d, 0x9d, + 0xfb, 0x77, 0x08, 0x6c, 0xaa, 0x07, 0x2c, 0xf8, 0xf9, 0x1f, 0xbd, 0x83, 0x14, 0x2b, 0xe0, + 0xbc, 0x7a, 0xf9, 0xdf, 0x13, 0x4b, 0x60, 0x5a, 0x02, 0x99, 0x93, 0x41, 0x1a, 0xb6, 0x5f, + 0x3b, 0x9c, 0xb5, 0xb2, 0x55, 0x70, 0x78, 0x2f, 0x38, 0x52, 0x0e, 0xd1, 0x8a, 0x2c, 0x23, + 0xc0, 0x3a, 0x0a, 0xd7, 0xed, 0xf6, 0x1f, 0xa6, 0x50, 0xf0, 0x27, 0x65, 0x8a, 0xd4, 0xde, + 0xa7, 0x1b, 0x41, 0x67, 0xc5, 0x6d, 0x47, 0x84, 0x37, 0x92, 0x2b, 0xb7, 0xb6, 0x4d, 0xb0, + 0x1a, 0xda, 0xf6, 0x50, 0x82, 0xf1, 0x57, 0x31, 0x69, 0xce, 0xe0, 0xef, 0xcd, 0x64, 0xaa, + 0x78, 0x08, 0xea, 0x4e, 0x45, 0xec, 0xa5, 0x89, 0x68, 0x5d, 0xb4, 0xa0, 0x23, 0xaf, 0xff, + 0x9c, 0x0f, 0x8c, 0x83, 0x7c, 0xf8, 0xe1, 0x8e, 0x32, 0x8e, 0x61, 0xfc, 0x5b, 0xbd, 0xd4, + 0x46, 0xe1 + }; + } + } +} diff --git a/src/Renci.SshNet.Tests/Common/TestBase.cs b/src/Renci.SshNet.Tests/Common/TestBase.cs index 4ae86fb41..5973aa4a3 100644 --- a/src/Renci.SshNet.Tests/Common/TestBase.cs +++ b/src/Renci.SshNet.Tests/Common/TestBase.cs @@ -49,9 +49,9 @@ protected void CreateTestFile(string fileName, int size) } } - protected Stream GetData(string name) + protected static Stream GetData(string name) { return ExecutingAssembly.GetManifestResourceStream(string.Format("Renci.SshNet.Tests.Data.{0}", name)); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet/Common/ObjectIdentifier.cs b/src/Renci.SshNet/Common/ObjectIdentifier.cs index f1b2d88df..a02a40908 100644 --- a/src/Renci.SshNet/Common/ObjectIdentifier.cs +++ b/src/Renci.SshNet/Common/ObjectIdentifier.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Security.Cryptography; namespace Renci.SshNet.Common { @@ -32,5 +34,19 @@ public ObjectIdentifier(params ulong[] identifiers) Identifiers = identifiers; } + + internal static ObjectIdentifier FromHashAlgorithmName(HashAlgorithmName hashAlgorithmName) + { + var oid = CryptoConfig.MapNameToOID(hashAlgorithmName.Name); + + if (oid is null) + { + throw new ArgumentException($"Could not map `{hashAlgorithmName}` to OID.", nameof(hashAlgorithmName)); + } + + var identifiers = oid.Split('.').Select(ulong.Parse).ToArray(); + + return new ObjectIdentifier(identifiers); + } } } diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs index e7bd6cdd2..a8089900a 100644 --- a/src/Renci.SshNet/ConnectionInfo.cs +++ b/src/Renci.SshNet/ConnectionInfo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Security.Cryptography; using System.Text; using Renci.SshNet.Abstractions; @@ -9,6 +10,7 @@ using Renci.SshNet.Messages.Authentication; using Renci.SshNet.Messages.Connection; using Renci.SshNet.Security; +using Renci.SshNet.Security.Cryptography; using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; @@ -396,6 +398,8 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy { "ecdsa-sha2-nistp256", data => new KeyHostAlgorithm("ecdsa-sha2-nistp256", new EcdsaKey(), data) }, { "ecdsa-sha2-nistp384", data => new KeyHostAlgorithm("ecdsa-sha2-nistp384", new EcdsaKey(), data) }, { "ecdsa-sha2-nistp521", data => new KeyHostAlgorithm("ecdsa-sha2-nistp521", new EcdsaKey(), data) }, + { "rsa-sha2-512", data => { var key = new RsaKey(); return new KeyHostAlgorithm("rsa-sha2-512", key, data, new RsaDigitalSignature(key, HashAlgorithmName.SHA512)); }}, + { "rsa-sha2-256", data => { var key = new RsaKey(); return new KeyHostAlgorithm("rsa-sha2-256", key, data, new RsaDigitalSignature(key, HashAlgorithmName.SHA256)); }}, { "ssh-rsa", data => new KeyHostAlgorithm("ssh-rsa", new RsaKey(), data) }, { "ssh-dss", data => new KeyHostAlgorithm("ssh-dss", new DsaKey(), data) }, }; diff --git a/src/Renci.SshNet/IHostAlgorithmsProvider.cs b/src/Renci.SshNet/IHostAlgorithmsProvider.cs new file mode 100644 index 000000000..89ac27128 --- /dev/null +++ b/src/Renci.SshNet/IHostAlgorithmsProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +using Renci.SshNet.Security; + +namespace Renci.SshNet +{ + /// + /// Represents a collection of host algorithms. + /// + public interface IHostAlgorithmsProvider + { + /// + /// The host algorithms provided by this . + /// + /// + /// In situations where there is a preferred order of usage of the host algorithms, + /// the collection should be ordered from most preferred to least. + /// + IReadOnlyCollection HostAlgorithms { get; } + } +} diff --git a/src/Renci.SshNet/IPrivateKeySource.cs b/src/Renci.SshNet/IPrivateKeySource.cs index fc3405462..466a36d2a 100644 --- a/src/Renci.SshNet/IPrivateKeySource.cs +++ b/src/Renci.SshNet/IPrivateKeySource.cs @@ -1,15 +1,26 @@ -using Renci.SshNet.Security; +using System; +using System.ComponentModel; + +using Renci.SshNet.Security; namespace Renci.SshNet { /// /// Represents private key source interface. /// - public interface IPrivateKeySource + /// + /// This interface has been replaced by + /// and is obsolete. + /// + [Obsolete($"Use {nameof(IHostAlgorithmsProvider)} instead. " + + $"{nameof(IPrivateKeySource)} may be removed in a future release. " + + $"See https://github.com/sshnet/SSH.NET/issues/1174 for details.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IPrivateKeySource : IHostAlgorithmsProvider { /// /// Gets the host key. /// HostAlgorithm HostKey { get; } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet/NetConfClient.cs b/src/Renci.SshNet/NetConfClient.cs index bbedca3c2..95cc99f7d 100644 --- a/src/Renci.SshNet/NetConfClient.cs +++ b/src/Renci.SshNet/NetConfClient.cs @@ -107,7 +107,7 @@ public NetConfClient(string host, string username, string password) /// is invalid, -or- is null or contains only whitespace characters. /// is not within and . [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")] - public NetConfClient(string host, int port, string username, params IPrivateKeySource[] keyFiles) + public NetConfClient(string host, int port, string username, params IHostAlgorithmsProvider[] keyFiles) : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true) { } @@ -120,7 +120,7 @@ public NetConfClient(string host, int port, string username, params IPrivateKeyS /// Authentication private key file(s) . /// is null. /// is invalid, -or- is null or contains only whitespace characters. - public NetConfClient(string host, string username, params IPrivateKeySource[] keyFiles) + public NetConfClient(string host, string username, params IHostAlgorithmsProvider[] keyFiles) : this(host, ConnectionInfo.DefaultPort, username, keyFiles) { } diff --git a/src/Renci.SshNet/PrivateKeyAuthenticationMethod.cs b/src/Renci.SshNet/PrivateKeyAuthenticationMethod.cs index 960a68b20..cb82cd7ac 100644 --- a/src/Renci.SshNet/PrivateKeyAuthenticationMethod.cs +++ b/src/Renci.SshNet/PrivateKeyAuthenticationMethod.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using Renci.SshNet.Common; @@ -30,7 +31,7 @@ public override string Name /// /// Gets the key files used for authentication. /// - public ICollection KeyFiles { get; private set; } + public ICollection KeyFiles { get; private set; } /// /// Initializes a new instance of the class. @@ -38,7 +39,7 @@ public override string Name /// The username. /// The key files. /// is whitespace or null. - public PrivateKeyAuthenticationMethod(string username, params IPrivateKeySource[] keyFiles) + public PrivateKeyAuthenticationMethod(string username, params IHostAlgorithmsProvider[] keyFiles) : base(username) { if (keyFiles is null) @@ -46,7 +47,7 @@ public PrivateKeyAuthenticationMethod(string username, params IPrivateKeySource[ throw new ArgumentNullException(nameof(keyFiles)); } - KeyFiles = new Collection(keyFiles); + KeyFiles = new Collection(keyFiles); } /// @@ -64,24 +65,26 @@ public override AuthenticationResult Authenticate(Session session) session.RegisterMessage("SSH_MSG_USERAUTH_PK_OK"); + var hostAlgorithms = KeyFiles.SelectMany(x => x.HostAlgorithms).ToList(); + try { - foreach (var keyFile in KeyFiles) + foreach (var hostAlgorithm in hostAlgorithms) { _ = _authenticationCompleted.Reset(); _isSignatureRequired = false; var message = new RequestMessagePublicKey(ServiceName.Connection, Username, - keyFile.HostKey.Name, - keyFile.HostKey.Data); + hostAlgorithm.Name, + hostAlgorithm.Data); - if (KeyFiles.Count < 2) + if (hostAlgorithms.Count == 1) { // If only one key file provided then send signature for very first request var signatureData = new SignatureData(message, session.SessionId).GetBytes(); - message.Signature = keyFile.HostKey.Sign(signatureData); + message.Signature = hostAlgorithm.Sign(signatureData); } // Send public key authentication request @@ -95,12 +98,12 @@ public override AuthenticationResult Authenticate(Session session) var signatureMessage = new RequestMessagePublicKey(ServiceName.Connection, Username, - keyFile.HostKey.Name, - keyFile.HostKey.Data); + hostAlgorithm.Name, + hostAlgorithm.Data); var signatureData = new SignatureData(message, session.SessionId).GetBytes(); - signatureMessage.Signature = keyFile.HostKey.Sign(signatureData); + signatureMessage.Signature = hostAlgorithm.Sign(signatureData); // Send public key authentication request with signature session.SendMessage(signatureMessage); @@ -108,7 +111,7 @@ public override AuthenticationResult Authenticate(Session session) session.WaitOnHandle(_authenticationCompleted); - if (_authenticationResult == AuthenticationResult.Success) + if (_authenticationResult is AuthenticationResult.Success or AuthenticationResult.PartialSuccess) { break; } diff --git a/src/Renci.SshNet/PrivateKeyConnectionInfo.cs b/src/Renci.SshNet/PrivateKeyConnectionInfo.cs index 29b48ffaa..5ea0dacfd 100644 --- a/src/Renci.SshNet/PrivateKeyConnectionInfo.cs +++ b/src/Renci.SshNet/PrivateKeyConnectionInfo.cs @@ -17,7 +17,7 @@ public class PrivateKeyConnectionInfo : ConnectionInfo, IDisposable /// /// Gets the key files used for authentication. /// - public ICollection KeyFiles { get; private set; } + public ICollection KeyFiles { get; private set; } /// /// Initializes a new instance of the class. @@ -41,7 +41,7 @@ public PrivateKeyConnectionInfo(string host, string username, params PrivateKeyF /// Connection port. /// Connection username. /// Connection key files. - public PrivateKeyConnectionInfo(string host, int port, string username, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, int port, string username, params IHostAlgorithmsProvider[] keyFiles) : this(host, port, username, ProxyTypes.None, string.Empty, 0, string.Empty, string.Empty, keyFiles) { } @@ -56,7 +56,7 @@ public PrivateKeyConnectionInfo(string host, int port, string username, params I /// The proxy host. /// The proxy port. /// The key files. - public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, params IHostAlgorithmsProvider[] keyFiles) : this(host, port, username, proxyType, proxyHost, proxyPort, string.Empty, string.Empty, keyFiles) { } @@ -72,7 +72,7 @@ public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTyp /// The proxy port. /// The proxy username. /// The key files. - public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, params IHostAlgorithmsProvider[] keyFiles) : this(host, port, username, proxyType, proxyHost, proxyPort, proxyUsername, string.Empty, keyFiles) { } @@ -86,7 +86,7 @@ public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTyp /// The proxy host. /// The proxy port. /// The key files. - public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, params IHostAlgorithmsProvider[] keyFiles) : this(host, DefaultPort, username, proxyType, proxyHost, proxyPort, string.Empty, string.Empty, keyFiles) { } @@ -101,7 +101,7 @@ public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyTy /// The proxy port. /// The proxy username. /// The key files. - public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, params IHostAlgorithmsProvider[] keyFiles) : this(host, DefaultPort, username, proxyType, proxyHost, proxyPort, proxyUsername, string.Empty, keyFiles) { } @@ -117,7 +117,7 @@ public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyTy /// The proxy username. /// The proxy password. /// The key files. - public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, string proxyPassword, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, string proxyPassword, params IHostAlgorithmsProvider[] keyFiles) : this(host, DefaultPort, username, proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword, keyFiles) { } @@ -134,10 +134,10 @@ public PrivateKeyConnectionInfo(string host, string username, ProxyTypes proxyTy /// The proxy username. /// The proxy password. /// The key files. - public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, string proxyPassword, params IPrivateKeySource[] keyFiles) + public PrivateKeyConnectionInfo(string host, int port, string username, ProxyTypes proxyType, string proxyHost, int proxyPort, string proxyUsername, string proxyPassword, params IHostAlgorithmsProvider[] keyFiles) : base(host, port, username, proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword, new PrivateKeyAuthenticationMethod(username, keyFiles)) { - KeyFiles = new Collection(keyFiles); + KeyFiles = new Collection(keyFiles); } /// diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs index 3a2dbe049..ebd50fa42 100644 --- a/src/Renci.SshNet/PrivateKeyFile.cs +++ b/src/Renci.SshNet/PrivateKeyFile.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -63,18 +65,48 @@ namespace Renci.SshNet /// /// /// - public class PrivateKeyFile : IPrivateKeySource, IDisposable + public class PrivateKeyFile : IHostAlgorithmsProvider, +#pragma warning disable CS0618 // Type or member is obsolete + IPrivateKeySource, +#pragma warning restore CS0618 // Type or member is obsolete + IDisposable { private static readonly Regex PrivateKeyRegex = new Regex(@"^-+ *BEGIN (?\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?[A-Z0-9-]+),(?[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k PRIVATE KEY *-+", RegexOptions.Compiled | RegexOptions.Multiline); + private readonly List _hostAlgorithms = new List(); private Key _key; private bool _isDisposed; /// /// Gets the host key. /// - public HostAlgorithm HostKey { get; private set; } + /// + /// This property returns the first item in . + /// + public HostAlgorithm HostKey + { + get + { + return _hostAlgorithms[0]; + } + private set + { + Debug.Assert(_hostAlgorithms.Count == 0, $"Only expected to set {nameof(HostKey)} at most once."); + _hostAlgorithms.Add(value); + } + } + + /// + /// The supported host algorithms for this key file. + /// + public IReadOnlyCollection HostAlgorithms + { + get + { + return _hostAlgorithms; + } + } /// /// Initializes a new instance of the class. @@ -92,6 +124,7 @@ public PrivateKeyFile(Key key) public PrivateKeyFile(Stream privateKey) { Open(privateKey, passPhrase: null); + Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKey)} is not set."); } /// @@ -127,6 +160,8 @@ public PrivateKeyFile(string fileName, string passPhrase) { Open(keyFile, passPhrase); } + + Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKey)} is not set."); } /// @@ -138,6 +173,7 @@ public PrivateKeyFile(string fileName, string passPhrase) public PrivateKeyFile(Stream privateKey, string passPhrase) { Open(privateKey, passPhrase); + Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKey)} is not set."); } /// @@ -222,8 +258,11 @@ private void Open(Stream privateKey, string passPhrase) switch (keyName) { case "RSA": - _key = new RsaKey(decryptedData); - HostKey = new KeyHostAlgorithm("ssh-rsa", _key); + var rsaKey = new RsaKey(decryptedData); + _key = rsaKey; + _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512))); + _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256))); + _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key)); break; case "DSA": _key = new DsaKey(decryptedData); @@ -235,7 +274,17 @@ private void Open(Stream privateKey, string passPhrase) break; case "OPENSSH": _key = ParseOpenSshV1Key(decryptedData, passPhrase); - HostKey = new KeyHostAlgorithm(_key.ToString(), _key); + if (_key is RsaKey parsedRsaKey) + { + _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA512))); + _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA256))); + _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key)); + } + else + { + HostKey = new KeyHostAlgorithm(_key.ToString(), _key); + } + break; case "SSH2 ENCRYPTED": var reader = new SshDataReader(decryptedData); @@ -290,8 +339,11 @@ private void Open(Stream privateKey, string passPhrase) var inverseQ = reader.ReadBigIntWithBits(); // u var q = reader.ReadBigIntWithBits(); // p var p = reader.ReadBigIntWithBits(); // q - _key = new RsaKey(modulus, exponent, d, p, q, inverseQ); - HostKey = new KeyHostAlgorithm("ssh-rsa", _key); + var decryptedRsaKey = new RsaKey(modulus, exponent, d, p, q, inverseQ); + _key = decryptedRsaKey; + _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA512))); + _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA256))); + _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key)); } else if (keyType == "dl-modp{sign{dsa-nist-sha1},dh{plain}}") { diff --git a/src/Renci.SshNet/ScpClient.cs b/src/Renci.SshNet/ScpClient.cs index 177a83bb5..fc12413de 100644 --- a/src/Renci.SshNet/ScpClient.cs +++ b/src/Renci.SshNet/ScpClient.cs @@ -146,7 +146,7 @@ public ScpClient(string host, string username, string password) /// is invalid, -or- is null or contains only whitespace characters. /// is not within and . [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")] - public ScpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles) + public ScpClient(string host, int port, string username, params IHostAlgorithmsProvider[] keyFiles) : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true) { } @@ -159,7 +159,7 @@ public ScpClient(string host, int port, string username, params IPrivateKeySourc /// Authentication private key file(s) . /// is null. /// is invalid, -or- is null or contains only whitespace characters. - public ScpClient(string host, string username, params IPrivateKeySource[] keyFiles) + public ScpClient(string host, string username, params IHostAlgorithmsProvider[] keyFiles) : this(host, ConnectionInfo.DefaultPort, username, keyFiles) { } diff --git a/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs b/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs index fda7fc5b5..749083c03 100644 --- a/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs +++ b/src/Renci.SshNet/Security/Cryptography/Ciphers/RsaCipher.cs @@ -8,8 +8,6 @@ namespace Renci.SshNet.Security.Cryptography.Ciphers /// public class RsaCipher : AsymmetricCipher { - private readonly bool _isPrivate; - private readonly RsaKey _key; /// @@ -24,7 +22,6 @@ public RsaCipher(RsaKey key) } _key = key; - _isPrivate = !_key.D.IsZero; } /// @@ -116,7 +113,9 @@ private byte[] Transform(byte[] data, int offset, int length) BigInteger result; - if (_isPrivate) + var isPrivate = !_key.D.IsZero; + + if (isPrivate) { var random = BigInteger.One; var max = _key.Modulus - 1; diff --git a/src/Renci.SshNet/Security/Cryptography/DsaKey.cs b/src/Renci.SshNet/Security/Cryptography/DsaKey.cs index d9664bdd9..1c239aebe 100644 --- a/src/Renci.SshNet/Security/Cryptography/DsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/DsaKey.cs @@ -84,7 +84,7 @@ public override int KeyLength /// /// Gets the digital signature. /// - protected override DigitalSignature DigitalSignature + protected internal override DigitalSignature DigitalSignature { get { diff --git a/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs b/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs index 99892fa59..84dc61178 100644 --- a/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs +++ b/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs @@ -62,7 +62,7 @@ public override int KeyLength /// /// Gets the digital signature. /// - protected override DigitalSignature DigitalSignature + protected internal override DigitalSignature DigitalSignature { get { diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs index 0958ef0ce..f6bd50bd0 100644 --- a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs @@ -128,7 +128,7 @@ public override int KeyLength /// /// Gets the digital signature. /// - protected override DigitalSignature DigitalSignature + protected internal override DigitalSignature DigitalSignature { get { diff --git a/src/Renci.SshNet/Security/Cryptography/Key.cs b/src/Renci.SshNet/Security/Cryptography/Key.cs index e23673b1e..fcc01efbc 100644 --- a/src/Renci.SshNet/Security/Cryptography/Key.cs +++ b/src/Renci.SshNet/Security/Cryptography/Key.cs @@ -17,9 +17,9 @@ public abstract class Key protected BigInteger[] _privateKey; /// - /// Gets the key specific digital signature. + /// Gets the default digital signature implementation for this key. /// - protected abstract DigitalSignature DigitalSignature { get; } + protected internal abstract DigitalSignature DigitalSignature { get; } /// /// Gets or sets the public key. diff --git a/src/Renci.SshNet/Security/Cryptography/RsaDigitalSignature.cs b/src/Renci.SshNet/Security/Cryptography/RsaDigitalSignature.cs index 9af398b46..790a0da64 100644 --- a/src/Renci.SshNet/Security/Cryptography/RsaDigitalSignature.cs +++ b/src/Renci.SshNet/Security/Cryptography/RsaDigitalSignature.cs @@ -1,6 +1,5 @@ using System; using System.Security.Cryptography; -using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Security.Cryptography.Ciphers; @@ -14,13 +13,23 @@ public class RsaDigitalSignature : CipherDigitalSignature, IDisposable private HashAlgorithm _hash; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with the SHA-1 hash algorithm. /// /// The RSA key. public RsaDigitalSignature(RsaKey rsaKey) - : base(new ObjectIdentifier(1, 3, 14, 3, 2, 26), new RsaCipher(rsaKey)) + : this(rsaKey, HashAlgorithmName.SHA1) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The RSA key. + /// The hash algorithm to use in the digital signature. + public RsaDigitalSignature(RsaKey rsaKey, HashAlgorithmName hashAlgorithmName) + : base(ObjectIdentifier.FromHashAlgorithmName(hashAlgorithmName), new RsaCipher(rsaKey)) { - _hash = CryptoAbstraction.CreateSHA1(); + _hash = CryptoConfig.CreateFromName(hashAlgorithmName.Name) as HashAlgorithm + ?? throw new ArgumentException($"Could not create {nameof(HashAlgorithm)} from `{hashAlgorithmName}`.", nameof(hashAlgorithmName)); } /// diff --git a/src/Renci.SshNet/Security/Cryptography/RsaKey.cs b/src/Renci.SshNet/Security/Cryptography/RsaKey.cs index dbb5ebb02..7b088ea6e 100644 --- a/src/Renci.SshNet/Security/Cryptography/RsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/RsaKey.cs @@ -153,10 +153,14 @@ public override int KeyLength } private RsaDigitalSignature _digitalSignature; + /// - /// Gets the digital signature. + /// /// - protected override DigitalSignature DigitalSignature + /// + /// An implementation of an RSA digital signature using the SHA-1 hash algorithm. + /// + protected internal override DigitalSignature DigitalSignature { get { diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs index 771ac6bf4..96a912b66 100644 --- a/src/Renci.SshNet/Security/KeyExchange.cs +++ b/src/Renci.SshNet/Security/KeyExchange.cs @@ -318,6 +318,28 @@ protected bool CanTrustHostKey(KeyHostAlgorithm host) /// true if exchange hash is valid; otherwise false. protected abstract bool ValidateExchangeHash(); + private protected bool ValidateExchangeHash(byte[] encodedKey, byte[] encodedSignature) + { + var exchangeHash = CalculateHash(); + + var signatureData = new KeyHostAlgorithm.SignatureKeyData(); + signatureData.Load(encodedSignature); + + var keyAlgorithm = Session.ConnectionInfo.HostKeyAlgorithms[signatureData.AlgorithmName](encodedKey); + + Session.ConnectionInfo.CurrentHostKeyAlgorithm = signatureData.AlgorithmName; + + if (CanTrustHostKey(keyAlgorithm)) + { + // keyAlgorithm.VerifySignature decodes the signature data before verifying. + // But as we have already decoded the data to find the signature algorithm, + // we just verify the decoded data directly through the DigitalSignature. + return keyAlgorithm.DigitalSignature.Verify(exchangeHash, signatureData.Signature); + } + + return false; + } + /// /// Calculates key exchange hash value. /// diff --git a/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs b/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs index 836c607f3..5cdba5b4d 100644 --- a/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs +++ b/src/Renci.SshNet/Security/KeyExchangeDiffieHellman.cs @@ -1,5 +1,4 @@ using System; -using System.Text; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; @@ -72,20 +71,7 @@ internal abstract class KeyExchangeDiffieHellman : KeyExchange /// protected override bool ValidateExchangeHash() { - var exchangeHash = CalculateHash(); - - var length = Pack.BigEndianToUInt32(_hostKey); - var algorithmName = Encoding.UTF8.GetString(_hostKey, 4, (int)length); - var key = Session.ConnectionInfo.HostKeyAlgorithms[algorithmName](_hostKey); - - Session.ConnectionInfo.CurrentHostKeyAlgorithm = algorithmName; - - if (CanTrustHostKey(key)) - { - return key.VerifySignature(exchangeHash, _signature); - } - - return false; + return ValidateExchangeHash(_hostKey, _signature); } /// diff --git a/src/Renci.SshNet/Security/KeyExchangeEC.cs b/src/Renci.SshNet/Security/KeyExchangeEC.cs index 864f55d2f..f2ae22fb7 100644 --- a/src/Renci.SshNet/Security/KeyExchangeEC.cs +++ b/src/Renci.SshNet/Security/KeyExchangeEC.cs @@ -1,7 +1,4 @@ -using System.Text; - -using Renci.SshNet.Common; -using Renci.SshNet.Messages.Transport; +using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security { @@ -76,20 +73,7 @@ protected override byte[] CalculateHash() /// protected override bool ValidateExchangeHash() { - var exchangeHash = CalculateHash(); - - var length = Pack.BigEndianToUInt32(_hostKey); - var algorithmName = Encoding.UTF8.GetString(_hostKey, 4, (int)length); - var key = Session.ConnectionInfo.HostKeyAlgorithms[algorithmName](_hostKey); - - Session.ConnectionInfo.CurrentHostKeyAlgorithm = algorithmName; - - if (CanTrustHostKey(key)) - { - return key.VerifySignature(exchangeHash, _signature); - } - - return false; + return ValidateExchangeHash(_hostKey, _signature); } /// diff --git a/src/Renci.SshNet/Security/KeyHostAlgorithm.cs b/src/Renci.SshNet/Security/KeyHostAlgorithm.cs index aad8e20c1..f0d2b41d8 100644 --- a/src/Renci.SshNet/Security/KeyHostAlgorithm.cs +++ b/src/Renci.SshNet/Security/KeyHostAlgorithm.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; +using System.Text; + using Renci.SshNet.Common; using Renci.SshNet.Security.Chaos.NaCl; +using Renci.SshNet.Security.Cryptography; namespace Renci.SshNet.Security { @@ -10,38 +13,76 @@ namespace Renci.SshNet.Security public class KeyHostAlgorithm : HostAlgorithm { /// - /// Gets the key. + /// The key used in this host key algorithm. /// public Key Key { get; private set; } /// - /// Gets the public key data. + /// The signature implementation used in this host key algorithm. + /// + public DigitalSignature DigitalSignature { get; private set; } + + /// + /// Gets the encoded public key data. /// public override byte[] Data { get { - return new SshKeyData(Name, Key.Public).GetBytes(); + var keyFormatIdentifier = Key is RsaKey ? "ssh-rsa" : Name; + return new SshKeyData(keyFormatIdentifier, Key.Public).GetBytes(); } } /// /// Initializes a new instance of the class. /// - /// Host key name. - /// Host key. + /// The signature format identifier. + /// + /// + /// This constructor is typically passed a private key in order to create an encoded signature for later + /// verification by the host. + /// public KeyHostAlgorithm(string name, Key key) : base(name) { Key = key; + DigitalSignature = key.DigitalSignature; } /// /// Initializes a new instance of the class. /// - /// Host key name. - /// Host key. + /// The signature format identifier. + /// + /// + /// + /// + /// This constructor is typically passed a private key in order to create an encoded signature for later + /// verification by the host. + /// + /// The key used by is intended to be equal to . + /// This is not verified. + /// + public KeyHostAlgorithm(string name, Key key, DigitalSignature digitalSignature) + : base(name) + { + Key = key; + DigitalSignature = digitalSignature; + } + + /// + /// Initializes a new instance of the class + /// with the given encoded public key data. The data will be decoded into . + /// + /// The signature format identifier. + /// /// Host key encoded data. + /// + /// This constructor is typically passed a new or reusable instance in + /// order to verify an encoded signature sent by the host, created by the private counterpart + /// to the host's public key, which is encoded in . + /// public KeyHostAlgorithm(string name, Key key, byte[] data) : base(name) { @@ -50,34 +91,66 @@ public KeyHostAlgorithm(string name, Key key, byte[] data) var sshKey = new SshKeyData(); sshKey.Load(data); Key.Public = sshKey.Keys; + + DigitalSignature = key.DigitalSignature; } /// - /// Signs the specified data. + /// Initializes a new instance of the class + /// with the given encoded public key data. The data will be decoded into . /// - /// The data. + /// The signature format identifier. + /// + /// Host key encoded data. + /// + /// + /// + /// This constructor is typically passed a new or reusable instance in + /// order to verify an encoded signature sent by the host, created by the private counterpart + /// to the host's public key, which is encoded in . + /// + /// The key used by is intended to be equal to . + /// This is not verified. + /// + public KeyHostAlgorithm(string name, Key key, byte[] data, DigitalSignature digitalSignature) + : base(name) + { + Key = key; + + var sshKey = new SshKeyData(); + sshKey.Load(data); + Key.Public = sshKey.Keys; + + DigitalSignature = digitalSignature; + } + + /// + /// Signs and encodes the specified data. + /// + /// The data to be signed. /// - /// Signed data. + /// The encoded signature. /// public override byte[] Sign(byte[] data) { - return new SignatureKeyData(Name, Key.Sign(data)).GetBytes(); + return new SignatureKeyData(Name, DigitalSignature.Sign(data)).GetBytes(); } /// /// Verifies the signature. /// - /// The data. - /// The signature. + /// The data to verify the signature against. + /// The encoded signature data. /// - /// True is signature was successfully verifies; otherwise false. + /// if is the result of signing + /// with the corresponding private key to . /// public override bool VerifySignature(byte[] data, byte[] signature) { var signatureData = new SignatureKeyData(); signatureData.Load(signature); - return Key.VerifySignature(data, signatureData.Signature); + return DigitalSignature.Verify(data, signatureData.Signature); } private sealed class SshKeyData : SshData @@ -170,15 +243,12 @@ protected override void SaveData() } } - private sealed class SignatureKeyData : SshData + internal sealed class SignatureKeyData : SshData { /// - /// Gets or sets the name of the algorithm as UTF-8 encoded byte array. + /// Gets or sets the signature format identifier /// - /// - /// The name of the algorithm. - /// - private byte[] AlgorithmName { get; set; } + public string AlgorithmName { get; set; } /// /// Gets the signature. @@ -200,7 +270,7 @@ protected override int BufferCapacity { var capacity = base.BufferCapacity; capacity += 4; // AlgorithmName length - capacity += AlgorithmName.Length; // AlgorithmName + capacity += Encoding.UTF8.GetByteCount(AlgorithmName); // AlgorithmName capacity += 4; // Signature length capacity += Signature.Length; // Signature return capacity; @@ -213,7 +283,7 @@ public SignatureKeyData() public SignatureKeyData(string name, byte[] signature) { - AlgorithmName = Utf8.GetBytes(name); + AlgorithmName = name; Signature = signature; } @@ -222,7 +292,7 @@ public SignatureKeyData(string name, byte[] signature) /// protected override void LoadData() { - AlgorithmName = ReadBinary(); + AlgorithmName = Encoding.UTF8.GetString(ReadBinary()); Signature = ReadBinary(); } @@ -231,7 +301,7 @@ protected override void LoadData() /// protected override void SaveData() { - WriteBinaryString(AlgorithmName); + WriteBinaryString(Encoding.UTF8.GetBytes(AlgorithmName)); WriteBinaryString(Signature); } } diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index e6f9cb2d6..b5f757b96 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -218,7 +218,7 @@ public SftpClient(string host, string username, string password) /// is invalid. -or- is nunullll or contains only whitespace characters. /// is not within and . [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")] - public SftpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles) + public SftpClient(string host, int port, string username, params IHostAlgorithmsProvider[] keyFiles) : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true) { } @@ -231,7 +231,7 @@ public SftpClient(string host, int port, string username, params IPrivateKeySour /// Authentication private key file(s) . /// is null. /// is invalid. -or- is null or contains only whitespace characters. - public SftpClient(string host, string username, params IPrivateKeySource[] keyFiles) + public SftpClient(string host, string username, params IHostAlgorithmsProvider[] keyFiles) : this(host, ConnectionInfo.DefaultPort, username, keyFiles) { } diff --git a/src/Renci.SshNet/SshClient.cs b/src/Renci.SshNet/SshClient.cs index fb8bb37f0..e596caaa0 100644 --- a/src/Renci.SshNet/SshClient.cs +++ b/src/Renci.SshNet/SshClient.cs @@ -103,7 +103,7 @@ public SshClient(string host, string username, string password) /// is invalid, -or- is null or contains only whitespace characters. /// is not within and . [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")] - public SshClient(string host, int port, string username, params IPrivateKeySource[] keyFiles) + public SshClient(string host, int port, string username, params IHostAlgorithmsProvider[] keyFiles) : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true) { } @@ -120,7 +120,7 @@ public SshClient(string host, int port, string username, params IPrivateKeySourc /// /// is null. /// is invalid, -or- is null or contains only whitespace characters. - public SshClient(string host, string username, params IPrivateKeySource[] keyFiles) + public SshClient(string host, string username, params IHostAlgorithmsProvider[] keyFiles) : this(host, ConnectionInfo.DefaultPort, username, keyFiles) { }