133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"net/http"
133728e barerepo 1mo
5
"net/url"
133728e barerepo 1mo
6
"os"
133728e barerepo 1mo
7
"os/exec"
133728e barerepo 1mo
8
"path/filepath"
133728e barerepo 1mo
9
"regexp"
133728e barerepo 1mo
10
"strings"
133728e barerepo 1mo
11
"testing"
133728e barerepo 1mo
14
// nonceIn pulls the nonce a page is showing, which is the value the next command signs.
133728e barerepo 1mo
15
var nonceIn = regexp.MustCompile(`printf '%s' '([^']+)'`)
133728e barerepo 1mo
17
// Chapters 10 and 31: an account is a name and a key, and signing in is one ssh-keygen line.
133728e barerepo 1mo
18
func TestSignupAndSignInWithARealKey(t *testing.T) {
133728e barerepo 1mo
19
if _, err := exec.LookPath("ssh-keygen"); err != nil {
133728e barerepo 1mo
20
t.Skip("ssh-keygen is not installed")
133728e barerepo 1mo
22
in := newInstance(t)
133728e barerepo 1mo
23
dir := t.TempDir()
133728e barerepo 1mo
24
key := filepath.Join(dir, "id")
133728e barerepo 1mo
25
run(t, "", "ssh-keygen", "-t", "ed25519", "-N", "", "-C", "john", "-f", key, "-q")
133728e barerepo 1mo
26
pub, err := os.ReadFile(key + ".pub")
133728e barerepo 1mo
27
if err != nil {
133728e barerepo 1mo
28
t.Fatal(err)
133728e barerepo 1mo
31
// The form collects a name and a key, and answers with a nonce to sign. Chapter 10.
133728e barerepo 1mo
32
body := form(t, in, "/signup", url.Values{"name": {"john"}, "pubkey": {string(pub)}})
133728e barerepo 1mo
33
nonce := nonceIn.FindStringSubmatch(body)
133728e barerepo 1mo
34
if nonce == nil {
133728e barerepo 1mo
35
t.Fatalf("signup did not answer with a nonce to sign:\n%s", body)
133728e barerepo 1mo
38
// A signature under the sign-in namespace must not claim an account. Chapter 10.
133728e barerepo 1mo
39
wrong := sign(t, key, nonce[1], "barerepo-auth")
133728e barerepo 1mo
40
body = form(t, in, "/signup", url.Values{"nonce": {nonce[1]}, "signature": {wrong}})
133728e barerepo 1mo
41
if !strings.Contains(body, "barerepo-signup") {
133728e barerepo 1mo
42
t.Errorf("a barerepo-auth signature was accepted for signup, so one can be replayed:\n%s", body)
133728e barerepo 1mo
44
// A wrong signature spends the nonce, so the page says so and asks for the form again.
133728e barerepo 1mo
45
if !strings.Contains(body, "the nonce is spent") {
133728e barerepo 1mo
46
t.Errorf("the page does not say the nonce is gone, so the next try looks broken:\n%s", body)
133728e barerepo 1mo
49
body = form(t, in, "/signup", url.Values{"name": {"john"}, "pubkey": {string(pub)}})
133728e barerepo 1mo
50
nonce = nonceIn.FindStringSubmatch(body)
133728e barerepo 1mo
51
if nonce == nil {
133728e barerepo 1mo
52
t.Fatalf("signup did not answer with a second nonce:\n%s", body)
133728e barerepo 1mo
54
right := sign(t, key, nonce[1], "barerepo-signup")
133728e barerepo 1mo
55
resp := formResponse(t, in, "/signup", url.Values{"nonce": {nonce[1]}, "signature": {right}})
133728e barerepo 1mo
56
if resp.StatusCode != http.StatusFound {
133728e barerepo 1mo
57
t.Fatalf("finishing signup answered %d:\n%s", resp.StatusCode, resp.body)
133728e barerepo 1mo
59
if sessionCookie(resp) == "" {
133728e barerepo 1mo
60
t.Error("signup did not sign the new account in")
133728e barerepo 1mo
63
// Now the other door, which is the one a returning reader takes. Chapter 31.3.
133728e barerepo 1mo
64
body = form(t, in, "/auth/challenge", url.Values{"name": {"john"}})
133728e barerepo 1mo
65
nonce = nonceIn.FindStringSubmatch(body)
133728e barerepo 1mo
66
if nonce == nil {
133728e barerepo 1mo
67
t.Fatalf("the challenge did not answer with a nonce:\n%s", body)
133728e barerepo 1mo
69
// One line, reading stdin and writing stdout, so nothing is left on disk. Chapter 31.3.
133728e barerepo 1mo
70
if !strings.Contains(body, "ssh-keygen -Y sign") || !strings.Contains(body, "-n barerepo-auth") {
133728e barerepo 1mo
71
t.Errorf("the page does not show the command that signs the nonce:\n%s", body)
133728e barerepo 1mo
73
resp = formResponse(t, in, "/auth/verify",
133728e barerepo 1mo
74
url.Values{"nonce": {nonce[1]}, "signature": {sign(t, key, nonce[1], "barerepo-auth")}})
133728e barerepo 1mo
75
if resp.StatusCode != http.StatusFound {
133728e barerepo 1mo
76
t.Fatalf("verifying answered %d", resp.StatusCode)
133728e barerepo 1mo
78
cookie := sessionCookie(resp)
133728e barerepo 1mo
79
if cookie == "" {
133728e barerepo 1mo
80
t.Fatal("signing in handed back no session")
133728e barerepo 1mo
83
// The session is the whole point, so it has to open the one page that needs one.
133728e barerepo 1mo
84
req, err := http.NewRequest(http.MethodGet, in.http.URL+"/keys", nil)
133728e barerepo 1mo
85
if err != nil {
133728e barerepo 1mo
86
t.Fatal(err)
133728e barerepo 1mo
88
req.AddCookie(&http.Cookie{Name: "barerepo_session", Value: cookie})
133728e barerepo 1mo
89
keys, err := http.DefaultClient.Do(req)
133728e barerepo 1mo
90
if err != nil {
133728e barerepo 1mo
91
t.Fatal(err)
133728e barerepo 1mo
93
defer keys.Body.Close()
133728e barerepo 1mo
94
page := readAll(t, keys)
133728e barerepo 1mo
95
if keys.StatusCode != http.StatusOK || !strings.Contains(page, "ssh keys") {
133728e barerepo 1mo
96
t.Errorf("the session does not open the keys page: %d\n%s", keys.StatusCode, page)
133728e barerepo 1mo
100
// sign is chapter 31.3's line: one command, stdin to stdout, nothing left behind.
133728e barerepo 1mo
101
func sign(t *testing.T, key, nonce, namespace string) string {
133728e barerepo 1mo
102
t.Helper()
133728e barerepo 1mo
103
cmd := exec.Command("ssh-keygen", "-Y", "sign", "-f", key, "-n", namespace, "-")
133728e barerepo 1mo
104
cmd.Stdin = strings.NewReader(nonce)
133728e barerepo 1mo
105
out, err := cmd.Output()
133728e barerepo 1mo
106
if err != nil {
133728e barerepo 1mo
107
t.Fatalf("ssh-keygen -Y sign: %v", err)
133728e barerepo 1mo
108
}
133728e barerepo 1mo
109
return string(out)
133728e barerepo 1mo
112
func form(t *testing.T, in *instance, path string, values url.Values) string {
133728e barerepo 1mo
113
t.Helper()
133728e barerepo 1mo
114
resp := formResponse(t, in, path, values)
133728e barerepo 1mo
115
return resp.body
133728e barerepo 1mo
118
type formResult struct {
133728e barerepo 1mo
119
*http.Response
133728e barerepo 1mo
120
body string
133728e barerepo 1mo
123
func formResponse(t *testing.T, in *instance, path string, values url.Values) formResult {
133728e barerepo 1mo
124
t.Helper()
133728e barerepo 1mo
125
req, err := http.NewRequest(http.MethodPost, in.http.URL+path, strings.NewReader(values.Encode()))
133728e barerepo 1mo
126
if err != nil {
133728e barerepo 1mo
127
t.Fatal(err)
133728e barerepo 1mo
128
}
133728e barerepo 1mo
129
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
133728e barerepo 1mo
130
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
133728e barerepo 1mo
131
return http.ErrUseLastResponse
133728e barerepo 1mo
132
}}
133728e barerepo 1mo
133
resp, err := client.Do(req)
133728e barerepo 1mo
134
if err != nil {
133728e barerepo 1mo
135
t.Fatal(err)
133728e barerepo 1mo
136
}
133728e barerepo 1mo
137
defer resp.Body.Close()
133728e barerepo 1mo
138
return formResult{resp, readAll(t, resp)}
133728e barerepo 1mo
141
func sessionCookie(r formResult) string {
133728e barerepo 1mo
142
for _, c := range r.Cookies() {
133728e barerepo 1mo
143
if c.Name == "barerepo_session" {
133728e barerepo 1mo
144
return c.Value
133728e barerepo 1mo
145
}
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
return ""