diff --git a/src/Renci.SshNet/Security/Cryptography/DsaDigitalSignature.cs b/src/Renci.SshNet/Security/Cryptography/DsaDigitalSignature.cs index 8fdb6a8f7..82fcfe9bc 100644 --- a/src/Renci.SshNet/Security/Cryptography/DsaDigitalSignature.cs +++ b/src/Renci.SshNet/Security/Cryptography/DsaDigitalSignature.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Security.Cryptography; using Renci.SshNet.Common; @@ -11,8 +12,6 @@ namespace Renci.SshNet.Security.Cryptography public class DsaDigitalSignature : DigitalSignature, IDisposable { private readonly DsaKey _key; - private HashAlgorithm _hash; - private bool _isDisposed; /// /// Initializes a new instance of the class. @@ -27,71 +26,23 @@ public DsaDigitalSignature(DsaKey key) } _key = key; - - _hash = SHA1.Create(); } - /// - /// Verifies the signature. - /// - /// The input. - /// The signature. - /// - /// if signature was successfully verified; otherwise . - /// - /// Invalid signature. + /// public override bool Verify(byte[] input, byte[] signature) { - var hashInput = _hash.ComputeHash(input); - - var hm = new BigInteger(hashInput.Reverse().Concat(new byte[] { 0 })); - - if (signature.Length != 40) - { - throw new InvalidOperationException("Invalid signature."); - } - - // Extract r and s numbers from the signature - var rBytes = new byte[21]; - var sBytes = new byte[21]; - - for (int i = 0, j = 20; i < 20; i++, j--) - { - rBytes[i] = signature[j - 1]; - sBytes[i] = signature[j + 20 - 1]; - } - - var r = new BigInteger(rBytes); - var s = new BigInteger(sBytes); - - // Reject the signature if 0 < r < q or 0 < s < q is not satisfied. - if (r <= 0 || r >= _key.Q) +#if NETSTANDARD2_1_OR_GREATER || NET + return _key.DSA.VerifyData(input, signature, HashAlgorithmName.SHA1); +#else + // VerifyData does not exist on netstandard2.0. + // It does exist on net462, but in order to keep the path tested, + // use it on netfx as well. + using (var sha1 = SHA1.Create()) { - return false; + var hash = sha1.ComputeHash(input); + return _key.DSA.VerifySignature(hash, signature); } - - if (s <= 0 || s >= _key.Q) - { - return false; - } - - // Calculate w = s−1 mod q - var w = BigInteger.ModInverse(s, _key.Q); - - // Calculate u1 = H(m)·w mod q - var u1 = (hm * w) % _key.Q; - - // Calculate u2 = r * w mod q - var u2 = (r * w) % _key.Q; - - u1 = BigInteger.ModPow(_key.G, u1, _key.P); - u2 = BigInteger.ModPow(_key.Y, u2, _key.P); - - // Calculate v = ((g pow u1 * y pow u2) mod p) mod q - var v = ((u1 * u2) % _key.P) % _key.Q; - - // The signature is valid if v = r - return v == r; +#endif } /// @@ -104,60 +55,18 @@ public override bool Verify(byte[] input, byte[] signature) /// Invalid DSA key. public override byte[] Sign(byte[] input) { - var hashInput = _hash.ComputeHash(input); - - var m = new BigInteger(hashInput.Reverse().Concat(new byte[] { 0 })); - - BigInteger s; - BigInteger r; - - do +#if NETSTANDARD2_1_OR_GREATER || NET + return _key.DSA.SignData(input, HashAlgorithmName.SHA1); +#else + // SignData does not exist on netstandard2.0. + // It does exist on net462, but in order to keep the path tested, + // use it on netfx as well. + using (var sha1 = SHA1.Create()) { - var k = BigInteger.Zero; - - do - { - // Generate a random per-message value k where 0 < k < q - var bitLength = _key.Q.BitLength; - - if (_key.Q < BigInteger.Zero) - { - throw new SshException("Invalid DSA key."); - } - - while (k <= 0 || k >= _key.Q) - { - k = BigInteger.Random(bitLength); - } - - // Calculate r = ((g pow k) mod p) mod q - r = BigInteger.ModPow(_key.G, k, _key.P) % _key.Q; - - // In the unlikely case that r = 0, start again with a different random k - } - while (r.IsZero); - - // Calculate s = ((k pow −1)(H(m) + x*r)) mod q - k = BigInteger.ModInverse(k, _key.Q) * (m + (_key.X * r)); - - s = k % _key.Q; - - // In the unlikely case that s = 0, start again with a different random k + var hash = sha1.ComputeHash(input); + return _key.DSA.CreateSignature(hash); } - while (s.IsZero); - - // The signature is (r, s) - var signature = new byte[40]; - - // issue #1918: pad part with zero's on the left if length is less than 20 - var rBytes = r.ToByteArray().Reverse().TrimLeadingZeros(); - Array.Copy(rBytes, 0, signature, 20 - rBytes.Length, rBytes.Length); - - // issue #1918: pad part with zero's on the left if length is less than 20 - var sBytes = s.ToByteArray().Reverse().TrimLeadingZeros(); - Array.Copy(sBytes, 0, signature, 40 - sBytes.Length, sBytes.Length); - - return signature; +#endif } /// @@ -175,30 +84,6 @@ public void Dispose() /// to release both managed and unmanaged resources; to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (_isDisposed) - { - return; - } - - if (disposing) - { - var hash = _hash; - if (hash != null) - { - hash.Dispose(); - _hash = null; - } - - _isDisposed = true; - } - } - - /// - /// Finalizes an instance of the class. - /// - ~DsaDigitalSignature() - { - Dispose(disposing: false); } } } diff --git a/src/Renci.SshNet/Security/Cryptography/DsaKey.cs b/src/Renci.SshNet/Security/Cryptography/DsaKey.cs index b0f915cc0..fa7126184 100644 --- a/src/Renci.SshNet/Security/Cryptography/DsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/DsaKey.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable +using System; +using System.Security.Cryptography; using Renci.SshNet.Common; using Renci.SshNet.Security.Cryptography; @@ -10,8 +12,9 @@ namespace Renci.SshNet.Security /// public class DsaKey : Key, IDisposable { - private DsaDigitalSignature _digitalSignature; - private bool _isDisposed; + private DsaDigitalSignature? _digitalSignature; + + internal DSA DSA { get; } /// /// Gets the P. @@ -39,10 +42,10 @@ public class DsaKey : Key, IDisposable public BigInteger X { get; } /// - /// Gets the length of the key. + /// Gets the length of the key in bits. /// /// - /// The length of the key. + /// The bit-length of the key. /// public override int KeyLength { @@ -104,6 +107,8 @@ public DsaKey(SshKeyData publicKeyData) Q = publicKeyData.Keys[1]; G = publicKeyData.Keys[2]; Y = publicKeyData.Keys[3]; + + DSA = LoadDSA(); } /// @@ -130,6 +135,8 @@ public DsaKey(byte[] privateKeyData) { throw new InvalidOperationException("Invalid private key (expected EOF)."); } + + DSA = LoadDSA(); } /// @@ -147,6 +154,54 @@ public DsaKey(BigInteger p, BigInteger q, BigInteger g, BigInteger y, BigInteger G = g; Y = y; X = x; + + DSA = LoadDSA(); + } + +#pragma warning disable CA1859 // Use concrete types when possible for improved performance +#pragma warning disable CA5384 // Do Not Use Digital Signature Algorithm (DSA) + private DSA LoadDSA() + { +#if NETFRAMEWORK + // On .NET Framework we use the concrete CNG type which is FIPS-186-3 + // compatible. The CryptoServiceProvider type returned by DSA.Create() + // is limited to FIPS-186-1 (max 1024 bit key). + var dsa = new DSACng(); +#else + var dsa = DSA.Create(); +#endif + dsa.ImportParameters(GetDSAParameters()); + + return dsa; + } +#pragma warning restore CA5384 // Do Not Use Digital Signature Algorithm (DSA) +#pragma warning restore CA1859 // Use concrete types when possible for improved performance + + internal DSAParameters GetDSAParameters() + { + // P, G, Y, Q are required. + // P, G, Y must have the same length. + // If X is present, it must have the same length as Q. + + // See https://github.com/dotnet/runtime/blob/fadd8313653f71abd0068c8bf914be88edb2c8d3/src/libraries/Common/src/System/Security/Cryptography/DSACng.ImportExport.cs#L23 + // and https://github.com/dotnet/runtime/blob/fadd8313653f71abd0068c8bf914be88edb2c8d3/src/libraries/Common/src/System/Security/Cryptography/DSAKeyFormatHelper.cs#L18 + // (and similar code in RsaKey.cs) + + var ret = new DSAParameters + { + P = P.ToByteArray(isUnsigned: true, isBigEndian: true), + Q = Q.ToByteArray(isUnsigned: true, isBigEndian: true), + }; + + ret.G = G.ExportKeyParameter(ret.P.Length); + ret.Y = Y.ExportKeyParameter(ret.P.Length); + + if (!X.IsZero) + { + ret.X = X.ExportKeyParameter(ret.Q.Length); + } + + return ret; } /// @@ -164,30 +219,11 @@ public void Dispose() /// to release both managed and unmanaged resources; to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (_isDisposed) - { - return; - } - if (disposing) { - var digitalSignature = _digitalSignature; - if (digitalSignature != null) - { - digitalSignature.Dispose(); - _digitalSignature = null; - } - - _isDisposed = true; + _digitalSignature?.Dispose(); + DSA.Dispose(); } } - - /// - /// Finalizes an instance of the class. - /// - ~DsaKey() - { - Dispose(disposing: false); - } } } diff --git a/src/Renci.SshNet/Security/Cryptography/RsaKey.cs b/src/Renci.SshNet/Security/Cryptography/RsaKey.cs index 71390d713..472b097e0 100644 --- a/src/Renci.SshNet/Security/Cryptography/RsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/RsaKey.cs @@ -92,10 +92,10 @@ public override string ToString() public BigInteger InverseQ { get; } /// - /// Gets the length of the key. + /// Gets the length of the key in bits. /// /// - /// The length of the key. + /// The bit-length of the key. /// public override int KeyLength { diff --git a/test/Data/Key.SSH2.DSA.pub b/test/Data/Key.SSH2.DSA.pub index 3b07844b3..654400140 100644 --- a/test/Data/Key.SSH2.DSA.pub +++ b/test/Data/Key.SSH2.DSA.pub @@ -1 +1 @@ -ssh-dss AAAAB3NzaC1kc3MAAACBAI8gyHFchkVhkPiwkhkjFDqN6w2nFWTqVy9sLjFs38oEWLMpAw9+c132erUptAhNQ6JZUAVZGllv/3V5hksSDyChe9WY5IfsOlh6X0dcZCwBKysEzQlPyMFqAtbc9uv7oUWNzBfvEbtV6WN/VmcmXf7dyo3EBVXbBFdPl1NKC7W9AAAAFQDY1+bTt7s2iNmYoBE4C9hdWRCyeQAAAIAEtj09ugx/Tdl6bo7X6mX17hcgVgIxcYj5VNONg2k6IHmRFriLviYaS68mIB4SG3jmvvxbXAGqR1bWBUrv90n0wpxxcuuNoCFylJQyuqUkzSsUHb0WMcncZ/tBQt+NJnRB1Zp9sw8n20ocpg3WVPdaXTtc4pk83NYB6ywG6UFPvgAAAIAX+De5dwo33LMl9W8IvA4dY8Q1wshdycAGJzhy+qYF9dCcwD1Pg+4EbPjYPmzJopsVrK97v9QhxyYcXMr/iHhngGwd9nYNzzSKx665vkSjzyeJWpeQ+fvNV3CLItP01ypbUreM+s+Vz1wor5joLKcDS4X0oQ0RIVZNEHnekuLuFg== +ssh-dss AAAAB3NzaC1kc3MAAACBAOCSGuOxxY/DQBowG7fkS30AJmwN4fDPXToyTaAaxwpOWnGJXFhgP4Il+GSKlpaxnUWkajKpMc1b1hhawPWN4sxoT8QRb6SAW30ErnT/pUtsxqoFlkFla4xvWSgNAuHAVkUBrgPsJ4sHehSbNFn3I6q8Rgle2jyHDHTOqPj2KEXhAAAAFQC740YkVJdVpTJRTxd9Myi0Nx3t4wAAAIArvP7AGh5jY+zxeQRb52zxcUamRBkVYL/ferdJNi9hoM8ZaO4++Xgs8wMbpmoEch9DsXdtufjqXWpk7ywlPjcdhhsb3MxJAeEeFtTRsu2/IUTKqKPHIOgoiPzs8q69AxWhV10aDDUdYWLkqPV/tMGl6S/jC7vTJLmhZum4BUv8MQAAAIEAt19oHPIPyv/8mbMaYpu8I6kvj1/97Ejw0neN7Cd9cGZLqIPBwTyUHEQvKSAm4BvNP0Le3JDn3RFayhRmf+8RrAmS4d1MjrCOs6fmeyLnk2kTpRPFZ2x0H1udIRAhzRehyfj6OsAHn7Jclr+mqDhoq7nIfC3tSgWxFH5g948+7ks= imported-openssh-key diff --git a/test/Data/Key.SSH2.DSA.txt b/test/Data/Key.SSH2.DSA.txt index e345b1ef5..a4dc6d077 100644 --- a/test/Data/Key.SSH2.DSA.txt +++ b/test/Data/Key.SSH2.DSA.txt @@ -1,12 +1,13 @@ ---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +Comment: "imported-openssh-key" P2/56wAAAgIAAAAmZGwtbW9kcHtzaWdue2RzYS1uaXN0LXNoYTF9LGRoe3BsYWlufX0AAA -AEbm9uZQAAAcQAAAHAAAAAAAAABACPIMhxXIZFYZD4sJIZIxQ6jesNpxVk6lcvbC4xbN/K -BFizKQMPfnNd9nq1KbQITUOiWVAFWRpZb/91eYZLEg8goXvVmOSH7DpYel9HXGQsASsrBM -0JT8jBagLW3Pbr+6FFjcwX7xG7Veljf1ZnJl3+3cqNxAVV2wRXT5dTSgu1vQAAA/sEtj09 -ugx/Tdl6bo7X6mX17hcgVgIxcYj5VNONg2k6IHmRFriLviYaS68mIB4SG3jmvvxbXAGqR1 -bWBUrv90n0wpxxcuuNoCFylJQyuqUkzSsUHb0WMcncZ/tBQt+NJnRB1Zp9sw8n20ocpg3W -VPdaXTtc4pk83NYB6ywG6UFPvgAAAKDY1+bTt7s2iNmYoBE4C9hdWRCyeQAAA/0X+De5dw -o33LMl9W8IvA4dY8Q1wshdycAGJzhy+qYF9dCcwD1Pg+4EbPjYPmzJopsVrK97v9QhxyYc -XMr/iHhngGwd9nYNzzSKx665vkSjzyeJWpeQ+fvNV3CLItP01ypbUreM+s+Vz1wor5joLK -cDS4X0oQ0RIVZNEHnekuLuFgAAAJ4j+lpXSvEZexhbiACKenUniZ/Qkg== +AEbm9uZQAAAcQAAAHAAAAAAAAABADgkhrjscWPw0AaMBu35Et9ACZsDeHwz106Mk2gGscK +TlpxiVxYYD+CJfhkipaWsZ1FpGoyqTHNW9YYWsD1jeLMaE/EEW+kgFt9BK50/6VLbMaqBZ +ZBZWuMb1koDQLhwFZFAa4D7CeLB3oUmzRZ9yOqvEYJXto8hwx0zqj49ihF4QAAA/4rvP7A +Gh5jY+zxeQRb52zxcUamRBkVYL/ferdJNi9hoM8ZaO4++Xgs8wMbpmoEch9DsXdtufjqXW +pk7ywlPjcdhhsb3MxJAeEeFtTRsu2/IUTKqKPHIOgoiPzs8q69AxWhV10aDDUdYWLkqPV/ +tMGl6S/jC7vTJLmhZum4BUv8MQAAAKC740YkVJdVpTJRTxd9Myi0Nx3t4wAABAC3X2gc8g +/K//yZsxpim7wjqS+PX/3sSPDSd43sJ31wZkuog8HBPJQcRC8pICbgG80/Qt7ckOfdEVrK +FGZ/7xGsCZLh3UyOsI6zp+Z7IueTaROlE8VnbHQfW50hECHNF6HJ+Po6wAefslyWv6aoOG +iruch8Le1KBbEUfmD3jz7uSwAAAJ9mUEtdk3zSMZJ1umUnNSo5zC+UxA== ---- END SSH2 ENCRYPTED PRIVATE KEY ---- diff --git a/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs b/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs index d827fb47c..e685bddf6 100644 --- a/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs +++ b/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs @@ -26,7 +26,7 @@ public void TearDown() [TestMethod] public void SshDss() { - DoTest(HostKeyAlgorithm.SshDss, HostKeyFile.Dsa, 2048); + DoTest(HostKeyAlgorithm.SshDss, HostKeyFile.Dsa, 1024); } [TestMethod] diff --git a/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs b/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs index 10dfb294f..5562338c2 100644 --- a/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs +++ b/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs @@ -3,7 +3,7 @@ public sealed class HostKeyFile { public static readonly HostKeyFile Rsa = new HostKeyFile("ssh-rsa", "/etc/ssh/ssh_host_rsa_key", new byte[] { 0x3d, 0x90, 0xd8, 0x0d, 0xd5, 0xe0, 0xb6, 0x13, 0x42, 0x7c, 0x78, 0x1e, 0x19, 0xa3, 0x99, 0x2b }); - public static readonly HostKeyFile Dsa = new HostKeyFile("ssh-dsa", "/etc/ssh/ssh_host_dsa_key", new byte[] { 0x50, 0xe0, 0xd5, 0x11, 0xf7, 0xed, 0x54, 0x75, 0x0d, 0x03, 0xc6, 0x52, 0x9b, 0x3b, 0x3c, 0x9f }); + public static readonly HostKeyFile Dsa = new HostKeyFile("ssh-dsa", "/etc/ssh/ssh_host_dsa_key", new byte[] { 0xcc, 0xb4, 0x4c, 0xe1, 0xba, 0x6d, 0x15, 0x79, 0xec, 0xe1, 0x31, 0x9f, 0xc0, 0x4a, 0x07, 0x9d }); public static readonly HostKeyFile Ed25519 = new HostKeyFile("ssh-ed25519", "/etc/ssh/ssh_host_ed25519_key", new byte[] { 0xb3, 0xb9, 0xd0, 0x1b, 0x73, 0xc4, 0x60, 0xb4, 0xce, 0xed, 0x06, 0xf8, 0x58, 0x49, 0xa3, 0xda }); public const string Ecdsa = "/etc/ssh/ssh_host_ecdsa_key"; diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index 3482f9692..aaefb81ea 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs @@ -32,6 +32,10 @@ public static InfrastructureFixture Instance public SshUser User = new SshUser("sshnet", "ssh4ever"); + // To get the sshd logs (also uncomment WithOutputConsumer below) + private readonly Stream _fsOut = Stream.Null; // File.Create("fsout.txt"); + private readonly Stream _fsErr = Stream.Null; // File.Create("fserr.txt"); + public async Task InitializeAsync() { _sshServerImage = new ImageFromDockerfileBuilder() @@ -47,6 +51,7 @@ public async Task InitializeAsync() .WithHostname("renci-ssh-tests-server") .WithImage(_sshServerImage) .WithPortBinding(22, true) + //.WithOutputConsumer(Consume.RedirectStdoutAndStderrToStream(_fsOut, _fsErr)) .Build(); await _sshServer.StartAsync(); @@ -74,6 +79,9 @@ public async Task DisposeAsync() { await _sshServerImage.DisposeAsync(); } + + _fsOut.Dispose(); + _fsErr.Dispose(); } public void Dispose() diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_dsa_key b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_dsa_key index eedaafb05..967ffbc6d 100644 --- a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_dsa_key +++ b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_dsa_key @@ -1,20 +1,12 @@ -----BEGIN DSA PRIVATE KEY----- -MIIDPgIBAAKCAQEAuXza5HoqeOTKgTBY0iTglJGVLmmGvp9mWbrx20Xj8V1ouy8u -0ceju7/4AR6m9BzYWm2sAMAwvQcDeUi6pD4C4oIRzQSOg/nuUJO6RkneLQjMYEzD -61FokmxcUzHXQiKtqRRGL97naxj5fFIOppQXfllRASuvHeiG+I6EiFJL4zL7Uwen -CshEkpZsLZ2Xj8nfaD8yPmviDT/QWRUsZgw8lte7MonYVdKd0yeRQwS3vgJwusZv -fFHP4X7aXSwDJTlTGagxFV7jCktwtSc6QFoLWv5LZ2OAJxmgBM1HJQKOnP8dvn56 -EZub3DQrx3IpAgtsxa/8bxt/xFbbfp4sDHwLLQIVAKTEaiNtqneHljGoEGhUWJrs -+kdpAoIBAQCdBG7aHBIV83/icpkELAZ87I/0XDA9pVG+Sgs/OFgUd24tXi9S+dwp -LsVMVaBnN9TaEwZYR6z7Zg12r2j2q8BDTrRwwYHYJvwjHtsZVqaHi35fgBT2RO4T -SqRKYjrjb4mtPodUEo7CzK5+rLpvLM1SiiHfeqmUJqbkDwxQ9xXkCjRP50huJ+tA -ccgQIUyOYioz9omszJGANZlF5ZabzbAiTcXews2p97OeFWNTGTbXebV3FPSV+KBO -c0A5jxzQhEo3Kk58GXuog8t3OksNISdZPIJxHn+th644ZOj0L1v6PrUbXshPL1hp -VNlbn9fO4/HbQzL4NThmgzaZkT2FqxPxAoIBADuwcLTKtLX2cy9cqFiraeEaBXT3 -lQiPTFSLQKVm/k+iumXuOy5Fh3Akzu35MpNLK2gsdoWN9ZRQ8eWODdcnFXSJrnqX -cMWV6ONQ+nZ9YHrRp47KHKWKe+2c0T++S8QZAimb3KCjSOyEwn+i4aAGIvoaYIoH -+tRKmeL+7z1Ff/zJEB1FYVDmcqxhUKd74En6O17EmUHPfiQvwwTYvP5NvlLB23Hz -9ZO4nwrUSUIyVsWYT01s0JThkjI06N0dqKS1we94Ht1mT7iNJ5x5DhVR6qSNOgQH -FMKxKdXHdSFopwwHUrzm3BpKzKW3NuQHazcdZEl1vHb6LpfTv6O6bZIANyACFGBZ -9othW6gmt8t4cI6IyoaLCtLp +MIIBugIBAAKBgQDK1HgEGb/kOVsvVlJpCOpnotUAwoYN1EIAl2Zqj+sxwtUZGR9j +pBV3XYo2IBqTk+2h8Wup8MD3MyoEEMcOem9Jam3VZmvKFTIMBqJglbb+srszsSqw +OiXueV/TPrqMSIEFfNVfPPgHIJ0PwhZ2f3uPidWDEc6t3GzzYRnBFM2zawIVANqN +Qhz4qsDa7V8UamatuXMlhdkHAoGAf28PeajaIq09LwxxN3Ed7FAzsJ9DkodSpjD3 +2E/ezv3eyw1RaFR34g2G0R8xRql0ZgJpb48my4JT5JLmcDtZX0xTdy5Gh2Ke/qxR +smazFLds8CSqVK7e6CVaS6xv2dfI+KgsIVXhDL1GCy8KRJjMmYdx/+4KHgNzGhCZ +lA04mw4CgYBQtVdXgipcs8jLUI/9BkUvgiPJibi5SZibxpL+vVCEOKiMK3I82TuM +YR7S/SJWz1W3KQb9Lmrd/nVwKlk1Ey7Q4J487UpnOuN5Mu7P6PbZi1TXd442XG2r +WbFVGIJVr78ZURaa4mykZJ2DRaanmemRAPSvWTnFYQ6qC2CoYDPaLwIUYatoN3LK +IwehRooj2lc7/l2xxXQ= -----END DSA PRIVATE KEY----- diff --git a/test/Renci.SshNet.Tests/Classes/Security/Cryptography/DsaDigitalSignatureTest.cs b/test/Renci.SshNet.Tests/Classes/Security/Cryptography/DsaDigitalSignatureTest.cs new file mode 100644 index 000000000..87b2e435f --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Security/Cryptography/DsaDigitalSignatureTest.cs @@ -0,0 +1,56 @@ +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Renci.SshNet.Abstractions; +using Renci.SshNet.Security; +using Renci.SshNet.Security.Cryptography; +using Renci.SshNet.Tests.Common; + +namespace Renci.SshNet.Tests.Classes.Security.Cryptography +{ + [TestClass] + public class DsaDigitalSignatureTest : TestBase + { + [TestMethod] + public void Verify() + { + byte[] data = Encoding.UTF8.GetBytes("Hello, World!"); + + DsaKey dsaKey = GetDsaKey("Key.DSA.txt"); + + Assert.AreEqual(1024, dsaKey.P.BitLength); + Assert.AreEqual(160, dsaKey.Q.BitLength); + + var digitalSignature = new DsaDigitalSignature(dsaKey); + + byte[] signedBytes = digitalSignature.Sign(data); + + // We can't compare signatures for value equality because they have a source of randomness + Assert.AreEqual(40, signedBytes.Length); + Assert.IsTrue(digitalSignature.Verify(data, signedBytes)); + + byte[] signatureToVerify = new byte[] + { + // Generated with a previous DsaDigitalSignature implementation in order to confirm consistent + // behaviour. We can't seem to validate against openssl because openssl outputs a DER signature, + // where as we want IEEE P1363 (fixed size) format. + 0x07, 0x4c, 0x5e, 0x15, 0x53, 0x36, 0x21, 0xbe, 0x5a, 0x82, 0x35, 0xd5, 0xb6, 0xe6, 0x7d, 0x2f, + 0x01, 0x2a, 0x78, 0x9b, 0x16, 0x4a, 0xe5, 0x8d, 0x85, 0xa6, 0x34, 0x56, 0x9d, 0x38, 0xd6, 0x1a, + 0xa4, 0xa1, 0x5b, 0x98, 0x7d, 0xd5, 0x35, 0x40 + }; + + Assert.IsTrue(digitalSignature.Verify(data, signatureToVerify)); + + Assert.IsFalse(digitalSignature.Verify(data, CryptoAbstraction.GenerateRandom(40))); + } + + private static DsaKey GetDsaKey(string fileName, string passPhrase = null) + { + using (var stream = GetData(fileName)) + { + return (DsaKey)new PrivateKeyFile(stream, passPhrase).Key; + } + } + } +} diff --git a/test/Renci.SshNet.Tests/Classes/Security/Cryptography/DsaKeyTest.cs b/test/Renci.SshNet.Tests/Classes/Security/Cryptography/DsaKeyTest.cs new file mode 100644 index 000000000..831b49a73 --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Security/Cryptography/DsaKeyTest.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Renci.SshNet.Security; +using Renci.SshNet.Tests.Common; + +namespace Renci.SshNet.Tests.Classes.Security.Cryptography +{ + [TestClass] + public class DsaKeyTest : TestBase + { + private static DsaKey GetDsaKey(string fileName, string passPhrase = null) + { + using (var stream = GetData(fileName)) + { + return (DsaKey)new PrivateKeyFile(stream, passPhrase).Key; + } + } + + // This is just to line up any differences in the assertion message. + private static void AssertEqual(byte[] actualBytes, string expectedHex) + { + string actualHex = BitConverter.ToString(actualBytes).Replace("-", ""); + + Assert.AreEqual(expectedHex, actualHex, + $"{Environment.NewLine}Expected: {expectedHex}{Environment.NewLine} Actual: {actualHex}"); + } + + // These tests generated by converting the keys to PKCS8, importing them to BCL DSA, + // and printing out the expected DSAParameter values. + + // Some useful commands: + + // Generate a new params file with specific parameters: + // openssl genpkey -genparam -algorithm dsa -pkeyopt pbits:1024 -pkeyopt qbits:160 -out dsa.1024.params + + // Generate PKCS8 key file from the params: + // openssl genpkey -paramfile dsa.1024.params -out dsa.1024.txt + + // Convert to PKCS1: + // openssl pkcs8 -in dsa.1024.txt -nocrypt -traditional -out dsa.1024.pkcs1.txt + + // Convert PKCS1 to ssh.com: + // puttygen dsa.1024.pkcs1.txt -O private-sshcom -o dsa.1024.ssh2.txt + + // Convert to PKCS8: + // openssl pkcs8 -topk8 -nocrypt -in Key.DSA.txt -out Key.DSA.PKCS8.txt + + /* + + using IndentedTextWriter tw = new(Console.Out); + + foreach (string filePath in Directory.EnumerateFiles(dir, "*.DSA.*txt")) + { + string pkFile = Path.GetFileNameWithoutExtension(filePath); + + tw.WriteLine("[TestMethod]"); + tw.WriteLine($"public void {pkFile.Replace('.', '_')}()"); + tw.WriteLine("{"); + tw.Indent++; + + tw.WriteLine($"DsaKey dsaKey = GetDsaKey(\"{pkFile}.txt\");"); + tw.WriteLine(); + tw.WriteLine("DSAParameters p = dsaKey.GetDSAParameters();"); + tw.WriteLine(); + + using DSA dsa = DSA.Create(); + + dsa.ImportFromPem(File.ReadAllText(filePath)); + + DSAParameters p = dsa.ExportParameters(true); + + WriteParamAssert(p.P); + WriteParamAssert(p.G); + WriteParamAssert(p.Y); + WriteParamAssert(p.Q); + WriteParamAssert(p.X); + + tw.Indent--; + tw.WriteLine("}"); + tw.WriteLine(); + } + + void WriteParamAssert(byte[] bytes, [CallerArgumentExpression(nameof(bytes))] string name = null) + { + tw.WriteLine($"AssertEqual({name}, \"{Convert.ToHexString(bytes)}\");"); + } + */ + + [TestMethod] + public void Key_DSA() + { + DsaKey dsaKey = GetDsaKey("Key.DSA.txt"); + + Assert.AreEqual(1024, dsaKey.P.BitLength); + Assert.AreEqual(160, dsaKey.Q.BitLength); + + DSAParameters p = dsaKey.GetDSAParameters(); + + AssertEqual(p.P, "B565DDF69ED8EE2AC2C00AF794944A15F428C50D3FECA5FEE4F79461FD4FF669B671D296B4F19D35970A5D20F752847826849C30E12F19B8682BF5020E01FF2BDC338BB7E2A92668D2F2D8B880C62A9DA1B65C346EB53EAC2A779203929DFC2C1B27F2D99BD76C6EF4D6A5A547CE892101D5CC82AF8CC564CAE6D30B5DC89415"); + AssertEqual(p.G, "0E549E37E14011DC79FA940E6758D7C53AE5151F75BB9C968FD054098883F1EC651B7713BDAAD0CD4DB5A458BDDCF6AC79F81ECE95EE3133B72FC973EB3505180C7085952F947B6C7721E26B91D7D9907F5E3CFDB9CB9034278FDCFBC5D7BD06A3E330399DFC35DE8CB93EDC9DEDDDCAAB9B440CCF0A8957488709178D40373D"); + AssertEqual(p.Y, "9BC2066506AD4BB33A01F3484CD586E1323B6766914232DBF7F316248203EDCFD5438CDD4D9746DE4A64D068FD9F8C6B7A8A4AE4D99801D8FCAED15F3C18265E5B6C2EAB7E6C1717929C56FFFED60B6F563975B7B7DD3249387E716B967EBF5F57B99FE1097FEAEFD9220A5036F9CD61ECDACEDDDF2BC1178C8D5D01712E4311"); + AssertEqual(p.Q, "AEFA2364A9FFD838062362B1D20871665807C461"); + AssertEqual(p.X, "18463B393E02299EA1BF7AE04F3E1EC40D97275E"); + } + + + [TestMethod] + public void Key_SSH2_DSA_Encrypted_Des_CBC_12345() + { + DsaKey dsaKey = GetDsaKey("Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt", "12345"); + + Assert.AreEqual(1024, dsaKey.P.BitLength); + Assert.AreEqual(160, dsaKey.Q.BitLength); + + DSAParameters p = dsaKey.GetDSAParameters(); + + AssertEqual(p.P, "8F20C8715C86456190F8B0921923143A8DEB0DA71564EA572F6C2E316CDFCA0458B329030F7E735DF67AB529B4084D43A2595005591A596FFF7579864B120F20A17BD598E487EC3A587A5F475C642C012B2B04CD094FC8C16A02D6DCF6EBFBA1458DCC17EF11BB55E9637F5667265DFEDDCA8DC40555DB04574F97534A0BB5BD"); + AssertEqual(p.G, "04B63D3DBA0C7F4DD97A6E8ED7EA65F5EE17205602317188F954D38D83693A20799116B88BBE261A4BAF26201E121B78E6BEFC5B5C01AA4756D6054AEFF749F4C29C7172EB8DA02172949432BAA524CD2B141DBD1631C9DC67FB4142DF8D267441D59A7DB30F27DB4A1CA60DD654F75A5D3B5CE2993CDCD601EB2C06E9414FBE"); + AssertEqual(p.Y, "17F837B9770A37DCB325F56F08BC0E1D63C435C2C85DC9C006273872FAA605F5D09CC03D4F83EE046CF8D83E6CC9A29B15ACAF7BBFD421C7261C5CCAFF887867806C1DF6760DCF348AC7AEB9BE44A3CF27895A9790F9FBCD57708B22D3F4D72A5B52B78CFACF95CF5C28AF98E82CA7034B85F4A10D1121564D1079DE92E2EE16"); + AssertEqual(p.Q, "D8D7E6D3B7BB3688D998A011380BD85D5910B279"); + AssertEqual(p.X, "23FA5A574AF1197B185B88008A7A7527899FD092"); + } + + [TestMethod] + public void Key_SSH2_DSA() + { + DsaKey dsaKey = GetDsaKey("Key.SSH2.DSA.txt"); + + Assert.AreEqual(1024, dsaKey.P.BitLength); + Assert.AreEqual(160, dsaKey.Q.BitLength); + + DSAParameters p = dsaKey.GetDSAParameters(); + + AssertEqual(p.P, "E0921AE3B1C58FC3401A301BB7E44B7D00266C0DE1F0CF5D3A324DA01AC70A4E5A71895C58603F8225F8648A9696B19D45A46A32A931CD5BD6185AC0F58DE2CC684FC4116FA4805B7D04AE74FFA54B6CC6AA059641656B8C6F59280D02E1C0564501AE03EC278B077A149B3459F723AABC46095EDA3C870C74CEA8F8F62845E1"); + AssertEqual(p.G, "2BBCFEC01A1E6363ECF179045BE76CF17146A644191560BFDF7AB749362F61A0CF1968EE3EF9782CF3031BA66A04721F43B1776DB9F8EA5D6A64EF2C253E371D861B1BDCCC4901E11E16D4D1B2EDBF2144CAA8A3C720E82888FCECF2AEBD0315A1575D1A0C351D6162E4A8F57FB4C1A5E92FE30BBBD324B9A166E9B8054BFC31"); + AssertEqual(p.Y, "B75F681CF20FCAFFFC99B31A629BBC23A92F8F5FFDEC48F0D2778DEC277D70664BA883C1C13C941C442F292026E01BCD3F42DEDC90E7DD115ACA14667FEF11AC0992E1DD4C8EB08EB3A7E67B22E7936913A513C5676C741F5B9D211021CD17A1C9F8FA3AC0079FB25C96BFA6A83868ABB9C87C2DED4A05B1147E60F78F3EEE4B"); + AssertEqual(p.Q, "BBE34624549755A532514F177D3328B4371DEDE3"); + AssertEqual(p.X, "66504B5D937CD2319275BA6527352A39CC2F94C4"); + } + + [TestMethod] + public void Key_DSA_3072_256() + { + // Not supported by OpenSSH but easy enough to test here. + + var keyString = """ + -----BEGIN DSA PRIVATE KEY----- + MIIE1gIBAAKCAYEA6Rb9Cogx64CZcrnP35Nr8W9sjcUqoCpSfZrJdvIhTxgyAkEl + d9U868azCTZ1QGvUuPOCGB5Ll8nRQ6QSK9bicSQ3q3C69BcjzTfZcFuZEi533wEl + 9m9xZTbMAW8643jgARKszoNWIeTGsk2JcXQ2c6+VfPOml6F7yMv3KDIpqjUQlpK+ + RoZn5Eg5R2+2VqcNwwqbP8dyQGnGmgX2hlzbWx9Cld8NTG2b0B6Taaea4zqiMLHC + MeOUh+3WkbGM+aVQ7kthQbkhTEMmZeB/zyJrlEQGmKJl/cuvx69iO8nPSIZJDSEp + sZwg6p+Fqlm0+IyaFaxJMF+SuQirYLR+ee5oor5lcWS3Szaaikz0u1ONO3ndWAmO + eEHq7BL4vuHc/SDxfO5RouhEirCUFtGwtYq/Kf7x53ccd0Jmlj1FalubKHaiyYSg + baHrvN8rFv12XDMI0vPDeOxLVaxaB3zSJTz/ZwDjUcFx7PbuWsvOoSZeagd0sCm9 + aVNQoe7dy/5YCrM7AiEA1oznSuokVWnPt9GaBlmggzk9lMjZ/XVr9MFuDzWDycsC + ggGAYVSdm43WuonZQ40M9An5oBZuWsv22ZNXFnMXzcrKkvdslWGP+za/Ipa+tm+j + YE82vuqCdGjDuDJViJbrx2FProe4J/to2Gpd0jc0uV2c1PFO0B1hDk7tkglQyhmV + q9GAQatA4XUM5cIiInseF5JxHMmfgwEKmfIUDotWnpFQImMvehHxZfI29ngrB2ND + Ba4wBIj1Ua4UqsQi2beNfope7+XSsBQm98prswjvdR6n6wg9Xkvtn4ti3lLbEHzO + ZlwnxoRxFlNE/s2RaygGySRRYAe9TPriGWeMpWirYF3/SDAQyt5qDJsPQU0fWAuF + WsuxDlOsaLmJNJNnvySwx5Qmiw4e17U+6/IKptVbOHWowq05PxZKSVYmcP5+Jbt2 + eVzozKPNGkEw6aBy18l4t+ehx7fiUha7uCPaZ+VTPodLRZE6sKT1uiKOBtPqhrsp + bxKJ3zzmF1KZpoOG++1JHzqOPL+Npokd+K3ce9vNfU7eWTYP2DuiooyF9EktJt7w + AVilAoIBgBCpFSwJxhdAIQRefnQmweW+eXRbx8KL+8t53YsPS/fQhdzIEVEi5LWz + cJ0iAU2l2Mj43AC/6yehA8KF6vJEy4LWSXNwoGVqEoSA2LxBaomLxLARqlFyfCZN + yBlUVfXDiqFw2ajRaLQKUIxWbfC6ard69brWZuS61YQd8Jrk782VAa+sQO6Ca6Ii + vtyyRjNKxbeYwKLkZydi9JFdYYR6kmVG1ge7spemMHlozza6VvNcDU3hE4T4PwbY + Ns555ihK79EWGO2zCNjhPEIN34IjN2WjbUidCBWPGgXLhw9BYyEEREhfP1QUTe2q + S3gup/j8//v46O+OZFcB6g/MZB2IFpRRSw16qM2+pNZFKVTvXs/dUq5tEXylbMCg + +7jS5eLVAkDYFwSUai4Ht9VOHGASz7VyfPfngL8nx+KLNyegB12OLwr6ho5tc9dE + Rib63kEJnsK6CzIjg1+iFblWy4pQsHnKEgvBWxk4+sLyEZFTtjCe7KjigdH6WRDH + U5ejA3bnFQIhAIkT3ff8AjkByyJg1CRkpwDCvFag1fbPXEdg1Ru1E+l/ + -----END DSA PRIVATE KEY----- + """; + + using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(keyString)); + + DsaKey dsaKey = (DsaKey)new PrivateKeyFile(stream).Key; + + Assert.AreEqual(3072, dsaKey.P.BitLength); + Assert.AreEqual(256, dsaKey.Q.BitLength); + + DSAParameters p = dsaKey.GetDSAParameters(); + + AssertEqual(p.P, "E916FD0A8831EB809972B9CFDF936BF16F6C8DC52AA02A527D9AC976F2214F183202412577D53CEBC6B3093675406BD4B8F382181E4B97C9D143A4122BD6E2712437AB70BAF41723CD37D9705B99122E77DF0125F66F716536CC016F3AE378E00112ACCE835621E4C6B24D8971743673AF957CF3A697A17BC8CBF7283229AA35109692BE468667E44839476FB656A70DC30A9B3FC7724069C69A05F6865CDB5B1F4295DF0D4C6D9BD01E9369A79AE33AA230B1C231E39487EDD691B18CF9A550EE4B6141B9214C432665E07FCF226B94440698A265FDCBAFC7AF623BC9CF4886490D2129B19C20EA9F85AA59B4F88C9A15AC49305F92B908AB60B47E79EE68A2BE657164B74B369A8A4CF4BB538D3B79DD58098E7841EAEC12F8BEE1DCFD20F17CEE51A2E8448AB09416D1B0B58ABF29FEF1E7771C774266963D456A5B9B2876A2C984A06DA1EBBCDF2B16FD765C3308D2F3C378EC4B55AC5A077CD2253CFF6700E351C171ECF6EE5ACBCEA1265E6A0774B029BD695350A1EEDDCBFE580AB33B"); + AssertEqual(p.G, "61549D9B8DD6BA89D9438D0CF409F9A0166E5ACBF6D99357167317CDCACA92F76C95618FFB36BF2296BEB66FA3604F36BEEA827468C3B832558896EBC7614FAE87B827FB68D86A5DD23734B95D9CD4F14ED01D610E4EED920950CA1995ABD18041AB40E1750CE5C222227B1E1792711CC99F83010A99F2140E8B569E915022632F7A11F165F236F6782B07634305AE300488F551AE14AAC422D9B78D7E8A5EEFE5D2B01426F7CA6BB308EF751EA7EB083D5E4BED9F8B62DE52DB107CCE665C27C68471165344FECD916B2806C924516007BD4CFAE219678CA568AB605DFF483010CADE6A0C9B0F414D1F580B855ACBB10E53AC68B989349367BF24B0C794268B0E1ED7B53EEBF20AA6D55B3875A8C2AD393F164A49562670FE7E25BB76795CE8CCA3CD1A4130E9A072D7C978B7E7A1C7B7E25216BBB823DA67E5533E874B45913AB0A4F5BA228E06D3EA86BB296F1289DF3CE6175299A68386FBED491F3A8E3CBF8DA6891DF8ADDC7BDBCD7D4EDE59360FD83BA2A28C85F4492D26DEF00158A5"); + AssertEqual(p.Y, "10A9152C09C6174021045E7E7426C1E5BE79745BC7C28BFBCB79DD8B0F4BF7D085DCC8115122E4B5B3709D22014DA5D8C8F8DC00BFEB27A103C285EAF244CB82D6497370A0656A128480D8BC416A898BC4B011AA51727C264DC8195455F5C38AA170D9A8D168B40A508C566DF0BA6AB77AF5BAD666E4BAD5841DF09AE4EFCD9501AFAC40EE826BA222BEDCB246334AC5B798C0A2E4672762F4915D61847A926546D607BBB297A6307968CF36BA56F35C0D4DE11384F83F06D836CE79E6284AEFD11618EDB308D8E13C420DDF82233765A36D489D08158F1A05CB870F4163210444485F3F54144DEDAA4B782EA7F8FCFFFBF8E8EF8E645701EA0FCC641D881694514B0D7AA8CDBEA4D6452954EF5ECFDD52AE6D117CA56CC0A0FBB8D2E5E2D50240D81704946A2E07B7D54E1C6012CFB5727CF7E780BF27C7E28B3727A0075D8E2F0AFA868E6D73D7444626FADE41099EC2BA0B3223835FA215B956CB8A50B079CA120BC15B1938FAC2F2119153B6309EECA8E281D1FA5910C75397A30376E715"); + AssertEqual(p.Q, "D68CE74AEA245569CFB7D19A0659A083393D94C8D9FD756BF4C16E0F3583C9CB"); + AssertEqual(p.X, "8913DDF7FC023901CB2260D42464A700C2BC56A0D5F6CF5C4760D51BB513E97F"); + } + } +}