| 1 | //go:build !windows |
| 2 | |
| 3 | package remote |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "net" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "golang.org/x/crypto/ssh" |
| 14 | "golang.org/x/crypto/ssh/agent" |
| 15 | |
| 16 | "reasonix/internal/remote/sshtest" |
| 17 | ) |
| 18 | |
| 19 | func TestClientFallsBackFromEmptyAgentToIdentityFile(t *testing.T) { |
| 20 | pemBytes, authorized, err := sshtest.GenerateKeyPEM() |
| 21 | if err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | srv := sshtest.Start(t, sshtest.Options{AuthorizedKey: authorized}) |
| 25 | keyPath := filepath.Join(t.TempDir(), "id_ed25519") |
| 26 | if err := writeFile0600(keyPath, pemBytes); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | |
| 30 | // A running but empty agent reproduces the desktop failure: the first |
| 31 | // publickey source has no signers, so the explicit identity must be tried |
| 32 | // as a second publickey attempt. |
| 33 | agentDir, err := os.MkdirTemp("", "reasonix-agent-") |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | t.Cleanup(func() { _ = os.RemoveAll(agentDir) }) |
| 38 | sock := filepath.Join(agentDir, "agent.sock") |
| 39 | listener, err := net.Listen("unix", sock) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | t.Cleanup(func() { _ = listener.Close() }) |
| 44 | go func() { |
| 45 | conn, acceptErr := listener.Accept() |
| 46 | if acceptErr != nil { |
| 47 | return |
| 48 | } |
| 49 | defer conn.Close() |
| 50 | _ = agent.ServeAgent(agent.NewKeyring(), conn) |
| 51 | }() |
| 52 | t.Setenv("SSH_AUTH_SOCK", sock) |
| 53 | |
| 54 | c := newTestClient(t, srv, Options{}) |
| 55 | c.opts.Host.IdentityFile = keyPath |
| 56 | |
| 57 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 58 | defer cancel() |
| 59 | if err := c.Start(ctx); err != nil { |
| 60 | t.Fatalf("Start with empty agent and explicit identity: %v", err) |
| 61 | } |
| 62 | defer c.Close() |
| 63 | if c.Status().Status != StatusConnected { |
| 64 | t.Fatalf("status = %v, want connected", c.Status().Status) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | func TestIdentitiesOnlyUsesOnlyConfiguredAgentIdentity(t *testing.T) { |
| 69 | correctPEM, authorized, err := sshtest.GenerateKeyPEM() |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | wrongPEM, _, err := sshtest.GenerateKeyPEM() |
| 74 | if err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | srv := sshtest.Start(t, sshtest.Options{AuthorizedKey: authorized}) |
| 78 | keyPath := filepath.Join(t.TempDir(), "id_ed25519.pub") |
| 79 | if err := writeFile0600(keyPath, ssh.MarshalAuthorizedKey(authorized)); err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | keyring := agent.NewKeyring() |
| 83 | for _, pemBytes := range [][]byte{wrongPEM, correctPEM} { |
| 84 | privateKey, err := ssh.ParseRawPrivateKey(pemBytes) |
| 85 | if err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | if err := keyring.Add(agent.AddedKey{PrivateKey: privateKey}); err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | agentDir, err := os.MkdirTemp("", "reasonix-identities-only-") |
| 94 | if err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | t.Cleanup(func() { _ = os.RemoveAll(agentDir) }) |
| 98 | sock := filepath.Join(agentDir, "agent.sock") |
| 99 | listener, err := net.Listen("unix", sock) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | t.Cleanup(func() { _ = listener.Close() }) |
| 104 | go func() { |
| 105 | for { |
| 106 | conn, acceptErr := listener.Accept() |
| 107 | if acceptErr != nil { |
| 108 | return |
| 109 | } |
| 110 | go func() { |
| 111 | defer conn.Close() |
| 112 | _ = agent.ServeAgent(keyring, conn) |
| 113 | }() |
| 114 | } |
| 115 | }() |
| 116 | t.Setenv("SSH_AUTH_SOCK", sock) |
| 117 | |
| 118 | c := newTestClient(t, srv, Options{}) |
| 119 | c.opts.Host.IdentityFile = keyPath |
| 120 | c.opts.Host.IdentityFiles = []string{keyPath} |
| 121 | c.opts.Host.IdentitiesOnly = true |
| 122 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 123 | defer cancel() |
| 124 | if err := c.Start(ctx); err != nil { |
| 125 | t.Fatalf("Start with configured agent identity and IdentitiesOnly: %v", err) |
| 126 | } |
| 127 | defer c.Close() |
| 128 | if c.Status().Status != StatusConnected { |
| 129 | t.Fatalf("status = %v, want connected", c.Status().Status) |
| 130 | } |
| 131 | } |
| 132 |