Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@arnehormann
Last active May 11, 2020 12:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arnehormann/9486751 to your computer and use it in GitHub Desktop.
Save arnehormann/9486751 to your computer and use it in GitHub Desktop.
convert openssl pem private key files to putty ppk files (stdin -> stdout)
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
"flag"
"hash"
"io/ioutil"
"math/big"
"os"
"strconv"
)
/*
load
http://www.nanobit.net/doxy/putty_suite/SSH_8H.html#a3af5f5142bd6b680556426910dae7c09
with alg rsa2
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a2bb0abdf389a18bd3b0c9f0cb171692f
using for blobs
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C_source.html#l00517
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a3ad0809b62fc0d0c99f24f58bc68f97e
using for createkey
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a889e2bb4dc454d99686e367259f99cdd
*/
const (
aesBlockSize = aes.BlockSize
aes256KeySize = 32
plainpk = "plain"
encryptedpk = "aes256-cbc"
)
var (
// errors
errKeyShort = errors.New("key is not in putty ssh2:rsa format, it is too short")
errKeyUnknown = errors.New("key is not in putty ssh2:rsa format")
errNilKey = errors.New("key must not be nil")
// header values and internal strings
ppkKeytype = []byte("ssh-rsa")
ppkEncrypted = []byte(encryptedpk)
ppkPlain = []byte(plainpk)
ppkHashPrefix = []byte("putty-private-key-file-mac-key")
// header keys
ppkKeyFile = []byte("PuTTY-User-Key-File-2")
ppkEncryption = []byte("Encryption")
ppkComment = []byte("Comment")
ppkPublic = []byte("Public-Lines")
ppkPrivate = []byte("Private-Lines")
ppkMac = []byte("Private-MAC")
// aggregated for faster copying
ppkStartPrefix = []byte("" +
string(ppkKeyFile) + ": " + string(ppkKeytype) + "\n" +
string(ppkEncryption) + ": ")
ppkStartPostfix = []byte("\n" + string(ppkComment) + ": ")
ppkStartEncrypted = []byte("" +
string(ppkStartPrefix) +
string(ppkEncrypted) +
string(ppkStartPostfix))
ppkStartPlain = []byte("" +
string(ppkStartPrefix) +
string(ppkPlain) +
string(ppkStartPostfix))
ppkSectionPub = []byte(string(ppkPublic) + ": ")
ppkSectionPriv = []byte(string(ppkPrivate) + ": ")
ppkSectionMac = []byte(string(ppkMac) + ": ")
ppkMinLen = 0 +
len(ppkStartPlain) + 1 +
len(ppkSectionPub) + 1 + 1 +
len(ppkSectionPriv) + 1 + 1 +
len(ppkSectionMac) + 2*sha1.Size + 1
)
func appendBytes(dst []byte, value []byte) []byte {
offset := len(dst)
dst = append(dst, 0, 0, 0, 0)
binary.BigEndian.PutUint32(dst[offset:offset+4], uint32(len(value)))
return append(dst, value...)
}
func appendMpint(dst []byte, value *big.Int) []byte {
offset := len(dst)
dst = append(dst, 0, 0, 0, 0)
data := value.Bytes()
datalen := uint32(len(data))
if data[0]&0x80 != 0 {
// handle a set first bit: prefix with 0 byte
datalen++
dst = append(dst, 0)
}
binary.BigEndian.PutUint32(dst[offset:offset+4], datalen)
dst = append(dst, data...)
// negative values start with a 1-bit
if value.Sign() < 0 {
dst[offset+4] |= 0x80
}
return dst
}
func appendUint32(dst []byte, value uint32) []byte {
const maxlen = 4 + 1 + 4
var datalen int
var ext [maxlen]byte
switch {
case value > 0xffffff:
datalen = 4
case value > 0xffff:
datalen = 3
case value > 0xff:
datalen = 2
default:
datalen = 1
}
// data
binary.BigEndian.PutUint32(ext[4+1:], value)
if ext[maxlen-datalen]&0x80 != 0 {
datalen++
}
// shrink to start of length
data := ext[maxlen-datalen-4:]
// length of data
binary.BigEndian.PutUint32(data[:4], uint32(datalen))
return append(dst, data...)
}
func base64BlockLines(data []byte) int {
return (len(data) + 47) / 48
}
func appendBase64Block(dst, data []byte) []byte {
buffer := [65]byte{}
buffer[64] = '\n'
b64 := buffer[:64]
line := buffer[:]
for ; len(data) > 48; data = data[48:] {
base64.StdEncoding.Encode(b64, data[:48])
dst = append(dst, line...)
}
if len(data) == 0 {
return dst
}
chars64 := 4 * ((len(data) + 2) / 3)
base64.StdEncoding.Encode(line[:chars64], data)
line[chars64] = '\n'
return append(dst, line[:chars64+1]...)
}
// hashPrefix hashes data into h with
// a uint32 big endian length prefix
func hashPrefixed(h hash.Hash, data []byte) {
var buffer = []byte{0, 0, 0, 0}
binary.BigEndian.PutUint32(buffer, uint32(len(data)))
h.Write(buffer)
h.Write(data)
}
// cryptgen is a decrypter or encrypter generation function from
// cipher.
type cryptgen func(b cipher.Block, iv []byte) cipher.BlockMode
// cryptPrivate en- or decrypts data from src into dst.
// src and dst may overlap.
func cryptPrivate(dst, src []byte, password string, sha hash.Hash, crypt cryptgen) {
var twosha1 [2 * sha1.Size]byte
// create weird putty cipher key
passBytes := make([]byte, 4+len(password))
// round 1: base is 0 0 0 0 <password>
copy(passBytes[4:], password)
sha.Reset()
sha.Write(passBytes)
sha.Sum(twosha1[0:0])
// round 2: base is 0 0 0 1 <password>
passBytes[3] = 1
sha.Reset()
sha.Write(passBytes)
sha.Sum(twosha1[sha1.Size:sha1.Size])
// encrypt src with aes256-cbc
block, _ := aes.NewCipher(twosha1[:aes256KeySize])
iv0 := [aesBlockSize]byte{}
crypter := crypt(block, iv0[:])
crypter.CryptBlocks(dst, src)
}
// nextLine returns the index of the next non '\r' / '\n' byte
// after offset.
func nextLine(src []byte, offset int) int {
for i, b := range src[next:] {
switch b {
case '\r', '\n':
next++
default:
return offset + i
}
}
return -1
}
// parseLine parses a single header starting at src[offset:].
// It returns the value and the next offset.
func parseLine(key, src []byte, offset int) (value []byte, next int) {
// Header format:
// ([^:\r\n]*) ": " ([^\r\n]*)
if len(src) < offset+len(key)+2 ||
!bytes.Equal(key, src[offset:offset+len(key)]) {
return
}
offset += len(key)
value = src[offset:]
if string(value[:2]) != ": " {
return
}
offset += 2
value = src[offset:]
nextterm := bytes.IndexByte(value, '\n')
if nextterm > 0 {
nextrl := bytes.IndexByte(value[:nextterm], '\r')
if nextrl > 0 {
// ending with '\r' instead of '\n'
nextterm = nextrl
}
}
value = src[offset:nextterm]
next = nextLine(src, offset+len(value))
return
}
// parseBlob64 parses the base64 encoded blob in src[offset:] of 64 byte long '\n' terminated lines
// into dst and returns a slice of dst, the next offset after the read blob.
func parseBlob64(dst, src []byte, offset, lines int) (blob []byte, next int, err error) {
lastline := offset + (lines-1)*65
end := lastline + bytes.IndexByte(src[lastline:], '\n')
if end < lastline {
err = errKeyUnknown
return
}
blob64 := src[offset:end]
if requiredLen := 6 * ((len(blob64) - numlines) / 8); len(dst) < requiredLen {
dst = make([]byte, requiredLen)
}
n, err := base64.StdEncoding.Decode(dst, blob64)
if err != nil {
return
}
blob = dst[:n]
next = nextLine(src, end+1)
return
}
// parseMpint parses an multi-precision integer (mpint).
// An mpint is prefixed with the length of its data part as a 4 byte big endian number.
// The data part is a big endian array of bytes.
// If the first bit is set, it is negative.
func parseMpint(src []byte, offset int) (*big.Int, int) {
if len(src) < offset+5 {
return
}
src = src[offset:]
length := binary.BigEndian.Uint32(src[:4])
if len(src) < offset+4+length {
return
}
byte0 := src[4]
data := src[4 : 4+length]
value := big.NewInt(0)
if byte0&0x7f == 0 {
data = data[1:]
}
value.SetBytes(data)
if byte0&0x80 != 0 {
value = value.Neg(value)
}
src[4] = byte0
return value, offset + 4 + length
}
// ParsePpk parses
// does not perform key validation
func ParsePpk(ppk []byte, password string) (key *rsa.PrivateKey, comment string, err error) {
if len(ppk) < ppkMinLen {
err = errKeyShort
return
}
// prepare sha1 hmac
hasher := sha1.New()
hashbuff := make([]byte, hasher.Size())
hasher.Reset()
hasher.Write(ppkHashPrefix)
hasher.Write([]byte(password))
hashed := hasher.Sum(hashbuff[:0])
sha1hmac := hmac.New(sha1.New, hashed)
// parse file
var data []byte
var offset, next int
// file format
data, next = parseLine(ppkKeyFile, ppk, 0)
if next <= offset || !bytes.Equal(data, ppkKeytype) {
err = errKeyUnknown
return
}
hashPrefixed(sha1hmac, data)
offset = next
// encryption
data, next = parseLine(ppkEncryption, ppk, offset)
var encrypted bool
switch string(data) {
default:
err = errKeyUnknown
return
case encryptedpk:
encrypted = true
case plainpk:
// nothing
}
hashPrefixed(sha1hmac, data)
offset = next
// comment
comm, next := parseLine(ppkComment, ppk, offset)
if next <= offset {
err = errKeyUnknown
return
}
hashPrefixed(sha1hmac, comm)
offset = next
// declare blob buffer
var blobbuffer [1024]byte
var numlines int
// public key lines
data, next = parseLine(ppkPublic, ppk, offset)
if next <= offset {
err = errKeyUnknown
return
}
numlines, err = strconv.ParseInt(string(data), 10, 0)
if err != nil {
err = errKeyUnknown
return
}
offset = next
data, next = parseBlob(blobbuffer[:], ppk, offset)
if next <= offset {
err = errKeyUnknown
return
}
// key values
var N, E *big.Int
var nextval int
N, nextval = parseMpint(data, 0)
if nextval <= 0 {
err = errKeyUnknown
return
}
E, nextval = parseMpint(data, nextval)
if nextval <= 0 {
err = errKeyUnknown
return
}
hashPrefixed(sha1hmac, data)
offset = next
// private key lines
data, next = parseLine(ppkPrivate, ppk, offset)
if next <= offset {
err = errKeyUnknown
return
}
numlines, err = strconv.ParseInt(string(data), 10, 0)
if err != nil {
err = errKeyUnknown
return
}
offset = next
data, next = parseBlob(blobbuffer[:], ppk, offset)
if next <= offset {
err = errKeyUnknown
return
}
if encrypted {
cryptPrivate(data, data, password, hasher, cipher.NewCBCDecrypter)
}
var D, P, Q *big.Int
D, nextval = parseMpint(data, 0)
if nextval <= 0 {
err = errKeyUnknown
return
}
P, nextval = parseMpint(data, nextval)
if nextval <= 0 {
err = errKeyUnknown
return
}
Q, nextval = parseMpint(data, nextval)
if nextval <= 0 {
err = errKeyUnknown
return
}
// ignore Q^-1 mod P and padding
hashPrefixed(sha1hmac, data)
offset = next
// hmac
data, next = parseLine(ppkMac, ppk, offset)
if next <= offset || next != len(ppk) {
err = errKeyUnknown
return
}
mac := hex.Decode(hashbuff[:0], data)
hash0 := hashbuff[:]
hash1 := sha1hmac.Sum(nil)
// no need for constant time / side channel safety,
// this is just a key conversion api.
if !bytes.Equal(hash0, hash1) {
err = errKeyUnknown
return
}
key = &rsa.PrivateKey{
PublicKey: PublicKey{N: N, E: int(E.Int64())},
D: D,
Primes: []*big.Int{P, Q},
}
comment = string(comm)
return
}
func AppendPpk(dst []byte, key *rsa.PrivateKey, password, comment string) ([]byte, error) {
if key == nil {
return nil, errNilKey
}
key.Precompute()
blobBuffer := [1024]byte{}
twosha1 := [2 * sha1.Size]byte{}
hasher := sha1.New()
hasher.Reset()
hasher.Write(ppkHashPrefix)
hasher.Write([]byte(password))
hashed := hasher.Sum(twosha1[:0])
sha1hmac := hmac.New(sha1.New, hashed)
hashPrefixed(sha1hmac, ppkKeytype)
ppk := dst[:0]
// PPK LEAD IN
if password == "" {
ppk = append(ppk, ppkStartPlain...)
hashPrefixed(sha1hmac, ppkPlain)
} else {
ppk = append(ppk, ppkStartEncrypted...)
hashPrefixed(sha1hmac, ppkEncrypted)
}
// PPK SECTION: COMMENT
hashPrefixed(sha1hmac, []byte(comment))
ppk = append(ppk, comment...)
ppk = append(ppk, '\n')
// PPK SECTION: PUBLIC LINES
var blob []byte
var bloblines int
ppk = append(ppk, ppkSectionPub...)
blob = blobBuffer[:0]
blob = appendBytes(blob, ppkKeytype)
blob = appendUint32(blob, uint32(key.PublicKey.E)) // public exponent
blob = appendMpint(blob, key.N) // modulus
hashPrefixed(sha1hmac, blob)
bloblines = base64BlockLines(blob)
ppk = strconv.AppendUint(ppk, uint64(bloblines), 10)
ppk = append(ppk, '\n')
ppk = appendBase64Block(ppk, blob)
// PPK SECTION: PRIVATE LINES
ppk = append(ppk, ppkSectionPriv...)
blob = blobBuffer[:0]
blob = appendMpint(blob, key.D) // private exponent
blob = appendMpint(blob, key.Primes[0]) // P
blob = appendMpint(blob, key.Primes[1]) // Q
blob = appendMpint(blob, key.Precomputed.Qinv) // Q^-1 mod P
// if encrypted, pad before MAC
if password != "" {
// add padding if not multiple of block size
bytesMissing := (aesBlockSize - len(blob)%aesBlockSize) % aesBlockSize
if bytesMissing > 0 {
hasher.Reset()
hasher.Write(blob)
padding := hasher.Sum(twosha1[:0])
blob = append(blob, padding[:bytesMissing]...)
}
}
hashPrefixed(sha1hmac, blob)
bloblines = base64BlockLines(blob)
ppk = strconv.AppendUint(ppk, uint64(bloblines), 10)
ppk = append(ppk, '\n')
// encrypt private blob after MAC
if password != "" {
cryptPrivate(blob, blob, password, hasher, cipher.NewCBCEncrypter)
}
ppk = appendBase64Block(ppk, blob)
// PPK SECTION: PRIVATE MAC
ppk = append(ppk, ppkSectionMac...)
// len(sha1store) has the length of hex encoded sha1
offset := len(ppk)
ppk = append(ppk, twosha1[:]...)
privateMac := sha1hmac.Sum(twosha1[:0])
hex.Encode(ppk[offset:], privateMac)
return append(ppk, '\n'), nil
}
func main() {
var comment, passin, passout string
flag.StringVar(&comment, "comment", "", "comment for the public key (e.g. user account)")
flag.StringVar(&passin, "passin", "", "password to decrypt the pem file")
flag.StringVar(&passout, "passout", "", "password to encrypt the ppk file")
flag.Parse()
pemBlock, err := ioutil.ReadAll(os.Stdin)
if err != nil {
os.Stderr.WriteString("could not read pem block: " + err.Error())
return
}
block, _ := pem.Decode(pemBlock)
if x509.IsEncryptedPEMBlock(block) {
contents, err := x509.DecryptPEMBlock(block, []byte(passin))
if err != nil {
os.Stderr.WriteString("could not decrypt pem block: " + err.Error())
return
}
block.Bytes = contents
}
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
os.Stderr.WriteString("could not parse private key: " + err.Error())
return
}
buffer := make([]byte, 2048)
ppk, err := AppendPpk(buffer, rsaKey, passout, comment)
os.Stdout.Write(ppk)
}
@arnehormann
Copy link
Author

