Commit ed99c936ad
Verified · cmc
Layout: unified · split
internal/seal/seal.go added +310
| @@ -0,0 +1,310 @@ | |||
| 1 | // Package seal encrypts the secret columns of the database with | ||
| 2 | // AES-256-GCM under keys held in a file outside the database and outside | ||
| 3 | // server.root, so neither a copy of the database nor a backup opens them | ||
| 4 | // (#273). A key file that cannot be re-read after it changes fails | ||
| 5 | // closed: Seal and Open return errors until the file is fixed. | ||
| 6 | package seal | ||
| 7 | |||
| 8 | import ( | ||
| 9 | "bufio" | ||
| 10 | "bytes" | ||
| 11 | "crypto/aes" | ||
| 12 | "crypto/cipher" | ||
| 13 | "crypto/rand" | ||
| 14 | "encoding/base64" | ||
| 15 | "encoding/hex" | ||
| 16 | "errors" | ||
| 17 | "fmt" | ||
| 18 | "io" | ||
| 19 | "os" | ||
| 20 | "path/filepath" | ||
| 21 | "strings" | ||
| 22 | "sync" | ||
| 23 | "syscall" | ||
| 24 | ) | ||
| 25 | |||
| 26 | // Prefix marks a sealed value: "gbs1:<key id>:<base64 nonce||ciphertext>". | ||
| 27 | const Prefix = "gbs1:" | ||
| 28 | |||
| 29 | // maxKeyFile is the largest key file ReadKeys accepts. | ||
| 30 | const maxKeyFile = 1 << 20 | ||
| 31 | |||
| 32 | // Key is one line of the key file. | ||
| 33 | type Key struct { | ||
| 34 | ID string // 8 lowercase hex characters | ||
| 35 | Secret []byte // 32 bytes | ||
| 36 | } | ||
| 37 | |||
| 38 | // NewKey returns a key with a random id and secret. | ||
| 39 | func NewKey() (Key, error) { | ||
| 40 | id := make([]byte, 4) | ||
| 41 | secret := make([]byte, 32) | ||
| 42 | if _, err := rand.Read(id); err != nil { | ||
| 43 | return Key{}, err | ||
| 44 | } | ||
| 45 | if _, err := rand.Read(secret); err != nil { | ||
| 46 | return Key{}, err | ||
| 47 | } | ||
| 48 | return Key{ID: hex.EncodeToString(id), Secret: secret}, nil | ||
| 49 | } | ||
| 50 | |||
| 51 | // ReadKeys reads the key file. The last key seals; every key opens. | ||
| 52 | func ReadKeys(path string) ([]Key, error) { | ||
| 53 | f, err := os.Open(path) | ||
| 54 | if err != nil { | ||
| 55 | return nil, err | ||
| 56 | } | ||
| 57 | defer f.Close() | ||
| 58 | fi, err := f.Stat() | ||
| 59 | if err != nil { | ||
| 60 | return nil, err | ||
| 61 | } | ||
| 62 | if !fi.Mode().IsRegular() { | ||
| 63 | return nil, fmt.Errorf("%s is not a regular file", path) | ||
| 64 | } | ||
| 65 | if perm := fi.Mode().Perm(); perm&0o077 != 0 { | ||
| 66 | return nil, fmt.Errorf("%s is mode %04o; it must be readable by its owner alone (0600)", path, perm) | ||
| 67 | } | ||
| 68 | data, err := io.ReadAll(io.LimitReader(f, maxKeyFile+1)) | ||
| 69 | if err != nil { | ||
| 70 | return nil, err | ||
| 71 | } | ||
| 72 | if len(data) > maxKeyFile { | ||
| 73 | return nil, fmt.Errorf("%s is larger than %d bytes", path, maxKeyFile) | ||
| 74 | } | ||
| 75 | var keys []Key | ||
| 76 | seen := map[string]bool{} | ||
| 77 | sc := bufio.NewScanner(bytes.NewReader(data)) | ||
| 78 | for n := 1; sc.Scan(); n++ { | ||
| 79 | line := strings.TrimSpace(sc.Text()) | ||
| 80 | if line == "" || strings.HasPrefix(line, "#") { | ||
| 81 | continue | ||
| 82 | } | ||
| 83 | f := strings.Fields(line) | ||
| 84 | if len(f) != 2 || !validID(f[0]) { | ||
| 85 | return nil, fmt.Errorf("%s:%d: want \"<8 hex id> <base64 32-byte key>\"", path, n) | ||
| 86 | } | ||
| 87 | secret, err := base64.StdEncoding.DecodeString(f[1]) | ||
| 88 | if err != nil || len(secret) != 32 { | ||
| 89 | return nil, fmt.Errorf("%s:%d: key is not 32 bytes of base64", path, n) | ||
| 90 | } | ||
| 91 | if seen[f[0]] { | ||
| 92 | return nil, fmt.Errorf("%s:%d: key id %s appears twice", path, n, f[0]) | ||
| 93 | } | ||
| 94 | seen[f[0]] = true | ||
| 95 | keys = append(keys, Key{ID: f[0], Secret: secret}) | ||
| 96 | } | ||
| 97 | if err := sc.Err(); err != nil { | ||
| 98 | return nil, err | ||
| 99 | } | ||
| 100 | if len(keys) == 0 { | ||
| 101 | return nil, fmt.Errorf("%s holds no keys", path) | ||
| 102 | } | ||
| 103 | return keys, nil | ||
| 104 | } | ||
| 105 | |||
| 106 | // WriteKeys replaces the key file: a temporary file in the same | ||
| 107 | // directory, mode 0600, given the existing file's owner when there is | ||
| 108 | // one (rotation runs as root; the daemon reads the file as its own | ||
| 109 | // user), then renamed over it. Keys ReadKeys would refuse are refused | ||
| 110 | // before anything is written. | ||
| 111 | func WriteKeys(path string, keys []Key) error { | ||
| 112 | if len(keys) == 0 { | ||
| 113 | return errors.New("no keys to write") | ||
| 114 | } | ||
| 115 | seen := map[string]bool{} | ||
| 116 | for _, k := range keys { | ||
| 117 | if !validID(k.ID) || len(k.Secret) != 32 || seen[k.ID] { | ||
| 118 | return fmt.Errorf("key %q is not an 8-hex-id, 32-byte key or appears twice", k.ID) | ||
| 119 | } | ||
| 120 | seen[k.ID] = true | ||
| 121 | } | ||
| 122 | var b strings.Builder | ||
| 123 | b.WriteString("# gitbay secret keys, \"<id> <base64 key>\" per line. The last line seals\n") | ||
| 124 | b.WriteString("# new values; the others open values sealed before a rotation.\n") | ||
| 125 | b.WriteString("# Keep a copy off this host: backups do not carry this file.\n") | ||
| 126 | for _, k := range keys { | ||
| 127 | fmt.Fprintf(&b, "%s %s\n", k.ID, base64.StdEncoding.EncodeToString(k.Secret)) | ||
| 128 | } | ||
| 129 | dir := filepath.Dir(path) | ||
| 130 | tmp, err := os.CreateTemp(dir, ".secret-key-*") | ||
| 131 | if err != nil { | ||
| 132 | return err | ||
| 133 | } | ||
| 134 | defer os.Remove(tmp.Name()) | ||
| 135 | fail := func(err error) error { | ||
| 136 | tmp.Close() | ||
| 137 | return err | ||
| 138 | } | ||
| 139 | if err := tmp.Chmod(0o600); err != nil { | ||
| 140 | return fail(err) | ||
| 141 | } | ||
| 142 | if fi, err := os.Stat(path); err == nil { | ||
| 143 | if st, ok := fi.Sys().(*syscall.Stat_t); ok { | ||
| 144 | if err := tmp.Chown(int(st.Uid), int(st.Gid)); err != nil { | ||
| 145 | return fail(err) | ||
| 146 | } | ||
| 147 | } | ||
| 148 | } | ||
| 149 | if _, err := tmp.WriteString(b.String()); err != nil { | ||
| 150 | return fail(err) | ||
| 151 | } | ||
| 152 | if err := tmp.Sync(); err != nil { | ||
| 153 | return fail(err) | ||
| 154 | } | ||
| 155 | if err := tmp.Close(); err != nil { | ||
| 156 | return err | ||
| 157 | } | ||
| 158 | if err := os.Rename(tmp.Name(), path); err != nil { | ||
| 159 | return err | ||
| 160 | } | ||
| 161 | // Losing the file loses every sealed value, so the rename is made | ||
| 162 | // durable before returning. | ||
| 163 | d, err := os.Open(dir) | ||
| 164 | if err == nil { | ||
| 165 | err = d.Sync() | ||
| 166 | d.Close() | ||
| 167 | } | ||
| 168 | if err != nil { | ||
| 169 | return fmt.Errorf("%s was replaced, but syncing %s failed: %w", path, dir, err) | ||
| 170 | } | ||
| 171 | return nil | ||
| 172 | } | ||
| 173 | |||
| 174 | // Keyring is the loaded key file. It re-reads the file whenever the file | ||
| 175 | // changes, so a running daemon follows a rotation without a restart. | ||
| 176 | type Keyring struct { | ||
| 177 | path string | ||
| 178 | |||
| 179 | mu sync.Mutex | ||
| 180 | fi os.FileInfo | ||
| 181 | cur string | ||
| 182 | aead map[string]cipher.AEAD | ||
| 183 | } | ||
| 184 | |||
| 185 | // Load reads the key file at path and returns a Keyring over it. It | ||
| 186 | // fails when ReadKeys would. | ||
| 187 | func Load(path string) (*Keyring, error) { | ||
| 188 | k := &Keyring{path: path} | ||
| 189 | if err := k.refresh(); err != nil { | ||
| 190 | return nil, err | ||
| 191 | } | ||
| 192 | return k, nil | ||
| 193 | } | ||
| 194 | |||
| 195 | // refresh reloads the file unless it is the one last read. Callers hold k.mu. | ||
| 196 | func (k *Keyring) refresh() error { | ||
| 197 | fi, err := os.Stat(k.path) | ||
| 198 | if err != nil { | ||
| 199 | return err | ||
| 200 | } | ||
| 201 | if k.fi != nil && os.SameFile(k.fi, fi) && fi.ModTime().Equal(k.fi.ModTime()) && fi.Size() == k.fi.Size() { | ||
| 202 | return nil | ||
| 203 | } | ||
| 204 | keys, err := ReadKeys(k.path) | ||
| 205 | if err != nil { | ||
| 206 | return err | ||
| 207 | } | ||
| 208 | aead := make(map[string]cipher.AEAD, len(keys)) | ||
| 209 | for _, key := range keys { | ||
| 210 | block, err := aes.NewCipher(key.Secret) | ||
| 211 | if err != nil { | ||
| 212 | return err | ||
| 213 | } | ||
| 214 | g, err := cipher.NewGCM(block) | ||
| 215 | if err != nil { | ||
| 216 | return err | ||
| 217 | } | ||
| 218 | aead[key.ID] = g | ||
| 219 | } | ||
| 220 | k.fi, k.cur, k.aead = fi, keys[len(keys)-1].ID, aead | ||
| 221 | return nil | ||
| 222 | } | ||
| 223 | |||
| 224 | // CurrentID is the id of the key that seals new values. | ||
| 225 | func (k *Keyring) CurrentID() (string, error) { | ||
| 226 | k.mu.Lock() | ||
| 227 | defer k.mu.Unlock() | ||
| 228 | if err := k.refresh(); err != nil { | ||
| 229 | return "", err | ||
| 230 | } | ||
| 231 | return k.cur, nil | ||
| 232 | } | ||
| 233 | |||
| 234 | // Seal encrypts plain under the current key with a random nonce. aad | ||
| 235 | // names the column, so a value copied into another column does not open | ||
| 236 | // there. | ||
| 237 | func (k *Keyring) Seal(aad, plain string) (string, error) { | ||
| 238 | if aad == "" { | ||
| 239 | return "", errNoAAD | ||
| 240 | } | ||
| 241 | k.mu.Lock() | ||
| 242 | defer k.mu.Unlock() | ||
| 243 | if err := k.refresh(); err != nil { | ||
| 244 | return "", err | ||
| 245 | } | ||
| 246 | g := k.aead[k.cur] | ||
| 247 | nonce := make([]byte, g.NonceSize()) | ||
| 248 | if _, err := rand.Read(nonce); err != nil { | ||
| 249 | return "", err | ||
| 250 | } | ||
| 251 | ct := g.Seal(nonce, nonce, []byte(plain), []byte(aad)) | ||
| 252 | return Prefix + k.cur + ":" + base64.RawStdEncoding.EncodeToString(ct), nil | ||
| 253 | } | ||
| 254 | |||
| 255 | // Open decrypts a value Seal produced under any key the file holds. | ||
| 256 | func (k *Keyring) Open(aad, sealed string) (string, error) { | ||
| 257 | if aad == "" { | ||
| 258 | return "", errNoAAD | ||
| 259 | } | ||
| 260 | id, body, ok := split(sealed) | ||
| 261 | if !ok { | ||
| 262 | return "", errors.New("not a sealed value") | ||
| 263 | } | ||
| 264 | k.mu.Lock() | ||
| 265 | defer k.mu.Unlock() | ||
| 266 | if err := k.refresh(); err != nil { | ||
| 267 | return "", err | ||
| 268 | } | ||
| 269 | g, ok := k.aead[id] | ||
| 270 | if !ok { | ||
| 271 | return "", fmt.Errorf("sealed with key %s, which %s does not hold", id, k.path) | ||
| 272 | } | ||
| 273 | ct, err := base64.RawStdEncoding.DecodeString(body) | ||
| 274 | if err != nil || len(ct) < g.NonceSize()+g.Overhead() { | ||
| 275 | return "", fmt.Errorf("value sealed with key %s is malformed", id) | ||
| 276 | } | ||
| 277 | plain, err := g.Open(nil, ct[:g.NonceSize()], ct[g.NonceSize():], []byte(aad)) | ||
| 278 | if err != nil { | ||
| 279 | return "", fmt.Errorf("value sealed with key %s does not open: wrong key, wrong column or altered value", id) | ||
| 280 | } | ||
| 281 | return string(plain), nil | ||
| 282 | } | ||
| 283 | |||
| 284 | var errNoAAD = errors.New("seal: additional data (table.column) is required") | ||
| 285 | |||
| 286 | // IsSealed reports whether v carries the sealed prefix. | ||
| 287 | func IsSealed(v string) bool { return strings.HasPrefix(v, Prefix) } | ||
| 288 | |||
| 289 | // KeyID is the id of the key that sealed v. | ||
| 290 | func KeyID(v string) (string, bool) { | ||
| 291 | id, _, ok := split(v) | ||
| 292 | return id, ok | ||
| 293 | } | ||
| 294 | |||
| 295 | func split(v string) (id, body string, ok bool) { | ||
| 296 | rest, ok := strings.CutPrefix(v, Prefix) | ||
| 297 | if !ok { | ||
| 298 | return "", "", false | ||
| 299 | } | ||
| 300 | id, body, ok = strings.Cut(rest, ":") | ||
| 301 | return id, body, ok && validID(id) | ||
| 302 | } | ||
| 303 | |||
| 304 | func validID(s string) bool { | ||
| 305 | if len(s) != 8 || strings.ToLower(s) != s { | ||
| 306 | return false | ||
| 307 | } | ||
| 308 | _, err := hex.DecodeString(s) | ||
| 309 | return err == nil | ||
| 310 | } | ||
internal/seal/seal_test.go added +234
| @@ -0,0 +1,234 @@ | |||
| 1 | package seal | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/base64" | ||
| 5 | "os" | ||
| 6 | "path/filepath" | ||
| 7 | "strings" | ||
| 8 | "testing" | ||
| 9 | ) | ||
| 10 | |||
| 11 | func 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 | |||
| 20 | func 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 | |||
| 29 | func 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. | ||
| 56 | func 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 | |||
| 67 | func 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. | ||
| 89 | func 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 | |||
| 118 | func 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 | |||
| 128 | func 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 | |||
| 139 | func 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 | |||
| 157 | func 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 | |||
| 174 | func 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 | |||
| 195 | func 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 | |||
| 209 | func 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 | } | ||