Skip to content

Commit

Permalink
Support for Ed25519 Host- and Private-Keys
Browse files Browse the repository at this point in the history
  • Loading branch information
darinkes committed Dec 6, 2018
1 parent b8a9736 commit 2b51cf9
Show file tree
Hide file tree
Showing 13 changed files with 1,435 additions and 8 deletions.
18 changes: 18 additions & 0 deletions THIRD-PARTY-NOTICES.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,21 @@ SHA512
written by Christian Winnerlein (CodesInChaos)
public domain
directly from the specification

License notice for BCrypt
-------------------------

Copyright (c) 2006 Damien Miller <[email protected]>
Copyright (c) 2010 Ryan D. Emerle

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
5 changes: 4 additions & 1 deletion src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@
<Compile Include="..\Renci.SshNet\Security\Cryptography\AsymmetricCipher.cs">
<Link>Security\Cryptography\AsymmetricCipher.cs</Link>
</Compile>
<Compile Include="..\Renci.SshNet\Security\Cryptography\Bcrypt.cs">
<Link>Security\Cryptography\Bcrypt.cs</Link>
</Compile>
<Compile Include="..\Renci.SshNet\Security\Cryptography\BlockCipher.cs">
<Link>Security\Cryptography\BlockCipher.cs</Link>
</Compile>
Expand Down Expand Up @@ -987,4 +990,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
30 changes: 29 additions & 1 deletion src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,34 @@ public void ConstructorWithFileNameAndPassPhraseShouldBeAbleToReadFileThatIsShar
}
}

/// <summary>
/// A test for opening an openssh v1 keyfile where there is no passphrase.
///</summary>
[TestMethod()]
[Owner("bhalbright")]
[TestCategory("PrivateKey")]
public void TestOpenSshV1KeyFileNoPassphrase()
{
using (var stream = GetData("Key.OPENSSH.ED25519.txt"))
{
new PrivateKeyFile(stream);
}
}

/// <summary>
/// A test for opening an openssh v1 keyfile where there is a passphrase.
///</summary>
[TestMethod()]
[Owner("bhalbright")]
[TestCategory("PrivateKey")]
public void TestOpenSshV1KeyFileWithPassphrase()
{
using (var stream = GetData("Key.OPENSSH.ED25519.Encrypted.txt"))
{
new PrivateKeyFile(stream, "password");
}
}

private void SaveStreamToFile(Stream stream, string fileName)
{
var buffer = new byte[4000];
Expand All @@ -567,4 +595,4 @@ private string GetTempFileName()
return tempFile;
}
}
}
}
9 changes: 9 additions & 0 deletions src/Renci.SshNet.Tests/Data/Key.OPENSSH.ED25519.Encrypted.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABBg
HWh+J0IG6OfYxD74SoT9AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGFd
yflleGqSPOhgSYZf7ZQFlG0zEL9VDGC69UbtaaByAAAAoDLm8u8wFwlqjzZRfVxj
wzGTYFJFtfkHRqfFBE4xKgknHNRbCT1OQb7rgE7nZbUXIlb1NCTZLbXti9AYNZpz
ycvPD4Dc6lB03b8pNHoFVSkrCwxrWB5bKtIM4OZNcDK1lZDBEWE2aZXf9puRHbu3
ccrK/F5GjRi2pUa8qnfqThN1mNPZwFTx4oSKeRaUMdeHBrNwDtaxq32A6Q4KHoYO
KPM=
-----END OPENSSH PRIVATE KEY-----
8 changes: 8 additions & 0 deletions src/Renci.SshNet.Tests/Data/Key.OPENSSH.ED25519.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
c2gtZWQyNTUxOQAAACANCWZw0K8UGXDQC32WBuyzwFtTGBBr1VuZ43uzpTBjIgAA
AKBATgCiQE4AogAAAAtzc2gtZWQyNTUxOQAAACANCWZw0K8UGXDQC32WBuyzwFtT
GBBr1VuZ43uzpTBjIgAAAEAAzBF1MPUxrs+ycpJh28zzo/F3m6WcKO+orsSbR5Lw
KQ0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7OlMGMiAAAAFGVkMjU1MTkta2V5
LTIwMTgxMTI3AQIDBAUGBwgJ
-----END OPENSSH PRIVATE KEY-----
4 changes: 4 additions & 0 deletions src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,10 @@
<EmbeddedResource Include="Data\Key.ECDSA384.Encrypted.txt" />
<EmbeddedResource Include="Data\Key.ECDSA521.Encrypted.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Key.OPENSSH.ED25519.Encrypted.txt" />
<EmbeddedResource Include="Data\Key.OPENSSH.ED25519.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.
Expand Down
1 change: 1 addition & 0 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy

