package main import ( "context" "encoding/json" "os" "path/filepath" "reflect" "sort" "strings" "testing" "time" ) // Public keys made for these tests only; their private halves were never kept. const ( edPub = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXSwIW0g4H2OOL8D3K21xsmE9p6f9xaj2os361ZKx2A op@laptop" rsaPub = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDPb48PksTrIhBQxAVc7r1WHzOVQXa5XlwvUNLt0mkcZlSv4K9nHdKRK3LET7Tkuw2PgLhN4ttb058GGILQh24uD2/dfr7R5cCQTS6Z4iRTvTk2EG/HU9RKaNUJWrQc8kKQmx4+H4IgKIarqSpRw/ZTTyyylBV6+xNSMuZGJAHTZNN9Wn6VHlJEE5/IV95Uj+RqEO4+q7RtQC0dV+GWKUXvT5BFEpoU3AfS8yAgGwyotz8RxJktwel6YnafaHD8iNlj1rLo6k9HDXSKAEWk3hBjwO0YIquw6YuJFXGkoY72lbLt+h8/1sfTjjvZosRlWkejkAsU5W/Nb0Y37nt1nXwR old@ci" edFP = "SHA256:qgWtIXM3wAqg5va+Mzzx7SJGwA7etBQT6Z7Bs3fLVCY" rsaFP = "SHA256:6Fb092rWEQVP4y+ewi3r2hORvWlm6iFkfHT6BNB9T/Q" ) type call struct { env []string name string args []string } type fake struct { answer func(c call) Ran calls []call } func (f *fake) run(_ context.Context, env []string, name string, args ...string) Ran { c := call{env, name, args} f.calls = append(f.calls, c) if f.answer == nil { return Ran{Status: 1} } return f.answer(c) } func home(t *testing.T, files map[string]string) string { t.Helper() h := t.TempDir() if err := os.MkdirAll(filepath.Join(h, ".ssh", "config.d"), 0o700); err != nil { t.Fatal(err) } os.Chmod(filepath.Join(h, ".ssh"), 0o700) for name, content := range files { mode := os.FileMode(0o600) if strings.HasSuffix(name, ".pub") { mode = 0o644 } p := filepath.Join(h, ".ssh", name) os.MkdirAll(filepath.Dir(p), 0o700) if err := os.WriteFile(p, []byte(strings.ReplaceAll(content, "HOME", h)), mode); err != nil { t.Fatal(err) } } return h } func client(h string, f *fake) *Client { return &Client{Home: h, UID: 4242, Run: f.run, Now: func() time.Time { return time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC) }} } // The layout this module writes: its region first, bringing in config.d; its own hosts in 00-mesh; // a found predecessor file and a module's drop-in beside it; the operator's lines below. var layout = map[string]string{ "config": "# BEGIN mesh ssh-client.config\n# the mesh's region\nInclude ~/.ssh/config.d/*\n# END mesh ssh-client.config\n\n" + "ServerAliveInterval 30\nHost *\n AddKeysToAgent yes\nHost ace\n User wrong\nHost box\n HostName 192.0.2.7\n IdentityFile ~/.ssh/id_box\n", "config.d/00-mesh": "# Generated by the mesh. Do not edit\n\nHost ace ace.internal\n HostName ace.internal\n User ace\n\nHost shanks shanks.internal\n HostName shanks.internal\n User op\n", "config.d/mesh": "Host ace\n\tHostName ace.internal\n\tUser ace\n", "config.d/work": "# Generated by the mesh. Do not edit\nHost forge.example\n Port 2222\n", } func TestHostsSayWhereEachCameFromInTheOrderSshReadsThem(t *testing.T) { c := client(home(t, layout), &fake{}) got, err := c.Hosts() if err != nil { t.Fatal(err) } var who []string for _, s := range got["sections"].([]Section) { who = append(who, s.Patterns[0]+"@"+s.Source+"/"+s.From) } want := []string{"ace@config.d/00-mesh/mesh", "shanks@config.d/00-mesh/mesh", "ace@config.d/mesh/drop-in", "forge.example@config.d/work/drop-in", "*@config/operator", "ace@config/operator", "box@config/operator"} if !reflect.DeepEqual(who, want) { t.Fatalf("order/source:\n%v\nwant\n%v", who, want) } if !reflect.DeepEqual(got["global"], []string{"config:6 ServerAliveInterval 30"}) { t.Errorf("an operator line after the include read as part of the last included section: %v", got["global"]) } dups := got["duplicates"].([]map[string]any) if len(dups) != 1 || dups[0]["host"] != "ace" || dups[0]["wins"] != "config.d/00-mesh:3 (mesh)" { t.Errorf("duplicates: %v", dups) } sections := got["sections"].([]Section) if !sections[3].Mesh || sections[2].Mesh { t.Errorf("a drop-in under the mesh's header is the mesh's; one without is found: %+v %+v", sections[3], sections[2]) } } func TestTheMeshsRegionMustComeFirstAndBringInConfigD(t *testing.T) { h := home(t, layout) if !client(h, &fake{}).meshIncludeFirst() { t.Fatal("the written layout was not recognised") } os.WriteFile(filepath.Join(h, ".ssh", "config"), []byte("Host early\n User x\n"+layout["config"]), 0o600) if client(h, &fake{}).meshIncludeFirst() { t.Fatal("a host above the mesh's region passed") } } func TestKeysAreDescribedByTheirPublicHalfAndAPassphraseIsAskedNotRead(t *testing.T) { files := map[string]string{"config": layout["config"], "id_ed25519": "-----BEGIN OPENSSH PRIVATE KEY-----\nnot a key\n", "id_ed25519.pub": edPub + "\n", "id_box": "-----BEGIN OPENSSH PRIVATE KEY-----\nnot a key\n", "id_box.pub": rsaPub + "\n", "stray": "-----BEGIN RSA PRIVATE KEY-----\nnot a key\n", "notes.txt": "hello\n"} f := &fake{answer: func(c call) Ran { if c.name == "ssh-keygen" && c.args[0] == "-y" { if strings.HasSuffix(c.args[len(c.args)-1], "id_box") { return Ran{Status: 1, Stderr: "Load key \"id_box\": incorrect passphrase supplied to decrypt private key\n"} } if strings.HasSuffix(c.args[len(c.args)-1], "id_ed25519") { return Ran{Stdout: edPub + "\n"} } } if c.name == "ssh-keygen" && c.args[0] == "-l" { return Ran{Stdout: "256 " + edFP + " no comment (ED25519)\n"} } return Ran{Status: 1, Stderr: "unexpected"} }} keys, err := client(home(t, files), f).Keys(context.Background()) if err != nil { t.Fatal(err) } by := map[string]Key{} for _, k := range keys { by[filepath.Base(k.Path)] = k } if len(keys) != 3 { t.Fatalf("%+v", keys) } if k := by["id_ed25519"]; k.Fingerprint != edFP || k.Passphrase != "none" || !strings.HasPrefix(k.OfferedBy, "default name") || k.Comment != "op@laptop" { t.Errorf("ed25519: %+v", k) } if k := by["id_box"]; k.Passphrase != "yes" || k.Bits != 2048 || k.Weak == "" || !strings.HasPrefix(k.OfferedBy, "IdentityFile at config:") { t.Errorf("box: %+v", k) } if k := by["stray"]; !k.PublicMissing || k.OfferedBy != "" || k.Fingerprint != edFP { t.Errorf("stray: %+v", k) } for _, c := range f.calls { if c.name == "ssh-keygen" && c.args[0] == "-y" && !reflect.DeepEqual(c.args[:3], []string{"-y", "-P", ""}) { t.Errorf("a passphrase check could prompt: %v", c.args) } } } func TestAPublicHalfThatIsAnotherKeysIsFound(t *testing.T) { files := map[string]string{"config": layout["config"], "id_ed25519": "-----BEGIN OPENSSH PRIVATE KEY-----\n", "id_ed25519.pub": rsaPub + "\n"} f := &fake{answer: func(c call) Ran { return Ran{Stdout: edPub + "\n"} }} keys, _ := client(home(t, files), f).Keys(context.Background()) if len(keys) != 1 || keys[0].PublicMismatch == "" || keys[0].Fingerprint != edFP { t.Fatalf("%+v", keys) } } func TestFingerprintsAreSshKeygens(t *testing.T) { k, err := ParseKeyLine("from=\"10.0.0.0/8,192.0.2.1\",no-pty " + edPub) if err != nil || k.Fingerprint != edFP || k.Bits != 256 || k.Comment != "op@laptop" || !strings.HasPrefix(k.Options, "from=") { t.Fatalf("%+v %v", k, err) } k, _ = ParseKeyLine(rsaPub) if k.Fingerprint != rsaFP || k.Bits != 2048 || !strings.Contains(k.Weak, "below 3072") { t.Fatalf("%+v", k) } if _, err := ParseKeyLine("ssh-ed25519 !!!notbase64"); err == nil { t.Fatal("garbage accepted") } } func TestAuthorizedListsFingerprintsNeverKeys(t *testing.T) { h := home(t, map[string]string{"config": layout["config"], "authorized_keys": "# mine\n" + edPub + "\n" + rsaPub + "\nnonsense line\n" + edPub + "\n"}) got, err := client(h, &fake{}).Authorized() if err != nil { t.Fatal(err) } b, _ := json.Marshal(got) if strings.Contains(string(b), "AAAA") { t.Fatalf("a key was printed: %s", b) } if got["count"] != 3 || !reflect.DeepEqual(got["duplicates"], []string{edFP}) || !reflect.DeepEqual(got["unreadable_lines"], []int{4}) { t.Fatalf("%s", b) } } func TestRevokeKeepsTheFileFirstAndRefusesALockout(t *testing.T) { original := "# mine\n" + edPub + "\n" + rsaPub + "\n" h := home(t, map[string]string{"config": layout["config"], "authorized_keys": original}) c := client(h, &fake{}) got, err := c.Revoke(rsaFP) if err != nil { t.Fatal(err) } now, _ := os.ReadFile(filepath.Join(h, ".ssh", "authorized_keys")) if string(now) != "# mine\n"+edPub+"\n" { t.Fatalf("after: %q", now) } kept, _ := os.ReadFile(got["backup"].(string)) if string(kept) != original { t.Fatal("the backup is not the file as it was") } if info, _ := os.Stat(filepath.Join(h, ".ssh", "authorized_keys")); info.Mode().Perm() != 0o600 { t.Errorf("mode changed: %v", info.Mode()) } if _, err := c.Revoke(edFP); err == nil || !strings.Contains(err.Error(), "lock ssh out") { t.Fatalf("the last key was revoked: %v", err) } if _, err := c.Revoke("SHA256:nothing"); err == nil { t.Fatal("an unknown fingerprint was accepted") } h = home(t, map[string]string{"authorized_keys": "# BEGIN mesh ssh-client.authorized\n" + rsaPub + "\n# END mesh ssh-client.authorized\n" + edPub + "\n"}) if _, err := client(h, &fake{}).Revoke(rsaFP); err == nil || !strings.Contains(err.Error(), "mesh's region") { t.Fatalf("a key of the mesh's region was revoked: %v", err) } } func scanOf(host, pub string) string { return host + " " + pub + "\n" } func TestKnownHostComparesWhatIsKnownWithWhatIsOffered(t *testing.T) { h := home(t, map[string]string{"config": layout["config"], "known_hosts": "ace.internal " + edPub + "\n"}) offered := edPub f := &fake{answer: func(c call) Ran { switch { case c.name == "ssh-keygen" && c.args[0] == "-F": return Ran{Stdout: "# Host ace.internal found: line 1\nace.internal " + edPub + "\n"} case c.name == "ssh-keyscan": return Ran{Stdout: scanOf("ace.internal", offered)} case c.name == "ssh-keygen" && c.args[0] == "-R": return Ran{} } return Ran{Status: 1} }} c := client(h, f) got, err := c.KnownHost(context.Background(), "ace.internal", 22, false) if err != nil || got["state"] != "matches" { t.Fatalf("%v %v", got, err) } offered = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZha2VrZXlmYWtla2V5ZmFrZWtleWZha2VrZXlmYWs=" got, _ = c.KnownHost(context.Background(), "ace.internal", 22, false) if !strings.HasPrefix(got["state"].(string), "changed") { t.Fatalf("%v", got["state"]) } got, err = c.KnownHost(context.Background(), "ace.internal", 22, true) if err != nil || got["refreshed"] != true { t.Fatalf("%v %v", got, err) } after, _ := os.ReadFile(filepath.Join(h, ".ssh", "known_hosts")) if !strings.Contains(string(after), offered) { t.Fatalf("not appended: %s", after) } sawRemove := false for _, c := range f.calls { if c.name == "ssh-keygen" && reflect.DeepEqual(c.args[:2], []string{"-R", "ace.internal"}) { sawRemove = true } if c.name == "ssh-keyscan" && !reflect.DeepEqual(c.args[:4], []string{"-T", "5", "-p", "22"}) { t.Errorf("an unbounded scan: %v", c.args) } } if !sawRemove { t.Fatal("the old entry was not removed through ssh-keygen (which keeps known_hosts.old)") } if _, err := c.KnownHost(context.Background(), "-oProxyCommand=x", 22, false); err == nil { t.Fatal("an option was taken for a host") } } func TestAnUnreachableHostIsNotRefreshed(t *testing.T) { h := home(t, map[string]string{"known_hosts": ""}) f := &fake{answer: func(c call) Ran { if c.name == "ssh-keyscan" { return Ran{Status: 1} } return Ran{Status: 1} }} got, err := client(h, f).KnownHost(context.Background(), "gone.example", 22, false) if err != nil || !strings.HasPrefix(got["state"].(string), "unreachable") { t.Fatalf("%v %v", got, err) } if _, err := client(h, f).KnownHost(context.Background(), "gone.example", 22, true); err == nil { t.Fatal("refreshed from nothing") } } func TestTestSaysHowItAuthenticatedOrWhyNot(t *testing.T) { ok := "debug1: Connecting to ace.internal [10.0.0.2] port 22.\ndebug1: Server accepts key: /h/.ssh/id_ed25519 ED25519 " + edFP + "\nAuthenticated to ace.internal ([10.0.0.2]:22) using \"publickey\".\n" f := &fake{answer: func(c call) Ran { return Ran{Stderr: ok} }} got, err := client(t.TempDir(), f).Test(context.Background(), "ace") if err != nil || got["ok"] != true || got["method"] != "publickey" || got["connected_to"] != "10.0.0.2:22" { t.Fatalf("%v %v", got, err) } args := strings.Join(f.calls[0].args, " ") if !strings.Contains(args, "BatchMode=yes") || !strings.Contains(args, "StrictHostKeyChecking=yes") || !strings.HasSuffix(args, "ace true") { t.Fatalf("not a batch test: %s", args) } denied := "debug1: Offering public key: /h/.ssh/id_box RSA " + rsaFP + "\nop@ace.internal: Permission denied (publickey).\n" f = &fake{answer: func(c call) Ran { return Ran{Status: 255, Stderr: denied} }} got, _ = client(t.TempDir(), f).Test(context.Background(), "ace") if got["ok"] != false || got["why"] != "no key it offered was accepted" || !reflect.DeepEqual(got["offered"], []string{"/h/.ssh/id_box"}) { t.Fatalf("%v", got) } if !strings.Contains(got["note"].(string), "agent") { t.Fatalf("no word on the agent: %v", got) } } func TestCheckFindsWhatIsWrongAndSaysWhatItDidNotCheck(t *testing.T) { files := map[string]string{"config": "Host early\n User x\n" + layout["config"], "config.d/00-mesh": layout["config.d/00-mesh"], "config.d/mesh": layout["config.d/mesh"], "id_ed25519": "-----BEGIN OPENSSH PRIVATE KEY-----\n", "id_ed25519.pub": edPub + "\n", "authorized_keys": rsaPub + "\n", "known_hosts": "", "config.bak-1": "x"} h := home(t, files) os.Chmod(filepath.Join(h, ".ssh", "config"), 0o666) os.Chmod(filepath.Join(h, ".ssh", "id_ed25519"), 0o644) f := &fake{answer: func(c call) Ran { switch { case c.name == "ssh-keygen" && c.args[0] == "-y": return Ran{Stdout: edPub} case c.name == "ssh-keyscan": return Ran{Stdout: scanOf("x", edPub)} } return Ran{Status: 1} }} got, err := client(h, f).Check(context.Background(), true) if err != nil { t.Fatal(err) } var whats []string for _, x := range got["findings"].([]Finding) { whats = append(whats, x.What) } all := strings.Join(whats, "\n") for _, want := range []string{"writable by others (0666)", "private key readable by others (0644)", "no passphrase", "not the first thing in the file", "Host ace is defined 3 times", "RSA of 2048 bits", "not known: the first connection would ask", "config.bak-1"} { if !strings.Contains(all, want) { t.Errorf("missing %q in:\n%s", want, all) } } if got["ok"] != false || len(got["not_checked"].([]string)) == 0 { t.Errorf("%v", got) } } func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { Tools []string `json:"tools"` } json.Unmarshal(raw, &m) served := []string{} for _, tool := range tools(client(t.TempDir(), &fake{})) { if !strings.HasPrefix(tool.Name, "ssh_client_") || tool.Description == "" { t.Errorf("tool %q", tool.Name) } served = append(served, tool.Name) } sort.Strings(served) sort.Strings(m.Tools) if !reflect.DeepEqual(served, m.Tools) { t.Fatalf("served %v, manifest %v", served, m.Tools) } } // The module's own region, as the manifest declares it, read by ssh: the mesh's hosts first, a // drop-in next, the operator's lines after, and an operator line after the region global again. func TestTheManifestsRegionIsWhatSshReads(t *testing.T) { raw, _ := os.ReadFile("../../module.json") var m struct { Resources []map[string]any `json:"resources"` } json.Unmarshal(raw, &m) var region string for _, r := range m.Resources { if r["id"] == "config" { region = r["content"].(string) if r["into"] != "block" || r["at"] != "start" { t.Fatalf("the region is not written into the start: %v", r) } } } if !strings.Contains(region, "Include ~/.ssh/config.d/*") { t.Fatalf("no include: %q", region) } h := home(t, map[string]string{"config": "# BEGIN mesh ssh-client.config\n" + region + "# END mesh ssh-client.config\n\nCompression yes\nHost ace\n User wrong\n", "config.d/00-mesh": layout["config.d/00-mesh"]}) p, err := Parse(h) if err != nil { t.Fatal(err) } if p.Sections[0].Source != MeshFile || p.Sections[0].Options["user"] != "ace" || len(p.Global) != 1 || !strings.HasSuffix(p.Global[0], " Compression yes") { t.Fatalf("%+v %v", p.Sections, p.Global) } }