Skip to content

Instantly share code, notes, and snippets.

@canton7
Last active July 1, 2022 08:28
Show Gist options
  • Save canton7/5670788 to your computer and use it in GitHub Desktop.
Save canton7/5670788 to your computer and use it in GitHub Desktop.
C# class to convert OpenSSL private keys into PuTTY'-format private keys. Can't handle encryption or anything else funky
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
// Usage:
// var keyLines = File.ReadAllLines(@"keyfile");
// var keyBytes = System.Convert.FromBase64String(string.Join("", keyLines.Skip(1).Take(keyLines.Length - 2)));
// var puttyKey = RSAConverter.FromDERPrivateKey(keyBytes).ToPuttyPrivateKey();
namespace Keyconv
{
public class RSAConverter
{
public RSACryptoServiceProvider CryptoServiceProvider { get; private set; }
public string Comment { get; set; }
public RSAConverter(RSACryptoServiceProvider cryptoServiceProvider)
{
this.CryptoServiceProvider = cryptoServiceProvider;
this.Comment = "imported-key";
}
public static RSAConverter FromDERPrivateKey(byte[] privateKey)
{
return new RSAConverter(DecodeRSAPrivateKey(privateKey));
}
// Adapted from http://www.jensign.com/opensslkey/opensslkey.cs
public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey)
{
var RSA = new RSACryptoServiceProvider();
var RSAparams = new RSAParameters();
// --------- Set up stream to decode the asn.1 encoded RSA private key ------
using (BinaryReader binr = new BinaryReader(new MemoryStream(privkey)))
{
byte bt = 0;
ushort twobytes = 0;
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
throw new Exception("Unexpected value read");
twobytes = binr.ReadUInt16();
if (twobytes != 0x0102) //version number
throw new Exception("Unexpected version");
bt = binr.ReadByte();
if (bt != 0x00)
throw new Exception("Unexpected value read");
//------ all private key components are Integer sequences ----
RSAparams.Modulus = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.Exponent = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.D = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.P = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.Q = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.DP = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.DQ = binr.ReadBytes(GetIntegerSize(binr));
RSAparams.InverseQ = binr.ReadBytes(GetIntegerSize(binr));
}
RSA.ImportParameters(RSAparams);
return RSA;
}
public string ToPuttyPrivateKey()
{
var publicParameters = this.CryptoServiceProvider.ExportParameters(false);
byte[] publicBuffer = new byte[3 + 7 + 4 + 1 + publicParameters.Exponent.Length + 4 + 1 + publicParameters.Modulus.Length + 1];
using (var bw = new BinaryWriter(new MemoryStream(publicBuffer)))
{
bw.Write(new byte[] { 0x00, 0x00, 0x00 });
bw.Write("ssh-rsa");
PutPrefixed(bw, publicParameters.Exponent, true);
PutPrefixed(bw, publicParameters.Modulus, true);
}
var publicBlob = System.Convert.ToBase64String(publicBuffer);
var privateParameters = this.CryptoServiceProvider.ExportParameters(true);
byte[] privateBuffer = new byte[4 + 1 + privateParameters.D.Length + 4 + 1 + privateParameters.P.Length + 4 + 1 + privateParameters.Q.Length + 4 + 1 + privateParameters.InverseQ.Length];
using (var bw = new BinaryWriter(new MemoryStream(privateBuffer)))
{
PutPrefixed(bw, privateParameters.D, true);
PutPrefixed(bw, privateParameters.P, true);
PutPrefixed(bw, privateParameters.Q, true);
PutPrefixed(bw, privateParameters.InverseQ, true);
}
var privateBlob = System.Convert.ToBase64String(privateBuffer);
HMACSHA1 hmacsha1 = new HMACSHA1(new SHA1CryptoServiceProvider().ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key")));
byte[] bytesToHash = new byte[4 + 7 + 4 + 4 + 4 + this.Comment.Length + 4 + publicBuffer.Length + 4 + privateBuffer.Length];
using (var bw = new BinaryWriter(new MemoryStream(bytesToHash)))
{
PutPrefixed(bw, Encoding.ASCII.GetBytes("ssh-rsa"));
PutPrefixed(bw, Encoding.ASCII.GetBytes("none"));
PutPrefixed(bw, Encoding.ASCII.GetBytes(this.Comment));
PutPrefixed(bw, publicBuffer);
PutPrefixed(bw, privateBuffer);
}
var hash = string.Join("", hmacsha1.ComputeHash(bytesToHash).Select(x => string.Format("{0:x2}", x)));
var sb = new StringBuilder();
sb.AppendLine("PuTTY-User-Key-File-2: ssh-rsa");
sb.AppendLine("Encryption: none");
sb.AppendLine("Comment: " + this.Comment);
var publicLines = SpliceText(publicBlob, 64);
sb.AppendLine("Public-Lines: " + publicLines.Length);
foreach (var line in publicLines)
{
sb.AppendLine(line);
}
var privateLines = SpliceText(privateBlob, 64);
sb.AppendLine("Private-Lines: " + privateLines.Length);
foreach (var line in privateLines)
{
sb.AppendLine(line);
}
sb.AppendLine("Private-MAC: " + hash);
return sb.ToString();
}
private static int GetIntegerSize(BinaryReader binr)
{
byte bt = 0;
byte lowbyte = 0x00;
byte highbyte = 0x00;
int count = 0;
bt = binr.ReadByte();
if (bt != 0x02) //expect integer
throw new Exception("Expected integer");
bt = binr.ReadByte();
if (bt == 0x81)
{
count = binr.ReadByte(); // data size in next byte
}
else if (bt == 0x82)
{
highbyte = binr.ReadByte(); // data size in next 2 bytes
lowbyte = binr.ReadByte();
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
count = BitConverter.ToInt32(modint, 0);
}
else
{
count = bt; // we already have the data size
}
while (binr.ReadByte() == 0x00)
{ //remove high order zeros in data
count -= 1;
}
binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte
return count;
}
private static void PutPrefixed(BinaryWriter bw, byte[] bytes, bool addLeadingNull = false)
{
bw.Write(BitConverter.GetBytes(bytes.Length + (addLeadingNull ? 1 : 0)).Reverse().ToArray());
if (addLeadingNull)
bw.Write(new byte[] { 0x00 });
bw.Write(bytes);
}
// http://stackoverflow.com/questions/7768373/c-sharp-line-break-every-n-characters
private static string[] SpliceText(string text, int lineLength)
{
return Regex.Matches(text, ".{1," + lineLength + "}").Cast<Match>().Select(m => m.Value).ToArray();
}
}
}
@davidehnis
Copy link