HostKeyAlgorithms = new Dictionary<string, Func<byte[], KeyHostAlgorithm>>
{
{"ssh-ed25519", data => new KeyHostAlgorithm("ssh-ed25519", new ED25519Key(), data)},
#if FEATURE_ECDSA
{"ecdsa-sha2-nistp256", data => new KeyHostAlgorithm("ecdsa-sha2-nistp256", new EcdsaKey(), data)},
{"ecdsa-sha2-nistp384", data => new KeyHostAlgorithm("ecdsa-sha2-nistp384", new EcdsaKey(), data)},
Expand Down
159 changes: 155 additions & 4 deletions src/Renci.SshNet/PrivateKeyFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
using System.Diagnostics.CodeAnalysis;
using Renci.SshNet.Security.Cryptography;

namespace Renci.SshNet
{
Expand All @@ -25,13 +26,16 @@ namespace Renci.SshNet
/// The following private keys are supported:
/// <list type="bullet">
/// <item>
/// <description>RSA in OpenSSH and ssh.com format</description>
/// <description>RSA in OpenSSL PEM and ssh.com format</description>
/// </item>
/// <item>
/// <description>DSA in OpenSSH and ssh.com format</description>
/// <description>DSA in OpenSSL PEM and ssh.com format</description>
/// </item>
/// <item>
/// <description>ECDSA 256/384/521 in OpenSSH format</description>
/// <description>ECDSA 256/384/521 in OpenSSL PEM format</description>
/// </item>
/// <item>
/// <description>ED25519 in OpenSSH key format</description>
/// </item>
/// </list>
/// </para>
Expand Down Expand Up @@ -214,6 +218,10 @@ private void Open(Stream privateKey, string passPhrase)
HostKey = new KeyHostAlgorithm(_key.ToString(), _key);
break;
#endif
case "OPENSSH":
_key = ParseOpenSshV1Key(decryptedData, passPhrase);
HostKey = new KeyHostAlgorithm(_key.ToString(), _key);
break;
case "SSH2 ENCRYPTED":
var reader = new SshDataReader(decryptedData);
var magicNumber = reader.ReadUInt32();
Expand Down Expand Up @@ -358,7 +366,145 @@ private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, strin
return cipher.Decrypt(cipherData);
}

#region IDisposable Members
/// <summary>
/// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec:
/// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
/// </summary>
/// <param name="keyFileData">the key file data (i.e. base64 encoded data between the header/footer)</param>
/// <param name="passPhrase">passphrase or null if there isn't one</param>
/// <returns></returns>
private ED25519Key ParseOpenSshV1Key(byte [] keyFileData, string passPhrase)
{
var keyReader = new SshDataReader(keyFileData);

//check magic header
var authMagic = Encoding.UTF8.GetBytes("openssh-key-v1\0");
var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length);
if (!authMagic.IsEqualTo(keyHeaderBytes))
{
throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
}

//cipher will be "aes256-cbc" if using a passphrase, "none" otherwise
var cipherName = keyReader.ReadString(Encoding.UTF8);
//key derivation function (kdf): bcrypt or nothing
var kdfName = keyReader.ReadString(Encoding.UTF8);
//kdf options length: 24 if passphrase, 0 if no passphrase
var kdfOptionsLen = (int)keyReader.ReadUInt32();
byte[] salt = null;
int rounds = 0;
if (kdfOptionsLen > 0)
{
var saltLength = (int)keyReader.ReadUInt32();
salt = keyReader.ReadBytes(saltLength);
rounds = (int)keyReader.ReadUInt32();
}

//number of public keys, only supporting 1 for now
var numberOfPublicKeys = (int)keyReader.ReadUInt32();
if (numberOfPublicKeys != 1)
{
throw new SshException("At this time only one public key in the openssh key is supported.");
}

//length of first public key section
keyReader.ReadUInt32();
var keyType = keyReader.ReadString(Encoding.UTF8);
if(keyType != "ssh-ed25519")
{
throw new SshException("openssh key type: " + keyType + " is not supported");
}

//read public key
var publicKeyLength = (int)keyReader.ReadUInt32(); //32
var publicKey = keyReader.ReadBytes(publicKeyLength);

//possibly encrypted private key
var privateKeyLength = (int)keyReader.ReadUInt32();
var privateKeyBytes = keyReader.ReadBytes(privateKeyLength);

//decrypt private key if necessary
if (cipherName == "aes256-cbc")
{
if (string.IsNullOrEmpty(passPhrase))
{
throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
}
if (string.IsNullOrEmpty(kdfName) || kdfName != "bcrypt")
{
throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
}

//inspired by the SSHj library (https://github.com/hierynomus/sshj)
//apply the kdf to derive a key and iv from the passphrase
var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
byte[] keyiv = new byte[48];
new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
byte[] key = new byte[32];
Array.Copy(keyiv, 0, key, 0, 32);
byte[] iv = new byte[16];
Array.Copy(keyiv, 32, iv, 0, 16);

//now that we have the key/iv, use a cipher to decrypt the bytes
var cipher = new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding());
privateKeyBytes = cipher.Decrypt(privateKeyBytes);
}
else if (cipherName != "none")
{
throw new SshException("cipher name " + cipherName + " for openssh key file is not supported");
}

