Commit 2b840816c9
Verified · cmc
Layout: unified · split
e2e/revoke_test.go added +155
| @@ -0,0 +1,155 @@ | |||
| 1 | package e2e | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "fmt" | ||
| 6 | "io" | ||
| 7 | "os" | ||
| 8 | "os/exec" | ||
| 9 | "path/filepath" | ||
| 10 | "strconv" | ||
| 11 | "strings" | ||
| 12 | "testing" | ||
| 13 | "time" | ||
| 14 | ) | ||
| 15 | |||
| 16 | // pkt frames one pkt-line. | ||
| 17 | func pkt(s string) string { return fmt.Sprintf("%04x%s", len(s)+4, s) } | ||
| 18 | |||
| 19 | // readPkt reads one pkt-line; a flush reads as "". | ||
| 20 | func readPkt(r *bufio.Reader) (string, error) { | ||
| 21 | var n [4]byte | ||
| 22 | if _, err := io.ReadFull(r, n[:]); err != nil { | ||
| 23 | return "", err | ||
| 24 | } | ||
| 25 | size, err := strconv.ParseUint(string(n[:]), 16, 16) | ||
| 26 | if err != nil { | ||
| 27 | return "", err | ||
| 28 | } | ||
| 29 | if size == 0 { | ||
| 30 | return "", nil | ||
| 31 | } | ||
| 32 | buf := make([]byte, size-4) | ||
| 33 | _, err = io.ReadFull(r, buf) | ||
| 34 | return string(buf), err | ||
| 35 | } | ||
| 36 | |||
| 37 | // fingerprint is the SHA256 fingerprint of a public key file. | ||
| 38 | func fingerprint(t *testing.T, pubPath string) string { | ||
| 39 | t.Helper() | ||
| 40 | out, err := exec.Command("ssh-keygen", "-lf", pubPath).Output() | ||
| 41 | if err != nil { | ||
| 42 | t.Fatalf("ssh-keygen -lf: %v", err) | ||
| 43 | } | ||
| 44 | return strings.Fields(string(out))[1] | ||
| 45 | } | ||
| 46 | |||
| 47 | // Removing a key cuts the connections it opened: every session | ||
| 48 | // multiplexed on a ControlMaster, and a push in flight, which moves no | ||
| 49 | // ref (#256). | ||
| 50 | func TestRemovedKeyCutsMultiplexedConnection(t *testing.T) { | ||
| 51 | t.Parallel() | ||
| 52 | inst := startInstance(t) | ||
| 53 | aliceKey := setupPublicRepo(t, inst, "alice/app") | ||
| 54 | spare := inst.newKey(t, "spare") | ||
| 55 | pub, err := os.ReadFile(spare + ".pub") | ||
| 56 | if err != nil { | ||
| 57 | t.Fatal(err) | ||
| 58 | } | ||
| 59 | if _, errOut, code := inst.ssh(t, aliceKey, string(pub), "keys", "add"); code != 0 { | ||
| 60 | t.Fatalf("keys add: %s", errOut) | ||
| 61 | } | ||
| 62 | |||
| 63 | // The control socket sits under the system temp dir with a short | ||
| 64 | // name: t.TempDir() or %C on macOS passes the 104-byte socket path | ||
| 65 | // limit. | ||
| 66 | cmDir, err := os.MkdirTemp("", "cm") | ||
| 67 | if err != nil { | ||
| 68 | t.Fatal(err) | ||
| 69 | } | ||
| 70 | t.Cleanup(func() { os.RemoveAll(cmDir) }) | ||
| 71 | muxArgs := []string{ | ||
| 72 | "-p", fmt.Sprint(inst.port), | ||
| 73 | "-i", aliceKey, | ||
| 74 | "-o", "IdentitiesOnly=yes", | ||
| 75 | "-o", "StrictHostKeyChecking=no", | ||
| 76 | "-o", "UserKnownHostsFile=" + filepath.Join(inst.sshDir, "known_hosts"), | ||
| 77 | "-o", "BatchMode=yes", | ||
| 78 | "-o", "ControlMaster=auto", | ||
| 79 | "-o", "ControlPath=" + filepath.Join(cmDir, "s"), | ||
| 80 | "-o", "ControlPersist=60", | ||
| 81 | } | ||
| 82 | mux := func(args ...string) *exec.Cmd { | ||
| 83 | return exec.Command("ssh", append(append([]string{}, muxArgs...), args...)...) | ||
| 84 | } | ||
| 85 | t.Cleanup(func() { mux("-O", "exit", "git@127.0.0.1").Run() }) | ||
| 86 | |||
| 87 | if out, err := mux("git@127.0.0.1", "whoami").Output(); err != nil || strings.TrimSpace(string(out)) != "alice" { | ||
| 88 | var stderr []byte | ||
| 89 | if ee, ok := err.(*exec.ExitError); ok { | ||
| 90 | stderr = ee.Stderr | ||
| 91 | } | ||
| 92 | t.Fatalf("whoami over the master: %v %q %s", err, out, stderr) | ||
| 93 | } | ||
| 94 | |||
| 95 | // A push held open mid-pack: the ref update is sent, the pack is not. | ||
| 96 | push := mux("git@127.0.0.1", "git-receive-pack", "alice/app") | ||
| 97 | stdin, err := push.StdinPipe() | ||
| 98 | if err != nil { | ||
| 99 | t.Fatal(err) | ||
| 100 | } | ||
| 101 | stdout, err := push.StdoutPipe() | ||
| 102 | if err != nil { | ||
| 103 | t.Fatal(err) | ||
| 104 | } | ||
| 105 | if err := push.Start(); err != nil { | ||
| 106 | t.Fatal(err) | ||
| 107 | } | ||
| 108 | t.Cleanup(func() { push.Process.Kill() }) | ||
| 109 | adv := bufio.NewReader(stdout) | ||
| 110 | first, err := readPkt(adv) | ||
| 111 | if err != nil || len(first) < 40 { | ||
| 112 | t.Fatalf("advertisement: %q %v", first, err) | ||
| 113 | } | ||
| 114 | oldSHA := first[:40] | ||
| 115 | for { | ||
| 116 | line, err := readPkt(adv) | ||
| 117 | if err != nil { | ||
| 118 | t.Fatalf("advertisement: %v", err) | ||
| 119 | } | ||
| 120 | if line == "" { | ||
| 121 | break | ||
| 122 | } | ||
| 123 | } | ||
| 124 | newSHA := strings.Repeat("1", 40) | ||
| 125 | io.WriteString(stdin, pkt(oldSHA+" "+newSHA+" refs/heads/main\x00report-status\n")+"0000") | ||
| 126 | // A pack header announcing one object, and no object. | ||
| 127 | stdin.Write([]byte("PACK\x00\x00\x00\x02\x00\x00\x00\x01")) | ||
| 128 | exited := make(chan error, 1) | ||
| 129 | go func() { | ||
| 130 | io.Copy(io.Discard, adv) | ||
| 131 | exited <- push.Wait() | ||
| 132 | }() | ||
| 133 | |||
| 134 | if _, errOut, code := inst.ssh(t, spare, "", "keys", "remove", fingerprint(t, aliceKey+".pub")); code != 0 { | ||
| 135 | t.Fatalf("keys remove: %s", errOut) | ||
| 136 | } | ||
| 137 | select { | ||
| 138 | case err := <-exited: | ||
| 139 | if err == nil { | ||
| 140 | t.Fatal("the push exited cleanly after its key was removed") | ||
| 141 | } | ||
| 142 | case <-time.After(10 * time.Second): | ||
| 143 | t.Fatal("the push outlived its key") | ||
| 144 | } | ||
| 145 | |||
| 146 | // The master went with the connection; a new one authenticates | ||
| 147 | // again, and the key is unknown. | ||
| 148 | if out, err := mux("git@127.0.0.1", "whoami").CombinedOutput(); err == nil { | ||
| 149 | t.Fatalf("whoami after removal succeeded: %s", out) | ||
| 150 | } | ||
| 151 | refs := mustGit(t, t.TempDir(), inst.gitEnv(spare), "ls-remote", inst.sshURL("alice/app"), "refs/heads/main") | ||
| 152 | if !strings.HasPrefix(refs, oldSHA) { | ||
| 153 | t.Fatalf("main moved: %s, want %s", refs, oldSHA) | ||
| 154 | } | ||
| 155 | } | ||