From 0687f662082af080249b099a4473cd30acb2c8f2 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Tue, 28 Jun 2022 16:26:18 -0500 Subject: [PATCH 01/11] Use `codespaces.auto` instead for the automatic ssh keys --- pkg/cmd/codespace/ssh.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 292bae0d7..c869f536e 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -24,6 +24,8 @@ import ( "github.com/spf13/cobra" ) +const automaticPrivateKeyName = "codespaces.auto" + type sshOptions struct { codespace string profile string @@ -44,6 +46,11 @@ func newSSHCmd(app *App) *cobra.Command { Long: heredoc.Doc(` The 'ssh' command is used to SSH into a codespace. In its simplest form, you can run 'gh cs ssh', select a codespace interactively, and connect. + + By default, the 'ssh' command will create a public/private ssh key pair to + authenticate with the codespace (if they haven't already been created) inside the + ~/.ssh directory. Any private keys for which the public key has been uploaded to + your GitHub profile may also be used instead, if desired. The 'ssh' command also supports deeper integration with OpenSSH using a '--config' option that generates per-codespace ssh configuration in OpenSSH @@ -125,7 +132,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e startSSHOptions := liveshare.StartSSHServerOptions{} if shouldGenerateSSHKeys(args, opts) && sshContext.HasKeygen() { - keyPair, err := sshContext.GenerateSSHKey("codespaces", "") + keyPair, err := sshContext.GenerateSSHKey(automaticPrivateKeyName, "") if err != nil && !errors.Is(err, ssh.ErrKeyAlreadyExists) { return fmt.Errorf("failed to generate ssh keys: %w", err) } @@ -381,6 +388,11 @@ func newCpCmd(app *App) *cobra.Command { be evaluated on the remote machine, subject to expansion of tildes, braces, globs, environment variables, and backticks. For security, do not use this flag with arguments provided by untrusted users; see for discussion. + + By default, the 'cp' command will create a public/private ssh key pair to authenticate with + the codespace (if they haven't already been created) inside the ~/.ssh directory. Any private + keys for which the public key has been uploaded to your GitHub profile may also be used + instead, if desired. `, "`"), Example: heredoc.Doc(` $ gh codespace cp -e README.md 'remote:/workspaces/$RepositoryName/' From 19b540081149cf312acbb9c52ad35dba079992f4 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:46:44 -0500 Subject: [PATCH 02/11] Handle back compat --- pkg/cmd/codespace/ssh.go | 76 +++++++++++++++++++++++---- pkg/cmd/codespace/ssh_test.go | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index c869f536e..3072d251b 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -24,6 +24,10 @@ import ( "github.com/spf13/cobra" ) +// In 2.13.0 these commands started automatically generating key pairs named 'codespaces' and 'codespaces.pub' +// which could collide with suggested the ssh config also named 'codespaces'. We now use 'codespaces.auto' +// and 'codespaces.auto.pub' in order to avoid that collision. +const automaticPrivateKeyNameOld = "codespaces" const automaticPrivateKeyName = "codespaces.auto" type sshOptions struct { @@ -48,9 +52,7 @@ func newSSHCmd(app *App) *cobra.Command { run 'gh cs ssh', select a codespace interactively, and connect. By default, the 'ssh' command will create a public/private ssh key pair to - authenticate with the codespace (if they haven't already been created) inside the - ~/.ssh directory. Any private keys for which the public key has been uploaded to - your GitHub profile may also be used instead, if desired. + authenticate with the codespace inside the ~/.ssh directory. The 'ssh' command also supports deeper integration with OpenSSH using a '--config' option that generates per-codespace ssh configuration in OpenSSH @@ -131,9 +133,9 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e sshContext := ssh.Context{} startSSHOptions := liveshare.StartSSHServerOptions{} - if shouldGenerateSSHKeys(args, opts) && sshContext.HasKeygen() { - keyPair, err := sshContext.GenerateSSHKey(automaticPrivateKeyName, "") - if err != nil && !errors.Is(err, ssh.ErrKeyAlreadyExists) { + if shouldUseAutomaticSSHKeys(args, opts) { + keyPair, err := setupAutomaticSSHKeys(sshContext) + if err != nil { return fmt.Errorf("failed to generate ssh keys: %w", err) } @@ -214,7 +216,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e } } -func shouldGenerateSSHKeys(args []string, opts sshOptions) bool { +func shouldUseAutomaticSSHKeys(args []string, opts sshOptions) bool { if opts.profile != "" { // The profile may specify the identity file so cautiously don't override anything with that option return false @@ -235,6 +237,62 @@ func shouldGenerateSSHKeys(args []string, opts sshOptions) bool { return true } +func setupAutomaticSSHKeys(sshContext ssh.Context) (*ssh.KeyPair, error) { + keyPair := checkAndUpdateOldKeyPair(sshContext) + if keyPair != nil { + return keyPair, nil + } + + keyPair, err := sshContext.GenerateSSHKey(automaticPrivateKeyName, "") + if err != nil && !errors.Is(err, ssh.ErrKeyAlreadyExists) { + return nil, err + } + + return keyPair, nil +} + +func checkAndUpdateOldKeyPair(sshContext ssh.Context) *ssh.KeyPair { + publicKeys, err := sshContext.LocalPublicKeys() + if err != nil { + return nil + } + + for _, publicKey := range publicKeys { + if !strings.HasSuffix(publicKey, automaticPrivateKeyNameOld+".pub") { + continue + } + + privateKey := strings.TrimSuffix(publicKey, ".pub") + _, err := os.Stat(privateKey) + if err != nil { + continue + } + + // Both old public and private keys exist, rename them to the new name + + privateKeyNew := strings.Replace(privateKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) + err = os.Rename(privateKey, privateKeyNew) + if err != nil { + return nil + } + + publicKeyNew := strings.Replace(publicKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) + err = os.Rename(publicKey, publicKeyNew) + if err != nil { + return nil + } + + keyPair := &ssh.KeyPair{ + PublicKeyPath: publicKeyNew, + PrivateKeyPath: privateKeyNew, + } + + return keyPair + } + + return nil +} + func (a *App) printOpenSSHConfig(ctx context.Context, opts sshOptions) (err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -390,9 +448,7 @@ func newCpCmd(app *App) *cobra.Command { provided by untrusted users; see for discussion. By default, the 'cp' command will create a public/private ssh key pair to authenticate with - the codespace (if they haven't already been created) inside the ~/.ssh directory. Any private - keys for which the public key has been uploaded to your GitHub profile may also be used - instead, if desired. + the codespace inside the ~/.ssh directory. `, "`"), Example: heredoc.Doc(` $ gh codespace cp -e README.md 'remote:/workspaces/$RepositoryName/' diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 63a8961f3..753772c7d 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -2,10 +2,14 @@ package codespace import ( "context" + "io/ioutil" + "os" + "path/filepath" "testing" "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/ssh" ) func TestPendingOperationDisallowsSSH(t *testing.T) { @@ -20,6 +24,100 @@ func TestPendingOperationDisallowsSSH(t *testing.T) { } } +func TestAutomaticSSHKeyPairs(t *testing.T) { + dir := t.TempDir() + + sshContext := ssh.Context{ + ConfigDir: dir, + } + + tests := []struct { + // These files exist when calling setupAutomaticSSHKeys + existingFiles []string + // These files should exist after setupAutomaticSSHKeys finishes + wantFinalFiles []string + }{ + // Basic case: no existing keys, they should be created + { + nil, + []string{automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + }, + // Basic case: keys already exist + { + []string{automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + []string{automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + }, + // Backward compatibility: both old keys exist, they should be renamed + { + []string{automaticPrivateKeyNameOld, automaticPrivateKeyNameOld + ".pub"}, + []string{automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + }, + // Backward compatibility: old private key exists but not the public key, the new keys should be created + { + []string{automaticPrivateKeyNameOld}, + []string{automaticPrivateKeyNameOld, automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + }, + // Backward compatibility: old public key exists but not the private key, the new keys should be created + { + []string{automaticPrivateKeyNameOld + ".pub"}, + []string{automaticPrivateKeyNameOld + ".pub", automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + }, + } + + for _, tt := range tests { + err := os.RemoveAll(dir) + if err != nil { + t.Errorf("Failed to clean test directory: %v", err) + } + os.MkdirAll(dir, 0711) + if err != nil { + t.Errorf("Failed to set up test directory: %v", err) + } + + for _, file := range tt.existingFiles { + if _, err := os.Create(filepath.Join(dir, file)); err != nil { + t.Errorf("Failed to setup test files: %v", err) + } + } + + keyPair, err := setupAutomaticSSHKeys(sshContext) + if err != nil { + t.Errorf("Unexpected error from setupAutomaticSSHKeys: %v", err) + } + if keyPair == nil { + t.Error("Unexpected nil KeyPair from setupAutomaticSSHKeys") + } + + // Check that all the expected files are present + for _, file := range tt.wantFinalFiles { + if _, err := os.Stat(filepath.Join(dir, file)); err != nil { + t.Errorf("Want file %q to exist after setupAutomaticSSHKeys but it doesn't", file) + } + } + + // Check that no unexpected files are present + allExistingFiles, err := ioutil.ReadDir(dir) + if err != nil { + t.Errorf("Failed to list files in test directory: %v", err) + } + for _, file := range allExistingFiles { + filename := file.Name() + isWantedFile := false + for _, wantedFile := range tt.wantFinalFiles { + if filename == wantedFile { + isWantedFile = true + break + } + } + + if !isWantedFile { + t.Errorf("Unexpected file %q exists after setupAutomaticSSHKeys", filename) + } + } + } + +} + func testingSSHApp() *App { user := &api.User{Login: "monalisa"} disabledCodespace := &api.Codespace{ From c5b07762d16e6a85df80a323518931f155253132 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:51:17 -0500 Subject: [PATCH 03/11] Check the key paths too --- pkg/cmd/codespace/ssh_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 753772c7d..2939739d7 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/cli/cli/v2/internal/codespaces/api" @@ -87,6 +88,12 @@ func TestAutomaticSSHKeyPairs(t *testing.T) { if keyPair == nil { t.Error("Unexpected nil KeyPair from setupAutomaticSSHKeys") } + if !strings.HasSuffix(keyPair.PrivateKeyPath, automaticPrivateKeyName) { + t.Errorf("Expected private key path %v, got %v", automaticPrivateKeyName, keyPair.PrivateKeyPath) + } + if !strings.HasSuffix(keyPair.PublicKeyPath, automaticPrivateKeyName+".pub") { + t.Errorf("Expected public key path %v, got %v", automaticPrivateKeyName+".pub", keyPair.PublicKeyPath) + } // Check that all the expected files are present for _, file := range tt.wantFinalFiles { From 2ac379f6891954266edfde683efb1193b3cdfbe9 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:54:07 -0500 Subject: [PATCH 04/11] Comment --- pkg/cmd/codespace/ssh.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 3072d251b..5ded0a8a7 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -251,6 +251,9 @@ func setupAutomaticSSHKeys(sshContext ssh.Context) (*ssh.KeyPair, error) { return keyPair, nil } +// checkAndUpdateOldKeyPair handles backward compatibility with the old keypair names. +// If the old public and private keys both exist they are renamed to the new name. +// The return value is non-nil only if the rename happens. func checkAndUpdateOldKeyPair(sshContext ssh.Context) *ssh.KeyPair { publicKeys, err := sshContext.LocalPublicKeys() if err != nil { From 77ecd0a1472fc9b8ea80457532cad29631c69cfd Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:56:31 -0500 Subject: [PATCH 05/11] Rename public key first for edge cases --- pkg/cmd/codespace/ssh.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 5ded0a8a7..762f7db9c 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -273,14 +273,14 @@ func checkAndUpdateOldKeyPair(sshContext ssh.Context) *ssh.KeyPair { // Both old public and private keys exist, rename them to the new name - privateKeyNew := strings.Replace(privateKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) - err = os.Rename(privateKey, privateKeyNew) + publicKeyNew := strings.Replace(publicKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) + err = os.Rename(publicKey, publicKeyNew) if err != nil { return nil } - publicKeyNew := strings.Replace(publicKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) - err = os.Rename(publicKey, publicKeyNew) + privateKeyNew := strings.Replace(privateKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) + err = os.Rename(privateKey, privateKeyNew) if err != nil { return nil } From 1bae759d3e3953d31f1219fb1e51a8803af0e8a5 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:10:50 -0500 Subject: [PATCH 06/11] Don't use strings.Replace --- pkg/cmd/codespace/ssh.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 762f7db9c..4cc08e084 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -273,13 +273,15 @@ func checkAndUpdateOldKeyPair(sshContext ssh.Context) *ssh.KeyPair { // Both old public and private keys exist, rename them to the new name - publicKeyNew := strings.Replace(publicKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) + sshDir := filepath.Dir(publicKey) + + publicKeyNew := filepath.Join(sshDir, automaticPrivateKeyName+".pub") err = os.Rename(publicKey, publicKeyNew) if err != nil { return nil } - privateKeyNew := strings.Replace(privateKey, automaticPrivateKeyNameOld, automaticPrivateKeyName, -1) + privateKeyNew := filepath.Join(sshDir, automaticPrivateKeyName) err = os.Rename(privateKey, privateKeyNew) if err != nil { return nil From fb4ad53dd074ca6786389fe3ae46f531c1958dff Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:17:24 -0500 Subject: [PATCH 07/11] Check MkdirAll error --- pkg/cmd/codespace/ssh_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 2939739d7..895a53b9f 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -70,7 +70,7 @@ func TestAutomaticSSHKeyPairs(t *testing.T) { if err != nil { t.Errorf("Failed to clean test directory: %v", err) } - os.MkdirAll(dir, 0711) + err = os.MkdirAll(dir, 0711) if err != nil { t.Errorf("Failed to set up test directory: %v", err) } From 1aa457499d37a1987f3b664a9e1f10968c9afb94 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:27:18 -0500 Subject: [PATCH 08/11] Use Fatal instead to avoid nil-ref --- pkg/cmd/codespace/ssh_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 895a53b9f..eb1b93ab9 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -86,7 +86,7 @@ func TestAutomaticSSHKeyPairs(t *testing.T) { t.Errorf("Unexpected error from setupAutomaticSSHKeys: %v", err) } if keyPair == nil { - t.Error("Unexpected nil KeyPair from setupAutomaticSSHKeys") + t.Fatal("Unexpected nil KeyPair from setupAutomaticSSHKeys") } if !strings.HasSuffix(keyPair.PrivateKeyPath, automaticPrivateKeyName) { t.Errorf("Expected private key path %v, got %v", automaticPrivateKeyName, keyPair.PrivateKeyPath) From b5348f661e2e0842965fc53cad70129f8ee76079 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:36:27 -0500 Subject: [PATCH 09/11] Handle case of partial name match --- pkg/cmd/codespace/ssh.go | 2 +- pkg/cmd/codespace/ssh_test.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 4cc08e084..e52422e51 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -261,7 +261,7 @@ func checkAndUpdateOldKeyPair(sshContext ssh.Context) *ssh.KeyPair { } for _, publicKey := range publicKeys { - if !strings.HasSuffix(publicKey, automaticPrivateKeyNameOld+".pub") { + if filepath.Base(publicKey) != automaticPrivateKeyNameOld+".pub" { continue } diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index eb1b93ab9..3f2a8a23b 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -63,6 +63,11 @@ func TestAutomaticSSHKeyPairs(t *testing.T) { []string{automaticPrivateKeyNameOld + ".pub"}, []string{automaticPrivateKeyNameOld + ".pub", automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, }, + // Backward compatibility (edge case): files exist which contains old key name as a substring, the new keys should be created + { + []string{"foo" + automaticPrivateKeyNameOld + ".pub", "foo" + automaticPrivateKeyNameOld}, + []string{"foo" + automaticPrivateKeyNameOld + ".pub", "foo" + automaticPrivateKeyNameOld, automaticPrivateKeyName, automaticPrivateKeyName + ".pub"}, + }, } for _, tt := range tests { From 4eac4fbf4cca256082d6baca260f572d84305f42 Mon Sep 17 00:00:00 2001 From: Caleb Brose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:48:23 -0500 Subject: [PATCH 10/11] Make a new dir per test to work around windows failures --- pkg/cmd/codespace/ssh_test.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 3f2a8a23b..adad394d3 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -26,12 +26,6 @@ func TestPendingOperationDisallowsSSH(t *testing.T) { } func TestAutomaticSSHKeyPairs(t *testing.T) { - dir := t.TempDir() - - sshContext := ssh.Context{ - ConfigDir: dir, - } - tests := []struct { // These files exist when calling setupAutomaticSSHKeys existingFiles []string @@ -71,15 +65,14 @@ func TestAutomaticSSHKeyPairs(t *testing.T) { } for _, tt := range tests { - err := os.RemoveAll(dir) - if err != nil { - t.Errorf("Failed to clean test directory: %v", err) - } - err = os.MkdirAll(dir, 0711) - if err != nil { - t.Errorf("Failed to set up test directory: %v", err) + dir := t.TempDir() + + sshContext := ssh.Context{ + ConfigDir: dir, } + defer os.RemoveAll(dir) + for _, file := range tt.existingFiles { if _, err := os.Create(filepath.Join(dir, file)); err != nil { t.Errorf("Failed to setup test files: %v", err) From b2fe32901128e45c5fc9f365b150614deb2a8e7e Mon Sep 17 00:00:00 2001 From: cmbrose <5447118+cmbrose@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:28:21 -0500 Subject: [PATCH 11/11] Close files to actually fix windows --- pkg/cmd/codespace/ssh_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index adad394d3..8bc6c517f 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -71,12 +71,13 @@ func TestAutomaticSSHKeyPairs(t *testing.T) { ConfigDir: dir, } - defer os.RemoveAll(dir) - for _, file := range tt.existingFiles { - if _, err := os.Create(filepath.Join(dir, file)); err != nil { + f, err := os.Create(filepath.Join(dir, file)) + if err != nil { t.Errorf("Failed to setup test files: %v", err) } + // If the file isn't closed here windows will have errors about file already in use + f.Close() } keyPair, err := setupAutomaticSSHKeys(sshContext)