| 1 | //go:build linux |
| 2 | |
| 3 | package provisioner |
| 4 | |
| 5 | import ( |
| 6 | "archive/tar" |
| 7 | "bytes" |
| 8 | "context" |
| 9 | "filippo.io/age" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "testing" |
| 13 | ) |
| 14 | |
| 15 | func TestEncryptedBackupRestoreAndSurvivingFloor(t *testing.T) { |
| 16 | state, floor, backups := persistentDirectory(t), persistentDirectory(t), persistentDirectory(t) |
| 17 | password := []byte("synthetic backup password") |
| 18 | initial, e := Initialize(state, floor, password) |
| 19 | if e != nil { |
| 20 | t.Fatal(e) |
| 21 | } |
| 22 | j, e := existingJournal(state, floor, password) |
| 23 | if e != nil { |
| 24 | t.Fatal(e) |
| 25 | } |
| 26 | payload := []byte("PRIVATE-BACKUP-OWNER@example.invalid") |
| 27 | if _, e = j.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "empty", payload); e != nil { |
| 28 | t.Fatal(e) |
| 29 | } |
| 30 | closeExisting(j) |
| 31 | path := filepath.Join(backups, "current.age") |
| 32 | info, e := Backup(state, floor, path, password) |
| 33 | if e != nil || info.Sequence != 1 || len(info.Digest) != 64 { |
| 34 | t.Fatal("backup", e) |
| 35 | } |
| 36 | data, e := os.ReadFile(path) |
| 37 | if e != nil { |
| 38 | t.Fatal(e) |
| 39 | } |
| 40 | if bytes.Contains(data, payload) || bytes.Contains(data, []byte(initial.Registry)) { |
| 41 | t.Fatal("plaintext archive") |
| 42 | } |
| 43 | restored := persistentDirectory(t) |
| 44 | got, e := Restore(restored, floor, path, password) |
| 45 | if e != nil || got.Registry != initial.Registry || got.Sequence != 1 { |
| 46 | t.Fatal("restore", e) |
| 47 | } |
| 48 | j, e = existingJournal(restored, floor, password) |
| 49 | if e != nil { |
| 50 | t.Fatal(e) |
| 51 | } |
| 52 | if _, e = j.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "empty", payload); e != nil { |
| 53 | t.Fatal("restored private replay", e) |
| 54 | } |
| 55 | if _, e = j.append(context.Background(), testToken(5), testToken(7), "directory.activate-consumer", "pending", []byte("next private record")); e != nil { |
| 56 | t.Fatal(e) |
| 57 | } |
| 58 | closeExisting(j) |
| 59 | stale := persistentDirectory(t) |
| 60 | if _, e = Restore(stale, floor, path, password); e != ErrRefused { |
| 61 | t.Fatal("old archive accepted", e) |
| 62 | } |
| 63 | if _, e = Restore(persistentDirectory(t), floor, path, []byte("wrong")); e != ErrRefused { |
| 64 | t.Fatal("wrong archive password", e) |
| 65 | } |
| 66 | if _, e = Backup(restored, floor, path, password); e != ErrRefused { |
| 67 | t.Fatal("exclusive archive replaced", e) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestRestoreRefusesUnauthenticatedCompletionAndLinks(t *testing.T) { |
| 72 | state, floor, backups := persistentDirectory(t), persistentDirectory(t), persistentDirectory(t) |
| 73 | password := []byte("synthetic malformed archive") |
| 74 | if _, e := Initialize(state, floor, password); e != nil { |
| 75 | t.Fatal(e) |
| 76 | } |
| 77 | path := filepath.Join(backups, "good.age") |
| 78 | if _, e := Backup(state, floor, path, password); e != nil { |
| 79 | t.Fatal(e) |
| 80 | } |
| 81 | data, e := os.ReadFile(path) |
| 82 | if e != nil { |
| 83 | t.Fatal(e) |
| 84 | } |
| 85 | damaged := filepath.Join(backups, "truncated.age") |
| 86 | if e = os.WriteFile(damaged, data[:len(data)-10], 0600); e != nil { |
| 87 | t.Fatal(e) |
| 88 | } |
| 89 | target := persistentDirectory(t) |
| 90 | if _, e = Restore(target, floor, damaged, password); e != ErrRefused { |
| 91 | t.Fatal("truncated encrypted stream accepted", e) |
| 92 | } |
| 93 | if _, e = os.Stat(filepath.Join(target, "credential.age")); !os.IsNotExist(e) { |
| 94 | t.Fatal("unauthenticated archive published credentials") |
| 95 | } |
| 96 | recipient, e := age.NewScryptRecipient(string(password)) |
| 97 | if e != nil { |
| 98 | t.Fatal(e) |
| 99 | } |
| 100 | var ciphertext bytes.Buffer |
| 101 | encrypted, e := age.Encrypt(&ciphertext, recipient) |
| 102 | if e != nil { |
| 103 | t.Fatal(e) |
| 104 | } |
| 105 | archive := tar.NewWriter(encrypted) |
| 106 | if e = archive.WriteHeader(&tar.Header{Name: "registry.db", Mode: 0600, Typeflag: tar.TypeSymlink, Linkname: "outside"}); e != nil { |
| 107 | t.Fatal(e) |
| 108 | } |
| 109 | if archive.Close() != nil || encrypted.Close() != nil { |
| 110 | t.Fatal("synthetic archive") |
| 111 | } |
| 112 | link := filepath.Join(backups, "link.age") |
| 113 | if e = os.WriteFile(link, ciphertext.Bytes(), 0600); e != nil { |
| 114 | t.Fatal(e) |
| 115 | } |
| 116 | if _, e = Restore(persistentDirectory(t), floor, link, password); e != ErrRefused { |
| 117 | t.Fatal("archive link accepted", e) |
| 118 | } |
| 119 | } |
| 120 | |