Previous | Contents | Index | Next
This appendix documents the file format used by PuTTY to store private keys.
In this appendix, binary data structures are described using data type representations such as ‘uint32
’, ‘string
’ and ‘mpint
’ as used in the SSH protocol standards themselves. These are defined authoritatively by RFC 4251 section 5, ‘Data Type Representations Used in the SSH Protocols’.
A PPK file stores a private key, and the corresponding public key. Both are contained in the same file.
The file format can be completely unencrypted, or it can encrypt the private key. The public key is stored in cleartext in both cases. (This enables PuTTY to send the public key to an SSH server to see whether it will accept it, and not bother prompting for the passphrase unless the server says yes.)
When the key file is encrypted, the encryption key is derived from a passphrase. An encrypted PPK file is also tamper-proofed using a MAC (authentication code), also derived from the same passphrase. The MAC protects the encrypted private key data, but it also covers the cleartext parts of the file. So you can't edit the public half of the key without invalidating the MAC and causing the key file as a whole to become useless.
This MAC protects the key file against active cryptographic attacks in which the public half of a key pair is modified in a controlled way that allows an attacker to deduce information about the private half from the resulting corrupted signatures. Any attempt to do that to a PPK file should be reliably caught by the MAC failing to validate.
(Such an attack would only be useful if the key file was stored in a location where the attacker could modify it without also having full access to the process that you type passphrases into. But that's not impossible; for example, if your home directory was on a network file server, then the file server's administrator could access the key file but not processes on the client machine.)
The MAC also covers the comment on the key. This stops an attacker from swapping keys with each other and editing the comments to disguise the fact. As a consequence, PuTTYgen cannot edit the comment on a key unless you decrypt the key with your passphrase first.
(The circumstances in which that attack would be useful are even more restricted. One example might be that the different keys trigger specific actions on the server you're connecting to and one of those actions is more useful to the attacker than the other. But once you have a MAC at all, it's no extra effort to make it cover as much as possible, and usually sensible.)
The outer layer of a PPK file is text-based. The PuTTY tools will always use LF line termination when writing PPK files, but will tolerate CR+LF and CR-only on input.
The first few lines identify it as a PPK, and give some initial data about what's stored in it and how. They look like this:
PuTTY-User-Key-File-version: algorithm-name
Encryption: encryption-type
Comment: key-comment-string
version is a decimal number giving the version number of the file format itself. The current file format version is 3.
algorithm-name is the SSH protocol identifier for the public key algorithm that this key is used for (such as ‘ssh-dss
’ or ‘ecdsa-sha2-nistp384
’).
encryption-type indicates whether this key is stored encrypted, and if so, by what method. Currently the only supported encryption types are ‘aes256-cbc
’ and ‘none
’.
key-comment-string is a free text field giving the comment. This can contain any byte values other than 13 and 10 (CR and LF).
The next part of the file gives the public key. This is stored unencrypted but base64-encoded (RFC 4648), and is preceded by a header line saying how many lines of base64 data are shown, looking like this:
Public-Lines: number-of-lines
that many lines of base64 data
The base64-encoded data in this blob is formatted in exactly the same way as an SSH public key sent over the wire in the SSH protocol itself. That is also the same format as the base64 data stored in OpenSSH's authorized_keys
file, except that in a PPK file the base64 data is split across multiple lines. But if you remove the newlines from the middle of this section, the resulting base64 blob is in the right format to go in an authorized_keys
line.
If the key is encrypted (i.e. encryption-type is not ‘none
’), then the next thing that appears is a sequence of lines specifying how the keys for encrypting the file are to be derived from the passphrase:
Key-Derivation: argon2-flavour
Argon2-Memory: decimal-integer
Argon2-Passes: decimal-integer
Argon2-Parallelism: decimal-integer
Argon2-Salt: hex-string
argon2-flavour is one of the identifiers ‘Argon2d
’, ‘Argon2i
’ or ‘Argon2id
’, all describing variants of the Argon2 password-hashing function.
The three integer values are used as parameters for Argon2, which allows you to configure the amount of memory used (in Kbyte), the number of passes of the algorithm to run (to tune its running time), and the degree of parallelism required by the hash function. The salt is decoded into a sequence of binary bytes and used as an additional input to Argon2. (It is chosen randomly when the key file is written, so that a guessing attack can't be mounted in parallel against multiple key files.)
The next part of the file gives the private key. This is base64-encoded in the same way:
Private-Lines: number-of-lines
that many lines of base64 data
The binary data represented in this base64 blob may be encrypted, depending on the encryption-type field in the key file header shown above:
none
’, then this data is stored in plain text.
aes256-cbc
’, then this data is encrypted using AES, with a 256-bit key length, in the CBC cipher mode. The key and initialisation vector are derived from the passphrase: see section C.4.
In order to encrypt the private key data with AES, it must be a multiple of 16 bytes (the AES cipher block length). This is achieved by appending random padding to the data before encrypting it. When decoding it after decryption, the random data can be ignored: the internal structure of the data is enough to tell you when you've reached the end of the meaningful part.
Unlike public keys, the binary encoding of private keys is not specified at all in the SSH standards. See section C.3 for details of the private key format for each key type supported by PuTTY.
The final thing in the key file is the MAC:
Private-MAC: hex-mac-data
hex-mac-data is a hexadecimal-encoded value, 64 digits long (i.e. 32 bytes), generated using the HMAC-SHA-256 algorithm with the following binary data as input:
string
: the algorithm-name header field.
string
: the encryption-type header field.
string
: the key-comment-string header field.
string
: the binary public key data, as decoded from the base64 lines after the ‘Public-Lines
’ header.
string
: the plaintext of the binary private key data, as decoded from the base64 lines after the ‘Private-Lines
’ header. If that data was stored encrypted, then the decrypted version of it is used in this MAC preimage, including the random padding mentioned above.
The MAC key is derived from the passphrase: see section C.4.
This section describes the private key format for each key type supported by PuTTY.
Because the PPK format also contains the public key (and both public and private key are protected by the same MAC to ensure they can't be made inconsistent), there is no need for the private key section of the file to repeat data from the public section. So some of these formats are very short.
In all cases, a decoding application can begin reading from the start of the decrypted private key data, and know when it has read all that it needs. This allows random padding after the meaningful data to be safely ignored.
RSA keys are stored using an algorithm-name of ‘ssh-rsa
’. (Keys stored like this are also used by the updated RSA signature schemes that use hashes other than SHA-1.)
The public key data has already provided the key modulus and the public encoding exponent. The private data stores:
mpint
: the private decoding exponent of the key.
mpint
: one prime factor p of the key.
mpint
: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.)
mpint
: the multiplicative inverse of q modulo p.
DSA keys are stored using an algorithm-name of ‘ssh-dss
’.
The public key data has already provided the key parameters (the large prime p, the small prime q and the group generator g), and the public key y. The private key stores:
mpint
: the private key x, which is the discrete logarithm of y in the group generated by g mod p.
NIST elliptic-curve keys are stored using one of the following algorithm-name values, each corresponding to a different elliptic curve and key size:
ecdsa-sha2-nistp256
’
ecdsa-sha2-nistp384
’
ecdsa-sha2-nistp521
’
The public key data has already provided the public elliptic curve point. The private key stores:
mpint
: the private exponent, which is the discrete log of the public point.
EdDSA elliptic-curve keys are stored using one of the following algorithm-name values, each corresponding to a different elliptic curve and key size:
ssh-ed25519
’
ssh-ed448
’
The public key data has already provided the public elliptic curve point. The private key stores:
mpint
: the private exponent, which is the discrete log of the public point.
When a key file is encrypted, there are three pieces of key material that need to be computed from the passphrase:
If encryption-type is ‘aes256-cbc
’, then the symmetric cipher key is 32 bytes long, and the initialisation vector is 16 bytes (one cipher block). The length of the MAC key is also chosen to be 32 bytes.
If encryption-type is ‘none
’, then all three of these pieces of data have zero length. (The MAC is still generated and checked in the key file format, but it has a zero-length key.)
If the amount of key material required is not zero, then the passphrase is fed to the Argon2 key derivation function, in whichever mode is described in the ‘Key-Derivation
’ header in the key file, with parameters derived from the various ‘Argon2-
Parameter:
’ headers.
(If the key is unencrypted, then all those headers are omitted, and Argon2 is not run at all.)
Argon2 takes two extra string inputs in addition to the passphrase and the salt: a secret key, and some ‘associated data’. In PPK's use of Argon2, these are both set to the empty string.
The ‘tag length’ parameter to Argon2 (i.e. the amount of data it is asked to output) is set to the sum of the lengths of all of the data items required, i.e. (cipher key length + IV length + MAC key length). The output data is interpreted as the concatenation of the cipher key, the IV and the MAC key, in that order.
So, for ‘aes256-cbc
’, the tag length will be 32+16+32 = 80 bytes; of the 80 bytes of output data, the first 32 bytes are used as the 256-bit AES key, the next 16 as the CBC IV, and the final 32 bytes as the HMAC-SHA-256 key.
PPK version 2 was used by PuTTY 0.52 to 0.74 inclusive.
In PPK version 2, the MAC algorithm used was HMAC-SHA-1 (so the Private-MAC
line contained only 40 hex digits).
The ‘Key-Derivation:
’ header and all the ‘Argon2-
Parameter:
’ headers were absent. Instead of using Argon2, the key material for encrypting the private blob was derived from the passphrase in a totally different way, as follows.
The cipher key for ‘aes256-cbc
’ was constructed by generating two SHA-1 hashes, concatenating them, and taking the first 32 bytes of the result. (So you'd get all 20 bytes of the first hash output, and the first 12 of the second). Each hash preimage was as follows:
uint32
: a sequence number. This is 0 in the first hash, and 1 in the second. (The idea was to extend this mechanism to further hashes by continuing to increment the sequence number, if future changes required even longer keys.)
In PPK v2, the CBC initialisation vector was all zeroes.
The MAC key was 20 bytes long, and was a single SHA-1 hash of the following data:
putty-private-key-file-mac-key
’, without any prefix length field.
PPK version 1 was a badly designed format, only used during initial development, and not recommended for production use.
PPK version 1 was never used by a released version of PuTTY. It was only emitted by some early development snapshots between version 0.51 (which did not support SSH-2 public keys at all) and 0.52 (which already used version 2 of this file format). I hope there are no PPK v1 files in use anywhere. But just in case, the old badly designed format is documented here anyway.
In PPK version 1, the input to the MAC does not include any of the header fields or the public key. It is simply the private key data (still in plaintext and including random padding), all by itself (without a wrapping string
).
PPK version 1 keys must therefore be rigorously validated after loading, to ensure that the public and private parts of the key were consistent with each other.
PPK version 1 only supported the RSA and DSA key types. For RSA, this validation can be done using only the provided data (since the private key blob contains enough information to reconstruct the public values anyway). But for DSA, that isn't quite enough.
Hence, PPK version 1 DSA keys extended the private data so that immediately after x was stored an extra value:
string
: a SHA-1 hash of the public key data, whose preimage consists of
string
: the large prime p
string
: the small prime q
string
: the group generator g
The idea was that checking this hash would verify that the key parameters had not been tampered with, and then the loading application could directly verify that g^
x =
y.
In an unencrypted version 1 key file, the MAC is replaced by a plain SHA-1 hash of the private key data. This is indicated by the ‘Private-MAC:
’ header being replaced with ‘Private-Hash:
’ instead.
If you want to provide feedback on this manual or on the PuTTY tools themselves, see the Feedback page.
[Debian PuTTY release 0.78-2+deb12u2]