Create .ppk Files for plink/pscp on Windows Without Opening puttygen
Automation that runs plink/pscp from scripts often gets stuck needing a .ppk file when puttygen on Windows cannot convert it from the command line. The workaround is to write the PPK file yourself, which is straightforward once you know what goes into the MAC.
I was writing a deploy script that needed to run plink/pscp against a server automatically from a Windows machine and ran into the same old problem: it needs a private key in PuTTY's .ppk format and will not accept a regular OpenSSH file. Normally I would open puttygen and convert it, but when automation needs to generate a new key within the script, opening a GUI each time is not practical.
I tried running puttygen in batch mode and immediately hit a wall.
puttygen id_ed25519 -O private -o out.ppk
# เด้ง dialog: unrecognised option "-O"
The Windows version of puttygen.exe is GUI-only — the -O/-o flags available on Linux are omitted. It cannot perform these operations from the command line at all, so it cannot automate .ppk conversion/creation on its own.
Practical options
There are two ways — a shortcut if you have WSL, and a do-it-yourself approach if you want to avoid dependencies.
Use putty-tools on Linux
On Ubuntu in WSL, puttygen has a full CLI and can convert a key in one line. A good fit if the dependency is not a concern.
Write the PPK v3 file yourself
No extra installation needed. Once you know the MAC formula, you can assemble the file directly. Take it to another machine and run it. A good fit for automation.
The WSL shortcut:
sudo apt install putty-tools
puttygen id_ed25519 -O private -o out.ppk
Write the PPK v3 file yourself
PPK is a straightforward text format. The trickiest part is the MAC at the end of the file, which must be calculated correctly or plink will reject the key.
PPK v3 MAC = HMAC-SHA-256, calculated by concatenating the SSH-string representations of 5 items in order: the algorithm name, the encryption value ("none" if no passphrase is set), the comment, the public blob, and the private blob. An SSH-string is "a 4-byte big-endian length + the data itself", and the HMAC key is an empty string when the key is unencrypted.
import base64, hashlib, hmac, struct, sys
def ssh_string(b: bytes) -> bytes:
return struct.pack('>I', len(b)) + b
def ed25519_ppk(openssh_priv_path, out_path, comment='imported'):
# ---- ดึง seed 32 ไบต์ + public 32 ไบต์ จากไฟล์ OpenSSH ที่ decode แล้ว ----
# (ในของจริงผม parse ตัว "-----BEGIN OPENSSH PRIVATE KEY-----" ออกมาเป็น pub/seed)
pub, seed = load_openssh_ed25519(openssh_priv_path) # bytes(32), bytes(32)
alg = b'ssh-ed25519'
public_blob = ssh_string(alg) + ssh_string(pub)
private_blob = ssh_string(seed) # ed25519: seed 32 ไบต์แรก
pub_b64 = base64.b64encode(public_blob).decode()
priv_b64 = base64.b64encode(private_blob).decode()
mac_data = (ssh_string(alg) + ssh_string(b'none') +
ssh_string(comment.encode()) +
ssh_string(public_blob) + ssh_string(private_blob))
mac = hmac.new(b'', mac_data, hashlib.sha256).hexdigest()
def wrap(b64, per=64):
return [b64[i:i+per] for i in range(0, len(b64), per)]
lines = [f'PuTTY-User-Key-File-3: {alg.decode()}',
'Encryption: none', f'Comment: {comment}',
f'Public-Lines: {len(wrap(pub_b64))}', *wrap(pub_b64),
f'Private-Lines: {len(wrap(priv_b64))}', *wrap(priv_b64),
f'Private-MAC: {mac}']
open(out_path, 'w', newline='\r\n').write('\n'.join(lines) + '\n')
print('wrote', out_path)
The ed25519 private blob is the seed in the first 32 bytes of the 64-byte field in the OpenSSH file (the last 32 bytes repeat the public key). Take the wrong bytes and the key will not work even though the file looks "correct".
Test that it actually works
Do not assume the file is correct until plink actually authenticates:
plink -i out.ppk -batch user@host "echo OK; id"
# ต้องได้ OK + uid ของ user กลับมา ไม่ใช่ prompt ถามรหัส
If plink reports "Unable to load key file", the MAC or blob is wrong — go back and check that all 5 SSH-strings are concatenated in order and that the HMAC key really is an empty string.
The problem is not the key itself, but puttygen on Windows, which cannot be automated. Once I understood that PPK v3 just needs a MAC calculated with the right formula, I could generate everything within the script. Automated deployments that need to carry a key no longer need a GUI step in the middle. If you work on Windows→Linux automation, feel free to adapt this to your needs. If I have left anything out or made a typo, I will come back and fix it later.
Want us to set this up?
Get help with server setup, configuration, migration, or troubleshooting. Save time for the work that matters.
Hit something like this? Stuck somewhere?
Come trade notes on the LinuxThai board



Comments 0
No comments yet — be the first