packer-cn/helper/ssh/key_pair.go

259 lines
6.8 KiB
Go

package ssh
import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
gossh "golang.org/x/crypto/ssh"
)
const (
// defaultRsaBits is the default bits of entropy for a new RSA
// key pair. That's a lot of bits.
defaultRsaBits = 4096
// Markers for various SSH key pair types.
Default KeyPairType = ""
Rsa KeyPairType = "RSA"
Ecdsa KeyPairType = "ECDSA"
Dsa KeyPairType = "DSA"
Ed25519 KeyPairType = "ED25519"
)
// KeyPairType represents different types of SSH key pairs.
// See the 'const' block for details.
type KeyPairType string
func (o KeyPairType) String() string {
return string(o)
}
// KeyPair represents an SSH key pair.
type KeyPair struct {
// PrivateKeyPemBlock represents the key pair's private key in
// ASN.1 Distinguished Encoding Rules (DER) format in a
// Privacy-Enhanced Mail (PEM) block.
PrivateKeyPemBlock []byte
// PublicKeyAuthorizedKeysLine represents the key pair's public key
// as a line in OpenSSH authorized_keys.
PublicKeyAuthorizedKeysLine []byte
// Comment is the key pair's comment. This is typically used
// to identify the key pair's owner in the SSH user's
// 'authorized_keys' file.
Comment string
}
// KeyPairFromPrivateKey returns a KeyPair loaded from an existing private key.
//
// Supported key pair types include:
// - DSA
// - ECDSA
// - ED25519
// - RSA
func KeyPairFromPrivateKey(config FromPrivateKeyConfig) (KeyPair, error) {
privateKey, err := gossh.ParseRawPrivateKey(config.RawPrivateKeyPemBlock)
if err != nil {
return KeyPair{}, err
}
switch pk := privateKey.(type) {
case crypto.Signer:
// crypto.Signer is implemented by ecdsa.PrivateKey,
// ed25519.PrivateKey, and rsa.PrivateKey - separate cases
// for each PrivateKey type would be redundant.
publicKey, err := gossh.NewPublicKey(pk.Public())
if err != nil {
return KeyPair{}, err
}
return KeyPair{
PrivateKeyPemBlock: config.RawPrivateKeyPemBlock,
PublicKeyAuthorizedKeysLine: authorizedKeysLine(publicKey, config.Comment),
}, nil
case *dsa.PrivateKey:
publicKey, err := gossh.NewPublicKey(&pk.PublicKey)
if err != nil {
return KeyPair{}, err
}
return KeyPair{
PrivateKeyPemBlock: config.RawPrivateKeyPemBlock,
PublicKeyAuthorizedKeysLine: authorizedKeysLine(publicKey, config.Comment),
}, nil
}
return KeyPair{}, fmt.Errorf("Cannot parse existing SSH key pair - unknown key pair type")
}
// FromPrivateKeyConfig describes how an SSH key pair should be loaded from an
// existing private key.
type FromPrivateKeyConfig struct {
// RawPrivateKeyPemBlock is the raw private key that the key pair
// should be loaded from.
RawPrivateKeyPemBlock []byte
// Comment is the key pair's comment. This is typically used
// to identify the key pair's owner in the SSH user's
// 'authorized_keys' file.
Comment string
}
// NewKeyPair generates a new SSH key pair using the specified
// CreateKeyPairConfig.
func NewKeyPair(config CreateKeyPairConfig) (KeyPair, error) {
if config.Type == Default {
config.Type = Ecdsa
}
switch config.Type {
case Ecdsa:
return newEcdsaKeyPair(config)
case Rsa:
return newRsaKeyPair(config)
}
return KeyPair{}, fmt.Errorf("Unable to generate new key pair, type %s is not supported",
config.Type.String())
}
// newEcdsaKeyPair returns a new ECDSA SSH key pair.
func newEcdsaKeyPair(config CreateKeyPairConfig) (KeyPair, error) {
var curve elliptic.Curve
switch config.Bits {
case 0:
config.Bits = 521
fallthrough
case 521:
curve = elliptic.P521()
case 384:
curve = elliptic.P384()
case 256:
curve = elliptic.P256()
case 224:
// Not supported by "golang.org/x/crypto/ssh".
return KeyPair{}, fmt.Errorf("golang.org/x/crypto/ssh does not support %d bits", config.Bits)
default:
return KeyPair{}, fmt.Errorf("crypto/elliptic does not support %d bits", config.Bits)
}
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return KeyPair{}, err
}
sshPublicKey, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return KeyPair{}, err
}
privateRaw, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return KeyPair{}, err
}
privatePem, err := rawPemBlock(&pem.Block{
Type: "EC PRIVATE KEY",
Headers: nil,
Bytes: privateRaw,
})
if err != nil {
return KeyPair{}, err
}
return KeyPair{
PrivateKeyPemBlock: privatePem,
PublicKeyAuthorizedKeysLine: authorizedKeysLine(sshPublicKey, config.Comment),
Comment: config.Comment,
}, nil
}
// newRsaKeyPair returns a new RSA SSH key pair.
func newRsaKeyPair(config CreateKeyPairConfig) (KeyPair, error) {
if config.Bits == 0 {
config.Bits = defaultRsaBits
}
privateKey, err := rsa.GenerateKey(rand.Reader, config.Bits)
if err != nil {
return KeyPair{}, err
}
sshPublicKey, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return KeyPair{}, err
}
privatePemBlock, err := rawPemBlock(&pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
if err != nil {
return KeyPair{}, err
}
return KeyPair{
PrivateKeyPemBlock: privatePemBlock,
PublicKeyAuthorizedKeysLine: authorizedKeysLine(sshPublicKey, config.Comment),
Comment: config.Comment,
}, nil
}
// CreateKeyPairConfig describes how an SSH key pair should be created.
type CreateKeyPairConfig struct {
// Type describes the key pair's type.
Type KeyPairType
// Bits represents the key pair's bits of entropy. E.g., 4096 for
// a 4096 bit RSA key pair, or 521 for a ECDSA key pair with a
// 521-bit curve.
Bits int
// Comment is the resulting key pair's comment. This is typically
// used to identify the key pair's owner in the SSH user's
// 'authorized_keys' file.
Comment string
}
// rawPemBlock encodes a pem.Block to a slice of bytes.
func rawPemBlock(block *pem.Block) ([]byte, error) {
buffer := bytes.NewBuffer(nil)
err := pem.Encode(buffer, block)
if err != nil {
return []byte{}, err
}
return buffer.Bytes(), nil
}
// authorizedKeysLine serializes key for inclusion in an OpenSSH
// authorized_keys file. The return value ends without newline so
// a comment can be appended to the end.
func authorizedKeysLine(key gossh.PublicKey, comment string) []byte {
marshaledPublicKey := gossh.MarshalAuthorizedKey(key)
// Remove the mandatory unix new line. Awful, but the go
// ssh library automatically appends a unix new line.
// We remove it so a key comment can be safely appended to the
// end of the string.
marshaledPublicKey = bytes.TrimSpace(marshaledPublicKey)
if len(strings.TrimSpace(comment)) > 0 {
marshaledPublicKey = append(marshaledPublicKey, ' ')
marshaledPublicKey = append(marshaledPublicKey, comment...)
}
return marshaledPublicKey
}