internal/seal/seal_test.go
234 lines · 6184 bytes
1package seal
2
3import (
4 "encoding/base64"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func keyFile(t *testing.T, keys ...Key) string {
12 t.Helper()
13 path := filepath.Join(t.TempDir(), "secret.key")
14 if err := WriteKeys(path, keys); err != nil {
15 t.Fatal(err)
16 }
17 return path
18}
19
20func newKey(t *testing.T) Key {
21 t.Helper()
22 k, err := NewKey()
23 if err != nil {
24 t.Fatal(err)
25 }
26 return k
27}
28
29func TestSealOpenRoundTrip(t *testing.T) {
30 k := newKey(t)
31 ring, err := Load(keyFile(t, k))
32 if err != nil {
33 t.Fatal(err)
34 }
35 v, err := ring.Seal("build_secrets.value", "hunter2")
36 if err != nil {
37 t.Fatal(err)
38 }
39 if !strings.HasPrefix(v, Prefix+k.ID+":") || strings.Contains(v, "hunter2") {
40 t.Fatalf("sealed value %q", v)
41 }
42 if id, ok := KeyID(v); !ok || id != k.ID {
43 t.Fatalf("KeyID = %q, %v", id, ok)
44 }
45 got, err := ring.Open("build_secrets.value", v)
46 if err != nil || got != "hunter2" {
47 t.Fatalf("Open = %q, %v", got, err)
48 }
49 // Two seals of one value differ: the nonce is random.
50 if w, _ := ring.Seal("build_secrets.value", "hunter2"); w == v {
51 t.Fatal("two seals produced the same value")
52 }
53}
54
55// A value moved to another column does not open there.
56func TestOpenChecksAdditionalData(t *testing.T) {
57 ring, err := Load(keyFile(t, newKey(t)))
58 if err != nil {
59 t.Fatal(err)
60 }
61 v, _ := ring.Seal("mirrors.token", "tok")
62 if _, err := ring.Open("webhooks.secret", v); err == nil {
63 t.Fatal("opened under the wrong column")
64 }
65}
66
67func TestOpenRefusesAnAlteredValue(t *testing.T) {
68 ring, err := Load(keyFile(t, newKey(t)))
69 if err != nil {
70 t.Fatal(err)
71 }
72 v, _ := ring.Seal("mirrors.token", "tok")
73 // A character in the middle: the last one may carry only padding bits.
74 i := len(v) - 10
75 alt := byte('A')
76 if v[i] == 'A' {
77 alt = 'B'
78 }
79 if _, err := ring.Open("mirrors.token", v[:i]+string(alt)+v[i+1:]); err == nil {
80 t.Fatal("opened an altered value")
81 }
82 if _, err := ring.Open("mirrors.token", "tok"); err == nil {
83 t.Fatal("opened a clear value")
84 }
85}
86
87// A running daemon sees a rotation without a restart: the ring re-reads
88// the file when it changes.
89func TestKeyringFollowsTheFile(t *testing.T) {
90 old, next := newKey(t), newKey(t)
91 path := keyFile(t, old)
92 ring, err := Load(path)
93 if err != nil {
94 t.Fatal(err)
95 }
96 before, _ := ring.Seal("webhooks.secret", "s")
97 if err := WriteKeys(path, []Key{old, next}); err != nil {
98 t.Fatal(err)
99 }
100 after, err := ring.Seal("webhooks.secret", "s")
101 if err != nil {
102 t.Fatal(err)
103 }
104 if id, _ := KeyID(after); id != next.ID {
105 t.Fatalf("sealed under %s after rotation, want %s", id, next.ID)
106 }
107 if got, err := ring.Open("webhooks.secret", before); err != nil || got != "s" {
108 t.Fatalf("old value after rotation: %q, %v", got, err)
109 }
110 if err := WriteKeys(path, []Key{next}); err != nil {
111 t.Fatal(err)
112 }
113 if _, err := ring.Open("webhooks.secret", before); err == nil || !strings.Contains(err.Error(), old.ID) {
114 t.Fatalf("a retired key's value opened, or the error does not name the key: %v", err)
115 }
116}
117
118func TestReadKeysRefusesAReadableFile(t *testing.T) {
119 path := keyFile(t, newKey(t))
120 if err := os.Chmod(path, 0o640); err != nil {
121 t.Fatal(err)
122 }
123 if _, err := ReadKeys(path); err == nil || !strings.Contains(err.Error(), "0600") {
124 t.Fatalf("group-readable key file: %v", err)
125 }
126}
127
128func TestWriteKeysMode(t *testing.T) {
129 path := keyFile(t, newKey(t))
130 fi, err := os.Stat(path)
131 if err != nil {
132 t.Fatal(err)
133 }
134 if fi.Mode().Perm() != 0o600 {
135 t.Fatalf("mode %04o", fi.Mode().Perm())
136 }
137}
138
139func TestWriteKeysRefusesABadKey(t *testing.T) {
140 path := filepath.Join(t.TempDir(), "secret.key")
141 good := newKey(t)
142 for _, keys := range [][]Key{
143 nil,
144 {{ID: good.ID, Secret: good.Secret[:16]}},
145 {{ID: "XYZ12345", Secret: good.Secret}},
146 {good, good},
147 } {
148 if err := WriteKeys(path, keys); err == nil {
149 t.Errorf("wrote %d keys that do not read back", len(keys))
150 }
151 }
152 if _, err := os.Stat(path); !os.IsNotExist(err) {
153 t.Fatalf("a refused write left a file: %v", err)
154 }
155}
156
157func TestReadKeysRejectsMalformedLines(t *testing.T) {
158 for _, body := range []string{
159 "",
160 "# only a comment\n",
161 "XYZ12345 AAAA\n",
162 "0123abcd bm90IDMyIGJ5dGVz\n",
163 } {
164 path := filepath.Join(t.TempDir(), "k")
165 if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
166 t.Fatal(err)
167 }
168 if _, err := ReadKeys(path); err == nil {
169 t.Errorf("accepted %q", body)
170 }
171 }
172}
173
174func TestOpenRefusesMalformedInput(t *testing.T) {
175 k := newKey(t)
176 ring, err := Load(keyFile(t, k))
177 if err != nil {
178 t.Fatal(err)
179 }
180 for _, v := range []string{
181 "gbs1:",
182 "gbs1:" + k.ID + ":",
183 "gbs1:" + strings.ToUpper(k.ID) + ":AAAA",
184 "gbs1:" + k.ID[:7] + ":AAAA",
185 "gbs1:" + k.ID + ":!!!not base64!!!",
186 "gbs1:" + k.ID + ":AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", // 29 bytes
187 "gbs1:0badf00d:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
188 } {
189 if got, err := ring.Open("mirrors.token", v); err == nil {
190 t.Errorf("Open(%q) = %q, want an error", v, got)
191 }
192 }
193}
194
195func TestEmptyAdditionalDataRefused(t *testing.T) {
196 ring, err := Load(keyFile(t, newKey(t)))
197 if err != nil {
198 t.Fatal(err)
199 }
200 if _, err := ring.Seal("", "s"); err == nil {
201 t.Fatal("sealed with no column")
202 }
203 v, _ := ring.Seal("mirrors.token", "s")
204 if _, err := ring.Open("", v); err == nil {
205 t.Fatal("opened with no column")
206 }
207}
208
209func TestReadKeysRefusesDuplicatesDirectoriesAndLargeFiles(t *testing.T) {
210 k := newKey(t)
211 line := k.ID + " " + base64.StdEncoding.EncodeToString(k.Secret) + "\n"
212 dup := filepath.Join(t.TempDir(), "dup")
213 if err := os.WriteFile(dup, []byte(line+line), 0o600); err != nil {
214 t.Fatal(err)
215 }
216 if _, err := ReadKeys(dup); err == nil || !strings.Contains(err.Error(), "twice") {
217 t.Errorf("duplicate id: %v", err)
218 }
219 dir := filepath.Join(t.TempDir(), "d")
220 if err := os.Mkdir(dir, 0o700); err != nil {
221 t.Fatal(err)
222 }
223 if _, err := ReadKeys(dir); err == nil {
224 t.Error("read a directory")
225 }
226 big := filepath.Join(t.TempDir(), "big")
227 body := line + "#" + strings.Repeat("x", maxKeyFile) + "\n"
228 if err := os.WriteFile(big, []byte(body), 0o600); err != nil {
229 t.Fatal(err)
230 }
231 if _, err := ReadKeys(big); err == nil {
232 t.Error("read a file over the size limit")
233 }
234}