//validate private key length
privateKeyLength = privateKeyBytes.Length;
if (privateKeyLength % 8 != 0)
{
throw new SshException("The private key section must be a multiple of the block size (8)");
}

//now parse the data we called the private key, it actually contains the public key again
//so we need to parse through it to get the private key bytes, plus there's some
//validation we need to do.
var privateKeyReader = new SshDataReader(privateKeyBytes);

//check ints should match, they wouldn't match for example if the wrong passphrase was supplied
int checkInt1 = (int)privateKeyReader.ReadUInt32();
int checkInt2 = (int)privateKeyReader.ReadUInt32();
if (checkInt1 != checkInt2)
{
throw new SshException("The checkints differed, the openssh key was not correctly decoded.");
}

//key type, we already know it is ssh-ed25519
privateKeyReader.ReadString(Encoding.UTF8);

//public key length/bytes (again)
var publicKeyLength2 = (int)privateKeyReader.ReadUInt32();
privateKeyReader.ReadBytes(publicKeyLength2);

//length of private and public key (64)
privateKeyReader.ReadUInt32();
var unencryptedPrivateKey = privateKeyReader.ReadBytes(32);
//public key (again)
privateKeyReader.ReadBytes(32);

//comment, we don't need this but we could log it, not sure if necessary
var comment = privateKeyReader.ReadString(Encoding.UTF8);

//The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
//until the total length is a multiple of the cipher block size.
var padding = privateKeyReader.ReadBytes();
for (int i = 0; i < padding.Length; i++)
{
if ((int)padding[i] != i + 1)
{
throw new SshException("Padding of openssh key format contained wrong byte at position: " + i);
}
}

return new ED25519Key(publicKey.Reverse(), unencryptedPrivateKey);
}

#region IDisposable Members

private bool _isDisposed;

Expand Down Expand Up @@ -426,6 +572,11 @@ public SshDataReader(byte[] data)
return base.ReadBytes(length);
}

public new byte[] ReadBytes()
{
return base.ReadBytes();
}

/// <summary>
/// Reads next mpint data type from internal buffer where length specified in bits.
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion src/Renci.SshNet/Renci.SshNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,11 @@
<Compile Include="Security\Chaos.NaCl\Internal\Sha512Internal.cs" />
<Compile Include="Security\Chaos.NaCl\MontgomeryCurve25519.cs" />
<Compile Include="Security\Chaos.NaCl\Sha512.cs" />
<Compile Include="Security\Cryptography\ED25519DigitalSignature.cs" />
<Compile Include="Security\Cryptography\EcdsaDigitalSignature.cs" />
<Compile Include="Security\Cryptography\EcdsaKey.cs" />
<Compile Include="Security\Cryptography\ED25519Key.cs" />
<Compile Include="Security\Cryptography\Bcrypt.cs" />
<Compile Include="Security\Cryptography\HMACMD5.cs" />
<Compile Include="Security\Cryptography\HMACSHA1.cs" />
<Compile Include="Security\Cryptography\HMACSHA256.cs" />
Expand Down Expand Up @@ -622,4 +625,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
Loading

0 comments on commit 2b51cf9

Please sign in to comment.