Initial SSH key pair helper implementation.

This commit is contained in:
Stephen Fox 2019-01-30 21:59:56 -05:00
parent 20c2524881
commit f233e54992
2 changed files with 314 additions and 0 deletions

View File

@ -0,0 +1,221 @@
package common
// TODO: Make this available to other packer APIs.
// Perhaps through 'helper/ssh'?
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"strconv"
"golang.org/x/crypto/ssh"
)
const (
// That's a lot of bits.
defaultRsaBits = 4096
// rsaSsh is a SSH key pair of RSA type.
rsaSsh sshKeyPairType = "rsa"
// ecdsaSsh is a SSH key pair of ECDSA type.
ecdsaSsh sshKeyPairType = "ecdsa"
)
// sshKeyPairType represents different types of SSH key pairs.
// For example, RSA.
type sshKeyPairType string
func (o sshKeyPairType) String() string {
return string(o)
}
// sshKeyPairBuilder builds SSH key pairs.
type sshKeyPairBuilder interface {
// SetType sets the key pair type.
SetType(sshKeyPairType) sshKeyPairBuilder
// SetBits sets the key pair's bits of entropy.
SetBits(int) sshKeyPairBuilder
// Build returns a SSH key pair.
//
// The following defaults are used if not specified:
// Default type: ECDSA
// Default bits of entropy:
// - RSA: 4096
// - ECDSA: 521
Build() (sshKeyPair, error)
}
type defaultSshKeyPairBuilder struct {
// kind describes the resulting key pair's type.
kind sshKeyPairType
// bits is the resulting key pair's bits of entropy.
bits int
}
func (o *defaultSshKeyPairBuilder) SetType(kind sshKeyPairType) sshKeyPairBuilder {
o.kind = kind
return o
}
func (o *defaultSshKeyPairBuilder) SetBits(bits int) sshKeyPairBuilder {
o.bits = bits
return o
}
func (o *defaultSshKeyPairBuilder) Build() (sshKeyPair, error) {
switch o.kind {
case rsaSsh:
return newRsaSshKeyPair(o.bits)
case ecdsaSsh:
// Default case.
}
return newEcdsaSshKeyPair(o.bits)
}
// sshKeyPair represents a SSH key pair.
type sshKeyPair interface {
// Type returns the key pair's type.
Type() sshKeyPairType
// Bits returns the bits of entropy.
Bits() int
// PrivateKeyPemBlock returns a slice of bytes representing
// the private key in ASN.1, DER format in a PEM block.
PrivateKeyPemBlock() []byte
// PublicKeyAuthorizedKeysFormat returns a slice of bytes
// representing the public key in OpenSSH authorized_keys
// format with a trailing new line.
PublicKeyAuthorizedKeysFormat() []byte
}
type defaultSshKeyPair struct {
// kind is the key pair's type.
kind sshKeyPairType
// bits is the key pair's bits of entropy.
bits int
// privateKeyDerBytes is the private key's bytes
// in ASN.1 DER format
privateKeyDerBytes []byte
// publicKey is the key pair's public key.
publicKey ssh.PublicKey
}
func (o defaultSshKeyPair) Type() sshKeyPairType {
return o.kind
}
func (o defaultSshKeyPair) Bits() int {
return o.bits
}
func (o defaultSshKeyPair) PrivateKeyPemBlock() []byte {
t := "UNKNOWN PRIVATE KEY"
switch o.kind {
case ecdsaSsh:
t = "EC PRIVATE KEY"
case rsaSsh:
t = "RSA PRIVATE KEY"
}
return pem.EncodeToMemory(&pem.Block{
Type: t,
Headers: nil,
Bytes: o.privateKeyDerBytes,
})
}
func (o defaultSshKeyPair) PublicKeyAuthorizedKeysFormat() []byte {
return ssh.MarshalAuthorizedKey(o.publicKey)
}
// newEcdsaSshKeyPair returns a new ECDSA SSH key pair for the given bits
// of entropy.
func newEcdsaSshKeyPair(bits int) (sshKeyPair, error) {
var curve elliptic.Curve
switch bits {
case 0:
bits = 521
fallthrough
case 521:
curve = elliptic.P521()
case 384:
elliptic.P384()
case 256:
elliptic.P256()
case 224:
// Not supported by "golang.org/x/crypto/ssh".
return &defaultSshKeyPair{}, errors.New("golang.org/x/crypto/ssh does not support " +
strconv.Itoa(bits) + " bits")
default:
return &defaultSshKeyPair{}, errors.New("crypto/elliptic does not support " +
strconv.Itoa(bits) + " bits")
}
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return &defaultSshKeyPair{}, err
}
sshPublicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return &defaultSshKeyPair{}, err
}
raw, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return &defaultSshKeyPair{}, err
}
return &defaultSshKeyPair{
kind: ecdsaSsh,
bits: bits,
privateKeyDerBytes: raw,
publicKey: sshPublicKey,
}, nil
}
// newRsaSshKeyPair returns a new RSA SSH key pair for the given bits
// of entropy.
func newRsaSshKeyPair(bits int) (sshKeyPair, error) {
if bits == 0 {
bits = defaultRsaBits
}
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return &defaultSshKeyPair{}, err
}
sshPublicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return &defaultSshKeyPair{}, err
}
return &defaultSshKeyPair{
kind: rsaSsh,
bits: bits,
privateKeyDerBytes: x509.MarshalPKCS1PrivateKey(privateKey),
publicKey: sshPublicKey,
}, nil
}
func newSshKeyPairBuilder() sshKeyPairBuilder {
return &defaultSshKeyPairBuilder{}
}

