Added support for ssh.com private keys.

Fixes issue #1987.
This commit is contained in:
Gert Driesen
2014-05-02 09:41:41 +00:00
parent 78f13742b3
commit 43836a5b50
9 changed files with 259 additions and 31 deletions
@@ -16,6 +16,7 @@
New Features:
* Improved accuracy of IsConnected on .NET
* Added support for ssh.com (SSH-2) private keys (issue #1987)
Fixes:
@@ -775,10 +775,24 @@
<Link>Renci.SshNet.snk</Link>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Renci.SshNet.Tests\Data\Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt">
<Link>Data\Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt</Link>
</EmbeddedResource>
<EmbeddedResource Include="..\Renci.SshNet.Tests\Data\Key.SSH2.DSA.txt">
<Link>Data\Key.SSH2.DSA.txt</Link>
</EmbeddedResource>
<EmbeddedResource Include="..\Renci.SshNet.Tests\Data\Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt">
<Link>Data\Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt</Link>
</EmbeddedResource>
<EmbeddedResource Include="..\Renci.SshNet.Tests\Data\Key.SSH2.RSA.txt">
<Link>Data\Key.SSH2.RSA.txt</Link>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ProjectExtensions>
<VisualStudio>
<UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
<UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
</VisualStudio>
</ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
@@ -146,6 +146,113 @@ namespace Renci.SshNet.Tests.Classes
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_DSA()
{
using (var stream = this.GetData("Key.SSH2.DSA.txt"))
{
new PrivateKeyFile(stream);
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_RSA()
{
using (var stream = this.GetData("Key.SSH2.RSA.txt"))
{
new PrivateKeyFile(stream);
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_Encrypted_DSA_DES_CBC()
{
using (var stream = this.GetData("Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt"))
{
new PrivateKeyFile(stream, "12345");
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_Encrypted_RSA_DES_CBC()
{
using (var stream = this.GetData("Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt"))
{
new PrivateKeyFile(stream, "12345");
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_Encrypted_ShouldThrowSshExceptionWhenPassphraseIsWrong()
{
using (var stream = this.GetData("Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt"))
{
try
{
new PrivateKeyFile(stream, "34567");
Assert.Fail();
}
catch (SshException ex)
{
Assert.IsInstanceOfType(ex, typeof(SshException));
Assert.IsNull(ex.InnerException);
Assert.AreEqual("Invalid passphrase.", ex.Message);
}
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_Encrypted_ShouldThrowSshPassPhraseNullOrEmptyExceptionWhenPassphraseIsNull()
{
using (var stream = this.GetData("Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt"))
{
try
{
new PrivateKeyFile(stream, null);
Assert.Fail();
}
catch (SshPassPhraseNullOrEmptyException ex)
{
Assert.IsInstanceOfType(ex, typeof(SshPassPhraseNullOrEmptyException));
Assert.IsNull(ex.InnerException);
Assert.AreEqual("Private key is encrypted but passphrase is empty.", ex.Message);
}
}
}
[TestMethod]
[Owner("drieseng")]
[TestCategory("PrivateKey")]
public void Test_PrivateKey_SSH2_Encrypted_ShouldThrowSshPassPhraseNullOrEmptyExceptionWhenPassphraseIsEmpty()
{
using (var stream = this.GetData("Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt"))
{
try
{
new PrivateKeyFile(stream, string.Empty);
Assert.Fail();
}
catch (SshPassPhraseNullOrEmptyException ex)
{
Assert.IsInstanceOfType(ex, typeof(SshPassPhraseNullOrEmptyException));
Assert.IsNull(ex.InnerException);
Assert.AreEqual("Private key is encrypted but passphrase is empty.", ex.Message);
}
}
}
[TestMethod]
[Owner("olegkap")]
[TestCategory("PrivateKey")]
@@ -0,0 +1,12 @@
---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----
P2/56wAAAgoAAAAmZGwtbW9kcHtzaWdue2RzYS1uaXN0LXNoYTF9LGRoe3BsYWlufX0AAA
AIM2Rlcy1jYmMAAAHIxrKV3QJkISlRHox4JYLlWJG0ccdEfMmDF3IXW+up6nUQZeWyOS9/
csXyclUtJ/adA0qkpH5hcaIIOr1jQgvPxjMy7o3I5WJ/MvKB7omSDYG82wKZAaZQeSCaqY
Cn1DYHz6Xou5BIle37f0y71tq3d7YFCh33BjbCM8KHsar3TgRf3oegaawZyZuLnnZRy0L+
xdsdqgntc4fyhpmCmfwIYhWwD2R9P56FXTw59VuE0r/LxxLJW8Cq2R8i7bN0lb2ezb3qnU
SykSJ5PYvoW6e7HIZ3GEr9d+wDhqp0SKWHl+shed3e5xi1NF7fRR23bZ1xlAk3FdrBrH1b
aWYW4JFzdVc33Pg5oHLe+q5NQ3vQpMxXlN2IG9cdAigWKNJMdvLuxiYi+6lzNIGAH+yb01
56ksXYgUs65Gqnj0GMYVmf//8NN0gRKXl5dT57LyuU42jRePwANMyO/n9QOi9OKeea8Tku
f6ZMRiHQLDiZx2ShEXEhHRwmv42jt2xjuHNMbY0dZTKZEkFMUGVFVbkl5MEmN8Fj59rHJn
0adWRPLH5smwM8WXbaYt+E0r628UNZbedXCcjZ5c5egpxCBxRrSJqnK1f0l899fulB
---- END SSH2 ENCRYPTED PRIVATE KEY ----
@@ -0,0 +1,12 @@
---- BEGIN SSH2 ENCRYPTED PRIVATE 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==
---- END SSH2 ENCRYPTED PRIVATE KEY ----
@@ -0,0 +1,23 @@
---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----
Comment: "imported-openssh-key"
P2/56wAAA/MAAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS
1wa2NzMXYyLW9hZXB9fQAAAAgzZGVzLWNiYwAAA6A6Wt6IR5cz0PCsSJDOjcs3MdQscfdN
p54qdp1DPUltmJJCwxU31izkP+mdRoSBgTwAd2jIvq+xdxhe8ZYqFtf6QvbIsZMWC/giRU
mDcHLnGxWgWDY9adh5AscC7FOTEXhME/uNkGdYp3x5lmmC8uHC6cKt4RIqq7elBnOlnNvQ
Mmr0mJluLsqsu4pr57bM0TJ37s7ov33zLuINO40kZ1Yc9A0NebWoGZPB8Wq2UCVskAXM6v
/cJQRNu73Hn77vbLhzVEKnOokRdzDncKRMkItwJV0iz4/kuAcf6OdgwmPQ2e/lz7NR+5yI
a37bKeKKnOR4JGAIjSoGu/Skc8GKfcEPAnnZ2xYJ15fNPy5fBbga8SAQGJ3UCx1cPsaaTA
pt0fJ5si9X26V8XrK+T0mRBZPjj0s/eb2jOAzgWPCur4N3hqijyQO7RoTjh1jcL7ONT9zv
HJjqqaTXMqQGPeHEpSamyTyXMDwMZGRAaGQDM1awWX3jq1uNgGW/3/0TV/rq69nVso3dVv
DiSPGdK+GgZzVWsfZv/5X17YrQLEkr0s6Mpsm5L3j4NYtcppMR2bV3vsA9eCWb99lnvhIt
bR12MY21S/o1CwAtSYZojylcxZcWVaoZ20OTzrzTjsCOLxyIiQAzdKFG49F+YCUIJYM8ne
NA+jW34x7u1L+NKVHMPUx7gcYEE6RapEOQRx1p+9HV7efGNIOmpUCZJZ4itHFUSOxi6eao
FT8J2si0edrDMnWDdRBhOMQNkSt9zViWpIvLLx4g9jih6XEgAOsRTyD9GnpM6HBspd+Jsy
XfSgbfBsJpnHOpjx/mTiQ0AkfkBeOvgfGRWyjjSfJaxojnE59JHo1HR6+9XFuWGXjYbIYo
gpcLnJ1JY/to21ABQgsNCxH1VocPNGTKodcDpf91bYKqS4peri9CKSXsRP3Y4K7cffwXAH
kWFvvfw65pZHx9xTJfgv986jfn1JCDrO/6UOwnyrL2X8g9wrCEQ1R0QNQ82mx+cXu4atVO
oA2Hjj5OegwHScGTZ9LaC+CRyW847QrJF7sPDcYxbEUTvgSSGZ+dymAjFFNMlBdMAEmtNB
P0dBUFObJsP0vTyZrxnueal6zleQY1ox5csO3XF2nKpO+KjfuVd6qM6/7sBpqpA1bycM2O
8k6Wq2hSEhPVovarOb878P1Uiuzr4eb/eIt7tC1SHrcfAoBIYnUYVzUB5g4WIIy3qxEaGm
36ETbq3U+vVk9BatC4
---- END SSH2 ENCRYPTED PRIVATE KEY ----
@@ -0,0 +1,23 @@
---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----
Comment: "imported-openssh-key"
P2/56wAAA+4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS
1wa2NzMXYyLW9hZXB9fQAAAARub25lAAADnwAAA5sAAAARAQABAAAIAJoF62h2fdR02ncN
StzmpuKmvnAdQdbkAlzek1LBuXrJX2wsF6XcqsLSAvAHhvy71q6TK9GBQOgpGH3vn+Tjo2
POKZL/2u9sN52yanbQw0oFCDOHBy2xCRw/+NoOofcVtR59eeSKMy79gmkGQnsCliqdznXX
GUuZSxHIQQZhfcCbYuGHCjA6B8dsFeVzrzwju1lr3eEtuChCpgnWHevbyasOYWTj3/Mt/W
x0XEAQQB9jutL4VpzNzsE1D47dLfVWAflYppCcxFH8LS7Sq5NEUFs0Vje1LJK6XhrfieH8
YjQfpsikbpcTIebIHP74W3RjBhcKt7eBoHhcSj8vU5pPj3EAAAgA1cGcbXOCOZ54AvKgLu
7ibJfchKnHAOTG1BxMkxy/0pCoTPSDAXiHyNxjN4qjk8nqg1dfsPdjXCRpW+eBsQMhJZ1P
qIh4WB7fkK7+Wm4twhqQhLQ7uPwjnNflNYqRizeYJ5wUofuK2I30DgiNQjw2hV6Tos8ikv
VA7GCedjnScRYbUauV8rrXC6+Xny6mD2F3BGE/CQdYTZXNs7mKIkCpb9ziJXMM7juCfBSO
qQSwZy7XeEKuMqzP9diHCByqqCcippHc0RZTLONCTWa7ndoqAdhZpkk/pDqmFTEWEJuUPN
8jWCqukS4RzxXEE3lbzZbInYmsldlXIeboQoFJM+4WXwAABAC84bFrW0L1E2GtgVp2uZoi
Ve1ynoXmr7nHfCmbVJS5nEZBYTKT+62/VdlDiLm7QlnQqpkxyYw+d3pM3rvHLAb/6ntoIs
l5HJG4WWDl0vQVwjQR5CQpRrtLQiDIhMAxBdKGS5Ql9RpdQKvhFI+7STzttqcDCerOEVQG
SlT3vL52UAAABADgL9iwue1u7D6IrL26PwudbJSZ3lPt3YkZl25LDIVrGU5U0xPH3o+FQP
GTkQv5qrc43I96Jc+P9d7A2EyE6j+ArtSWKq5OrVUAhg16O49rc2ve5v4w53fOH0P20BHv
k1QxIEg5vHAJwgfZMJ68ub6+73gtkSHJ6W78T2Qpo+dHtwAABAD0Fti2uUlPoiDmhOIZUF
7qs3pxkC+OjZcWBgi7IsEjiBxZVmYH3iD722gmbA4om6ulAxs7F2q10l0YXY1ds250FOxQ
swT2var18A0MjOWa7RgHChBqiHxi+krLW6F9UvllWYmb4bz4E1T3rfE4OB0h5UW2oHD4Hn
smPA/t9MmWmQ==
---- END SSH2 ENCRYPTED PRIVATE KEY ----
@@ -342,6 +342,16 @@
<Link>Renci.SshNet.snk</Link>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Key.SSH2.RSA.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt" />
<EmbeddedResource Include="Data\Key.SSH2.DSA.txt" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
+56 -30
View File
@@ -16,17 +16,46 @@ using System.Diagnostics.CodeAnalysis;
namespace Renci.SshNet
{
/// <summary>
/// Represents private key information
/// Represents private key information.
/// </summary>
/// <example>
/// <code source="..\..\Renci.SshNet.Tests\Data\Key.RSA.txt" language="Text" title="Private RSA key example" />
/// </example>
/// <remarks>
/// <para>
/// Supports RSA and DSA private key in both <c>OpenSSH</c> and <c>ssh.com</c> format.
/// </para>
/// <para>
/// The following encryption algorithms are supported:
/// <list type="bullet">
/// <item>
/// <description>DES-EDE3-CBC</description>
/// </item>
/// <item>
/// <description>DES-EDE3-CFB</description>
/// </item>
/// <item>
/// <description>DES-CBC</description>
/// </item>
/// <item>
/// <description>AES-128-CBC</description>
/// </item>
/// <item>
/// <description>AES-192-CBC</description>
/// </item>
/// <item>
/// <description>AES-256-CBC</description>
/// </item>
/// </list>
/// </para>
/// </remarks>
public class PrivateKeyFile : IDisposable
{
private static readonly Regex PrivateKeyRegex = new Regex(@"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k<keyName> PRIVATE KEY *-+",
#if SILVERLIGHT
private static readonly Regex _privateKeyRegex = new Regex(@"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n(Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k<keyName> PRIVATE KEY *-+", RegexOptions.Multiline);
RegexOptions.Multiline);
#else
private static readonly Regex _privateKeyRegex = new Regex(@"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n(Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k<keyName> PRIVATE KEY *-+", RegexOptions.Compiled | RegexOptions.Multiline);
RegexOptions.Compiled | RegexOptions.Multiline);
#endif
private Key _key;
@@ -101,7 +130,7 @@ namespace Renci.SshNet
using (var sr = new StreamReader(privateKey))
{
var text = sr.ReadToEnd();
privateKeyMatch = _privateKeyRegex.Match(text);
privateKeyMatch = PrivateKeyRegex.Match(text);
}
if (!privateKeyMatch.Success)
@@ -177,7 +206,7 @@ namespace Renci.SshNet
throw new SshException("Invalid SSH2 private key.");
}
var totalLength = reader.ReadUInt32(); // Read total bytes length including magic number
reader.ReadUInt32(); // Read total bytes length including magic number
var keyType = reader.ReadString();
var ssh2CipherName = reader.ReadString();
var blobSize = (int)reader.ReadUInt32();
@@ -187,12 +216,15 @@ namespace Renci.SshNet
{
keyData = reader.ReadBytes(blobSize);
}
//else if (ssh2CipherName == "3des-cbc")
//{
// var key = GetCipherKey(passPhrase, 192 / 8);
// var ssh2Сipher = new TripleDesCipher(key, null, null);
// keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
//}
else if (ssh2CipherName == "3des-cbc")
{
if (string.IsNullOrEmpty(passPhrase))
throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
var key = GetCipherKey(passPhrase, 192 / 8);
var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
}
else
{
throw new SshException(string.Format("Cipher method '{0}' is not supported.", cipherName));
@@ -204,7 +236,7 @@ namespace Renci.SshNet
var decryptedLength = reader.ReadUInt32();
if (decryptedLength + 4 != blobSize)
if (decryptedLength > blobSize - 4)
throw new SshException("Invalid passphrase.");
if (keyType == "if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}")
@@ -245,22 +277,19 @@ namespace Renci.SshNet
private static byte[] GetCipherKey(string passphrase, int length)
{
List<byte> cipherKey = new List<byte>();
var cipherKey = new List<byte>();
using (var md5 = new MD5Hash())
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(passphrase);
var hash = md5.ComputeHash(passwordBytes.ToArray()).AsEnumerable();
var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
var hash = md5.ComputeHash(passwordBytes);
cipherKey.AddRange(hash);
while (cipherKey.Count < length)
{
hash = passwordBytes.Concat(hash);
hash = md5.ComputeHash(hash.ToArray());
hash = passwordBytes.Concat(hash).ToArray();
hash = md5.ComputeHash(hash);
cipherKey.AddRange(hash);
}
}
@@ -289,25 +318,22 @@ namespace Renci.SshNet
if (binarySalt == null)
throw new ArgumentNullException("binarySalt");
List<byte> cipherKey = new List<byte>();
var cipherKey = new List<byte>();
using (var md5 = new MD5Hash())
{
var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
// Use 8 bytes binary salkt
var initVector = passwordBytes.Concat(binarySalt.Take(8));
var hash = md5.ComputeHash(initVector.ToArray()).AsEnumerable();
// Use 8 bytes binary salt
var initVector = passwordBytes.Concat(binarySalt.Take(8)).ToArray();
var hash = md5.ComputeHash(initVector);
cipherKey.AddRange(hash);
while (cipherKey.Count < cipherInfo.KeySize / 8)
{
hash = hash.Concat(initVector);
hash = md5.ComputeHash(hash.ToArray());
hash = hash.Concat(initVector).ToArray();
hash = md5.ComputeHash(hash);
cipherKey.AddRange(hash);
}
}
@@ -401,7 +427,7 @@ namespace Renci.SshNet
{
var length = (int)base.ReadUInt32();
length = (int)(length + 7) / 8;
length = (length + 7) / 8;
var data = base.ReadBytes(length);
var bytesArray = new byte[data.Length + 1];