From ec365dccc26f35f3515abdf4f65504500d2910b4 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 15 Feb 2022 15:42:35 -0500 Subject: [PATCH] feat: add password protected keys support * Use caarlos0/sshmarshal * Add PublicKey(), PrivateKey(), and PrivateKeyPEM() * Use PrivateKey() and PrivateKeyPEM() to get the unencrypted private key * Generate password protected keys * Read password protected keys * Add tests --- go.mod | 6 +- go.sum | 16 +-- keygen.go | 256 ++++++++++++++++++++++---------------- keygen_test.go | 128 ++++++++++++++----- testdata/test_ecdsa | 9 ++ testdata/test_ecdsa.pub | 1 + testdata/test_ed25519 | 8 ++ testdata/test_ed25519.pub | 1 + testdata/test_rsa | 54 ++++++++ testdata/test_rsa.pub | 1 + 10 files changed, 332 insertions(+), 148 deletions(-) create mode 100644 testdata/test_ecdsa create mode 100644 testdata/test_ecdsa.pub create mode 100644 testdata/test_ed25519 create mode 100644 testdata/test_ed25519.pub create mode 100644 testdata/test_rsa create mode 100644 testdata/test_rsa.pub diff --git a/go.mod b/go.mod index 4561a13..955be9b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/charmbracelet/keygen go 1.17 require ( - github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a + github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 github.com/mitchellh/go-homedir v1.1.0 - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 ) -require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect +require golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect diff --git a/go.sum b/go.sum index b8c5249..6ba6dc9 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,16 @@ -github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= -github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= +github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 h1:w2ANoiT4ubmh4Nssa3/QW1M7lj3FZkma8f8V5aBDxXM= +github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/keygen.go b/keygen.go index 03ddb38..f35f590 100644 --- a/keygen.go +++ b/keygen.go @@ -3,12 +3,12 @@ package keygen import ( "bytes" + "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" - "crypto/x509" "encoding/pem" "errors" "fmt" @@ -17,7 +17,7 @@ import ( "os/user" "path/filepath" - "github.com/mikesmitty/edkey" + "github.com/caarlos0/sshmarshal" "github.com/mitchellh/go-homedir" "golang.org/x/crypto/ssh" ) @@ -38,6 +38,20 @@ const rsaDefaultBits = 4096 // have after generating. This should be an extreme edge case. var ErrMissingSSHKeys = errors.New("missing one or more keys; did something happen to them after they were generated?") +// ErrUnsupportedKeyType indicates an unsupported key type. +type ErrUnsupportedKeyType struct { + keyType string +} + +// Error implements the error interface for ErrUnsupportedKeyType +func (e ErrUnsupportedKeyType) Error() string { + err := "unsupported key type" + if e.keyType != "" { + err += fmt.Sprintf(": %s", e.keyType) + } + return err +} + // FilesystemErr is used to signal there was a problem creating keys at the // filesystem-level. For example, when we're unable to create a directory to // store new SSH keys in. @@ -51,7 +65,7 @@ func (e FilesystemErr) Error() string { return e.Err.Error() } -// Unwrap returne the underlying error. +// Unwrap returns the underlying error. func (e FilesystemErr) Unwrap() error { return e.Err } @@ -64,49 +78,60 @@ type SSHKeysAlreadyExistErr struct { // SSHKeyPair holds a pair of SSH keys and associated methods. type SSHKeyPair struct { - PrivateKeyPEM []byte - PublicKey []byte - KeyDir string - Filename string // private key filename; public key will have .pub appended + path string // private key filename path; public key will have .pub appended + passphrase []byte + keyType KeyType + privateKey crypto.PrivateKey } func (s SSHKeyPair) privateKeyPath() string { - return filepath.Join(s.KeyDir, s.Filename) + p := fmt.Sprintf("%s_%s", s.path, s.keyType) + return p } func (s SSHKeyPair) publicKeyPath() string { - return filepath.Join(s.KeyDir, s.Filename+".pub") + return s.privateKeyPath() + ".pub" } // New generates an SSHKeyPair, which contains a pair of SSH keys. -func New(path, name string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) { +func New(path string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) { var err error s := &SSHKeyPair{ - KeyDir: path, - Filename: fmt.Sprintf("%s_%s", name, keyType), + path: path, + keyType: keyType, + passphrase: passphrase, } - if s.IsKeyPairExists() { - pubData, err := ioutil.ReadFile(s.publicKeyPath()) + if s.KeyPairExists() { + privData, err := ioutil.ReadFile(s.privateKeyPath()) if err != nil { return nil, err } - s.PublicKey = pubData - privData, err := ioutil.ReadFile(s.privateKeyPath()) + var k interface{} + if len(passphrase) > 0 { + k, err = ssh.ParseRawPrivateKeyWithPassphrase(privData, passphrase) + } else { + k, err = ssh.ParseRawPrivateKey(privData) + } if err != nil { return nil, err } - s.PrivateKeyPEM = privData + switch k := k.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, *ed25519.PrivateKey: + s.privateKey = k + default: + return nil, ErrUnsupportedKeyType{fmt.Sprintf("%T", k)} + } return s, nil } switch keyType { case Ed25519: err = s.generateEd25519Keys() case RSA: - err = s.generateRSAKeys(rsaDefaultBits, passphrase) + err = s.generateRSAKeys(rsaDefaultBits) case ECDSA: - err = s.generateECDSAKeys() + err = s.generateECDSAKeys(elliptic.P384()) default: - return nil, fmt.Errorf("unsupported key type %s", keyType) + return nil, ErrUnsupportedKeyType{string(keyType)} } if err != nil { return nil, err @@ -115,12 +140,12 @@ func New(path, name string, passphrase []byte, keyType KeyType) (*SSHKeyPair, er } // NewWithWrite generates an SSHKeyPair and writes it to disk if not exist. -func NewWithWrite(path, name string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) { - s, err := New(path, name, passphrase, keyType) +func NewWithWrite(path string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) { + s, err := New(path, passphrase, keyType) if err != nil { return nil, err } - if !s.IsKeyPairExists() { + if !s.KeyPairExists() { if err = s.WriteKeys(); err != nil { return nil, err } @@ -128,111 +153,113 @@ func NewWithWrite(path, name string, passphrase []byte, keyType KeyType) (*SSHKe return s, nil } -// generateEd25519Keys creates a pair of EdD25519 keys for SSH auth. -func (s *SSHKeyPair) generateEd25519Keys() error { - // Generate keys - pubKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return err +// PrivateKey returns the unencrypted private key. +func (s *SSHKeyPair) PrivateKey() crypto.PrivateKey { + switch s.keyType { + case RSA, Ed25519, ECDSA: + return s.privateKey + default: + return nil } +} - // Encode PEM - pemBlock := pem.EncodeToMemory(&pem.Block{ - Type: "OPENSSH PRIVATE KEY", - Bytes: edkey.MarshalED25519PrivateKey(privateKey), - }) +// PrivateKeyPEM returns the unencrypted private key in OPENSSH PEM format. +func (s *SSHKeyPair) PrivateKeyPEM() []byte { + block, err := s.pemBlock(nil) + if err != nil { + return nil + } + return pem.EncodeToMemory(block) +} +// PublicKey returns the SSH public key (RFC 4253). Ready to be used in an +// OpenSSH authorized_keys file. +func (s *SSHKeyPair) PublicKey() []byte { + var pk crypto.PublicKey // Prepare public key - publicKey, err := ssh.NewPublicKey(pubKey) + switch s.keyType { + case RSA: + key, ok := s.privateKey.(*rsa.PrivateKey) + if !ok { + return nil + } + pk = key.Public() + case Ed25519: + key, ok := s.privateKey.(*ed25519.PrivateKey) + if !ok { + return nil + } + pk = key.Public() + case ECDSA: + key, ok := s.privateKey.(*ecdsa.PrivateKey) + if !ok { + return nil + } + pk = key.Public() + default: + return nil + } + p, err := ssh.NewPublicKey(pk) if err != nil { - return err + return nil } + // serialize public key + ak := ssh.MarshalAuthorizedKey(p) + return pubKeyWithMemo(ak) +} - // serialize for public key file on disk - serializedPublicKey := ssh.MarshalAuthorizedKey(publicKey) - - s.PrivateKeyPEM = pemBlock - s.PublicKey = pubKeyWithMemo(serializedPublicKey) - return nil +func (s *SSHKeyPair) pemBlock(passphrase []byte) (*pem.Block, error) { + key := s.PrivateKey() + if key == nil { + return nil, ErrMissingSSHKeys + } + switch s.keyType { + case RSA, Ed25519, ECDSA: + if len(passphrase) > 0 { + return sshmarshal.MarshalPrivateKeyWithPassphrase(key, "", passphrase) + } + return sshmarshal.MarshalPrivateKey(key, "") + default: + return nil, ErrUnsupportedKeyType{string(s.keyType)} + } } // generateEd25519Keys creates a pair of EdD25519 keys for SSH auth. -func (s *SSHKeyPair) generateECDSAKeys() error { +func (s *SSHKeyPair) generateEd25519Keys() error { // Generate keys - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + _, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return err } + s.privateKey = &privateKey - // Encode PEM - bts, err := x509.MarshalECPrivateKey(privateKey) - if err != nil { - return err - } - pemBlock := pem.EncodeToMemory(&pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: bts, - }) + return nil +} - // Prepare public key - publicKey, err := ssh.NewPublicKey(privateKey.Public()) +// generateEd25519Keys creates a pair of EdD25519 keys for SSH auth. +func (s *SSHKeyPair) generateECDSAKeys(curve elliptic.Curve) error { + // Generate keys + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) if err != nil { return err } - - // serialize for public key file on disk - serializedPublicKey := ssh.MarshalAuthorizedKey(publicKey) - - s.PrivateKeyPEM = pemBlock - s.PublicKey = pubKeyWithMemo(serializedPublicKey) + s.privateKey = privateKey return nil } // generateRSAKeys creates a pair for RSA keys for SSH auth. -func (s *SSHKeyPair) generateRSAKeys(bitSize int, passphrase []byte) error { +func (s *SSHKeyPair) generateRSAKeys(bitSize int) error { // Generate private key privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) if err != nil { return err } - // Validate private key err = privateKey.Validate() if err != nil { return err } - - // Get ASN.1 DER format - x509Encoded := x509.MarshalPKCS1PrivateKey(privateKey) - - block := &pem.Block{ - Type: "RSA PRIVATE KEY", - Headers: nil, - Bytes: x509Encoded, - } - - // encrypt private key with passphrase - if len(passphrase) > 0 { - block, err = x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, passphrase, x509.PEMCipherAES256) - if err != nil { - return err - } - } - - // Private key in PEM format - pemBlock := pem.EncodeToMemory(block) - - // Generate public key - publicRSAKey, err := ssh.NewPublicKey(privateKey.Public()) - if err != nil { - return err - } - - // serialize for public key file on disk - serializedPubKey := ssh.MarshalAuthorizedKey(publicRSAKey) - - s.PrivateKeyPEM = pemBlock - s.PublicKey = pubKeyWithMemo(serializedPubKey) + s.privateKey = privateKey return nil } @@ -244,16 +271,17 @@ func (s *SSHKeyPair) generateRSAKeys(bitSize int, passphrase []byte) error { func (s *SSHKeyPair) prepFilesystem() error { var err error - if s.KeyDir != "" { - s.KeyDir, err = homedir.Expand(s.KeyDir) + keyDir := filepath.Dir(s.path) + if keyDir != "" { + keyDir, err = homedir.Expand(keyDir) if err != nil { return err } - info, err := os.Stat(s.KeyDir) + info, err := os.Stat(keyDir) if os.IsNotExist(err) { // Directory doesn't exist: create it - return os.MkdirAll(s.KeyDir, 0700) + return os.MkdirAll(keyDir, 0700) } if err != nil { // There was another error statting the directory; something is awry @@ -261,11 +289,11 @@ func (s *SSHKeyPair) prepFilesystem() error { } if !info.IsDir() { // It exists but it's not a directory - return FilesystemErr{Err: fmt.Errorf("%s is not a directory", s.KeyDir)} + return FilesystemErr{Err: fmt.Errorf("%s is not a directory", keyDir)} } if info.Mode().Perm() != 0700 { // Permissions are wrong: fix 'em - if err := os.Chmod(s.KeyDir, 0700); err != nil { + if err := os.Chmod(keyDir, 0700); err != nil { return FilesystemErr{Err: err} } } @@ -285,32 +313,42 @@ func (s *SSHKeyPair) prepFilesystem() error { // WriteKeys writes the SSH key pair to disk. func (s *SSHKeyPair) WriteKeys() error { - if len(s.PrivateKeyPEM) == 0 || len(s.PublicKey) == 0 { + var err error + priv := s.PrivateKeyPEM() + pub := s.PublicKey() + if priv == nil || pub == nil { return ErrMissingSSHKeys } - if err := s.prepFilesystem(); err != nil { + // Encrypt private key with passphrase + if len(s.passphrase) > 0 { + block, err := s.pemBlock(s.passphrase) + if err != nil { + return err + } + priv = pem.EncodeToMemory(block) + } + if err = s.prepFilesystem(); err != nil { return err } - if err := writeKeyToFile(s.PrivateKeyPEM, s.privateKeyPath()); err != nil { + if err := writeKeyToFile(priv, s.privateKeyPath()); err != nil { return err } - if err := writeKeyToFile(s.PublicKey, s.publicKeyPath()); err != nil { + if err := writeKeyToFile(pub, s.publicKeyPath()); err != nil { return err } return nil } -// IsKeyPairExists checks if the SSH key pair exists on disk. -func (s *SSHKeyPair) IsKeyPairExists() bool { +// KeyPairExists checks if the SSH key pair exists on disk. +func (s *SSHKeyPair) KeyPairExists() bool { return fileExists(s.privateKeyPath()) && fileExists(s.publicKeyPath()) } func writeKeyToFile(keyBytes []byte, path string) error { - _, err := os.Stat(path) - if os.IsNotExist(err) { + if _, err := os.Stat(path); os.IsNotExist(err) { return ioutil.WriteFile(path, keyBytes, 0600) } return FilesystemErr{Err: fmt.Errorf("file %s already exists", path)} diff --git a/keygen_test.go b/keygen_test.go index 8028080..c5c5b35 100644 --- a/keygen_test.go +++ b/keygen_test.go @@ -1,14 +1,17 @@ package keygen import ( + "crypto/elliptic" + "fmt" + "io/ioutil" "os" "path/filepath" "testing" ) func TestNewSSHKeyPair(t *testing.T) { - dir := t.TempDir() - _, err := NewWithWrite(dir, "test", []byte(""), "rsa") + p := filepath.Join(t.TempDir(), "test") + _, err := NewWithWrite(p, []byte(""), RSA) if err != nil { t.Errorf("error creating SSH key pair: %v", err) } @@ -17,10 +20,11 @@ func TestNewSSHKeyPair(t *testing.T) { func TestGenerateEd25519Keys(t *testing.T) { // Create temp directory for keys dir := t.TempDir() + filename := "test" k := &SSHKeyPair{ - KeyDir: dir, - Filename: "test", + path: filepath.Join(dir, filename), + keyType: Ed25519, } t.Run("test generate SSH keys", func(t *testing.T) { @@ -31,47 +35,46 @@ func TestGenerateEd25519Keys(t *testing.T) { // TODO: is there a good way to validate these? Lengths seem to vary a bit, // so far now we're just asserting that the keys indeed exist. - if len(k.PrivateKeyPEM) == 0 { + if len(k.PrivateKeyPEM()) == 0 { t.Error("error creating SSH private key PEM; key is 0 bytes") } - if len(k.PublicKey) == 0 { + if len(k.PublicKey()) == 0 { t.Error("error creating SSH public key; key is 0 bytes") } }) t.Run("test write SSH keys", func(t *testing.T) { - k.KeyDir = filepath.Join(dir, "ssh1") + k.path = filepath.Join(dir, "ssh1", filename) if err := k.prepFilesystem(); err != nil { t.Errorf("filesystem error: %v\n", err) } if err := k.WriteKeys(); err != nil { - t.Errorf("error writing SSH keys to %s: %v", k.KeyDir, err) + t.Errorf("error writing SSH keys to %s: %v", k.path, err) } if testing.Verbose() { - t.Logf("Wrote keys to %s", k.KeyDir) + t.Logf("Wrote keys to %s", k.path) } }) t.Run("test not overwriting existing keys", func(t *testing.T) { - k.KeyDir = filepath.Join(dir, "ssh2") + k.path = filepath.Join(dir, "ssh2", filename) if err := k.prepFilesystem(); err != nil { t.Errorf("filesystem error: %v\n", err) } // Private key - filePath := filepath.Join(k.KeyDir, k.Filename) - if !createEmptyFile(t, filePath) { + if !createEmptyFile(t, k.privateKeyPath()) { return } if err := k.WriteKeys(); err == nil { t.Errorf("we wrote the private key over an existing file, but we were not supposed to") } - if err := os.Remove(filePath); err != nil { - t.Errorf("could not remove file %s", filePath) + if err := os.Remove(k.privateKeyPath()); err != nil { + t.Errorf("could not remove file %s", k.privateKeyPath()) } // Public key - if !createEmptyFile(t, filePath+".pub") { + if !createEmptyFile(t, k.publicKeyPath()) { return } if err := k.WriteKeys(); err == nil { @@ -83,61 +86,61 @@ func TestGenerateEd25519Keys(t *testing.T) { func TestGenerateECDSAKeys(t *testing.T) { // Create temp directory for keys dir := t.TempDir() + filename := "test" k := &SSHKeyPair{ - KeyDir: dir, - Filename: "test", + path: filepath.Join(dir, filename), + keyType: ECDSA, } t.Run("test generate SSH keys", func(t *testing.T) { - err := k.generateECDSAKeys() + err := k.generateECDSAKeys(elliptic.P384()) if err != nil { t.Errorf("error creating SSH key pair: %v", err) } // TODO: is there a good way to validate these? Lengths seem to vary a bit, // so far now we're just asserting that the keys indeed exist. - if len(k.PrivateKeyPEM) == 0 { + if len(k.PrivateKeyPEM()) == 0 { t.Error("error creating SSH private key PEM; key is 0 bytes") } - if len(k.PublicKey) == 0 { + if len(k.PublicKey()) == 0 { t.Error("error creating SSH public key; key is 0 bytes") } }) t.Run("test write SSH keys", func(t *testing.T) { - k.KeyDir = filepath.Join(dir, "ssh1") + k.path = filepath.Join(dir, "ssh1", filename) if err := k.prepFilesystem(); err != nil { t.Errorf("filesystem error: %v\n", err) } if err := k.WriteKeys(); err != nil { - t.Errorf("error writing SSH keys to %s: %v", k.KeyDir, err) + t.Errorf("error writing SSH keys to %s: %v", k.path, err) } if testing.Verbose() { - t.Logf("Wrote keys to %s", k.KeyDir) + t.Logf("Wrote keys to %s", k.path) } }) t.Run("test not overwriting existing keys", func(t *testing.T) { - k.KeyDir = filepath.Join(dir, "ssh2") + k.path = filepath.Join(dir, "ssh2", filename) if err := k.prepFilesystem(); err != nil { t.Errorf("filesystem error: %v\n", err) } // Private key - filePath := filepath.Join(k.KeyDir, k.Filename) - if !createEmptyFile(t, filePath) { + if !createEmptyFile(t, k.privateKeyPath()) { return } if err := k.WriteKeys(); err == nil { t.Errorf("we wrote the private key over an existing file, but we were not supposed to") } - if err := os.Remove(filePath); err != nil { - t.Errorf("could not remove file %s", filePath) + if err := os.Remove(k.privateKeyPath()); err != nil { + t.Errorf("could not remove file %s", k.privateKeyPath()) } // Public key - if !createEmptyFile(t, filePath+".pub") { + if !createEmptyFile(t, k.publicKeyPath()) { return } if err := k.WriteKeys(); err == nil { @@ -167,3 +170,70 @@ func createEmptyFile(t *testing.T, path string) (ok bool) { } return true } + +func TestGeneratePublicKeyWithEmptyDir(t *testing.T) { + for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} { + func(t *testing.T) { + k, err := NewWithWrite("testkey", nil, keyType) + if err != nil { + t.Fatalf("error creating SSH key pair: %v", err) + } + fn := fmt.Sprintf("testkey_%s", keyType) + f, err := os.Open(fn + ".pub") + if err != nil { + t.Fatalf("error opening SSH key file: %v", err) + } + defer f.Close() + fc, err := ioutil.ReadAll(f) + if err != nil { + t.Fatalf("error reading SSH key file: %v", err) + } + defer os.Remove(fn) + defer os.Remove(fn + ".pub") + if string(k.PublicKey()) != string(fc) { + t.Errorf("error key mismatch\nprivate key:\n%s\n\nactual file:\n%s", k.PrivateKey(), string(fc)) + } + }(t) + } +} + +func TestGenerateKeyWithPassphrase(t *testing.T) { + for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} { + ph := "testpass" + func(t *testing.T) { + _, err := NewWithWrite("testph", []byte(ph), keyType) + if err != nil { + t.Fatalf("error creating SSH key pair: %v", err) + } + fn := fmt.Sprintf("testph_%s", keyType) + f, err := os.Open(fn) + if err != nil { + t.Fatalf("error opening SSH key file: %v", err) + } + defer f.Close() + fc, err := ioutil.ReadAll(f) + if err != nil { + t.Fatalf("error reading SSH key file: %v", err) + } + defer os.Remove(fn) + defer os.Remove(fn + ".pub") + k, err := New("testph", []byte(ph), keyType) + if err != nil { + t.Fatalf("error reading SSH key pair: %v", err) + } + if string(k.PrivateKeyPEM()) == string(fc) { + t.Errorf("encrypted private key matches file contents") + } + }(t) + } +} + +func TestReadingKeyWithPassphrase(t *testing.T) { + for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} { + kp := filepath.Join("testdata", "test") + _, err := New(kp, []byte("test"), keyType) + if err != nil { + t.Fatalf("error reading SSH key pair: %v", err) + } + } +} diff --git a/testdata/test_ecdsa b/testdata/test_ecdsa new file mode 100644 index 0000000..b0fdae3 --- /dev/null +++ b/testdata/test_ecdsa @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABC5etmxl5 +eS5fzQPlCk8275AAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz +dHAyNTYAAABBBHfKSmcbLNhMQOGT2Mc/4lk+M8BR91paQQIbQqIGgf9OplvMSp4Inwifxo +4uNlXs5PvKCkC/v7jrMxD+LC6CZJkAAACwZYPdjSB7I/+claku+Bwj780I3d6VJR5pq/Tr +bI1RBsF2d0FAzdw9m5XE2gob1hXb8KIzbE8cA29TaoIq+41QoLC2hzu06d+i0yHZpriujH +yT3k4PvEgo4O1q0SurqdDg7oxpH6sgLIfQBtrQ5qZEcAQY830xJb/tc95CFzM1vZvuTmdX +L7uERyItxqG6fmTKPjEhboE6+TQo9JUG2TQP4huOEqZAWyHg9ROS8scQaFs= +-----END OPENSSH PRIVATE KEY----- diff --git a/testdata/test_ecdsa.pub b/testdata/test_ecdsa.pub new file mode 100644 index 0000000..d99d492 --- /dev/null +++ b/testdata/test_ecdsa.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHfKSmcbLNhMQOGT2Mc/4lk+M8BR91paQQIbQqIGgf9OplvMSp4Inwifxo4uNlXs5PvKCkC/v7jrMxD+LC6CZJk= ayman@Aymans-MBP.lan diff --git a/testdata/test_ed25519 b/testdata/test_ed25519 new file mode 100644 index 0000000..0ad9833 --- /dev/null +++ b/testdata/test_ed25519 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCS8X3QEC +Cmdhv2S36f2/eGAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEDglySq5lk5iJAj +9JYrKgAS8vj9mJ8cLnOWENcm/g6wAAAAoLMXg54nwArDFf+OxY5Vx4zcIx1QFagI8lxJtR +98tyqU8CJDiX3gVGwO8iIV9Lh+xYfn+FBw1oZ6IaZFryNeqB0FXbSlgY69s/S5GsIFq3GZ +nChWC/1eCcZ+6Yfeh1QVT2KeltG+aXuqd6kh8ZLjN2tqzAEj6k8deH8tjvAAeFAjAIqNiC +FKhjW3njdEc7bThEtcWU2Y9EudPyshxLXd0TE= +-----END OPENSSH PRIVATE KEY----- diff --git a/testdata/test_ed25519.pub b/testdata/test_ed25519.pub new file mode 100644 index 0000000..fc1e65d --- /dev/null +++ b/testdata/test_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDglySq5lk5iJAj9JYrKgAS8vj9mJ8cLnOWENcm/g6w ayman@Aymans-MBP.lan diff --git a/testdata/test_rsa b/testdata/test_rsa new file mode 100644 index 0000000..e3f22b3 --- /dev/null +++ b/testdata/test_rsa @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,ffaca8d08c10c96b1d70462f903ce639 + +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 +-----END RSA PRIVATE KEY----- diff --git a/testdata/test_rsa.pub b/testdata/test_rsa.pub new file mode 100644 index 0000000..6259989 --- /dev/null +++ b/testdata/test_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDOhYSKPmgNKMc+3Eexo3nrrY+tBXdUuKYzsR4zfIWZGnmRgFJ3Aqc3KgCFu6u4DbMLK+9q68wgrkTU3jO6S9k6Na7M7VgM4MaJLLaqjsrV4JmxjsFQOIVm2dm8Lk53abDnDsBB8vBUOSwiHtMzjsVsX3QuVd1bZUNbyRHS6qLqck8mS/H38i7MCVRrX90gMR5AyUZTBo66LAT9y5Ma8P6S7P68mCrc7+rzRpLGxb39Iu2JuDBG8ku7QYEG8zYYBESYCty6hYrybL01XxrjOvzZ+0Se086jPqFEKuBfSY2WI3+FhOQTIKpcNzIVjpS8LT4OJ0gD+JPKdfApw9KHO7giWaRHQUqRbMJQ5F2W8gU9iyEQwF8gOqmFp5zlWLKm6YAE5gW4hzyhXJ0VYc53F68V/cU4D1/IjQMf2fbYnMir3rqEWm4U2U1HxeTUBNibNXTSZhoyJmEjTk06miUHm6jHSgUeM3zyz7gtxTORACztiUrrDeDtegkmF5pj53FSBRpgowaxo/WMFvHzbLC5fLXNypepZGtE7ohBjDMTLMeuE0QVZZfdXsjTNrrX5m+bwKcMNo7PbCKQCMAhfpFilWjmIbhGBvIxgLz7dGChD2MOqt+FjXvJzEru9pA4FK5cZKYP1Z86gwUxPgm0l0J5kXUuIhlj4RbJe/wosWpM9eQKjw== ayman@Aymans-MBP.lan