View File

@ -0,0 +1,93 @@
package common
import (
"crypto/rand"
"errors"
"testing"
"golang.org/x/crypto/ssh"
)
func TestDefaultSshKeyPairBuilder_Build_Default(t *testing.T) {
kp, err := newSshKeyPairBuilder().Build()
if err != nil {
t.Fatal(err.Error())
}
if kp.Type() != ecdsaSsh {
t.Fatal("Expected key pair type to be",
ecdsaSsh.String(), "- got", kp.Type())
}
if kp.Bits() != 521 {
t.Fatal("Expected key pair to be 521 bits - got", kp.Bits())
}
err = verifySshKeyPair(kp)
if err != nil {
t.Fatal(err.Error())
}
}
func TestDefaultSshKeyPairBuilder_Build_EcdsaDefault(t *testing.T) {
kp, err := newSshKeyPairBuilder().SetType(ecdsaSsh).Build()
if err != nil {
t.Fatal(err.Error())
}
if kp.Type() != ecdsaSsh {
t.Fatal("Expected key pair type to be",
ecdsaSsh.String(), "- got", kp.Type())
}
if kp.Bits() != 521 {
t.Fatal("Expected key pair to be 521 bits - got", kp.Bits())
}
err = verifySshKeyPair(kp)
if err != nil {
t.Fatal(err.Error())
}
}
func TestDefaultSshKeyPairBuilder_Build_RsaDefault(t *testing.T) {
kp, err := newSshKeyPairBuilder().SetType(rsaSsh).Build()
if err != nil {
t.Fatal(err.Error())
}
if kp.Type() != rsaSsh {
t.Fatal("Expected default key pair type to be",
rsaSsh.String(), "- got", kp.Type())
}
if kp.Bits() != 4096 {
t.Fatal("Expected key pair to be", 4096, "bits - got", kp.Bits())
}
err = verifySshKeyPair(kp)
if err != nil {
t.Fatal(err.Error())
}
}
func verifySshKeyPair(kp sshKeyPair) error {
signer, err := ssh.ParsePrivateKey(kp.PrivateKeyPemBlock())
if err != nil {
return errors.New("failed to parse private key during verification - " + err.Error())
}
data := []byte{'b', 'r', '4', 'n', '3'}
signature, err := signer.Sign(rand.Reader, data)
if err != nil {
return errors.New("failed to sign test data during verification - " + err.Error())
}
err = signer.PublicKey().Verify(data, signature)
if err != nil {
return errors.New("failed to verify test data - " + err.Error())
}
return nil
}