276 lines
5.8 KiB
Go
276 lines
5.8 KiB
Go
|
package sshkeys
|
||
|
|
||
|
import (
|
||
|
"crypto/aes"
|
||
|
"crypto/cipher"
|
||
|
"crypto/dsa"
|
||
|
"crypto/ecdsa"
|
||
|
"crypto/rand"
|
||
|
"crypto/rsa"
|
||
|
"crypto/x509"
|
||
|
"encoding/asn1"
|
||
|
"encoding/pem"
|
||
|
"fmt"
|
||
|
"math/big"
|
||
|
mrand "math/rand"
|
||
|
|
||
|
"github.com/dchest/bcrypt_pbkdf"
|
||
|
"golang.org/x/crypto/ed25519"
|
||
|
"golang.org/x/crypto/ssh"
|
||
|
)
|
||
|
|
||
|
// Format of private key to use when Marshaling.
|
||
|
type Format int
|
||
|
|
||
|
const (
|
||
|
// FormatOpenSSHv1 encodes a private key using OpenSSH's PROTOCOL.key format: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
||
|
FormatOpenSSHv1 Format = iota
|
||
|
// FormatClassicPEM encodes private keys in PEM, with a key-specific encoding, as used by OpenSSH.
|
||
|
FormatClassicPEM
|
||
|
)
|
||
|
|
||
|
// MarshalOptions provides the Marshal function format and encryption options.
|
||
|
type MarshalOptions struct {
|
||
|
// Passphrase to encrypt private key with, if nil, the key will not be encrypted.
|
||
|
Passphrase []byte
|
||
|
// Format to encode the private key in.
|
||
|
Format Format
|
||
|
}
|
||
|
|
||
|
// Marshal converts a private key into an optionally encrypted format.
|
||
|
func Marshal(pk interface{}, opts *MarshalOptions) ([]byte, error) {
|
||
|
switch opts.Format {
|
||
|
case FormatOpenSSHv1:
|
||
|
return marshalOpenssh(pk, opts)
|
||
|
case FormatClassicPEM:
|
||
|
return marshalPem(pk, opts)
|
||
|
default:
|
||
|
return nil, fmt.Errorf("sshkeys: invalid format %d", opts.Format)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func marshalPem(pk interface{}, opts *MarshalOptions) ([]byte, error) {
|
||
|
var err error
|
||
|
var plain []byte
|
||
|
var pemType string
|
||
|
|
||
|
switch key := pk.(type) {
|
||
|
case *rsa.PrivateKey:
|
||
|
pemType = "RSA PRIVATE KEY"
|
||
|
plain = x509.MarshalPKCS1PrivateKey(key)
|
||
|
case *ecdsa.PrivateKey:
|
||
|
pemType = "EC PRIVATE KEY"
|
||
|
plain, err = x509.MarshalECPrivateKey(key)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
case *dsa.PrivateKey:
|
||
|
pemType = "DSA PRIVATE KEY"
|
||
|
plain, err = marshalDSAPrivateKey(key)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
case *ed25519.PrivateKey:
|
||
|
return nil, fmt.Errorf("sshkeys: ed25519 keys must be marshaled with FormatOpenSSHv1")
|
||
|
default:
|
||
|
return nil, fmt.Errorf("sshkeys: unsupported key type %T", pk)
|
||
|
}
|
||
|
|
||
|
if len(opts.Passphrase) > 0 {
|
||
|
block, err := x509.EncryptPEMBlock(rand.Reader, pemType, plain, opts.Passphrase, x509.PEMCipherAES128)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return pem.EncodeToMemory(block), nil
|
||
|
}
|
||
|
|
||
|
return pem.EncodeToMemory(&pem.Block{
|
||
|
Type: pemType,
|
||
|
Bytes: plain,
|
||
|
}), nil
|
||
|
}
|
||
|
|
||
|
type dsaOpenssl struct {
|
||
|
Version int
|
||
|
P *big.Int
|
||
|
Q *big.Int
|
||
|
G *big.Int
|
||
|
Pub *big.Int
|
||
|
Priv *big.Int
|
||
|
}
|
||
|
|
||
|
// https://github.com/golang/crypto/blob/master/ssh/keys.go#L793-L804
|
||
|
func marshalDSAPrivateKey(pk *dsa.PrivateKey) ([]byte, error) {
|
||
|
k := dsaOpenssl{
|
||
|
Version: 0,
|
||
|
P: pk.P,
|
||
|
Q: pk.Q,
|
||
|
G: pk.G,
|
||
|
Pub: pk.Y,
|
||
|
Priv: pk.X,
|
||
|
}
|
||
|
|
||
|
return asn1.Marshal(k)
|
||
|
}
|
||
|
|
||
|
const opensshv1Magic = "openssh-key-v1"
|
||
|
|
||
|
type opensshHeader struct {
|
||
|
CipherName string
|
||
|
KdfName string
|
||
|
KdfOpts string
|
||
|
NumKeys uint32
|
||
|
PubKey string
|
||
|
PrivKeyBlock string
|
||
|
}
|
||
|
|
||
|
type opensshKey struct {
|
||
|
Check1 uint32
|
||
|
Check2 uint32
|
||
|
Keytype string
|
||
|
Rest []byte `ssh:"rest"`
|
||
|
}
|
||
|
|
||
|
type opensshRsa struct {
|
||
|
N *big.Int
|
||
|
E *big.Int
|
||
|
D *big.Int
|
||
|
Iqmp *big.Int
|
||
|
P *big.Int
|
||
|
Q *big.Int
|
||
|
Comment string
|
||
|
Pad []byte `ssh:"rest"`
|
||
|
}
|
||
|
|
||
|
type opensshED25519 struct {
|
||
|
Pub []byte
|
||
|
Priv []byte
|
||
|
Comment string
|
||
|
Pad []byte `ssh:"rest"`
|
||
|
}
|
||
|
|
||
|
func padBytes(data []byte, blocksize int) []byte {
|
||
|
if blocksize != 0 {
|
||
|
var i byte
|
||
|
for i = byte(1); len(data)%blocksize != 0; i++ {
|
||
|
data = append(data, i&0xFF)
|
||
|
}
|
||
|
}
|
||
|
return data
|
||
|
}
|
||
|
|
||
|
func marshalOpenssh(pk interface{}, opts *MarshalOptions) ([]byte, error) {
|
||
|
var blocksize int
|
||
|
var keylen int
|
||
|
|
||
|
out := opensshHeader{
|
||
|
CipherName: "none",
|
||
|
KdfName: "none",
|
||
|
KdfOpts: "",
|
||
|
NumKeys: 1,
|
||
|
PubKey: "",
|
||
|
}
|
||
|
|
||
|
if len(opts.Passphrase) > 0 {
|
||
|
out.CipherName = "aes256-cbc"
|
||
|
out.KdfName = "bcrypt"
|
||
|
keylen = keySizeAES256
|
||
|
blocksize = aes.BlockSize
|
||
|
}
|
||
|
|
||
|
check := mrand.Uint32()
|
||
|
pk1 := opensshKey{
|
||
|
Check1: check,
|
||
|
Check2: check,
|
||
|
}
|
||
|
|
||
|
switch key := pk.(type) {
|
||
|
case *rsa.PrivateKey:
|
||
|
k := &opensshRsa{
|
||
|
N: key.N,
|
||
|
E: big.NewInt(int64(key.E)),
|
||
|
D: key.D,
|
||
|
Iqmp: key.Precomputed.Qinv,
|
||
|
P: key.Primes[0],
|
||
|
Q: key.Primes[1],
|
||
|
Comment: "",
|
||
|
}
|
||
|
|
||
|
data := ssh.Marshal(k)
|
||
|
pk1.Keytype = ssh.KeyAlgoRSA
|
||
|
pk1.Rest = data
|
||
|
publicKey, err := ssh.NewPublicKey(&key.PublicKey)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
out.PubKey = string(publicKey.Marshal())
|
||
|
|
||
|
case ed25519.PrivateKey:
|
||
|
k := opensshED25519{
|
||
|
Pub: key.Public().(ed25519.PublicKey),
|
||
|
Priv: key,
|
||
|
}
|
||
|
data := ssh.Marshal(k)
|
||
|
pk1.Keytype = ssh.KeyAlgoED25519
|
||
|
pk1.Rest = data
|
||
|
|
||
|
publicKey, err := ssh.NewPublicKey(key.Public())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
out.PubKey = string(publicKey.Marshal())
|
||
|
default:
|
||
|
return nil, fmt.Errorf("sshkeys: unsupported key type %T", pk)
|
||
|
}
|
||
|
|
||
|
if len(opts.Passphrase) > 0 {
|
||
|
rounds := 16
|
||
|
ivlen := blocksize
|
||
|
salt := make([]byte, blocksize)
|
||
|
_, err := rand.Read(salt)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
kdfdata, err := bcrypt_pbkdf.Key(opts.Passphrase, salt, rounds, keylen+ivlen)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
iv := kdfdata[keylen : ivlen+keylen]
|
||
|
aeskey := kdfdata[0:keylen]
|
||
|
|
||
|
block, err := aes.NewCipher(aeskey)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
pkblock := padBytes(ssh.Marshal(pk1), blocksize)
|
||
|
|
||
|
cbc := cipher.NewCBCEncrypter(block, iv)
|
||
|
cbc.CryptBlocks(pkblock, pkblock)
|
||
|
|
||
|
out.PrivKeyBlock = string(pkblock)
|
||
|
|
||
|
var opts struct {
|
||
|
Salt []byte
|
||
|
Rounds uint32
|
||
|
}
|
||
|
|
||
|
opts.Salt = salt
|
||
|
opts.Rounds = uint32(rounds)
|
||
|
|
||
|
out.KdfOpts = string(ssh.Marshal(&opts))
|
||
|
} else {
|
||
|
out.PrivKeyBlock = string(ssh.Marshal(pk1))
|
||
|
}
|
||
|
|
||
|
outBytes := []byte(opensshv1Magic)
|
||
|
outBytes = append(outBytes, 0)
|
||
|
outBytes = append(outBytes, ssh.Marshal(out)...)
|
||
|
block := &pem.Block{
|
||
|
Type: "OPENSSH PRIVATE KEY",
|
||
|
Bytes: outBytes,
|
||
|
}
|
||
|
return pem.EncodeToMemory(block), nil
|
||
|
}
|