diff --git a/cmd/ssh-keygen/main.go b/cmd/ssh-keygen/main.go new file mode 100644 index 000000000..20262d001 --- /dev/null +++ b/cmd/ssh-keygen/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "flag" + "io/ioutil" + "log" + "os" + "os/user" + "path/filepath" + + "github.com/hashicorp/packer/helper/communicator/sshkey" +) + +type options struct { + Type string + Bits int + Filename string +} + +func (o *options) AddFlagSets(fs *flag.FlagSet) { + fs.StringVar(&o.Type, "type", "rsa", `dsa | ecdsa | ed25519 | rsa +Specifies the type of key to create. The possible values are 'dsa', 'ecdsa', +'ed25519', or 'rsa'. +`) + fs.IntVar(&o.Bits, "bits", 0, `Specifies the number of bits in the key to create. By default maximum +number will be picked. For RSA keys, the minimum size is 1024 bits and the +default is 3072 bits. Generally, 3072 bits is considered sufficient. DSA +keys must be exactly 1024 bits as specified by FIPS 186-2. For ECDSA keys, +the bits flag determines the key length by selecting from one of three +elliptic curve sizes: 256, 384 or 521 bits. Attempting to use bit lengths +other than these three values for ECDSA keys will fail. Ed25519 keys have a +fixed length and the bits flag will be ignored. +`) + + defaultPath := "" + user, err := user.Current() + if err == nil { + defaultPath = filepath.Join(user.HomeDir, ".ssh", "tests") + } + + fs.StringVar(&o.Filename, "filename", defaultPath, `Specifies the filename of the key file. +`) +} + +func main() { + log.SetFlags(0) + log.SetPrefix("ssh-keygen: ") + fs := flag.NewFlagSet("ssh-keygen", flag.ContinueOnError) + cla := options{} + cla.AddFlagSets(fs) + if err := fs.Parse(os.Args[1:]); err != nil { + log.Fatal(err) + } + + algo, err := sshkey.AlgorithmString(cla.Type) + if err != nil { + log.Fatal(err) + } + + log.Printf("Generating public/private %s key pair.", algo) + + keypair, err := sshkey.GeneratePair(algo, nil, cla.Bits) + if err != nil { + log.Fatal(err) + } + + if isDir(cla.Filename) { + cla.Filename = filepath.Join(cla.Filename, "id_"+algo.String()) + } + if fileExists(cla.Filename) { + log.Fatalf("%s already exists.", cla.Filename) + } + log.Printf("Saving private key to %s", cla.Filename) + if err := ioutil.WriteFile(cla.Filename, keypair.Private, 0600); err != nil { + log.Fatal(err) + } + publicFilename := cla.Filename + ".pub" + log.Printf("Saving public key to %s", publicFilename) + if err := ioutil.WriteFile(publicFilename, keypair.Public, 0644); err != nil { + log.Fatal(err) + } +} + +func isDir(filename string) bool { + info, err := os.Stat(filename) + if err != nil { + log.Fatal(err) + } + return info.IsDir() +} + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} diff --git a/helper/communicator/sshkey/algorithm_enumer.go b/helper/communicator/sshkey/algorithm_enumer.go new file mode 100644 index 000000000..1a300735b --- /dev/null +++ b/helper/communicator/sshkey/algorithm_enumer.go @@ -0,0 +1,52 @@ +// Code generated by "enumer -type Algorithm -transform snake"; DO NOT EDIT. + +// +package sshkey + +import ( + "fmt" +) + +const _AlgorithmName = "rsadsaecdsaed25519" + +var _AlgorithmIndex = [...]uint8{0, 3, 6, 11, 18} + +func (i Algorithm) String() string { + if i < 0 || i >= Algorithm(len(_AlgorithmIndex)-1) { + return fmt.Sprintf("Algorithm(%d)", i) + } + return _AlgorithmName[_AlgorithmIndex[i]:_AlgorithmIndex[i+1]] +} + +var _AlgorithmValues = []Algorithm{0, 1, 2, 3} + +var _AlgorithmNameToValueMap = map[string]Algorithm{ + _AlgorithmName[0:3]: 0, + _AlgorithmName[3:6]: 1, + _AlgorithmName[6:11]: 2, + _AlgorithmName[11:18]: 3, +} + +// AlgorithmString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func AlgorithmString(s string) (Algorithm, error) { + if val, ok := _AlgorithmNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Algorithm values", s) +} + +// AlgorithmValues returns all values of the enum +func AlgorithmValues() []Algorithm { + return _AlgorithmValues +} + +// IsAAlgorithm returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Algorithm) IsAAlgorithm() bool { + for _, v := range _AlgorithmValues { + if i == v { + return true + } + } + return false +} diff --git a/helper/communicator/sshkey/generate.go b/helper/communicator/sshkey/generate.go new file mode 100644 index 000000000..a0f47f62e --- /dev/null +++ b/helper/communicator/sshkey/generate.go @@ -0,0 +1,255 @@ +package sshkey + +import ( + "crypto/dsa" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "fmt" + "io" + "math/big" + + "golang.org/x/crypto/ssh" +) + +type Algorithm int + +//go:generate enumer -type Algorithm -transform snake +const ( + RSA Algorithm = iota + DSA + ECDSA + ED25519 +) + +var ( + ErrUnknownAlgorithm = fmt.Errorf("sshkey: unknown private key algorithm") + ErrInvalidRSAKeySize = fmt.Errorf("sshkey: invalid private key rsa size: must be more than 1024") + ErrInvalidECDSAKeySize = fmt.Errorf("sshkey: invalid private key ecdsa size, must be one of 256, 384 or 521") + ErrInvalidDSAKeySize = fmt.Errorf("sshkey: invalid private key dsa size, must be one of 1024, 2048 or 3072") +) + +// Pair represents an ssh key pair, as in +type Pair struct { + Private []byte + Public []byte +} + +func NewPair(public, private interface{}) (*Pair, error) { + kb, err := x509.MarshalPKCS8PrivateKey(private) + if err != nil { + return nil, err + } + + privBlk := &pem.Block{ + Type: "PRIVATE KEY", + Headers: nil, + Bytes: kb, + } + + publicKey, err := ssh.NewPublicKey(public) + if err != nil { + return nil, err + } + return &Pair{ + Private: pem.EncodeToMemory(privBlk), + Public: ssh.MarshalAuthorizedKey(publicKey), + }, nil +} + +// PairFromED25519 marshalls a valid pair of openssh pem for ED25519 keypairs. +// NewPair can handle ed25519 pairs but generates the wrong format apparently: +// `Load key "id_ed25519": invalid format` is the error that happens when I try +// to ssh with such a key. +func PairFromED25519(public ed25519.PublicKey, private ed25519.PrivateKey) (*Pair, error) { + // see https://github.com/golang/crypto/blob/7f63de1d35b0f77fa2b9faea3e7deb402a2383c8/ssh/keys.go#L1273-L1443 + key := struct { + Pub []byte + Priv []byte + Comment string + Pad []byte `ssh:"rest"` + }{ + Pub: public, + Priv: private, + } + keyBytes := ssh.Marshal(key) + + pk1 := struct { + Check1 uint32 + Check2 uint32 + Keytype string + Rest []byte `ssh:"rest"` + }{ + Keytype: ssh.KeyAlgoED25519, + Rest: keyBytes, + } + pk1Bytes := ssh.Marshal(pk1) + + k := struct { + CipherName string + KdfName string + KdfOpts string + NumKeys uint32 + PubKey []byte + PrivKeyBlock []byte + }{ + CipherName: "none", + KdfName: "none", + KdfOpts: "", + NumKeys: 1, + PrivKeyBlock: pk1Bytes, + } + + const opensshV1Magic = "openssh-key-v1\x00" + + privBlk := &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Headers: nil, + Bytes: append([]byte(opensshV1Magic), ssh.Marshal(k)...), + } + publicKey, err := ssh.NewPublicKey(public) + if err != nil { + return nil, err + } + return &Pair{ + Private: pem.EncodeToMemory(privBlk), + Public: ssh.MarshalAuthorizedKey(publicKey), + }, nil +} + +// PairFromDSA marshalls a valid pair of openssh pem for dsa keypairs. +// x509.MarshalPKCS8PrivateKey does not know how to deal with dsa keys. +func PairFromDSA(key *dsa.PrivateKey) (*Pair, error) { + // see https://github.com/golang/crypto/blob/7f63de1d35b0f77fa2b9faea3e7deb402a2383c8/ssh/keys.go#L1186-L1195 + // and https://linux.die.net/man/1/dsa + k := struct { + Version int + P *big.Int + Q *big.Int + G *big.Int + Pub *big.Int + Priv *big.Int + }{ + Version: 0, + P: key.P, + Q: key.Q, + G: key.G, + Pub: key.Y, + Priv: key.X, + } + kb, err := asn1.Marshal(k) + if err != nil { + return nil, err + } + privBlk := &pem.Block{ + Type: "DSA PRIVATE KEY", + Headers: nil, + Bytes: kb, + } + publicKey, err := ssh.NewPublicKey(&key.PublicKey) + if err != nil { + return nil, err + } + return &Pair{ + Private: pem.EncodeToMemory(privBlk), + Public: ssh.MarshalAuthorizedKey(publicKey), + }, nil +} + +// GeneratePair generates a Private/Public key pair using algorithm t. +// +// When rand is nil "crypto/rand".Reader will be used. +// +// bits specifies the number of bits in the key to create. For RSA keys, the +// minimum size is 1024 bits and the default is 3072 bits. Generally, 3072 bits +// is considered sufficient. DSA keys must be exactly 1024 bits - or 2 or 3 +// times that - as specified by FIPS 186-2. For ECDSA keys, bits determines the +// key length by selecting from one of three elliptic curve sizes: 256, 384 or +// 521 bits. Attempting to use bit lengths other than these three values for +// ECDSA keys will fail. Ed25519 keys have a fixed length and the bits will +// be ignored. +func GeneratePair(t Algorithm, rand io.Reader, bits int) (*Pair, error) { + if rand == nil { + rand = cryptorand.Reader + } + switch t { + case DSA: + if bits == 0 { + // currently the ssh package can only decode 1024 bits dsa keys, so + // that's going be the default for now see + // https://github.com/golang/crypto/blob/7f63de1d35b0f77fa2b9faea3e7deb402a2383c8/ssh/keys.go#L411-L420 + bits = 1024 + } + var sizes dsa.ParameterSizes + switch bits { + case 1024: + sizes = dsa.L1024N160 + case 2048: + sizes = dsa.L2048N256 + case 3072: + sizes = dsa.L3072N256 + default: + return nil, ErrInvalidDSAKeySize + } + + params := dsa.Parameters{} + if err := dsa.GenerateParameters(¶ms, rand, sizes); err != nil { + return nil, err + } + + dsakey := &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: params, + }, + } + if err := dsa.GenerateKey(dsakey, rand); err != nil { + return nil, err + } + return PairFromDSA(dsakey) + case ECDSA: + if bits == 0 { + bits = 521 + } + var ecdsakey *ecdsa.PrivateKey + var err error + switch bits { + case 256: + ecdsakey, err = ecdsa.GenerateKey(elliptic.P256(), rand) + case 384: + ecdsakey, err = ecdsa.GenerateKey(elliptic.P384(), rand) + case 521: + ecdsakey, err = ecdsa.GenerateKey(elliptic.P521(), rand) + default: + ecdsakey, err = nil, ErrInvalidECDSAKeySize + } + if err != nil { + return nil, err + } + return NewPair(&ecdsakey.PublicKey, ecdsakey) + case ED25519: + publicKey, privateKey, err := ed25519.GenerateKey(rand) + if err != nil { + return nil, err + } + return PairFromED25519(publicKey, privateKey) + case RSA: + if bits == 0 { + bits = 4096 + } + if bits < 1024 { + return nil, ErrInvalidRSAKeySize + } + rsakey, err := rsa.GenerateKey(rand, bits) + if err != nil { + return nil, err + } + return NewPair(&rsakey.PublicKey, rsakey) + default: + return nil, ErrUnknownAlgorithm + } +} diff --git a/helper/communicator/sshkey/generate_test.go b/helper/communicator/sshkey/generate_test.go new file mode 100644 index 000000000..1de705a50 --- /dev/null +++ b/helper/communicator/sshkey/generate_test.go @@ -0,0 +1,40 @@ +package sshkey + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/crypto/ssh" +) + +func TestGeneratePair_parseable(t *testing.T) { + tests := []struct { + t Algorithm + }{ + {DSA}, + {RSA}, + {ECDSA}, + {ED25519}, + } + for _, tt := range tests { + t.Run(tt.t.String(), func(t *testing.T) { + got, err := GeneratePair(tt.t, nil, 0) + if err != nil { + t.Errorf("GeneratePair() error = %v", err) + return + } + + privateKey, err := ssh.ParsePrivateKey(got.Private) + if err != nil { + t.Fatal(err) + } + publicKey, _, _, _, err := ssh.ParseAuthorizedKey(got.Public) + if err != nil { + t.Fatalf("%v: %s", err, got.Public) + } + if diff := cmp.Diff(privateKey.PublicKey().Marshal(), publicKey.Marshal()); diff != "" { + t.Fatalf("wrong public key: %s", diff) + } + }) + } +}