add sshkey package and ssh-keygen comand (#10101)
* add sshkey.Generate function that returns an sshkey.Pair to be used with openssh. * add cmd/ssh-keygen/main.go for testing purposes * add a test calling ssh.ParsePrivateKey & ssh.ParseAuthorizedKey (which is very similar to what openssh would do to read a keypair) The wrapping of the keys should be handled by crypto/x509.MarshalPKCS8PrivateKey & x/crypto/ssh.NewPublicKey which does not work for ed25519 and dsa. x509.MarshalPKCS8PrivateKey marshals ed25519 keys but the keys did not work with openssh. x509.MarshalPKCS8PrivateKey does not handle dsa keys. So I had to 'wrap' those manually by reading the code of the openssh package. Note that ssh.NewPublicKey works with any keytype. I should probably do a PR to ssh to have a NewPrivateKey & Marshalling funcs
This commit is contained in:
parent
ef1b401a22
commit
b24911661f
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue