133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"fmt"
133728e barerepo 1mo
5
"os"
133728e barerepo 1mo
6
"os/exec"
133728e barerepo 1mo
7
"path/filepath"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
9
"testing"
133728e barerepo 1mo
12
// sshWrapper is what authorized_keys does, without an sshd: force one command with the account named.
133728e barerepo 1mo
13
func sshWrapper(t *testing.T, in *instance, account string) string {
133728e barerepo 1mo
14
t.Helper()
133728e barerepo 1mo
15
path := filepath.Join(t.TempDir(), "ssh")
133728e barerepo 1mo
16
// git sends its own options and the host before the command, so the command is the last argument.
133728e barerepo 1mo
17
script := fmt.Sprintf("#!/bin/sh\nfor a in \"$@\"; do cmd=\"$a\"; done\n"+
133728e barerepo 1mo
18
"SSH_ORIGINAL_COMMAND=\"$cmd\" exec %q ssh --config %q --account %q\n",
133728e barerepo 1mo
19
binary, in.cfg.Path, account)
133728e barerepo 1mo
20
if err := os.WriteFile(path, []byte(script), 0o700); err != nil {
133728e barerepo 1mo
21
t.Fatal(err)
133728e barerepo 1mo
23
return path
133728e barerepo 1mo
26
// Chapter 10 makes an ssh key the identity, so ssh is the transport, and it has to carry a push.
133728e barerepo 1mo
27
func TestGitOverSSHClonesAndPushes(t *testing.T) {
133728e barerepo 1mo
28
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
29
t.Skip("git is not installed")
133728e barerepo 1mo
31
in := newInstance(t)
133728e barerepo 1mo
32
john := in.account("john")
133728e barerepo 1mo
33
seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
35
wrapper := sshWrapper(t, in, "john")
133728e barerepo 1mo
36
dir := filepath.Join(t.TempDir(), "clone")
133728e barerepo 1mo
37
sshRun(t, "", wrapper, "git", "clone", "-q", "ssh://barerepo/john/johnbot", dir)
133728e barerepo 1mo
38
if _, err := os.Stat(filepath.Join(dir, "config.go")); err != nil {
133728e barerepo 1mo
39
t.Fatalf("the clone brought nothing: %v", err)
133728e barerepo 1mo
42
write(t, dir, "config.go", "package main\n\nvar over = \"ssh\"\n")
133728e barerepo 1mo
43
run(t, dir, "git", "-c", "user.email=t@x", "-c", "user.name=t", "commit", "-qam", "pushed over ssh")
133728e barerepo 1mo
44
sshRun(t, dir, wrapper, "git", "push", "-q", "origin", "master")
133728e barerepo 1mo
46
if _, _, page := get(t, in.http.URL+"/john/johnbot"); !strings.Contains(page, "pushed over ssh") {
133728e barerepo 1mo
47
t.Errorf("a push over ssh did not land:\n%s", page)
133728e barerepo 1mo
51
// Chapter 41.3: SSH_ORIGINAL_COMMAND is attacker controlled and must never reach a shell.
133728e barerepo 1mo
52
func TestSSHRefusesAnythingThatIsNotGit(t *testing.T) {
133728e barerepo 1mo
53
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
54
t.Skip("git is not installed")
133728e barerepo 1mo
56
in := newInstance(t)
133728e barerepo 1mo
57
john := in.account("john")
133728e barerepo 1mo
58
seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
60
for _, cmd := range []string{
133728e barerepo 1mo
61
"",
133728e barerepo 1mo
62
"sh",
133728e barerepo 1mo
63
"git-upload-pack 'john/johnbot'; touch /tmp/barerepo-owned",
133728e barerepo 1mo
64
"git-upload-pack 'john/johnbot' && whoami",
133728e barerepo 1mo
65
"scp -t /tmp",
133728e barerepo 1mo
66
// Well formed apart from the verb, so nothing but the list of three can refuse it.
133728e barerepo 1mo
67
"scp 'john/johnbot'",
133728e barerepo 1mo
68
"git-upload-pack '../../etc'",
133728e barerepo 1mo
69
} {
133728e barerepo 1mo
70
out, err := barerepoSSH(t, in, "john", cmd)
133728e barerepo 1mo
71
if err == nil {
133728e barerepo 1mo
72
t.Errorf("%q was accepted over ssh:\n%s", cmd, out)
133728e barerepo 1mo
73
continue
133728e barerepo 1mo
75
if strings.Contains(out, "panic") || strings.Contains(out, "goroutine ") {
133728e barerepo 1mo
76
t.Errorf("%q crashed rather than being refused:\n%s", cmd, out)
133728e barerepo 1mo
78
if !strings.Contains(out, "barerepo") {
133728e barerepo 1mo
79
t.Errorf("%q was refused without saying who refused it:\n%s", cmd, out)
133728e barerepo 1mo
82
if _, err := os.Stat("/tmp/barerepo-owned"); err == nil {
133728e barerepo 1mo
83
os.Remove("/tmp/barerepo-owned")
133728e barerepo 1mo
84
t.Fatal("a shell ran, so SSH_ORIGINAL_COMMAND reached one")
133728e barerepo 1mo
88
// barerepoSSH runs the entry point authorized_keys forces, with one command in the environment.
133728e barerepo 1mo
89
func barerepoSSH(t *testing.T, in *instance, account, cmd string) (string, error) {
133728e barerepo 1mo
90
t.Helper()
133728e barerepo 1mo
91
c := exec.Command(binary, "ssh", "--config", in.cfg.Path, "--account", account)
133728e barerepo 1mo
92
c.Env = append(os.Environ(), "SSH_ORIGINAL_COMMAND="+cmd)
133728e barerepo 1mo
93
out, err := c.CombinedOutput()
133728e barerepo 1mo
94
return string(out), err
133728e barerepo 1mo
97
// sshRun runs a git command with barerepo's ssh entry point standing in for the daemon.
133728e barerepo 1mo
98
func sshRun(t *testing.T, dir, wrapper, name string, args ...string) {
133728e barerepo 1mo
99
t.Helper()
133728e barerepo 1mo
100
cmd := exec.Command(name, args...)
133728e barerepo 1mo
101
cmd.Dir = dir
133728e barerepo 1mo
102
cmd.Env = append(os.Environ(),
133728e barerepo 1mo
103
"GIT_SSH_COMMAND="+wrapper,
133728e barerepo 1mo
104
"GIT_AUTHOR_NAME=tester", "GIT_AUTHOR_EMAIL=t@x",
133728e barerepo 1mo
105
"GIT_COMMITTER_NAME=tester", "GIT_COMMITTER_EMAIL=t@x",
133728e barerepo 1mo
106
"GIT_TERMINAL_PROMPT=0")
133728e barerepo 1mo
107
if out, err := cmd.CombinedOutput(); err != nil {
133728e barerepo 1mo
108
t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, out)
133728e barerepo 1mo
109
}