internal/sshd/revoke_test.go
112 lines · 3244 bytes
1package sshd
2
3import (
4 "bytes"
5 "errors"
6 "strings"
7 "testing"
8 "time"
9
10 "golang.org/x/crypto/ssh"
11)
12
13// execStatus runs cmd on a new session and returns its exit status and
14// stderr; -1 when the session could not run.
15func execStatus(client *ssh.Client, cmd string) (int, string) {
16 sess, err := client.NewSession()
17 if err != nil {
18 return -1, err.Error()
19 }
20 defer sess.Close()
21 var stderr bytes.Buffer
22 sess.Stderr = &stderr
23 err = sess.Run(cmd)
24 var exit *ssh.ExitError
25 switch {
26 case err == nil:
27 return 0, stderr.String()
28 case errors.As(err, &exit):
29 return exit.ExitStatus(), stderr.String()
30 }
31 return -1, err.Error()
32}
33
34// waitClosed fails unless the server closes the client's connection
35// within five seconds.
36func waitClosed(t *testing.T, client *ssh.Client) {
37 t.Helper()
38 done := make(chan struct{})
39 go func() { client.Wait(); close(done) }()
40 select {
41 case <-done:
42 case <-time.After(5 * time.Second):
43 t.Fatal("the connection stayed open")
44 }
45}
46
47// Each exec reads the key again. The rows change behind the store's
48// back here, so no revocation is announced and the connection stays up:
49// what refuses the command is the per-exec check alone.
50func TestExecRevalidatesKey(t *testing.T) {
51 ts := newTestServer(t)
52 if code, errOut := execStatus(ts.client, "whoami"); code != 0 {
53 t.Fatalf("whoami: %d %s", code, errOut)
54 }
55 if _, err := ts.st.DB.Exec("UPDATE ssh_keys SET scope = 'git' WHERE id = ?", ts.keyID); err != nil {
56 t.Fatal(err)
57 }
58 if code, errOut := execStatus(ts.client, "whoami"); code != 4 || !strings.Contains(errOut, "does not allow control commands") {
59 t.Fatalf("whoami after re-scope: %d %q", code, errOut)
60 }
61 if _, err := ts.st.DB.Exec("DELETE FROM ssh_keys WHERE id = ?", ts.keyID); err != nil {
62 t.Fatal(err)
63 }
64 if code, errOut := execStatus(ts.client, "whoami"); code != 4 || !strings.Contains(errOut, "no longer registered") {
65 t.Fatalf("whoami after delete: %d %q", code, errOut)
66 }
67}
68
69// Removing the key cuts the connection, ending a command running on it.
70func TestRemoveKeyCutsConnection(t *testing.T) {
71 ts := newTestServer(t)
72 withBuild(t, ts)
73 var stderr bytes.Buffer
74 sess := startFollow(t, ts.client, &stderr)
75 if err := ts.st.RemoveSSHKey(ts.uid, ts.fp); err != nil {
76 t.Fatal(err)
77 }
78 waited := make(chan error, 1)
79 go func() { waited <- sess.Wait() }()
80 select {
81 case err := <-waited:
82 if err == nil {
83 t.Fatal("the follow exited cleanly after its key was removed")
84 }
85 case <-time.After(5 * time.Second):
86 t.Fatal("the follow outlived its key")
87 }
88 waitClosed(t, ts.client)
89}
90
91func TestDisableCutsConnection(t *testing.T) {
92 ts := newTestServer(t)
93 if err := ts.st.SetUserDisabled(ts.uid, true); err != nil {
94 t.Fatal(err)
95 }
96 waitClosed(t, ts.client)
97}
98
99// A revocation made by another process is not announced here; the
100// sweep finds it. A live key survives the sweep.
101func TestSweepCutsOutOfProcessRevocation(t *testing.T) {
102 ts := newTestServer(t)
103 ts.srv.sweepOnce()
104 if code, errOut := execStatus(ts.client, "whoami"); code != 0 {
105 t.Fatalf("the sweep cut a live key: %d %s", code, errOut)
106 }
107 if _, err := ts.st.DB.Exec("UPDATE users SET disabled = 1 WHERE id = ?", ts.uid); err != nil {
108 t.Fatal(err)
109 }
110 ts.srv.sweepOnce()
111 waitClosed(t, ts.client)
112}