mesh/delivery-group group feat/900-node-files checking: composed check out of date: novox/mesh-controller@210f7cf5e64b was judged against trunk 611e2275cf7c, t…
mesh/merge-gate out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
535 lines
19 KiB
Go
535 lines
19 KiB
Go
package main
|
||
|
||
import (
|
||
crand "crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/x509"
|
||
"encoding/json"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"syscall"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// A token shaped as a secret, written into files the tests read.
|
||
const aToken = "ghp_abcdefghijklmnopqrstuvwxyz0123456789"
|
||
|
||
func testReader(t *testing.T, places ...string) (Reader, string) {
|
||
t.Helper()
|
||
dir := t.TempDir()
|
||
passwd := filepath.Join(dir, "passwd")
|
||
group := filepath.Join(dir, "group")
|
||
mountinfo := filepath.Join(dir, "mountinfo")
|
||
write(t, passwd, "root:x:0:0::/root:/usr/bin/bash\nops:x:1000:1000:The Operator:/home/ops:/usr/bin/zsh\n"+
|
||
"mesh-files:x:961:961::/var/lib/mesh-files:/usr/bin/nologin\n")
|
||
write(t, group, "root:x:0:\nwheel:x:998:ops\nops:x:1000:\nmesh-files:x:961:\ndocker:x:970:ops,builder\n")
|
||
data := filepath.Join(dir, "data")
|
||
write(t, mountinfo, "22 1 0:21 / / rw,relatime - btrfs /dev/nvme0n1p2 rw\n"+
|
||
"40 22 0:40 / "+strings.ReplaceAll(data, " ", `\040`)+" rw - nfs4 home-server:/tank/data rw\n")
|
||
r := Reader{Refuser: Refuser{Places: places}, Passwd: passwd, Group: group, MountInfo: mountinfo,
|
||
Now: time.Now}
|
||
return r, dir
|
||
}
|
||
|
||
func write(t *testing.T, path, content string) {
|
||
t.Helper()
|
||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// ADR 0394 §5: without the controller's list of secret places it does not start: a refusal it promises would be
|
||
// silent.
|
||
func TestItDoesNotStartWithoutTheNodesSecretPlaces(t *testing.T) {
|
||
if _, err := placesFrom(""); err == nil || !strings.Contains(err.Error(), PlacesWord) {
|
||
t.Errorf("no places: %v", err)
|
||
}
|
||
if p, err := placesFrom(`["/var/lib/mesh-vault"]`); err != nil || len(p) != 1 {
|
||
t.Errorf("a list: %v %v", p, err)
|
||
}
|
||
if p, err := placesFrom(`[]`); err != nil || len(p) != 0 {
|
||
t.Errorf("an empty list is a node with no secret place: %v %v", p, err)
|
||
}
|
||
}
|
||
|
||
// ADR 0394 §5: a path under the vault's directory, a module's own-secret directory and a known credential file
|
||
// are each refused by name before any read — though the account's modes would allow it — and a link to one is
|
||
// refused as the place is.
|
||
func TestSecretPlacesAreRefusedByNameBeforeAnyRead(t *testing.T) {
|
||
r0, dir := testReader(t)
|
||
vault := filepath.Join(dir, "var/lib/mesh-vault")
|
||
own := filepath.Join(dir, "var/lib/desk-channel")
|
||
r, _ := r0, dir
|
||
r.Places = []string{vault, own}
|
||
for _, p := range []string{
|
||
filepath.Join(vault, "ledger/entries.json"),
|
||
filepath.Join(own, "broker"),
|
||
filepath.Join(own, "settings.json"),
|
||
filepath.Join(dir, "home/ops/.claude/.credentials.json"),
|
||
filepath.Join(dir, "home/ops/.ssh/config"),
|
||
filepath.Join(dir, "home/ops/.pgpass"),
|
||
filepath.Join(dir, "etc/ssh/ssh_host_ed25519_key"),
|
||
filepath.Join(dir, "etc/shadow"),
|
||
filepath.Join(dir, "srv/tls/server.key"),
|
||
} {
|
||
write(t, p, "harmless words\n")
|
||
if _, err := r.Read(p, ReadAsk{}); err == nil || !strings.Contains(err.Error(), "refused by name") {
|
||
t.Errorf("%s was read: %v", p, err)
|
||
}
|
||
}
|
||
if _, err := r.List(vault); err == nil || !strings.Contains(err.Error(), "refused by name") {
|
||
t.Errorf("the vault's directory was listed: %v", err)
|
||
}
|
||
link := filepath.Join(dir, "tmp/innocent")
|
||
write(t, filepath.Join(dir, "tmp/x"), "")
|
||
if err := os.Symlink(filepath.Join(own, "broker"), link); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := r.Read(link, ReadAsk{}); err == nil || !strings.Contains(err.Error(), "leads to") {
|
||
t.Errorf("a link to a secret place was read: %v", err)
|
||
}
|
||
// A search under a directory holding a secret place skips it and names it.
|
||
write(t, filepath.Join(dir, "home/ops/notes.txt"), "harmless words\n")
|
||
a, err := r.Search(SearchAsk{Text: "harmless", Path: filepath.Join(dir, "home"), Depth: 8})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(a.Matches) != 1 || !strings.HasSuffix(a.Matches[0].File, "notes.txt") || len(a.Skipped) != 3 {
|
||
t.Errorf("search: matches %v, skipped %v", a.Matches, a.Skipped)
|
||
}
|
||
// What a public key or a certificate is, is not refused.
|
||
for _, p := range []string{filepath.Join(dir, "etc/ssh/ssh_host_ed25519_key.pub"), filepath.Join(dir, "etc/ssl/certs/ca.pem")} {
|
||
write(t, p, "harmless words\n")
|
||
if _, err := r.Read(p, ReadAsk{}); err != nil {
|
||
t.Errorf("%s was refused: %v", p, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ADR 0394 §3: what the account's file modes refuse is refused — the first bound, below the names.
|
||
func TestAFileTheAccountMayNotReadIsRefusedByItsMode(t *testing.T) {
|
||
if os.Geteuid() == 0 {
|
||
t.Skip("run as root, a file's mode refuses nothing")
|
||
}
|
||
r, dir := testReader(t)
|
||
p := filepath.Join(dir, "operators-only")
|
||
write(t, p, "the operator's words\n")
|
||
if err := os.Chmod(p, 0o000); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := r.Read(p, ReadAsk{}); err == nil || !strings.Contains(err.Error(), "may not reach it") {
|
||
t.Errorf("a file of mode 0000: %v", err)
|
||
}
|
||
closed := filepath.Join(dir, "closed")
|
||
write(t, filepath.Join(closed, "inside"), "x")
|
||
if err := os.Chmod(closed, 0o700&^0o100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer os.Chmod(closed, 0o755)
|
||
a, err := r.Stat([]string{filepath.Join(closed, "inside")}, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := a.Paths[0]; !strings.Contains(got.Stopped, "may not reach") {
|
||
t.Errorf("a path through a directory it may not search: %+v", got)
|
||
}
|
||
}
|
||
|
||
// ADR 0394 §2 and the bounds: a read over the size bound is cut at a line and says so; more than the most is
|
||
// refused; lines and a tail are answered as asked.
|
||
func TestAReadIsBounded(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
p := filepath.Join(dir, "big")
|
||
var b strings.Builder
|
||
for i := 1; i <= 20000; i++ {
|
||
fmt.Fprintf(&b, "line %d of the file\n", i)
|
||
}
|
||
write(t, p, b.String())
|
||
a, err := r.Read(p, ReadAsk{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(a.Text) > DefaultReadSize || !strings.Contains(a.Cut, fmt.Sprint(DefaultReadSize)) || a.From != 1 {
|
||
t.Errorf("a default read: %d bytes, cut %q", len(a.Text), a.Cut)
|
||
}
|
||
if _, err := r.Read(p, ReadAsk{Bytes: MaxReadSize + 1}); err == nil {
|
||
t.Error("more than the most a read answers was answered")
|
||
}
|
||
a, err = r.Read(p, ReadAsk{From: 10, Lines: 2})
|
||
if err != nil || a.Text != "line 10 of the file\nline 11 of the file\n" || a.From != 10 || a.Cut != "" {
|
||
t.Errorf("lines 10 and 11: %+v %v", a, err)
|
||
}
|
||
a, err = r.Read(p, ReadAsk{From: -2})
|
||
if err != nil || a.Text != "line 19999 of the file\nline 20000 of the file\n" || a.From != 19999 {
|
||
t.Errorf("the last two lines: %q from %d, %v", a.Text, a.From, err)
|
||
}
|
||
if _, err := r.Read(p, ReadAsk{From: 30000}); err == nil || !strings.Contains(err.Error(), "20000 lines") {
|
||
t.Errorf("a line past the end: %v", err)
|
||
}
|
||
}
|
||
|
||
// A binary file, a directory and a fifo are refused, and the fifo without waiting for a writer.
|
||
func TestOnlyATextFileIsRead(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
bin := filepath.Join(dir, "bin")
|
||
write(t, bin, "ELF\x00\x01\x02")
|
||
if _, err := r.Read(bin, ReadAsk{}); err == nil || !strings.Contains(err.Error(), "binary") {
|
||
t.Errorf("a binary file: %v", err)
|
||
}
|
||
if _, err := r.Read(dir, ReadAsk{}); err == nil || !strings.Contains(err.Error(), "directory") {
|
||
t.Errorf("a directory: %v", err)
|
||
}
|
||
fifo := filepath.Join(dir, "fifo")
|
||
if err := syscall.Mkfifo(fifo, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
done := make(chan error, 1)
|
||
go func() { _, err := r.Read(fifo, ReadAsk{}); done <- err }()
|
||
select {
|
||
case err := <-done:
|
||
if err == nil || !strings.Contains(err.Error(), "fifo") {
|
||
t.Errorf("a fifo: %v", err)
|
||
}
|
||
case <-time.After(5 * time.Second):
|
||
t.Fatal("a read of a fifo waits for a writer")
|
||
}
|
||
}
|
||
|
||
// ADR 0394 §4: a line shaped like a secret is withheld, its number said, and the rest is answered.
|
||
func TestALineShapedLikeASecretIsWithheld(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
p := filepath.Join(dir, "app.conf")
|
||
write(t, p, "listen = /run/app.sock\ntoken = "+aToken+"\nuser = app\n")
|
||
a, err := r.Read(p, ReadAsk{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(a.Text, aToken) || !strings.Contains(a.Text, "user = app") ||
|
||
!strings.Contains(a.Text, "listen = /run/app.sock") || fmt.Sprint(a.Withheld) != "[2]" {
|
||
t.Errorf("read: %q withheld %v", a.Text, a.Withheld)
|
||
}
|
||
m, err := r.Search(SearchAsk{Text: "token", Path: dir})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
body, _ := json.Marshal(m)
|
||
if strings.Contains(string(body), aToken) || len(m.Matches) != 1 || len(m.Withheld) != 1 {
|
||
t.Errorf("search: %s", body)
|
||
}
|
||
}
|
||
|
||
// The bounds of a search: matches, depth, the most asked, and time — each met is said.
|
||
func TestASearchIsBounded(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
for i := 0; i < 30; i++ {
|
||
write(t, filepath.Join(dir, fmt.Sprintf("f%02d", i)), "needle\nneedle\n")
|
||
}
|
||
a, err := r.Search(SearchAsk{Text: "needle", Path: dir, Matches: 7})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(a.Matches) != 7 || !strings.HasPrefix(a.Bound, "matches") {
|
||
t.Errorf("seven matches asked: %d, bound %q", len(a.Matches), a.Bound)
|
||
}
|
||
if _, err := r.Search(SearchAsk{Text: "needle", Path: dir, Matches: MaxMatches + 1}); err == nil {
|
||
t.Error("more matches than the most were asked and answered")
|
||
}
|
||
if _, err := r.Search(SearchAsk{Text: "needle", Path: dir, Depth: MaxDepth + 1}); err == nil {
|
||
t.Error("deeper than the most was asked and answered")
|
||
}
|
||
deep := t.TempDir()
|
||
write(t, filepath.Join(deep, "a/b/c/shallow"), "needle\n")
|
||
write(t, filepath.Join(deep, "a/b/c/d/e/deep"), "needle\n")
|
||
a, err = r.Search(SearchAsk{Text: "needle", Path: deep, Depth: 3})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(a.Matches) != 1 || !strings.HasSuffix(a.Matches[0].File, "shallow") {
|
||
t.Errorf("three deep: %+v", a.Matches)
|
||
}
|
||
// Case: as asked.
|
||
write(t, filepath.Join(deep, "case"), "NEEDLE\n")
|
||
if a, _ := r.Search(SearchAsk{Text: "needle", Path: filepath.Join(deep, "case")}); len(a.Matches) != 0 {
|
||
t.Error("a match whatever the case, unasked")
|
||
}
|
||
if a, _ := r.Search(SearchAsk{Text: "needle", Path: filepath.Join(deep, "case"), CaseInsensitive: true}); len(a.Matches) != 1 {
|
||
t.Error("no match whatever the case, asked")
|
||
}
|
||
// Time: a clock past the bound stops the walk and says so.
|
||
start := time.Now()
|
||
calls := 0
|
||
r.Now = func() time.Time { calls++; return start.Add(time.Duration(calls) * SearchTime / 3) }
|
||
a, err = r.Search(SearchAsk{Text: "needle", Path: dir})
|
||
if err != nil || !strings.HasPrefix(a.Bound, "time") {
|
||
t.Errorf("the time bound: %q %v", a.Bound, err)
|
||
}
|
||
// A fixed string: a pattern's characters are themselves.
|
||
write(t, filepath.Join(deep, "pattern"), "a.c\nabc\n")
|
||
if a, _ := r.Search(SearchAsk{Text: "a.c", Path: filepath.Join(deep, "pattern")}); len(a.Matches) != 1 || a.Matches[0].Line != 1 {
|
||
t.Errorf("a.c as a fixed string: %+v", a.Matches)
|
||
}
|
||
}
|
||
|
||
// The survey issue 418 was asked for: each component of a path with its owner, group, mode, whether it is a
|
||
// link and to what, and whether it is a mount point — the link not followed unless asked.
|
||
func TestStatSaysEachComponent(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
data := filepath.Join(dir, "data")
|
||
write(t, filepath.Join(data, "real/file"), "x")
|
||
if err := os.Chmod(filepath.Join(data, "real"), os.ModeSetgid|0o750); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.Symlink("real", filepath.Join(data, "link")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a, err := r.Stat([]string{filepath.Join(data, "link"), filepath.Join(data, "real/file")}, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
link := a.Paths[0]
|
||
last := link.Components[len(link.Components)-1]
|
||
if link.Components[0].Path != "/" || last.Type != "link" || last.Link != "real" || link.LeadsTo != "" {
|
||
t.Errorf("the link, not followed: %+v", link)
|
||
}
|
||
var dataC, realC Component
|
||
for _, c := range a.Paths[1].Components {
|
||
switch c.Path {
|
||
case data:
|
||
dataC = c
|
||
case filepath.Join(data, "real"):
|
||
realC = c
|
||
}
|
||
}
|
||
if dataC.Mount == nil || dataC.Mount.FS != "nfs4" {
|
||
t.Errorf("the mount point is not said: %+v", dataC)
|
||
}
|
||
if realC.Mode != "2750" || realC.Type != "directory" || realC.UID == nil || realC.Perms == "" {
|
||
t.Errorf("a component's mode: %+v", realC)
|
||
}
|
||
if uid := *realC.UID; uid == 1000 && realC.Owner != "ops" {
|
||
t.Errorf("owner %d named %q", uid, realC.Owner)
|
||
}
|
||
a, err = r.Stat([]string{filepath.Join(data, "link")}, true)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if a.Paths[0].LeadsTo != filepath.Join(data, "real") || a.Paths[0].Leads == nil || a.Paths[0].Leads.Type != "directory" {
|
||
t.Errorf("followed: %+v", a.Paths[0])
|
||
}
|
||
a, err = r.Stat([]string{filepath.Join(dir, "missing/deeper")}, false)
|
||
if err != nil || !strings.Contains(a.Paths[0].Stopped, "does not exist") {
|
||
t.Errorf("a missing path: %+v %v", a, err)
|
||
}
|
||
if _, err := r.Stat([]string{"relative/path"}, false); err == nil {
|
||
t.Error("a relative path was answered")
|
||
}
|
||
many := make([]string, MaxPaths+1)
|
||
for i := range many {
|
||
many[i] = "/"
|
||
}
|
||
if _, err := r.Stat(many, false); err == nil {
|
||
t.Error("more paths than the most were answered")
|
||
}
|
||
}
|
||
|
||
func TestReadlinkSaysTheTargetAndWhereItLeads(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
write(t, filepath.Join(dir, "target"), "")
|
||
if err := os.Symlink("target", filepath.Join(dir, "l1")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.Symlink("l1", filepath.Join(dir, "l2")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.Symlink("nowhere", filepath.Join(dir, "dangling")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a, err := r.Readlink(filepath.Join(dir, "l2"))
|
||
if err != nil || !a.IsLink || a.Link != "l1" || a.LeadsTo != filepath.Join(dir, "target") || !a.Exists {
|
||
t.Errorf("l2: %+v %v", a, err)
|
||
}
|
||
a, err = r.Readlink(filepath.Join(dir, "dangling"))
|
||
if err != nil || !a.IsLink || a.Exists || !strings.Contains(a.Stopped, "does not exist") {
|
||
t.Errorf("dangling: %+v %v", a, err)
|
||
}
|
||
}
|
||
|
||
func TestListSaysEveryEntry(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
write(t, filepath.Join(dir, "d/a"), "abc")
|
||
if err := os.Symlink("a", filepath.Join(dir, "d/b")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a, err := r.List(filepath.Join(dir, "d"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if a.Count != 2 || a.Entries[0].Name != "a" || *a.Entries[0].Size != 3 || a.Entries[1].Type != "link" || a.Entries[1].Link != "a" {
|
||
t.Errorf("list: %+v", a)
|
||
}
|
||
for i := 0; i < MaxEntries+5; i++ {
|
||
write(t, filepath.Join(dir, "many", fmt.Sprintf("e%05d", i)), "")
|
||
}
|
||
a, err = r.List(filepath.Join(dir, "many"))
|
||
if err != nil || len(a.Entries) != MaxEntries || a.Count != MaxEntries+5 || a.Cut == "" {
|
||
t.Errorf("a long directory: %d of %d, cut %q, %v", len(a.Entries), a.Count, a.Cut, err)
|
||
}
|
||
}
|
||
|
||
// getent passwd, getent group and id: an account with every group it is in, a group with its members, never a
|
||
// password field.
|
||
func TestAccountAndGroupLookup(t *testing.T) {
|
||
r, _ := testReader(t)
|
||
a, err := r.Account("ops", "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
u := a.Accounts[0]
|
||
sort.Strings(u.Groups)
|
||
if u.UID != 1000 || u.Group != "ops" || u.Shell != "/usr/bin/zsh" || fmt.Sprint(u.Groups) != "[docker ops wheel]" {
|
||
t.Errorf("ops: %+v", u)
|
||
}
|
||
if a, err := r.Account("961", ""); err != nil || a.Accounts[0].Name != "mesh-files" {
|
||
t.Errorf("by number: %+v %v", a, err)
|
||
}
|
||
g, err := r.Account("", "docker")
|
||
if err != nil || g.Groups[0].GID != 970 || fmt.Sprint(g.Groups[0].Members) != "[ops builder]" {
|
||
t.Errorf("docker: %+v %v", g, err)
|
||
}
|
||
if _, err := r.Account("nobody-here", ""); err == nil {
|
||
t.Error("a missing account was answered")
|
||
}
|
||
all, err := r.Account("", "")
|
||
if err != nil || len(all.Accounts) != 3 || len(all.Groups) != 5 {
|
||
t.Errorf("all: %+v %v", all, err)
|
||
}
|
||
body, _ := json.Marshal(all)
|
||
if strings.Contains(string(body), `"x"`) || strings.Contains(string(body), "password") {
|
||
t.Errorf("a password field is answered: %s", body)
|
||
}
|
||
}
|
||
|
||
// No answer is larger than the bound, whatever a verb made.
|
||
func TestNoAnswerIsLargerThanTheBound(t *testing.T) {
|
||
if _, err := bounded(strings.Repeat("x", MaxAnswer+1), nil); err == nil {
|
||
t.Error("an answer over the bound was given")
|
||
}
|
||
if _, err := bounded("small", nil); err != nil {
|
||
t.Error(err)
|
||
}
|
||
}
|
||
|
||
// The review of #244 (required 1): a link flipped between a harmless file and a secret place, while it is read
|
||
// and searched, never answers the secret place's content — what was opened is judged, not the name.
|
||
func TestALinkFlippedDuringAReadNeverReadsASecretPlace(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
secret := filepath.Join(dir, "var/lib/mesh-files")
|
||
r.Places = []string{secret}
|
||
write(t, filepath.Join(secret, "broker"), "THE-BROKER-CREDENTIAL\n")
|
||
write(t, filepath.Join(dir, "plain"), "THE-PLAIN-WORDS\n")
|
||
link := filepath.Join(dir, "shared/link")
|
||
if err := os.MkdirAll(filepath.Dir(link), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
stop := make(chan struct{})
|
||
done := make(chan struct{})
|
||
go func() {
|
||
defer close(done)
|
||
targets := []string{filepath.Join(dir, "plain"), filepath.Join(secret, "broker")}
|
||
for i := 0; ; i++ {
|
||
select {
|
||
case <-stop:
|
||
return
|
||
default:
|
||
}
|
||
tmp := link + ".new"
|
||
os.Remove(tmp)
|
||
if os.Symlink(targets[i%2], tmp) == nil {
|
||
os.Rename(tmp, link)
|
||
}
|
||
}
|
||
}()
|
||
leaked := 0
|
||
for i := 0; i < 1000; i++ {
|
||
if a, err := r.Read(link, ReadAsk{}); err == nil && strings.Contains(a.Text, "BROKER") {
|
||
leaked++
|
||
}
|
||
if a, err := r.Search(SearchAsk{Text: "BROKER", Path: filepath.Dir(link)}); err == nil && len(a.Matches) > 0 {
|
||
leaked++
|
||
}
|
||
}
|
||
close(stop)
|
||
<-done
|
||
if leaked > 0 {
|
||
t.Errorf("a secret place was answered %d times through a flipped link", leaked)
|
||
}
|
||
}
|
||
|
||
// The review of #244 (required 2): every line of a private key's block is withheld, in a read and in a search,
|
||
// though the file's name is not refused.
|
||
func TestEveryLineOfAKeyBlockIsWithheld(t *testing.T) {
|
||
r, dir := testReader(t)
|
||
key, err := rsa.GenerateKey(crand.Reader, 2048)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
body := string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}))
|
||
p := filepath.Join(dir, "server.pem")
|
||
write(t, p, "# the server's key\n"+body+"after = the block\n")
|
||
a, err := r.Read(p, ReadAsk{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
lines := strings.Count(body, "\n")
|
||
for _, l := range strings.Split(strings.TrimSpace(body), "\n")[1:] {
|
||
if strings.Contains(a.Text, l) && !strings.HasPrefix(l, "-----END") {
|
||
t.Fatalf("a line of the key was answered: %q", l)
|
||
}
|
||
}
|
||
if len(a.Withheld) != lines || !strings.Contains(a.Text, "after = the block") || !strings.Contains(a.Text, "the server's key") {
|
||
t.Errorf("withheld %d of the block's %d lines; text:\n%s", len(a.Withheld), lines, a.Text)
|
||
}
|
||
// A search for a character every base64 line holds answers none of the key.
|
||
m, err := r.Search(SearchAsk{Text: "A", Path: p, Matches: MaxMatches})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, x := range m.Matches {
|
||
if !strings.HasPrefix(x.Text, "‹withheld") {
|
||
t.Errorf("a search answered a line of the key: %q", x.Text)
|
||
}
|
||
}
|
||
}
|
||
|
||
// The review of #244 (required 3 and should): histories, browser profiles, a process's private entries and the
|
||
// cluster credentials are refused by name.
|
||
func TestHistoriesProfilesAndProcessesAreRefusedByName(t *testing.T) {
|
||
var r Refuser
|
||
for _, p := range []string{"/home/ops/.zsh_history", "/home/ops/.bash_history", "/root/.psql_history",
|
||
"/home/ops/.viminfo", "/home/ops/.mozilla/firefox/x.default/logins.json",
|
||
"/home/ops/.config/google-chrome/Default/Cookies", "/home/ops/.config/chromium/Default/Login Data",
|
||
"/proc/self/environ", "/proc/1234/mem", "/proc/self/fd/3", "/proc/thread-self/root/etc/shadow",
|
||
"/etc/rancher/k3s/k3s.yaml", "/etc/kubernetes/admin.conf"} {
|
||
if r.Refused(p) == "" {
|
||
t.Errorf("%s is not refused", p)
|
||
}
|
||
}
|
||
for _, p := range []string{"/proc/mdstat", "/proc/self/status", "/home/ops/.config/i3/config", "/etc/hosts"} {
|
||
if why := r.Refused(p); why != "" {
|
||
t.Errorf("%s is refused: %s", p, why)
|
||
}
|
||
}
|
||
}
|