From f233e54992af8618b41d3af87848a1b7d8155f18 Mon Sep 17 00:00:00 2001 From: Stephen Fox Date: Wed, 30 Jan 2019 21:59:56 -0500 Subject: [PATCH] Initial SSH key pair helper implementation. --- builder/virtualbox/common/sshkeypair.go | 221 +++++++++++++++++++ builder/virtualbox/common/sshkeypair_test.go | 93 ++++++++ 2 files changed, 314 insertions(+) create mode 100644 builder/virtualbox/common/sshkeypair.go create mode 100644 builder/virtualbox/common/sshkeypair_test.go diff --git a/builder/virtualbox/common/sshkeypair.go b/builder/virtualbox/common/sshkeypair.go new file mode 100644 index 000000000..dd926b9bc --- /dev/null +++ b/builder/virtualbox/common/sshkeypair.go @@ -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{} +} diff --git a/builder/virtualbox/common/sshkeypair_test.go b/builder/virtualbox/common/sshkeypair_test.go new file mode 100644 index 000000000..82cae75b8 --- /dev/null +++ b/builder/virtualbox/common/sshkeypair_test.go @@ -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 +}