This worked beautifully for me. Nice work!!

@venkomiri
Copy link

Hi Thanks for the artical..

  1. I have tried this block to create a Putty private key, and I am able to load the generated key from windows Microsoft store putty-gen app, but failed to load through putty-gen which is installed through Winscp.

  2. I am failed to use the key pair to authenticate when I convert the key to OpenSSH format through ssh-keygen command and install in ssh sftp server.

Here is my key

PuTTY-User-Key-File-2: ssh-rsa
Encryption: none
Comment: imported-key
Public-Lines: 6
AAAAB3NzaC1yc2EAAAAEAAEAAQAAAQEAvKAqb2Gvnfi51gvPyTx2j9SdG4D+u/zA
YIZUU99wE9jL6MNuk7zCaI8y/tc5aTWY3XUxHw4y3H8cBmqMlb5tnkHwrogBMsgI
bTbELKeuyeeAcqOQ1qM7s64dpR3BBsrCk5IYhe9BeUCczK1RuYrbuDmCnZOG/8dS
cL8uLUiaO2l2cZu+H8j6O2LK6a7Nb4YP0f28txYaieQ1lw9KdgNVUQyPPfm5aMKk
ay/VfApiZDyUA3/qw5c2VmD+3E1bH0UtZFcBpE2+BP4ax1aabTgeFxSCZF3dPoce
52gujlwz7KE/dcgt8yXNSy4IoEK8ANmIV09RjpEgFcjkmh+NMT7PEQ==
Private-Lines: 14
AAABAQBfGlYq8FLOUEDKZgwuxzh0DlvkKSbGe4o3YKMV4rsslotA4YBYJrzSYRjy
GmvM2wQm7FaG9O6587Can5AgU/IK7+484T/Rbb+p6QoCBc1/6SP/KO72+Tg5wNkb
jiPrm8F9DBUnOlmFnAkyvVROO2/Ks9xiPKa9Qa8UP6A1nrx8pTLVftalG5J2C8If
GRoF8w33OekEiHCjWm0nkX2kvSeDmFn+CU0wopaoODeKZEkZYCkLGl/UHfqxWu6+
xkDQDXbR9Cg44At2ry5rHQGDxwol2DLpwSi+QMaAf/jKUydJ9YBJswNo4PS9/Xeb
V0Mp6WEgS9di1tmsbYm15F+HrF/dAAAAgQDOSXADfwqxPjR1zX1h4/G1eV0rRAdy
dTUzTt2ux7FaSG0DX4A56H90kbwtzz9Sz7I1sbV1dv99FLUNTByZKw9X8O8bMX3F
fkV3T1vg1IkQJB2pF8+iXu3uL5++kLtBr3w2icx+zezz6L8tyqBgRpmO73RzBWvr
2seKmhbvxKwgUwAAAIEA6hUlQj960M4m7qyO0KZC35HX5BqXzReUp+Jq+gE2TAEp
15MM9D+QoOL7bnKGl7LG43U5fuEHNCEE2Fzq6tPI8aX9RS82qzVgHXNR4xP/BE3q
v8d5/1iqDPwA7A581pZT2di4d1dUdtBQXSYL3A4yiDWysXyRwAApWcmhVHsxdosA
AACBAJZIHU23gzL6fKSXhkGyQG5BV2L9ft2iDKQiuMQLdhq13j6uxlK6o/hnW9jB
SMoxf7Pw84xVBWTTv7zFJ6/+d2RTfm87j79fnUZRZ+jFWyEj/+oSv4P7GvyDai7Z
1LTn/nGhx0s7KnuyxEJs94TxJJSzON5+h71K6yHrT8sos9Gc
Private-MAC: 85c5a77eb9afbe937da4d625e11a31502d601ce7

