Merge pull request #7287 from stephen-fox/ephemeral-ssh-key-pair-issue-7225
virtualbox: create ephemeral SSH key pair for build process
This commit is contained in:
commit
3dc1dafe58
|
@ -0,0 +1,107 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/packer/common/uuid"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/helper/ssh"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
// StepSshKeyPair executes the business logic for setting the SSH key pair in
|
||||
// the specified communicator.Config.
|
||||
type StepSshKeyPair struct {
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
Comm *communicator.Config
|
||||
}
|
||||
|
||||
func (s *StepSshKeyPair) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
if s.Comm.SSHPassword != "" {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if s.Comm.SSHPrivateKeyFile != "" {
|
||||
ui.Say("Using existing SSH private key for the communicator...")
|
||||
privateKeyBytes, err := s.Comm.ReadSSHPrivateKeyFile()
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
kp, err := ssh.KeyPairFromPrivateKey(ssh.FromPrivateKeyConfig{
|
||||
RawPrivateKeyPemBlock: privateKeyBytes,
|
||||
Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()),
|
||||
})
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.Comm.SSHPrivateKey = privateKeyBytes
|
||||
s.Comm.SSHKeyPairName = kp.Comment
|
||||
s.Comm.SSHTemporaryKeyPairName = kp.Comment
|
||||
s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
if s.Comm.SSHAgentAuth {
|
||||
ui.Say("Using local SSH Agent to authenticate connections for the communicator...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Creating ephemeral key pair for SSH communicator...")
|
||||
|
||||
kp, err := ssh.NewKeyPair(ssh.CreateKeyPairConfig{
|
||||
Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()),
|
||||
})
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.Comm.SSHKeyPairName = kp.Comment
|
||||
s.Comm.SSHTemporaryKeyPairName = kp.Comment
|
||||
s.Comm.SSHPrivateKey = kp.PrivateKeyPemBlock
|
||||
s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine
|
||||
s.Comm.SSHClearAuthorizedKeys = true
|
||||
|
||||
ui.Say("Created ephemeral SSH key pair for communicator")
|
||||
|
||||
// If we're in debug mode, output the private key to the working
|
||||
// directory.
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf("Saving communicator private key for debug purposes: %s", s.DebugKeyPath))
|
||||
f, err := os.OpenFile(s.DebugKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write(kp.PrivateKeyPemBlock); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepSshKeyPair) Cleanup(state multistep.StateBag) {
|
||||
if s.Debug {
|
||||
if err := os.Remove(s.DebugKeyPath); err != nil {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error removing debug key '%s': %s", s.DebugKeyPath, err))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
|
@ -14,10 +15,20 @@ import (
|
|||
|
||||
const KeyLeftShift uint32 = 0xFFE1
|
||||
|
||||
// TODO: Should this be made available for other builders?
|
||||
// It is copy pasted in the VMWare builder as well.
|
||||
type bootCommandTemplateData struct {
|
||||
HTTPIP string
|
||||
// HTTPIP is the HTTP server's IP address.
|
||||
HTTPIP string
|
||||
|
||||
// HTTPPort is the HTTP server port.
|
||||
HTTPPort uint
|
||||
Name string
|
||||
|
||||
// Name is the VM's name.
|
||||
Name string
|
||||
|
||||
// SSHPublicKey is the SSH public key in OpenSSH authorized_keys format.
|
||||
SSHPublicKey string
|
||||
}
|
||||
|
||||
type StepTypeBootCommand struct {
|
||||
|
@ -26,6 +37,7 @@ type StepTypeBootCommand struct {
|
|||
VMName string
|
||||
Ctx interpolate.Context
|
||||
GroupInterval time.Duration
|
||||
Comm *communicator.Config
|
||||
}
|
||||
|
||||
func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -54,9 +66,10 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
|||
hostIP := "10.0.2.2"
|
||||
common.SetHTTPIP(hostIP)
|
||||
s.Ctx.Data = &bootCommandTemplateData{
|
||||
hostIP,
|
||||
httpPort,
|
||||
s.VMName,
|
||||
HTTPIP: hostIP,
|
||||
HTTPPort: httpPort,
|
||||
Name: s.VMName,
|
||||
SSHPublicKey: string(s.Comm.SSHPublicKey),
|
||||
}
|
||||
|
||||
sendCodes := func(codes []string) error {
|
||||
|
|
|
@ -227,6 +227,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
HTTPPortMin: b.config.HTTPPortMin,
|
||||
HTTPPortMax: b.config.HTTPPortMax,
|
||||
},
|
||||
&vboxcommon.StepSshKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName),
|
||||
Comm: &b.config.Comm,
|
||||
},
|
||||
new(vboxcommon.StepSuppressMessages),
|
||||
new(stepCreateVM),
|
||||
new(stepCreateDisk),
|
||||
|
@ -260,6 +265,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
GroupInterval: b.config.BootConfig.BootGroupInterval,
|
||||
Comm: &b.config.Comm,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
|
|
|
@ -64,6 +64,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
HTTPPortMin: b.config.HTTPPortMin,
|
||||
HTTPPortMax: b.config.HTTPPortMax,
|
||||
},
|
||||
&vboxcommon.StepSshKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName),
|
||||
Comm: &b.config.Comm,
|
||||
},
|
||||
&vboxcommon.StepDownloadGuestAdditions{
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
GuestAdditionsURL: b.config.GuestAdditionsURL,
|
||||
|
@ -112,6 +117,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
GroupInterval: b.config.BootConfig.BootGroupInterval,
|
||||
Comm: &b.config.Comm,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
|
|
|
@ -38,13 +38,29 @@ func (s *StepCleanupTempKeys) Run(_ context.Context, state multistep.StateBag) m
|
|||
|
||||
ui.Say("Trying to remove ephemeral keys from authorized_keys files")
|
||||
|
||||
cmd.Command = fmt.Sprintf("sed -i.bak '/ssh-rsa.*%s$/d' ~/.ssh/authorized_keys; rm ~/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName)
|
||||
// Per the OpenSSH manual (https://man.openbsd.org/sshd.8), a typical
|
||||
// line in the 'authorized_keys' file contains several fields that
|
||||
// are delimited by spaces. Here is an (abbreviated) example of a line:
|
||||
// ssh-rsa AAAAB3Nza...LiPk== user@example.net
|
||||
//
|
||||
// In the above example, 'ssh-rsa' is the key pair type,
|
||||
// 'AAAAB3Nza...LiPk==' is the base64 encoded public key,
|
||||
// and 'user@example.net' is a comment (in this case, describing
|
||||
// who the key belongs to).
|
||||
//
|
||||
// In the following 'sed' calls, the comment field will be equal to
|
||||
// the value of communicator.Config.SSHTemporaryKeyPairName.
|
||||
// We can remove an authorized public key using 'sed' by looking
|
||||
// for a line ending in ' packer-key-pair-comment' (note the
|
||||
// leading space).
|
||||
//
|
||||
// TODO: Why create a backup file if you are going to remove it?
|
||||
cmd.Command = fmt.Sprintf("sed -i.bak '/ %s$/d' ~/.ssh/authorized_keys; rm ~/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName)
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
log.Printf("Error cleaning up ~/.ssh/authorized_keys; please clean up keys manually: %s", err)
|
||||
}
|
||||
cmd = new(packer.RemoteCmd)
|
||||
cmd.Command = fmt.Sprintf("sudo sed -i.bak '/ssh-rsa.*%s$/d' /root/.ssh/authorized_keys; sudo rm /root/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName)
|
||||
|
||||
cmd.Command = fmt.Sprintf("sudo sed -i.bak '/ %s$/d' /root/.ssh/authorized_keys; sudo rm /root/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName)
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
log.Printf("Error cleaning up /root/.ssh/authorized_keys; please clean up keys manually: %s", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,449 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/common/uuid"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
pemRsa1024 = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWwIBAAKBgQDJEMFPpTBiWNDb3qEIPTSeEnIP8FZdBpG8njOrclcMoQQNhzZ+
|
||||
4uz37tqtHMp36Z7LB4/+85NN6epNXO+ekyZIHswiyBcJC2sT3KuH7nG1BESOooPY
|
||||
DfeCSM+CJT9GDIhy9nUXSsJjrceEyh/B5DjEtIbS0XfcRelrNTJodCmPJwIDAQAB
|
||||
AoGAK66GMOV0c4lUJtBhL8cMTWM4gJn4SVGKC+5az16R5t58YOwFPN/UF7E+tOlS
|
||||
W2bX5sgH0p3cXMr66j/Mlyjk4deLg7trDavulIP93MyVO2SUJ0cstQ0ZmRz2oGwx
|
||||
Gow+hD75Cet7uvepdmG4DKHJe8D/I72rtP1WKuZyd0vP6WECQQDua6wWlyEdIimx
|
||||
XoGWUvmywACWPnQmBnyHG7x5hxMjijQoQZu60zRxSU9I5q08BerTsvbTc+xLnDVv
|
||||
mFzlcjT/AkEA1+P7lcvViZeNKoDB1Qt+VV+pkcqL5aoRwdnLA51SyFJ9tXkxeZwA
|
||||
LOof3xtoRGhCld7ixi3kF5aZsafAJOZd2QJAH8rFyMFgTgU3MAqdFxF7cGV/7ojn
|
||||
bgahZlbBfCcR20Rbjh6piHEPZifTZbI02XMkjBQqK6oikTaEPZxAjuv6uwJANczu
|
||||
yWm+kUdfOpRTuY/fr87jJx3etyEmw7ROz1vJYXqNMUg+eBvUP10pDCR8W2/QCCE/
|
||||
Sjvtd6NkMc2oKInwIQJAFZ1xJte0EaQsXaCIoZwHrQJbK1dd5l1xTAzz51voAcKH
|
||||
2K23xgx4I+/eam2enjFa7wXLZFoW0xg/51xsaIjnrA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
pemRsa2048 = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA/ZPazeRmBapF01gzHXtJGpu0S936xHY+pOrIyIk6lEE06paf
|
||||
q5gh6BCuiN/60Keed5Nz+Es4dPGc73mql9pd7N0HOoEc1IQjZzJVqWOy3E55oWbz
|
||||
rXr1qbmMjw8bGHalZsVBov1UhyB6f2bKi88fGkThJi9HZ+Dc3Jr87eW+whS4D0bI
|
||||
JJe5dkY0VhDqB0YVEk299TxlAiDkeXD1EcMZrD/yHsusapwlXL2WHWmCgbPpbeYW
|
||||
YJhD1bScChYmf41iiInBwFymG7kz4bPsup7wCBXpcLJplY1iuXdtVVujNLDbJwlb
|
||||
Xi2oBm3WizPjYcUthvMlqOieuy6Z4KzyJd7EnQIDAQABAoIBAByZ8LQIbvl0myub
|
||||
ZyiMH1LA/TURdJd0PtybHsp/r/vI3w8WrivMnQZv2z/VA5VFUrpqB0qaMWP/XJQm
|
||||
RPebybxNseMHbRkLTnL1WnQgqqvurglmc1W96LecFh6MtaGswDs3RI/9wur63tY/
|
||||
4dijI/7yhfKoooU097RqRt0ObNW3BxGwNKUraMLKEZjtohv1cZBeRqzGZuui351E
|
||||
YsG1jt23/3OP3Acfd1xpzoi+daadxl9JTr02kE7lMjfq32quhTdzuNZP84sQsaV+
|
||||
RXLNEoiSufjzy3nHTEpG6QaEWQc4gszCIBVRabxr7LtIOqJn2KmXxtOyFE52AJJj
|
||||
ls3ifAECgYEA/9K+5oHdZBizWWUvNzQWXrdHUtVavCwjj+0v+yRFZSoAhKtVmLYl
|
||||
8R4NeG6NCIOoJsqmGVpgtCyPndR4PQ6yr4Jt1FJorjsNw21eYrjOVG+y9Z0DkCwJ
|
||||
uCRVUeqB42jLu7v9r1V3OBQdKLN6VxO4np05KEZyv1LOGGt0XC8NCykCgYEA/cC2
|
||||
NR7Y4Z5OjCc2GHMQNrVZ2HTDDir71RjcIIEmsIQ5/AMAELFLCSqzD73tJ87T5jPi
|
||||
aWeOpIcnK78jMvIIsbV0BXaDsjtlvCdQui2AoX63FuK4q4E+vwe5Q/TqY2nDh2io
|
||||
mGHfeXECyUx4gxIede2XEO9zYQ0lP8gxnjmLkFUCgYBO8LolqQcm/xRAzp9eOn14
|
||||
prekkN+Z10j1/avjpFKhn+9fAPu9zt8wYySm9/4fFXlK1xegFSpoDqQWgNzFgoaS
|
||||
7/1yGifhM6nQlywb7IkGtx0S+2uBDoXFQ7jsOR/xi4HqoVzrwMS0EkjZKWDkA9rh
|
||||
XwSnL+3yqduc33OdiotM2QKBgCgNCrVHsSOrQOqOJdOmFaEM7qljhIXv8t+nlNbs
|
||||
i5bAyAYm0xPPZ/CCdNC/QXdPBdMHzWylk7YUPvKAsKWR3h1ubmmOUysGhQA1lGBO
|
||||
XkcfIPbTwiIPvD+akHtRZM1cHCh7NGEY0ZTxaWcsUrkdWwFyBq39nVBsKrzudCZt
|
||||
HsIhAoGBAMv3erZQawzIgX9nOUHB2UJl8pS+LZSNMlYvFoMHKI2hLq0g/VxUlnsh
|
||||
Jzw9+fTLMVFdY+F3ydO6qQFd8wlfov7deyscdoSj8R5gjGKJsarBs+YVdFde2oLG
|
||||
gkFsXmbmc2boyqGg51CbAX34VJOhGQKhWgKCWqDGmoYXafmyiZc+
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
pemOpenSshRsa1024 = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAIEAzzknaHV741775aJOPacDpd2SiDpIDYmm7/w2sgY8lrinSakfLIVk
|
||||
1qn0IBRLNOzMxoF/pvIgGQXS51xvE1vB3QK8L+8vJwH06DuOXPP1WgVoDTU03gGvBJ7MNF
|
||||
5HcQYvBiIaU5XxG8l0OZO88B9RFhPP9r0XrYxAlSjuk9KKlEcAAAIYLQ46zy0OOs8AAAAH
|
||||
c3NoLXJzYQAAAIEAzzknaHV741775aJOPacDpd2SiDpIDYmm7/w2sgY8lrinSakfLIVk1q
|
||||
n0IBRLNOzMxoF/pvIgGQXS51xvE1vB3QK8L+8vJwH06DuOXPP1WgVoDTU03gGvBJ7MNF5H
|
||||
cQYvBiIaU5XxG8l0OZO88B9RFhPP9r0XrYxAlSjuk9KKlEcAAAADAQABAAAAgQDJ9Jq6jF
|
||||
08P/LhXug/38iHW0UW7S4Ru4jttHGd2MQt5DJtcJzIKA0ZxLL+nKibIPmFsOm2y5yKpolg
|
||||
IE7EoBVzTeg0LedbRayc0Kc5tY7PEz0Shi9ABIMYbNo2L2pNmsq9ns0xA8ur3OugfKHsH8
|
||||
XjJ1rdHsyLjoMx2ADfLY0xkQAAAEAvyrgW4jswENdErbF0rOdP+Y73B/8rxBaY/QBE2qtG
|
||||
oUp7bpOtUAH2Ip7RjXOX4xTAt4n2QeHBSfX7gfXRjmY6AAAAQQDmYlgSWtTYLV9VZSScLU
|
||||
OG+GkhQxYqkKN/N9LSpTP4Pwh81KpMp40yvIlufmKLgGihWVxUDzRap3aoR7PqIvHPAAAA
|
||||
QQDmQ47VwclxiVn5tVAht/Lk2ZVa7rSjeFlXAkAWZkUAiHboaH8IfW9W4gYV7o2BqJO11L
|
||||
0vi+vCq+le45F416wJAAAAImNocmlzQHBvZXRhc3Rlci5jb3JwLm11dHVhbGluay5uZXQ=
|
||||
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
`
|
||||
pemOpenSshRsa2048 = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEAxWfWNu0i8sbmwPqTUfKSeXOSt/fLMuqucn9KYU7rJ+83trznRhAn
|
||||
AHQzKgcSU8PBgkax+PDEUexYUB9gZApNI6K/2twVDYh3Hgwx7EjXf05rji7bQk6TFyKEp4
|
||||
n348CWAdK8iFmNUutSpJLy7GciyMPLu3BK+EsXpsnuPpIm184hEFOiADZyHTGeUgvsKOAc
|
||||
G7u5hBS3kty8LRZmL+pihbktFwGC4D5bapCcTaF2++zkUy4JKcVE5/2JfK1Ya6D0ATczjz
|
||||
1b6+r7j2RUg1mXfK6AwMHEcamzhgeuM9RdrPtMdhZI09LCJzjmXc9pzlGu1HCZzh3rJ3hd
|
||||
8PVmlAd3VQAAA+A9hesQPYXrEAAAAAdzc2gtcnNhAAABAQDFZ9Y27SLyxubA+pNR8pJ5c5
|
||||
K398sy6q5yf0phTusn7ze2vOdGECcAdDMqBxJTw8GCRrH48MRR7FhQH2BkCk0jor/a3BUN
|
||||
iHceDDHsSNd/TmuOLttCTpMXIoSniffjwJYB0ryIWY1S61KkkvLsZyLIw8u7cEr4Sxemye
|
||||
4+kibXziEQU6IANnIdMZ5SC+wo4Bwbu7mEFLeS3LwtFmYv6mKFuS0XAYLgPltqkJxNoXb7
|
||||
7ORTLgkpxUTn/Yl8rVhroPQBNzOPPVvr6vuPZFSDWZd8roDAwcRxqbOGB64z1F2s+0x2Fk
|
||||
jT0sInOOZdz2nOUa7UcJnOHesneF3w9WaUB3dVAAAAAwEAAQAAAQEAvA8Z8iWjX6nA9yM/
|
||||
6ZevluhVY9E60XzlR8qgL2ehet/YMcxwfzywCyyn+WfXO9mHpfZ3YfLs9Ca2U04w4900c7
|
||||
h+EaAMpmHVKNjxTmpucadhq4hT9S0pz6ZgvcMgVuaHgaEjXroBencYuhQMPM5cQurUUfK+
|
||||
WSAgnhJNV2qgeoEGgfDZoL1HkItckEZwIzmx4lfMVAuaeqVq5tJNcdv5ukNHpnIYl6fgDp
|
||||
WGUn/9F8sSHO7P7kGl67IZIsAz+1wW+6pFaVgxbZJ3baPiURtRp+nRSaKLYZSMph6MAiTu
|
||||
YC8EEVqi3X4m/ZHy+BkphfzR24ouwpt1Vv9QOAPzXXsPwQAAAIEAvmA+yiBdzsJplCifTA
|
||||
KljE+KpSuvLPRPTb7MGsBO0LIvxrkXOMCXZF4I2VP1zSUH+SDPPc6JeR1Q8liMqPC3Md6c
|
||||
CIkHfVFBAZL709d0ZtTiir1BipG/l5vIpBnepNX/bWIszIOMzPF2at0WF1lFe6THWujuE8
|
||||
Xjp2AJSFZlUjAAAACBAOMxr6FN38VwRC1nrDcZyo4TjWVhAdk4p3AkdNJhFSiKS/x5/yo2
|
||||
K1majzcKbrR8+fEPTVWGszAg+AXQdsOq17q+DMenfrBckQ9ZHr3upSZAaGN+keNwge/Kaj
|
||||
yOvYiKdYFXmAulQZCPQsDNp7e7Z1dTqxi5IlhUgDPzzO0vRGjNAAAAgQDeb0Ulv7fkYAav
|
||||
tZ+D0LohGjlGFwTeLdwErcVnq9wGyupdeNhlTXZXxRvF+yEt4FCV9UEUnRX75CAnpk2hT2
|
||||
D5uYMyixAEfSeIo59Ln27MmAy0alR3UnT7JnLEZRh4dnvFbSSMJ1rHxf8Eg6YFJmpH65fX
|
||||
exrJE+p69wgRVndoqQAAACJjaHJpc0Bwb2V0YXN0ZXIuY29ycC5tdXR1YWxpbmsubmV0AQ
|
||||
IDBAUGBw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
`
|
||||
pemDsa = `-----BEGIN DSA PRIVATE KEY-----
|
||||
MIIBuwIBAAKBgQDH/T+IkpbdA9nUM7O4MMRoeS0bn7iXWs63Amo2fsIyJPxDvjjF
|
||||
5HZBH5Rq045TFCCWHjymwiYof+wvwUMZIUH++ABTrKzes/r5qG5jXp42pFWf6nTI
|
||||
zHwttdjvNiXr+AgreXOrJKhjv6Ga3hq8MNcXMa9xFsIB83EZNMBPxbj0nwIVAJQW
|
||||
1eR4Uf8/8haQb4HkTsoH+R5/AoGBAK9FV5LIZxY1TeNsD5eXoqpTqCy1WROMggSG
|
||||
VZ4yN0rrKCtLd8am61m/L8VCMUWiO3IJQdq3yWBTEBbsShL/toau9beUdTl6rdB8
|
||||
wcEcNgtZnhypQR58HlmgUFWC45rW37hW4AUJuMDgLxgqSVuoF1pDcHrHSi/fZwgp
|
||||
7t0MKH2SAoGAJfUcLrXg5ZR8jbpZs4L/ubibUn+y35y+33aos07auMW1MesuNTcZ
|
||||
Ch42nbH2wKnbjk8eDxHdHLHzzOLGgYVMpUuBeuc7G5Q94rM/Z0I8HGQ6mvIkuFyp
|
||||
58Unu5yu33GsNUgGEHmriiMGezXNXGNH/72PmTXuyxEMSrad23c6NZoCFAtIqbal
|
||||
4tGCfnnmWU514A7ZzEKj
|
||||
-----END DSA PRIVATE KEY-----
|
||||
`
|
||||
pemEcdsa384 = `-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDAjuEIlmFyhGjFtJoAwD420FuPAjIknN3YwDZL4cfMFpB4YAK+7QVLs
|
||||
coAJ/ADuT7OgBwYFK4EEACKhZANiAASeXKyBr2prr4f4aOsM4dtVikYOUIL3yYnb
|
||||
GFOy7yHmauCnkIB48paXpvRE5m53Q8zgu7vkz/z9tcMBcC0GzpY3Sef37fmgTUuZ
|
||||
AJuJp36DMBdQel+j51TcQ79sizxCayg=
|
||||
-----END EC PRIVATE KEY-----
|
||||
`
|
||||
pemEcdsa521 = `-----BEGIN EC PRIVATE KEY-----
|
||||
MIHcAgEBBEIBVCiwcf/did2vCIu3aMe7OeTD35PULm0hqmfkAK9OKIosi/DjOFfA
|
||||
8h99rVNPaf+Cx/JNmEzR4bZNnYDyilSRCr+gBwYFK4EEACOhgYkDgYYABABHBMLP
|
||||
XbQoRF31ZGIeUj9jt9GqKES1dLBtGDEQSiiZFouL4tEIW7NfIZDpOIkA0khNcO8N
|
||||
xH6eylg0XOgcr01GRwCjY5VOapOahtn63SpajPGeKk+46F2dULIwrov9tWQuYNa3
|
||||
P50N8j3rx6fAdgyDENOcCJlfNdNcySvkH4bgL1xcsw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
`
|
||||
pemOpenSshEd25519 = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACAUftPhZQN17kAlThiiWJEgJvddm/pUhHvgrHUtpuYFOQAAAKjN+UhDzflI
|
||||
QwAAAAtzc2gtZWQyNTUxOQAAACAUftPhZQN17kAlThiiWJEgJvddm/pUhHvgrHUtpuYFOQ
|
||||
AAAEANXlEZdNU03RMmj77O2ojWh06Hbj8/qQ++H5wkt688NBR+0+FlA3XuQCVOGKJYkSAm
|
||||
912b+lSEe+CsdS2m5gU5AAAAImNocmlzQHBvZXRhc3Rlci5jb3JwLm11dHVhbGluay5uZX
|
||||
QBAgM=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
`
|
||||
)
|
||||
|
||||
func TestNewKeyPair_Default(t *testing.T) {
|
||||
kp, err := NewKeyPair(CreateKeyPairConfig{})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = verifyEcdsaKeyPair(kp, expectedData{
|
||||
bits: 521,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewKeyPair_ECDSA_Default(t *testing.T) {
|
||||
kp, err := NewKeyPair(CreateKeyPairConfig{
|
||||
Type: Ecdsa,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = verifyEcdsaKeyPair(kp, expectedData{
|
||||
bits: 521,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewKeyPair_ECDSA_Positive(t *testing.T) {
|
||||
for _, bits := range []int{521, 384, 256} {
|
||||
config := CreateKeyPairConfig{
|
||||
Type: Ecdsa,
|
||||
Bits: bits,
|
||||
Comment: uuid.TimeOrderedUUID(),
|
||||
}
|
||||
|
||||
kp, err := NewKeyPair(config)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = verifyEcdsaKeyPair(kp, expectedData{
|
||||
bits: bits,
|
||||
comment: config.Comment,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewKeyPair_ECDSA_Negative(t *testing.T) {
|
||||
for _, bits := range []int{224, 1, 2, 3} {
|
||||
_, err := NewKeyPair(CreateKeyPairConfig{
|
||||
Type: Ecdsa,
|
||||
Bits: bits,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected key pair generation to fail for %d bits", bits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewKeyPair_RSA_Positive(t *testing.T) {
|
||||
for _, bits := range []int{4096, 2048} {
|
||||
config := CreateKeyPairConfig{
|
||||
Type: Rsa,
|
||||
Bits: bits,
|
||||
Comment: uuid.TimeOrderedUUID(),
|
||||
}
|
||||
|
||||
kp, err := NewKeyPair(config)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = verifyRsaKeyPair(kp, expectedData{
|
||||
bits: config.Bits,
|
||||
comment: config.Comment,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyPairFromPrivateKey(t *testing.T) {
|
||||
m := map[string]fromPrivateExpectedData{
|
||||
pemRsa1024: {
|
||||
t: Rsa,
|
||||
d: expectedData{
|
||||
bits: 1024,
|
||||
},
|
||||
},
|
||||
pemRsa2048: {
|
||||
t: Rsa,
|
||||
d: expectedData{
|
||||
bits: 2048,
|
||||
},
|
||||
},
|
||||
pemOpenSshRsa1024: {
|
||||
t: Rsa,
|
||||
d: expectedData{
|
||||
bits: 1024,
|
||||
},
|
||||
},
|
||||
pemOpenSshRsa2048: {
|
||||
t: Rsa,
|
||||
d: expectedData{
|
||||
bits: 2048,
|
||||
},
|
||||
},
|
||||
pemDsa: {
|
||||
t: Dsa,
|
||||
d: expectedData{
|
||||
bits: 1024,
|
||||
},
|
||||
},
|
||||
pemEcdsa384: {
|
||||
t: Ecdsa,
|
||||
d: expectedData{
|
||||
bits: 384,
|
||||
},
|
||||
},
|
||||
pemEcdsa521: {
|
||||
t: Ecdsa,
|
||||
d: expectedData{
|
||||
bits: 521,
|
||||
},
|
||||
},
|
||||
pemOpenSshEd25519: {
|
||||
t: Ed25519,
|
||||
d: expectedData{
|
||||
bits: 256,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for rawPrivateKey, expected := range m {
|
||||
kp, err := KeyPairFromPrivateKey(FromPrivateKeyConfig{
|
||||
RawPrivateKeyPemBlock: []byte(rawPrivateKey),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
switch expected.t {
|
||||
case Dsa:
|
||||
err = verifyDsaKeyPair(kp, expected)
|
||||
case Ecdsa:
|
||||
err = verifyEcdsaKeyPair(kp, expected.d)
|
||||
case Ed25519:
|
||||
err = verifyEd25519KeyPair(kp, expected)
|
||||
case Rsa:
|
||||
err = verifyRsaKeyPair(kp, expected.d)
|
||||
default:
|
||||
err = fmt.Errorf("unexected SSH key pair type %s", expected.t.String())
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fromPrivateExpectedData struct {
|
||||
t KeyPairType
|
||||
d expectedData
|
||||
}
|
||||
|
||||
type expectedData struct {
|
||||
bits int
|
||||
comment string
|
||||
}
|
||||
|
||||
func verifyEcdsaKeyPair(kp KeyPair, e expectedData) error {
|
||||
privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pk, ok := privateKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("private key should be *ecdsa.PrivateKey")
|
||||
}
|
||||
|
||||
if pk.Curve.Params().BitSize != e.bits {
|
||||
return fmt.Errorf("bit size should be %d - got %d", e.bits, pk.Curve.Params().BitSize)
|
||||
}
|
||||
|
||||
publicKey, err := gossh.NewPublicKey(&pk.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n"))
|
||||
if len(e.comment) > 0 {
|
||||
expectedBytes = append(expectedBytes, ' ')
|
||||
expectedBytes = append(expectedBytes, e.comment...)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) {
|
||||
return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'",
|
||||
string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyRsaKeyPair(kp KeyPair, e expectedData) error {
|
||||
privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pk, ok := privateKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("private key should be *rsa.PrivateKey")
|
||||
}
|
||||
|
||||
if pk.N.BitLen() != e.bits {
|
||||
return fmt.Errorf("bit size should be %d - got %d", e.bits, pk.N.BitLen())
|
||||
}
|
||||
|
||||
publicKey, err := gossh.NewPublicKey(&pk.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n"))
|
||||
if len(e.comment) > 0 {
|
||||
expectedBytes = append(expectedBytes, ' ')
|
||||
expectedBytes = append(expectedBytes, e.comment...)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) {
|
||||
return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'",
|
||||
string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyDsaKeyPair(kp KeyPair, e fromPrivateExpectedData) error {
|
||||
privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pk, ok := privateKey.(*dsa.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("private key should be *rsa.PrivateKey")
|
||||
}
|
||||
|
||||
publicKey, err := gossh.NewPublicKey(&pk.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n"))
|
||||
if len(e.d.comment) > 0 {
|
||||
expectedBytes = append(expectedBytes, ' ')
|
||||
expectedBytes = append(expectedBytes, e.d.comment...)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) {
|
||||
return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'",
|
||||
string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyEd25519KeyPair(kp KeyPair, e fromPrivateExpectedData) error {
|
||||
privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pk, ok := privateKey.(*ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("private key should be *rsa.PrivateKey")
|
||||
}
|
||||
|
||||
publicKey, err := gossh.NewPublicKey(pk.Public())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n"))
|
||||
if len(e.d.comment) > 0 {
|
||||
expectedBytes = append(expectedBytes, ' ')
|
||||
expectedBytes = append(expectedBytes, e.d.comment...)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) {
|
||||
return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'",
|
||||
string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -381,6 +381,8 @@ contention. If you notice missing keys, you can tune this delay by specifying
|
|||
|
||||
<%= partial "partials/builders/boot-command" %>
|
||||
|
||||
<%= partial "partials/builders/virtualbox-ssh-key-pair" %>
|
||||
|
||||
Example boot command. This is actually a working boot command used to start an
|
||||
Ubuntu 12.04 installer:
|
||||
|
||||
|
|
|
@ -321,6 +321,8 @@ contention. If you notice missing keys, you can tune this delay by specifying
|
|||
|
||||
<%= partial "partials/builders/boot-command" %>
|
||||
|
||||
<%= partial "partials/builders/virtualbox-ssh-key-pair" %>
|
||||
|
||||
Example boot command. This is actually a working boot command used to start an
|
||||
Ubuntu 12.04 installer:
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
### SSH key pair automation
|
||||
|
||||
The VirtualBox builders can inject the current SSH key pair's public key into
|
||||
the template using the following variable:
|
||||
|
||||
- `SSHPublicKey` (*VirtualBox builders only*) - This is the SSH public key
|
||||
as a line in OpenSSH authorized_keys format.
|
||||
|
||||
When a private key is provided using `ssh_private_key_file`, the key's
|
||||
corresponding public key can be accessed using the above variables.
|
||||
|
||||
If `ssh_password` and `ssh_private_key_file` are not specified, Packer will
|
||||
automatically generate en ephemeral key pair. The key pair's public key can
|
||||
be accessed using the template variables.
|
||||
|
||||
For example, the public key can be provided in the boot command as a URL
|
||||
encoded string by appending `| urlquery` to the variable:
|
||||
```json
|
||||
{
|
||||
"type": "virtualbox-iso",
|
||||
"boot_command": [
|
||||
"<up><wait><tab> text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg PACKER_USER={{ user `username` }} PACKER_AUTHORIZED_KEY={{ .SSHPublicKey | urlquery }}<enter>"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A kickstart could then leverage those fields from the kernel command line by
|
||||
decoding the URL-encoded public key:
|
||||
```
|
||||
%post
|
||||
|
||||
# Newly created users need the file/folder framework for SSH key authentication.
|
||||
umask 0077
|
||||
mkdir /etc/skel/.ssh
|
||||
touch /etc/skel/.ssh/authorized_keys
|
||||
|
||||
# Loop over the command line. Set interesting variables.
|
||||
for x in $(cat /proc/cmdline)
|
||||
do
|
||||
case $x in
|
||||
PACKER_USER=*)
|
||||
PACKER_USER="${x#*=}"
|
||||
;;
|
||||
PACKER_AUTHORIZED_KEY=*)
|
||||
# URL decode $encoded into $PACKER_AUTHORIZED_KEY
|
||||
encoded=$(echo "${x#*=}" | tr '+' ' ')
|
||||
printf -v PACKER_AUTHORIZED_KEY '%b' "${encoded//%/\\x}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Create/configure packer user, if any.
|
||||
if [ -n "$PACKER_USER" ]
|
||||
then
|
||||
useradd $PACKER_USER
|
||||
echo "%$PACKER_USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/$PACKER_USER
|
||||
[ -n "$PACKER_AUTHORIZED_KEY" ] && echo $PACKER_AUTHORIZED_KEY >> $(eval echo ~"$PACKER_USER")/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
%end
|
||||
```
|
Loading…
Reference in New Issue