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();
}
}
}
@canton7
Copy link
Author

canton7 commented Jun 19, 2018

Right, so is it putty that's reading the PPK, or something else? It's it's putty, that's a bit strange. I never did figure out why I couldn't generate an identical file to puttygen, so the next step would be digging into the putty source (and the source of the bignum library it uses) to figure out where the remaining discrepancies come from.

@frankhommers
Copy link

frankhommers commented Jun 25, 2018

Yes. I don't know where de discrepancies come from either. I see that "WinSCP /keygen" and "PuttyGen" generate the exact same .ppk. (Except the Private-MAC line).

@fuji4
Copy link

fuji4 commented Nov 28, 2018

The class works great, but what to do if i need to secure my PPK with a passphrase? Any ideas how to encrypt it?

@amitbarkai
Copy link

the following private key , throws a bad data exception

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,E97EC262B2F5796A

yWFEU14wA65Mb+/GsgbgSBCTR3dPbNIB1zkj9iW6xQw3GCF7hfEfopKb8pvKQlHz
fN0QYiqyhB/AE7sckdaTLK+4glJ/jMjijxFkxJ/nHpvsj5m/UIAM/k0oUpTGuNyc
XZNYJIZj009CaPYGYhICPd/ppgjz2YtTBVHJ9kUl9PfqrPD9QmqGRPCBlPLPpKKd
Ixo9oL54LaI0hWsLy9Pm7yHxBR11qrfuHfvqx2M0xP2G+29z4MLk8ojwrMf+n21K
34RozQI+GdJyVKSsb31Me46qo6KccfbAf9m9rRvsMe8vcLDADEo6IXTPUwGC7rAT
3ocT4mMiJwmvZiFogLfSt36wl1zyRnyrrbzTihzzbEHVBtHuHK9gbru11mVnVOQk
J1k9ppIpDjKCESaWutife1eUY3+UW2BnO+WYNLk09aEV28RVd0sn7Gnr738iF/rI
YyfDbvs1iC4uICC8NrAIt0jFmvxTUyKOKM6VcrFjhFDWy3zTMToGO3a5E4Rc5CX6
DexRhk5MtDBPHN9bflRG2zTA9KjC109TneZ2/kPHgSs758nwu0x3RwhE5M0ont3/
IGhfprpEU2OcdpzxvyElTm9wtQeB2qbNJ3Dz1qa9BFoXw9aMB/6ldcff27ZUQLx9
sSncRdW4IjbTcwHUA+RvMrilCPBEx3pR4iUKyHAK4nG1Lvwa49JzTgdxgRXS14ju
OC7I1+J+U9FoRnPPLBwPSfH8qpVepxf2wM61U2oDlYKgyuELvgGfym8LzLdOxfxR
hphgKW/Eo1zAXkr5EUR7+OkDi9BjgconB4KNFASbEDG3z6ty01JJP0Q9S2FwOE0t
vYNvTmDBDw386bbtVc0XR7GGTkTbb9/yVWcJCQ6P+Wyy0tLwNoUa/BRaHM7lrfi7
NeoZBQzy7NXRpwYHe5IXP56+0lYm5ZJhIqEDb5Ne52lCPDg4YT1bjVjsR4nUrcrl
ZuMbJBKhMukOGCVQuP4vL8RV2sZYTq2gJBOAp3sQOHAlFkWkn1D29Rqxsx8EdDrG
Dv1oMLwNnO4By9YtJHTKrJAubbiXFijJmSsJW7z45lQ7Ql/ERa2okAIYX0EMoLlT
nLRjBraaPxNWI9A1UNxkxwB4s6+Iod47LtegmvEv7meAi9QQGDb5nAUAmD+HEy+H
YWZ6xUXr+AOQdKBUemsgWYtE8CIfJ6SMSz9AcBHDbVAISkGQJxaa7DT3h4BuMxU6
SsQu1awrQhkSw5lntnI7lGNMZzjp/DCb4bGOnDmw34TUXU1+IMI8eXZzQnvQisRM
UBb22u6Fgnc2o4c+Z/3YAk7lO+JMNCEwSznlBQasZpV54Y5aB5vzLDP94W1NtnYA
dgaQ8bwEIdwm25HPv8U0/E1vAci9S4+asjFIVkEwW3n2TPJvXR/0hUWEPfzfgOJk
qtrBsMJDOO2vl5qS9Go2bRQsW0rzUZ9ePPN/RAFdcId1C2PdHasprq6aqFC+k3Ag
bVdsqFJoW5/PQa4QFWXD0EaaCFpgmE57QS+hjQcvzHBnpT/LA8lT5bH9XK3e5/8V
5PUND75X6mn31JfnnWdc3cHfA7MI0vY9kfSaJvMPppVsMGp1HBOe7g==
-----END RSA PRIVATE KEY-----

can you please check , or explain what is the issue ?

@amitbarkai
Copy link

i've mistakenly sent an encrypted key
here is an example of another example of a key that throws the bad data exception

-----BEGIN RSA PRIVATE KEY-----

MIIEoQIBAAKCAQEAuWll7Dd97qYQJOeMcQOvI/KefF4Vogb25eUwIYuCKlSIXnu2
zPxOKN++kJIC/ufJHqGgW6xQWRVMkqY0tWJLqkN0LUiAFsiKm3wVyV0Ax73rGkOn
7YpBuHshmO+bDoiwR+4pH6DDRs7fhTlwQII5EuctvL3oQ/kgDIcGwhYHrwgAm/ug
3W/Z7PFB0Fo6jQ6M4Qd8My6TH1uFIs86G1sg9v75r6/yw0gq/C1lOEmvRcb62KrQ
j0s8B/fZypuOp3OO+95RLsnohaEGrhfgTRTDPSqHHuT1PBoy6CP+3zCf6l+Fwmbg
XzohJE+tzUhk8EqIN0+zBotsdHU9cxySDd3wMQIDAQABAoH/ZtgPsPcW40k5qz7E
p+tjOo1ZW6LUXQt/6AU4rWK00MYS3lXBD/LjZ8iz630SLFJlPJhqWm86Ii+uSann
krTzQ34m8vdEFV8ngxdQ/mCYDjNKNUdR3nDSA4JJIIvHdkhbf4qbRO/nYV9enIc/
vIh/H/0bYZX1P77wCALdvMp7eJoFojE9ZbwsMiJgQRUpUdnh4EI3mPPcBGFprF1U
QraFVZf4Q5OuMckrHVUkMofXyOmmguI9WaA2bWy1VQ1MQ4SqHRCIojq+MDVDlw9Z
DNFB6VbWDUOKco4Vlb86E7eA8sMtZLsEy4JPXGtFBIkcUV/MPRpqKejeBFo8g6pb
EZd5AoGBAObmhBhW6NZNl23PuTofzlyqiHP6Pg7vt/wpq7Mw9G1xahvb3p+4VTiD
FQjJ3dE4u81H7O55cRcr45yHDUswiLAXZcX5gwXp+GPueB5jJeuZLqCvNThH64Ow
E9kFjsszbelNuzhYCFMhbdd4kUlZM56t0Zd7eUsqGHeRRUeOtGz/AoGBAM2RBi8f
z+bm6QXH6ir4C4rYft021KZ+h7e/2ZtsIMkR/q81/LEzkr5e89feyYqNe/iE9jgy
8JDuwt4nJJBqoRWkDtBCUtNurTqMj6Te0XFeZripMkZ9BhBRm03UU0NKS4aKeG8x
gr4FLGstZ41ujbCLeVLzDKpv001VZfKjUDLPAoGBAKWG/4+LTmPDQBmC3qCiiIe5
4RRzguWmSFlHbkWJhNCoi47pMlGCDeXzYrLoNFJ2v3tMYrga603XMtbVolwSsQq7
20PvuVQWBPFu1UHDhj29lMWwlRCBzn6bTb840sMtXU/xX5Pm2CDwSBQ95LmWbwEE
Tsqvw6Z0yRF+XRINZZ71AoGAATUl5Sb5sLCQk+EdxgzY/ILTE/ebfjLmFzVAUQJs
muHJLjxR9LSJ2yZxpkX/xxmXrdkSHThnY2KTsHxoYZTOx3LER4LsO6O9zsc+nMhW
UKUuU01jJzjazUO9dtKVfqK0GOE9XeHbk8QyA5srrZAFsxDOsKcO3v1zL1QeGjPN
Z88CgYByrdTjPg54NBfaRN3uDzjOdCaCJb/RCUSYxKOmjNB4ALTSCmUgGGhIX53o
la30no931wQYvrLQ+KWHyCyya3p31XRnlKD1SReE/n6XSMFui6lLgPNdnCLdIAZq
YXXOjAn2FeoNqkKWlbj2linuAJa5FrRJ07O1DJXajWfbzX7tcA==
-----END RSA PRIVATE KEY-----

** using puttyGen the key is converted without any issue

@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