Skip to content

Commit

Permalink
Add export to PuTTYv3 Format
Browse files Browse the repository at this point in the history
- Add PuTTYv3 as new default format for
  PuTTY exports

- respect SshKeyGenerateInfo if exports
  get called for GeneratedPrivateKey without
  arguments.

- Rework cleanup public interface
  • Loading branch information
darinkes committed Mar 23, 2024
1 parent 65b7f1e commit b766957
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 77 deletions.
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SshNet.Keygen
=============
[SSH.NET](https://github.com/sshnet/SSH.NET) Extension to generate and export Authentication Keys in OpenSSH and PuTTY Format.
[SSH.NET](https://github.com/sshnet/SSH.NET) Extension to generate and export Authentication Keys in OpenSSH and PuTTY v2 and v3 Format.

[![License](https://img.shields.io/github/license/darinkes/SshNet.KeyGen)](https://github.com/darinkes/SshNet.KeyGen/blob/main/LICENSE)
[![NuGet](https://img.shields.io/nuget/v/SshNet.Keygen.svg?style=flat)](https://www.nuget.org/packages/SshNet.Keygen)
Expand Down Expand Up @@ -63,11 +63,11 @@ Console.WriteLine(client.RunCommand("hostname").Result);
```cs
var keyInfo = new SshKeyGenerateInfo
{
KeyFormat = SshKeyFormat.PuTTY
KeyFormat = SshKeyFormat.PuTTYv3
};
var key = SshKey.Generate("test.ppk", FileMode.Create, keyInfo);

var publicKey = key.ToPublic();
var publicKey = key.ToPublic(SshKeyFormat.OpenSSH);
var fingerprint = key.Fingerprint();

Console.WriteLine("Fingerprint: {0}", fingerprint);
Expand Down Expand Up @@ -100,17 +100,39 @@ client.Connect();
Console.WriteLine(client.RunCommand("hostname").Result);
```

### Generate an password protected RSA-2048 Key in Putty File, Show the Public Key and Connect with the Private Key
### Generate an password protected RSA-2048 Key in Putty v2 File, Show the Public Key and Connect with the Private Key

```cs
var keyInfo = new SshKeyGenerateInfo
{
KeyFormat = SshKeyFormat.PuTTY,
KeyFormat = SshKeyFormat.PuTTYv2,
Encryption = new SshKeyEncryptionAes256("12345")
};
var key = SshKey.Generate("test.ppk", FileMode.Create, keyInfo);

var publicKey = key.ToPublic();
var publicKey = key.ToPublic(SshKeyFormat.OpenSSH);
var fingerprint = key.Fingerprint();

Console.WriteLine("Fingerprint: {0}", fingerprint);
Console.WriteLine("Add this to your .ssh/authorized_keys on the SSH Server: {0}", publicKey);
Console.ReadLine();

using var client = new SshClient("ssh.foo.com", "root", key);
client.Connect();
Console.WriteLine(client.RunCommand("hostname").Result);
```

### Generate an password protected RSA-2048 Key in Putty v3 File with own Argon Options, Show the Public Key and Connect with the Private Key

```cs
var keyInfo = new SshKeyGenerateInfo
{
KeyFormat = SshKeyFormat.PuTTYv3,
Encryption = new SshKeyEncryptionAes256("12345", new PuttyV3Encryption { KeyDerivation = ArgonKeyDerivation.Argon2d, Iterations = 64, DegreeOfParallelism = 44 })
};
var key = SshKey.Generate("test.ppk", FileMode.Create, keyInfo);

var publicKey = key.ToPublic(SshKeyFormat.OpenSSH);
var fingerprint = key.Fingerprint();

Console.WriteLine("Fingerprint: {0}", fingerprint);
Expand Down Expand Up @@ -207,11 +229,13 @@ Console.WriteLine("Public Key: {0}", publicKey);
```cs
var keyFile = new PrivateKeyFile("test.key");

var privateKey = keyFile.ToOpenSshFormat(new SshKeyEncryptionAes256("12345"));
var puttyKey = keyFile.ToPuttyFormat(new SshKeyEncryptionAes256("12345"));
var privateKey = keyFile.ToOpenSshFormat("12345");
var puttyKey = keyFile.ToPuttyFormat("12345");
var publicKey = keyFile.ToPublic();
var puttyPublicKey = keyFile.ToPuttyPublicFormat();

Console.WriteLine("Private Key: {0}", privateKey);
Console.WriteLine("Putty Private Key: {0}", puttyKey);
Console.WriteLine("Public Key: {0}", publicKey);
Console.WriteLine("Putty Public Key: {0}", puttyPublicKey);
```
93 changes: 65 additions & 28 deletions SshNet.Keygen.Tests/TestKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public void TestExceptions()

keyInfo.KeyType = SshKeyType.RSA;
Assert.Throws<CryptographicException>(() => SshKey.Generate(keyInfo));

var key = SshKey.Generate();
Assert.Throws<NotSupportedException>(() => key.ToPuttyFormat(SshKeyFormat.OpenSSH));
}

[Test]
Expand Down Expand Up @@ -57,24 +60,14 @@ private static void KeyGenTest<TKey>(SshKeyType keyType, int keyLength = 0)
{
foreach (var sshKeyEncryption in sshKeyEncryptions)
{
TestContext.WriteLine($"File: {path} - Encryption: {sshKeyEncryption}");
TestContext.WriteLine($"File: '{path}' - Encryption: '{sshKeyEncryption}' - Comment: '{comment}'");

var keyInfo = new SshKeyGenerateInfo(keyType)
{
Encryption = sshKeyEncryption,
KeyLength = keyLength
};
if (!string.IsNullOrEmpty(comment))
keyInfo.Comment = comment;

var puttyKeyInfo = new SshKeyGenerateInfo(keyType)
{
KeyFormat = SshKeyFormat.PuTTY,
Encryption = sshKeyEncryption,
KeyLength = keyLength
KeyLength = keyLength,
Comment = comment
};
if (!string.IsNullOrEmpty(comment))
puttyKeyInfo.Comment = comment;

IPrivateKeySource keyFile;
if (string.IsNullOrEmpty(path))
Expand All @@ -85,31 +78,75 @@ private static void KeyGenTest<TKey>(SshKeyType keyType, int keyLength = 0)
}
else
{
_ = SshKey.Generate(path, FileMode.Create, keyInfo);
keyFile = new PrivateKeyFile(path, password);
var genKey = SshKey.Generate(path, FileMode.Create, keyInfo);
ClassicAssert.IsTrue(File.Exists(path));
keyFile = new PrivateKeyFile(path, password);
_ = new PrivateKeyFile(genKey.ToOpenSshFormat().ToStream(), password);

ClassicAssert.AreEqual(genKey.ToOpenSshPublicFormat(), genKey.ToPublic());
ClassicAssert.AreEqual(genKey.ToOpenSshPublicFormat(), keyFile.ToPublic());
ClassicAssert.AreEqual(1, genKey.ToPublic().Split('\n').Length - 1);
ClassicAssert.AreEqual(1, keyFile.ToPublic().Split('\n').Length - 1);

StringAssert.Contains(((KeyHostAlgorithm) genKey.HostKeyAlgorithms.First()).Key.ToString(), genKey.ToPublic());
StringAssert.Contains(comment ?? SshKeyGenerateInfo.DefaultSshKeyComment, genKey.ToPublic());
StringAssert.Contains(((KeyHostAlgorithm) genKey.HostKeyAlgorithms.First()).Key.ToString(), genKey.ToOpenSshPublicFormat());
StringAssert.Contains(comment ?? SshKeyGenerateInfo.DefaultSshKeyComment, genKey.ToOpenSshPublicFormat());

switch (sshKeyEncryption.CipherName)
foreach (var keyFormat in new List<SshKeyFormat> { SshKeyFormat.PuTTYv2 , SshKeyFormat.PuTTYv3 })
{
case "aes256-ctr":
Assert.Throws<NotSupportedException>(() => SshKey.Generate($"{path}.ppk", FileMode.Create, puttyKeyInfo));
break;
default:
File.Delete($"{path}.ppk");
_ = SshKey.Generate($"{path}.ppk", FileMode.Create, puttyKeyInfo);
ClassicAssert.IsTrue(File.Exists($"{path}.ppk"));
break;
keyInfo.KeyFormat = keyFormat;
var puttyFile = $"{path}-{keyFormat}.ppk";

switch (sshKeyEncryption.CipherName)
{
case "aes256-ctr":
Assert.Throws<NotSupportedException>(() => SshKey.Generate(puttyFile, FileMode.Create, keyInfo));
break;
default:
File.Delete(puttyFile);
var puttyKey = SshKey.Generate(puttyFile, FileMode.Create, keyInfo);
ClassicAssert.IsTrue(File.Exists(puttyFile));

foreach (var puttyContent in new List<string> { File.ReadAllText(puttyFile), puttyKey.ToPuttyFormat() })
{
StringAssert.Contains($"Comment: {comment ?? SshKeyGenerateInfo.DefaultSshKeyComment}", puttyContent);
StringAssert.Contains($"Encryption: {sshKeyEncryption.CipherName}", puttyContent);

switch (keyFormat)
{
case SshKeyFormat.PuTTYv2:
StringAssert.Contains("PuTTY-User-Key-File-2: ", puttyContent);
break;
case SshKeyFormat.PuTTYv3:
StringAssert.Contains("PuTTY-User-Key-File-3: ", puttyContent);
if (keyInfo.Encryption is SshKeyEncryptionAes256)
{
StringAssert.Contains("Key-Derivation: Argon2id", puttyContent);
StringAssert.Contains("Argon2-Memory: 8192", puttyContent);
StringAssert.Contains("Argon2-Passes: 22", puttyContent);
StringAssert.Contains("Argon2-Parallelism: 1", puttyContent);
StringAssert.Contains("Argon2-Salt:", puttyContent);
}
break;
}
}

var puttyPubContent = puttyKey.ToPuttyPublicFormat();
ClassicAssert.AreEqual(puttyPubContent, puttyKey.ToPublic());
StringAssert.Contains("---- BEGIN SSH2 PUBLIC KEY ----\n", puttyPubContent);
StringAssert.Contains($"Comment: \"{comment ?? SshKeyGenerateInfo.DefaultSshKeyComment}\"\n", puttyPubContent);
StringAssert.Contains("---- END SSH2 PUBLIC KEY ----\n", puttyPubContent);
break;
}
}
}

ClassicAssert.IsInstanceOf<TKey>(((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key);
if (keyLength != 0)
ClassicAssert.AreEqual(keyLength, (((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.KeyLength));

ClassicAssert.AreEqual(
string.IsNullOrEmpty(comment)
? $"{Environment.UserName}@{Environment.MachineName}"
: comment,
ClassicAssert.AreEqual(comment ?? SshKeyGenerateInfo.DefaultSshKeyComment,
((KeyHostAlgorithm) keyFile.HostKeyAlgorithms.First()).Key.Comment);
}
}
Expand Down
97 changes: 73 additions & 24 deletions SshNet.Keygen/Extensions/KeyExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public static class KeyExtension
{
#region Fingerprint

public static string Fingerprint(this Key key)
internal static string Fingerprint(this Key key)
{
return key.Fingerprint(SshKeyGenerateInfo.DefaultHashAlgorithmName);
}

public static string Fingerprint(this Key key, SshKeyHashAlgorithmName hashAlgorithm)
internal static string Fingerprint(this Key key, SshKeyHashAlgorithmName hashAlgorithm)
{
using var pubStream = new MemoryStream();
using var pubWriter = new BinaryWriter(pubStream);
Expand All @@ -29,14 +29,12 @@ public static string Fingerprint(this Key key, SshKeyHashAlgorithmName hashAlgor
? BitConverter.ToString(pubKeyHash).ToLower().Replace('-', ':')
: Convert.ToBase64String(pubKeyHash, 0, pubKeyHash.Length).TrimEnd('=');

return $"{key.KeyLength} {SshKeyHashAlgorithm.HashAlgorithmName(hashAlgorithm)}:{base64} {key.Comment ?? ""} ({key.KeyName()})";
return $"{key.KeyLength} {SshKeyHashAlgorithm.HashAlgorithmName(hashAlgorithm)}:{base64} {key.Comment} ({key.KeyName()})";
}

#endregion

#region Public

public static string ToPublic(this Key key)
internal static string ToPublic(this Key key)
{
using var pubStream = new MemoryStream();
using var pubWriter = new BinaryWriter(pubStream);
Expand All @@ -46,16 +44,14 @@ public static string ToPublic(this Key key)
return $"{key} {base64} {key.Comment ?? ""}\n";
}

#endregion

#region OpenSshFormat

public static string ToOpenSshFormat(this Key key)
internal static string ToOpenSshFormat(this Key key)
{
return key.ToOpenSshFormat(SshKeyGenerateInfo.DefaultSshKeyEncryption);
}

public static string ToOpenSshFormat(this Key key, ISshKeyEncryption encryption)
internal static string ToOpenSshFormat(this Key key, ISshKeyEncryption encryption)
{
var s = new StringWriter();
s.Write("-----BEGIN OPENSSH PRIVATE KEY-----\n");
Expand Down Expand Up @@ -139,7 +135,7 @@ private static string OpensshPrivateKeyData(this Key key, ISshKeyEncryption encr
throw new NotSupportedException($"Unsupported KeyType: {key}");
}
// comment
privWriter.EncodeBinary(key.Comment ?? "");
privWriter.EncodeBinary(key.Comment);

// private key padding (1, 2, 3, ...)
var pad = 0;
Expand All @@ -166,13 +162,30 @@ private static string OpensshPrivateKeyData(this Key key, ISshKeyEncryption encr

#region PuttyFormat

public static string ToPuttyFormat(this Key key)
internal static string ToPuttyPublicFormat(this Key key)
{
return key.ToPuttyFormat(SshKeyGenerateInfo.DefaultSshKeyEncryption);
using var pubStream = new MemoryStream();
using var pubWriter = new BinaryWriter(pubStream);
key.PublicKeyData(pubWriter);

var s = new StringWriter();
s.Write("---- BEGIN SSH2 PUBLIC KEY ----\n");
s.Write($"Comment: \"{key.Comment}\"\n");
s.Write(Convert.ToBase64String(pubStream.ToArray()).FormatNewLines(64) + "\n");
s.Write("---- END SSH2 PUBLIC KEY ----\n");
return s.ToString();
}

internal static string ToPuttyFormat(this Key key, SshKeyFormat sshKeyFormat)
{
return key.ToPuttyFormat(SshKeyGenerateInfo.DefaultSshKeyEncryption, sshKeyFormat);
}

public static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption)
internal static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption, SshKeyFormat sshKeyFormat)
{
if (sshKeyFormat is not SshKeyFormat.PuTTYv2 and not SshKeyFormat.PuTTYv3)
throw new NotSupportedException($"Unsupported PuTTY Key Format {sshKeyFormat}");

// Public Key
using var pubStream = new MemoryStream();
using var pubWriter = new BinaryWriter(pubStream);
Expand Down Expand Up @@ -215,34 +228,70 @@ public static string ToPuttyFormat(this Key key, ISshKeyEncryption encryption)
privWriter.Write((byte)++pad);
}

var encrypted = encryption.PuttyEncrypt(privStream.ToArray());
var privateBase64String = Convert.ToBase64String(encrypted).FormatNewLines(64);

// MAC
using var macStream = new MemoryStream();
using var macWriter = new BinaryWriter(macStream);
macWriter.EncodeBinary(key.ToString());
macWriter.EncodeBinary(encryption.CipherName);
macWriter.EncodeBinary(key.Comment ?? "");
macWriter.EncodeBinary(key.Comment);
macWriter.EncodeBinary(pubStream);
macWriter.EncodeBinary(privStream);

var hashData = macStream.ToArray();

using var sha1 = SHA1.Create();
var macKey = sha1.ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key" + encryption.Passphrase));
using var hmac = new HMACSHA1(macKey);
var macHash = hmac.ComputeHash(hashData);
string privateBase64String;
PuttyV3Encryption? puttyV3Encryption = null;
byte[] macHash = new byte[0];
if (sshKeyFormat is SshKeyFormat.PuTTYv2)
{
byte[] encrypted = encryption.PuttyV2Encrypt(privStream.ToArray());
privateBase64String = Convert.ToBase64String(encrypted).FormatNewLines(64);

using var sha1 = SHA1.Create();
var macKey = sha1.ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key" + encryption.Passphrase));
using var hmac = new HMACSHA1(macKey);
macHash = hmac.ComputeHash(hashData);
}
else
{
puttyV3Encryption = encryption.PuttyV3Encrypt(privStream.ToArray());
privateBase64String = Convert.ToBase64String(puttyV3Encryption.Result).FormatNewLines(64);

using var hmac = new HMACSHA256(puttyV3Encryption.MacKey);
macHash = hmac.ComputeHash(hashData);
}

var s = new StringWriter();
s.Write($"PuTTY-User-Key-File-2: {key}\n");
if (sshKeyFormat is SshKeyFormat.PuTTYv2)
{
s.Write($"PuTTY-User-Key-File-2: {key}\n");
s.Write($"Encryption: {encryption.CipherName}\n");
s.Write($"Comment: {key.Comment}\n");
s.Write($"Public-Lines: {publicBase64String.Split('\n').Length}\n");
s.Write($"{publicBase64String}\n");
s.Write($"Private-Lines: {privateBase64String.Split('\n').Length}\n");
s.Write($"{privateBase64String}\n");
s.Write($"Private-MAC: {BitConverter.ToString(macHash).Replace("-", "").ToLower()}\n");
return s.ToString();
}

s.Write($"PuTTY-User-Key-File-3: {key}\n");
s.Write($"Encryption: {encryption.CipherName}\n");
s.Write($"Comment: {key.Comment ?? ""}\n");
s.Write($"Comment: {key.Comment}\n");
s.Write($"Public-Lines: {publicBase64String.Split('\n').Length}\n");
s.Write($"{publicBase64String}\n");
if (puttyV3Encryption?.Salt is not null)
{
s.Write($"Key-Derivation: {puttyV3Encryption.KeyDerivation}\n");
s.Write($"Argon2-Memory: {puttyV3Encryption.MemorySize}\n");
s.Write($"Argon2-Passes: {puttyV3Encryption.Iterations}\n");
s.Write($"Argon2-Parallelism: {puttyV3Encryption.DegreeOfParallelism}\n");
s.Write($"Argon2-Salt: {BitConverter.ToString(puttyV3Encryption.Salt).Replace("-", "").ToLower()}\n");
}
s.Write($"Private-Lines: {privateBase64String.Split('\n').Length}\n");
s.Write($"{privateBase64String}\n");
s.Write($"Private-MAC: {BitConverter.ToString(macHash).Replace("-", "").ToLower()}\n");

return s.ToString();
}

Expand Down
Loading

0 comments on commit b766957

Please sign in to comment.