Last active
May 11, 2020 12:36
-
-
Save arnehormann/9486751 to your computer and use it in GitHub Desktop.
convert openssl pem private key files to putty ppk files (stdin -> stdout)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
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
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
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