PPK format documentation?

29 views
Skip to first unread message

Zac Morris

unread,
Jan 18, 2021, 4:39:33 PM1/18/21
to
I would like to be able to open a ppk keyfile in java, but I'm not having any luck finding any existing readers/utils, except for the paid Chilkat libraries.

I'm also not finding any documentation on what the ppk format consists of. Based on what little information I could find at:

https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/key-formats-natively.html

…it looks like PPK involves some kind of secondary digest/hashing/crypt to detect if the file has been altered (i.e. if you edit the file in a text editor, it can no longer be opened as a valid key in puttygen/pagent, etc.)

Since the source code is made available, I'm assuming that the PPK file creation/format is likewise open, just proprietary? Unfortunately, it would take me weeks to figure out the C code, so is there any sort of document, post, etc. that describes how the file is generated so that it can be un-generated into a Java PrivateKey?

THANKS!
-Zac

Simon Tatham

unread,
Jan 19, 2021, 4:51:23 AM1/19/21
to
Zac Morris <z...@zacwolf.com> wrote:
> I'm also not finding any documentation on what the ppk format
> consists of. [...] Since the source code is made available, I'm
> assuming that the PPK file creation/format is likewise open,

Yes, and there's a comment describing it in the code:

https://git.tartarus.org/?p=simon/putty.git;a=blob;f=sshpubk.c;h=b8d7ffb2014569b2654453dcdf6ba62ed76fc098;hb=6fc0eb29ac30421524c9d9db6e359c364db413d8#l473

Sorry it was hard for you to find. If you feel like pulling that
comment out into an appendix in the manual, I'd accept a patch.

Zac Morris

unread,
Jan 19, 2021, 8:51:53 AM1/19/21
to
Thank you, thank you, thank you!

I started digging through the code last night. I'm just so unfamiliar with C! But I will admit, a class named "sshpubk" is probably the last place I would have looked for information on the ppks private key writing format...

I would love to see it in the FAQ, so I'll figure out what's required to submit a patch!

Thanks!
-Zac

Zac Morris

unread,
Jan 31, 2021, 1:21:40 PM1/31/21
to
Ok, this has turned into quite the rabbit hole! So I think I have all the formats readable, but I'm stuck on generating the MAC.

In sshpubk.c:

* Finally, there is a line saying "Private-MAC: " plus a hex
* representation of a HMAC-SHA-1 of:
*
* string name of algorithm ("ssh-dss", "ssh-rsa")
* string encryption type
* string comment
* string public-blob
* string private-plaintext (the plaintext version of the
* private part, including the final
* padding)
*
* The key to the MAC is itself a SHA-1 hash of:
*
* data "putty-private-key-file-mac-key"
* data passphrase
*
* (An empty passphrase is used for unencrypted keys.)


Could you please give more detail on: "the plaintext version of the private part, including the final padding"

When I look through the code (Line 759-766) it looks like the string that is being built to HMAC hash includes the private_blob *post* byte64 decode AND *post* AES decryption? Is that correct? That is confusing me regarding your comment about the plaintext.

I'm super weak in C, and your usage of the BinarySink stuff is throwing me even more, so any guidance would be much appreciated.

THANKS!
-Zac





Zac Morris

unread,
Feb 1, 2021, 12:42:44 PM2/1/21
to
I found a PHP app that generates a mac-hash of a PPK file, but even its logic is hidden behind pack abstraction that makes it hard to understand exactly how the byte array to be mac-hashed is being generated. For example, it looks like the byte array being used by the mac-hash is:

[4-byte-int-type-length][type-string-to-byte][4-byte-int-encryption-length][encryption-string-to-byte][4-byte-int-comment-length][comment-string-to-byte][…]

Then the public key is Byte64 decoded into a byte array but does the hash use that entire byte array that is decoded or does it slice out a subset? For example, the byte array that is Base64 decoded is SSH Wire encoded (type-length, type, key-bytes-length, key-bytes).

Then the private key is Byte64 decoded, AND if the encryption value is set, it is decrypted into an SSH Wire encoded byte array that's specific to the type of key (RSA, EC, etc.). Does the mac-hashing use all the resulting bytes, or does it use a subset?

The PHP code I found looks like the entire resultant byte-arrays for public/private are added to the value-to-mac-hash using the same len/bytes encoding approach: [4-byte-lenth-public][public-byte-array][4-byte-length-private][private-byte-array].

Then the mac-hash key is: String("putty-private-key-file-mac-key"+passcode).toByteArray().

I have used all of this (and several different values), but have not able to generate the same mac-hash that's given in the PPK file.

Thanks for any help/direction!
-Zac

Simon Tatham

unread,
Feb 1, 2021, 3:48:56 PM2/1/21
to
Zac Morris <z...@zacwolf.com> wrote:
> Could you please give more detail on: "the plaintext version of the
> private part, including the final padding"
>
> When I look through the code (Line 759-766) it looks like the string
> that is being built to HMAC hash includes the private_blob *post* byte64
> decode AND *post* AES decryption? Is that correct? That is confusing me
> regarding your comment about the plaintext.

I'm not sure why that's confusing - plaintext is _before_ encryption
is put on, or alternatively, after it's taken off!

During creation:
* the private key data is padded to a multiple of the cipher block
size
* that data is used as the input to the MAC
* the same data is encrypted
* the encrypted data is base64 (not "byte64") encoded

So, during decoding:
* the base64 data is decoded to binary data
* that binary data is decrypted
* the decrypted data ("plaintext") is used to verify the MAC

--
import hashlib; print((lambda p,q,g,y,r,s,m: (lambda w:(pow(g,int(hashlib.sha1(
m.encode('ascii')).hexdigest(),16)*w%q,p)*pow(y,r*w%q,p)%p)%q)(pow(s,q-2,q))==r
and m)(0xb80b5dacabab6145,0xf70027d345023,0x7643bc4018957897,0x11c2e5d9951130c9
,0xa54d9cbe4e8ab,0x746c50eaa1910, "Simon Tatham <ana...@pobox.com>" ))

Zac Morris

unread,
Feb 1, 2021, 5:21:49 PM2/1/21
to
> So, during decoding:
> * the base64 data is decoded to binary data
> * that binary data is decrypted
> * the decrypted data ("plaintext") is used to verify the MAC

So after parsing: Type, Encryption, Comment, Pub, Priv, Mac from the PPK:
I Base64(sorry about the Byte64 typo) decode Pub/Priv, and if encryption is not null, decrypt Priv.

I think load up all of that into a new byte array:

final byte[] mackey = MessageDigest.getInstance("SHA-1").digest(("putty-private-key-file-mac-key"+(encryption!=null?passphrase:"")).getBytes());
final ByteArrayOutputStream valToMacHash = new ByteArrayOutputStream();
valToMacHash.write(toByteArray(type.length()));
valToMacHash.write(type.getBytes());
valToMacHash.write(toByteArray(encryption.length()));
valToMacHash.write(encryption.getBytes());
valToMacHash.write(toByteArray(comment.length()));
valToMacHash.write(comment.getBytes());
valToMacHash.write(toByteArray(pubblob.length));
valToMacHash.write(pubblob);
valToMacHash.write(toByteArray(privblob.length));
valToMacHash.write(privblob);
final SecretKeySpec sk = new SecretKeySpec(mackey, "HmacSHA1");
final Mac m = Mac.getInstance("HmacSHA1");
m.init(sk);
final byte[] mbytes = m.doFinal(valToMacHash.toByteArray());
final byte[] hexBytes = Hex.encode(mbytes);
final String mhash = new String(hexBytes);

The final mhash variable is not the same as the Mac value I parsed from the PPK file.

Anything jump out at you?

Thanks for the help on this!

Simon Tatham

unread,
Feb 2, 2021, 10:14:04 AM2/2/21
to
Zac Morris <z...@zacwolf.com> wrote:
> Anything jump out at you?

No, nothing obvious. So the next step is surely to debug everything in
detail, printing out all the intermediate values.

Here's a PPK file I generated just now using "puttygen -t ecdsa -o
z.ppk", with passphrase "test":

PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
Encryption: aes256-cbc
Comment: ecdsa-key-20210202
Public-Lines: 3
AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDqj3OwWLkl1
H5oMkLZyF8rqR23Hd3pcxFUy5klf4la7Qihh7x5h0idoAQ4mkkDDLo7jNfT76h+z
jtlETIf2gcN3DHPoQYA8Vr6UQ99pzOpvsZ7R0Ee9o3fkksZhd2BjiQ==
Private-Lines: 2
Jhjt0izpxomdH7WEf5h6a3qZiidBURir9X4gGLRwqqouCoOURyiMyUU4yKGH+qgv
v615YxyGlJZnpvzsjlg/zg==
Private-MAC: 11929e56c3dfa31faed5f12546581ba73585e8d8

Here is the precise binary data of the input to the hash function that
generates the MAC key:

70.75.74.74 79.2d.70.72 69.76.61.74 65.2d.6b.65 putty-private-ke
79.2d.66.69 6c.65.2d.6d 61.63.2d.6b 65.79.74.65 y-file-mac-keyte
73.74 st

The SHA-1 hash of that data is

50d1704c3bdc7447b29261e49041394c78c6f42b

Here is the precise binary data over which the MAC is computed using
that key:

00.00.00.13 65.63.64.73 61.2d.73.68 61.32.2d.6e ....ecdsa-sha2-n
69.73.74.70 33.38.34.00 00.00.0a.61 65.73.32.35 istp384....aes25
36.2d.63.62 63.00.00.00 12.65.63.64 73.61.2d.6b 6-cbc....ecdsa-k
65.79.2d.32 30.32.31.30 32.30.32.00 00.00.88.00 ey-20210202.....
00.00.13.65 63.64.73.61 2d.73.68.61 32.2d.6e.69 ...ecdsa-sha2-ni
73.74.70.33 38.34.00.00 00.08.6e.69 73.74.70.33 stp384....nistp3
38.34.00.00 00.61.04.3a a3.dc.ec.16 2e.49.75.1f 84...a.:.....Iu.
9a.0c.90.b6 72.17.ca.ea 47.6d.c7.77 7a.5c.c4.55 ....r...Gm.wz\.U
32.e6.49.5f e2.56.bb.42 28.61.ef.1e 61.d2.27.68 2.I_.V.B(a..a.'h
01.0e.26.92 40.c3.2e.8e e3.35.f4.fb ea.1f.b3.8e ..&.@....5......
d9.44.4c.87 f6.81.c3.77 0c.73.e8.41 80.3c.56.be .DL....w.s.A.<V.
94.43.df.69 cc.ea.6f.b1 9e.d1.d0.47 bd.a3.77.e4 .C.i..o....G..w.
92.c6.61.77 60.63.89.00 00.00.40.00 00.00.30.16 ..aw`c....@...0.
1b.e3.c0.b9 ee.d5.b2.62 84.2f.d8.aa ba.76.95.6a .......b./...v.j
64.60.a5.4a e1.8b.1a.e6 36.6c.6d.bd fe.12.c8.62 d`.J....6lm....b
d3.92.5f.c3 ad.b7.56.80 e6.88.db.2a 4d.89.c5.84 .._...V....*M...
d7.5c.aa.d5 a2.e7.a4.41 28.40.14 .\.....A(@.

And the HMAC-SHA-1 of that data, with the above key, is

11929e56c3dfa31faed5f12546581ba73585e8d8

If your implementation gives a different answer for this test file,
what part of that does it disagree with?

Zac Morris

unread,
Feb 2, 2021, 12:23:06 PM2/2/21
to
> > Anything jump out at you?
> No, nothing obvious. So the next step is surely to debug everything in
> detail, printing out all the intermediate values.

Thank you, this is exactly what I needed but wasn't sure how to ask for! I'm autistic spectrum, so questions are more difficult than answers. ;-)

UGGG! Turns out my "toByteArray" function was returning Little-Endian vs Big-Endian byte order! Switched that up, and BAM perfect hash.

Thanks again! I'm gonna wrap this all up, put it on Github and then take a stab at formulating all my lessons learned into an HTML blurb to maybe be used in the FAQ on your website?

Thanks again for your assistance!
-Zac

Reply all
Reply to author
Forward
0 new messages