133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"os"
133728e barerepo 1mo
5
"os/exec"
133728e barerepo 1mo
6
"path/filepath"
133728e barerepo 1mo
7
"strings"
133728e barerepo 1mo
8
"testing"
133728e barerepo 1mo
9
"time"
133728e barerepo 1mo
12
// signWith makes a key and points a working copy at it, which is all git needs to sign with ssh.
133728e barerepo 1mo
13
func signWith(t *testing.T, dir string) {
133728e barerepo 1mo
14
t.Helper()
133728e barerepo 1mo
15
signWithKey(t, dir, newKey(t, "signer"))
133728e barerepo 1mo
18
// newKey makes a key pair and hands back the private half's path.
133728e barerepo 1mo
19
func newKey(t *testing.T, comment string) string {
133728e barerepo 1mo
20
t.Helper()
133728e barerepo 1mo
21
key := filepath.Join(t.TempDir(), "id")
133728e barerepo 1mo
22
if out, err := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", "-C", comment,
133728e barerepo 1mo
23
"-f", key, "-q").CombinedOutput(); err != nil {
133728e barerepo 1mo
24
t.Fatalf("ssh-keygen: %v\n%s", err, out)
133728e barerepo 1mo
26
return key
133728e barerepo 1mo
29
func signWithKey(t *testing.T, dir, key string) {
133728e barerepo 1mo
30
t.Helper()
133728e barerepo 1mo
31
run(t, dir, "git", "config", "gpg.format", "ssh")
133728e barerepo 1mo
32
run(t, dir, "git", "config", "user.signingkey", key+".pub")
133728e barerepo 1mo
33
run(t, dir, "git", "config", "commit.gpgsign", "true")
133728e barerepo 1mo
36
// An official repository distributes what it holds, so require_signed_commits refuses an unsigned commit.
133728e barerepo 1mo
37
func TestARepositoryThatAsksForSignaturesRefusesAnUnsignedCommit(t *testing.T) {
133728e barerepo 1mo
38
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
39
t.Skip("git is not installed")
133728e barerepo 1mo
41
if _, err := exec.LookPath("ssh-keygen"); err != nil {
133728e barerepo 1mo
42
t.Skip("ssh-keygen is not installed")
133728e barerepo 1mo
44
in := newInstance(t)
133728e barerepo 1mo
45
john := in.account("john")
133728e barerepo 1mo
46
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
48
signWithKey(t, work, in.keyOf("john"))
133728e barerepo 1mo
49
write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+
133728e barerepo 1mo
50
"[access]\nrequire_signed_commits = true\n")
133728e barerepo 1mo
51
run(t, work, "git", "add", "-A")
133728e barerepo 1mo
52
run(t, work, "git", "commit", "-qm", "this repository takes only signed commits")
133728e barerepo 1mo
53
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
55
// The owner is not exempt, or the rule protects the repository from everyone except its owner.
133728e barerepo 1mo
56
run(t, work, "git", "config", "commit.gpgsign", "false")
133728e barerepo 1mo
57
commit(t, work, "package main\n\nfunc main() {}\n", "written without a signature")
133728e barerepo 1mo
58
out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
59
if err == nil {
133728e barerepo 1mo
60
t.Fatalf("an unsigned commit landed on a repository that asks for signatures:\n%s", out)
133728e barerepo 1mo
62
for _, want := range []string{"only signed commits", "require_signed_commits", "commit.gpgsign"} {
133728e barerepo 1mo
63
if !strings.Contains(out, want) {
133728e barerepo 1mo
64
t.Errorf("the rejection does not mention %q:\n%s", want, out)
133728e barerepo 1mo
68
// The same change, signed, is the push that must go through.
133728e barerepo 1mo
69
run(t, work, "git", "config", "commit.gpgsign", "true")
133728e barerepo 1mo
70
run(t, work, "git", "commit", "-q", "--amend", "--no-edit")
133728e barerepo 1mo
71
if out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"), "master"); err != nil {
133728e barerepo 1mo
72
t.Errorf("a signed commit was refused: %v\n%s", err, out)
133728e barerepo 1mo
76
// The key is off unless a repository asks for it, so nobody else's repository changes.
133728e barerepo 1mo
77
func TestEveryOtherRepositoryStillTakesUnsignedCommits(t *testing.T) {
133728e barerepo 1mo
78
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
79
t.Skip("git is not installed")
133728e barerepo 1mo
81
in := newInstance(t)
133728e barerepo 1mo
82
john := in.account("john")
133728e barerepo 1mo
83
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
85
commit(t, work, "package main\n\nfunc main() {}\n", "no signature, no config, no objection")
133728e barerepo 1mo
86
if out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"), "master"); err != nil {
133728e barerepo 1mo
87
t.Errorf("a plain repository refused an unsigned commit: %v\n%s", err, out)
133728e barerepo 1mo
89
if os.Getenv("CI") != "" {
133728e barerepo 1mo
90
t.Log("ran under CI")
133728e barerepo 1mo
94
// A commit parked on a proposal ref is still unsigned when a branch reaches for it.
133728e barerepo 1mo
95
func TestAnUnsignedCommitCannotEnterThroughAProposal(t *testing.T) {
133728e barerepo 1mo
96
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
97
t.Skip("git is not installed")
133728e barerepo 1mo
99
if _, err := exec.LookPath("ssh-keygen"); err != nil {
133728e barerepo 1mo
100
t.Skip("ssh-keygen is not installed")
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
in := newInstance(t)
133728e barerepo 1mo
103
john := in.account("john")
133728e barerepo 1mo
104
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
106
signWithKey(t, work, in.keyOf("john"))
133728e barerepo 1mo
107
write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+
133728e barerepo 1mo
108
"[access]\nrequire_signed_commits = true\n")
133728e barerepo 1mo
109
run(t, work, "git", "add", "-A")
133728e barerepo 1mo
110
run(t, work, "git", "commit", "-qm", "this repository takes only signed commits")
133728e barerepo 1mo
111
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
113
// The proposal namespace takes it, because a proposal is a request and not a landing.
133728e barerepo 1mo
114
run(t, work, "git", "config", "commit.gpgsign", "false")
133728e barerepo 1mo
115
commit(t, work, "package main\n\nfunc main() {}\n", "unsigned, offered as a proposal")
133728e barerepo 1mo
116
if out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"),
133728e barerepo 1mo
117
"HEAD:refs/proposals/new"); err != nil {
133728e barerepo 1mo
118
t.Fatalf("the proposal was refused: %v\n%s", err, out)
133728e barerepo 1mo
119
}
133728e barerepo 1mo
121
// The commit is now in the repository, so a walk of what is new to the repository would miss it.
133728e barerepo 1mo
122
out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
123
if err == nil {
133728e barerepo 1mo
124
t.Fatalf("an unsigned commit reached master through a proposal:\n%s", out)
133728e barerepo 1mo
125
}
133728e barerepo 1mo
126
if !strings.Contains(out, "only signed commits") {
133728e barerepo 1mo
127
t.Errorf("the rejection does not say why:\n%s", out)
133728e barerepo 1mo
128
}
133728e barerepo 1mo
131
// A signature is only worth the key behind it, so a key the server never saw does not count.
133728e barerepo 1mo
132
func TestASignatureFromAKeyTheServerDoesNotHoldIsRefused(t *testing.T) {
133728e barerepo 1mo
133
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
134
t.Skip("git is not installed")
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
if _, err := exec.LookPath("ssh-keygen"); err != nil {
133728e barerepo 1mo
137
t.Skip("ssh-keygen is not installed")
133728e barerepo 1mo
138
}
133728e barerepo 1mo
139
in := newInstance(t)
133728e barerepo 1mo
140
john := in.account("john")
133728e barerepo 1mo
141
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
143
// The account's own key, which the server wrote into the signers file when the account was made.
133728e barerepo 1mo
144
signWithKey(t, work, in.keyOf("john"))
133728e barerepo 1mo
145
write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+
133728e barerepo 1mo
146
"[access]\nrequire_signed_commits = true\n")
133728e barerepo 1mo
147
run(t, work, "git", "add", "-A")
133728e barerepo 1mo
148
run(t, work, "git", "commit", "-qm", "this repository takes only signed commits")
133728e barerepo 1mo
149
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
151
// A real signature, made by a key nobody published here.
133728e barerepo 1mo
152
signWithKey(t, work, newKey(t, "a stranger's key"))
133728e barerepo 1mo
153
commit(t, work, "package main\n\nfunc main() {}\n", "signed by a key the server never saw")
133728e barerepo 1mo
154
out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
155
if err == nil {
133728e barerepo 1mo
156
t.Fatalf("a signature from an unknown key was taken as proof:\n%s", out)
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
if !strings.Contains(out, "only signed commits") {
133728e barerepo 1mo
159
t.Errorf("the rejection does not say why:\n%s", out)
133728e barerepo 1mo
160
}
133728e barerepo 1mo
162
// The account's own key still works, so the rule is about the key and not about signing at all.
133728e barerepo 1mo
163
signWithKey(t, work, in.keyOf("john"))
133728e barerepo 1mo
164
run(t, work, "git", "commit", "-q", "--amend", "--no-edit")
133728e barerepo 1mo
165
if out, err := try(t, work, "git", "push", in.url(john, "/john/johnbot"), "master"); err != nil {
133728e barerepo 1mo
166
t.Errorf("a signature from a published key was refused: %v\n%s", err, out)
133728e barerepo 1mo
167
}
133728e barerepo 1mo
170
// A retired key must keep vouching for what it signed, or rotating a key rewrites the past.
133728e barerepo 1mo
171
func TestARetiredKeyStillVouchesForWhatItAlreadySigned(t *testing.T) {
133728e barerepo 1mo
172
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
173
t.Skip("git is not installed")
133728e barerepo 1mo
174
}
133728e barerepo 1mo
175
if _, err := exec.LookPath("ssh-keygen"); err != nil {
133728e barerepo 1mo
176
t.Skip("ssh-keygen is not installed")
133728e barerepo 1mo
177
}
133728e barerepo 1mo
178
in := newInstance(t)
133728e barerepo 1mo
179
john := in.account("john")
133728e barerepo 1mo
180
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
182
signWithKey(t, work, in.keyOf("john"))
133728e barerepo 1mo
183
write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+
133728e barerepo 1mo
184
"[access]\nrequire_signed_commits = true\n")
133728e barerepo 1mo
185
run(t, work, "git", "add", "-A")
133728e barerepo 1mo
186
run(t, work, "git", "commit", "-qm", "this repository takes only signed commits")
133728e barerepo 1mo
187
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
189
// Written and signed while the old key is still john's, which is the ordinary case.
133728e barerepo 1mo
190
commit(t, work, "package main\n\nfunc main() {}\n", "signed before the rotation")
133728e barerepo 1mo
192
// Then john rotates, which is what happens between writing a commit and pushing it.
133728e barerepo 1mo
193
keys, err := in.db.Keys(t.Context(), "john")
133728e barerepo 1mo
194
if err != nil || len(keys) != 1 {
133728e barerepo 1mo
195
t.Fatalf("john has %d keys, err %v", len(keys), err)
133728e barerepo 1mo
196
}
133728e barerepo 1mo
197
next := newKey(t, "john's new key")
133728e barerepo 1mo
198
pub, err := os.ReadFile(next + ".pub")
133728e barerepo 1mo
199
if err != nil {
133728e barerepo 1mo
200
t.Fatal(err)
133728e barerepo 1mo
201
}
133728e barerepo 1mo
202
if _, err := in.db.AddKey(t.Context(), "john", string(pub)); err != nil {
133728e barerepo 1mo
203
t.Fatal(err)
133728e barerepo 1mo
204
}
133728e barerepo 1mo
205
if err := in.db.DeleteKey(t.Context(), "john", keys[0].ID); err != nil {
133728e barerepo 1mo
206
t.Fatal(err)