internal/seal/seal_test.go

v1.37.0
gitbay/internal/seal/seal_test.go history · blame · raw

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}