Sources:

http://www.nanobit.net/doxy/putty_suite/SSHAES_8C.html
http://www.nanobit.net/doxy/putty_suite/SSHPUBK_8C.html
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html
http://www.nanobit.net/doxy/putty_suite/WINPGEN_8C.html
http://www.nanobit.net/doxy/putty_suite/WINPGNT_8C.html

from

  1. http://www.nanobit.net/doxy/putty_suite/WINPGEN_8C.html#a5a4664cad6192289e6cd097858730295
  2. http://www.nanobit.net/doxy/putty_suite/WINPGEN_8C.html#abc0555fc750fe9f486f0b6293e679ae2

with alg rsa2
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a2bb0abdf389a18bd3b0c9f0cb171692f

using for blobs
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C_source.html#l00517
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a3ad0809b62fc0d0c99f24f58bc68f97e

using for createkey
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a889e2bb4dc454d99686e367259f99cdd

with Bignum:
Bignum voidptr
BignumInt uint32
BignumDblInt uint64
BIGNUM_INT_BITS 32
BIGNUM_INT_BYTES 4

and Bignum format
BignumInt length:num used bytes of data | BignumInt* data:the number
those have to be prefixed with 0 if the first bit is 1

@kentor
Copy link

kentor commented Jul 13, 2015

Found this on google. Tried to compile (go version go1.4.2 darwin/amd64) and get:

❯ go build puttygen.go
# command-line-arguments
./puttygen.go:199: undefined: next
./puttygen.go:202: undefined: next
./puttygen.go:249: undefined: numlines
./puttygen.go:267: not enough arguments to return
./puttygen.go:271: invalid operation: offset + 4 + length (mismatched types int and uint32)
./puttygen.go:272: not enough arguments to return
./puttygen.go:285: invalid operation: offset + 4 + length (mismatched types int and uint32)
./puttygen.go:345: cannot assign int64 to numlines (type int) in multiple assignment
./puttygen.go:351: undefined: parseBlob
./puttygen.go:377: cannot assign int64 to numlines (type int) in multiple assignment
./puttygen.go:377: too many errors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment