| 1 | package remote |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // TestHopAuthDropsTargetCredentials pins the ProxyJump security fix: a jump |
| 9 | // host must never be handed the target's stored password/passphrase closures. |
| 10 | func TestHopAuthDropsTargetCredentials(t *testing.T) { |
| 11 | targetAuth := &AuthOptions{ |
| 12 | Password: func() (string, error) { return "target-secret", nil }, |
| 13 | Passphrase: func() (string, error) { return "target-key-pass", nil }, |
| 14 | SecretPrompt: func(_ context.Context, _ SecretKind, _, _ string) (string, error) { return "", nil }, |
| 15 | DisableAgent: true, |
| 16 | } |
| 17 | cfg := dialConfig{auth: targetAuth} |
| 18 | hop := ResolvedHost{Name: "bastion", HostName: "10.0.0.1", Port: 22, User: "jump"} |
| 19 | |
| 20 | hopAuth := cfg.hopAuthFor(hop) |
| 21 | if hopAuth.Password != nil { |
| 22 | t.Error("jump host auth carries the target's Password closure") |
| 23 | } |
| 24 | if hopAuth.Passphrase != nil { |
| 25 | t.Error("jump host auth carries the target's Passphrase closure") |
| 26 | } |
| 27 | if hopAuth.SecretPrompt == nil { |
| 28 | t.Error("jump host auth should still allow interactive prompting") |
| 29 | } |
| 30 | if !hopAuth.DisableAgent { |
| 31 | t.Error("jump host auth should inherit DisableAgent") |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // TestClientHopAuthIsPersistentAndPerHost pins that the Client's per-hop auth is |
| 36 | // credential-free, cached per hop (so reconnects don't re-prompt), and distinct |
| 37 | // between hops (so one hop's secret is never reused for another). |
| 38 | func TestClientHopAuthIsPersistentAndPerHost(t *testing.T) { |
| 39 | c, err := New(Options{ |
| 40 | Host: ResolvedHost{HostName: "target", Port: 22, User: "u"}, |
| 41 | Auth: AuthOptions{Password: func() (string, error) { return "target-secret", nil }}, |
| 42 | }) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | h1 := ResolvedHost{HostName: "hop1", Port: 22, User: "u"} |
| 47 | h2 := ResolvedHost{HostName: "hop2", Port: 22, User: "u"} |
| 48 | |
| 49 | a1 := c.hopAuthFor(h1) |
| 50 | if a1.Password != nil || a1.Passphrase != nil { |
| 51 | t.Fatal("hop auth carries target credentials") |
| 52 | } |
| 53 | if c.hopAuthFor(h1) != a1 { |
| 54 | t.Error("hop auth not cached: reconnect would re-prompt for jump secrets") |
| 55 | } |
| 56 | if c.hopAuthFor(h2) == a1 { |
| 57 | t.Error("distinct hops share one auth: a hop's secret could be reused for another") |
| 58 | } |
| 59 | } |
| 60 |