Could you please help me solve this?

@canton7
Copy link
Author

canton7 commented Apr 4, 2020

I'm afraid I wrote this to solve a problem that I had. I'm not an expert on putty key formats -- I hacked this together until it worked for me. This code doesn't come with any support -- if you've got a problem, I'm afraid it's up to you to fix it. If you do find and fix problems, please let me know and I can update the code.

@bosima
Copy link

bosima commented Jun 29, 2022

I may have solved the problem that the putty private key may not be generated correctly.

This is because the padding is not always required, which is related to the processing logic of putty:

dlen = (bignum_bitcount(rsa->private_exponent) + 8) / 8;
plen = (bignum_bitcount(rsa->p) + 8) / 8;
qlen = (bignum_bitcount(rsa->q) + 8) / 8;
ulen = (bignum_bitcount(rsa->iqmp) + 8) / 8;

We can do this now:

private const int prefixSize = 4;
private const int paddedPrefixSize = prefixSize + 1;
		
byte[] publicBuffer = new byte[3 + keyType.Length + GetPrefixSize(keyParameters.Exponent) + keyParameters.Exponent.Length +
                GetPrefixSize(keyParameters.Modulus) + keyParameters.Modulus.Length + 1];

using (var writer = new BinaryWriter(new MemoryStream(publicBuffer), Encoding.ASCII))
{
	writer.Write(new byte[] { 0x00, 0x00, 0x00 });
	writer.Write(keyType);
	WritePrefixed(writer, keyParameters.Exponent, CheckIsNeddPadding(keyParameters.Exponent));
	WritePrefixed(writer, keyParameters.Modulus, CheckIsNeddPadding(keyParameters.Modulus));
}
			
private static bool CheckIsNeddPadding(byte[] bytes)
{
	// 128 == 10000000
	// This means that the number of bits can be divided by 8.
	// According to the algorithm in putty, you need to add a padding.
	return bytes[0] >= 128;
}

private static int GetPrefixSize(byte[] bytes)
{
	return CheckIsNeddPadding(bytes) ? paddedPrefixSize : prefixSize;
}

I also added passphrase for ppk.

Full Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment