• PPK format documentation?

    From Zac Morris@21:1/5 to All on Mon Jan 18 13:39:31 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Simon Tatham@21:1/5 to zac@zacwolf.com on Tue Jan 19 09:51:20 2021
    Zac Morris <zac@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.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Zac Morris@21:1/5 to Simon Tatham on Tue Jan 19 05:51:51 2021
    On Tuesday, January 19, 2021 at 4:51:23 AM UTC-5, Simon Tatham wrote:
    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.

    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Zac Morris@21:1/5 to All on Sun Jan 31 10:21:39 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Zac Morris@21:1/5 to All on Mon Feb 1 09:42:43 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Simon Tatham@21:1/5 to zac@zacwolf.com on Mon Feb 1 20:48:53 2021
    Zac Morris <zac@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 <anakin@pobox.com>" ))

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Zac Morris@21:1/5 to All on Mon Feb 1 14:21:48 2021
    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!

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Simon Tatham@21:1/5 to zac@zacwolf.com on Tue Feb 2 15:14:00 2021
    Zac Morris <zac@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?

    --
    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 <anakin@pobox.com>" ))

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Zac Morris@21:1/5 to All on Tue Feb 2 09:23:04 2021
    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

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)