bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
// Gate: the pairing's request file is written by two processes (the daemon
|
||||
// files, the person's `backup approve`/`deny` decides), and neither loses
|
||||
// the other's write; an unreadable file is set aside, never wiped; and an
|
||||
// answer frees the room its request took.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func junkSidecar(tag string, i int) string {
|
||||
return "-----BEGIN AGE ENCRYPTED FILE-----\n" + tag + "-" + strconv.Itoa(i) + "\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
|
||||
// TestResealHelperProcess is not a test on its own: the concurrency test
|
||||
// below runs this binary again with these variables set, so each writer is a
|
||||
// separate process, as the daemon and the CLI are.
|
||||
func TestResealHelperProcess(t *testing.T) {
|
||||
dir := os.Getenv("PRIVATAE_RESEAL_HELPER_DIR")
|
||||
if dir == "" {
|
||||
t.Skip("run by TestResealFileKeepsEveryWriteAcrossProcesses")
|
||||
}
|
||||
tag := os.Getenv("PRIVATAE_RESEAL_HELPER_TAG")
|
||||
rounds, _ := strconv.Atoi(os.Getenv("PRIVATAE_RESEAL_HELPER_ROUNDS"))
|
||||
to := mustRecipient(t)
|
||||
for i := 0; i < rounds; i++ {
|
||||
r, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar(tag, i), To: to, Stamp: fixtureStamp})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: file %d: %v", tag, i, err)
|
||||
}
|
||||
if _, err := resealRefuse(dir, fixtureWho, r.ID, tag); err != nil {
|
||||
t.Fatalf("%s: deny %d: %v", tag, i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Several processes each file a request and deny it, over and over, on one
|
||||
// pairing's file. Every request filed is there at the end, and every one is
|
||||
// refused: none was dropped by a stale save, and no answer was reverted to
|
||||
// pending.
|
||||
func TestResealFileKeepsEveryWriteAcrossProcesses(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("starts processes")
|
||||
}
|
||||
const procs, rounds = 4, 15 // 60 answered requests: under resealMaxAnswered, so none is dropped
|
||||
if procs*rounds > resealMaxAnswered {
|
||||
t.Fatal("the test would lose answered requests to the bound, not to a race")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, procs)
|
||||
outs := make([][]byte, procs)
|
||||
for p := 0; p < procs; p++ {
|
||||
wg.Add(1)
|
||||
go func(p int) {
|
||||
defer wg.Done()
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestResealHelperProcess$", "-test.count=1")
|
||||
cmd.Env = append(os.Environ(), "PRIVATAE_RESEAL_HELPER_DIR="+dir,
|
||||
fmt.Sprintf("PRIVATAE_RESEAL_HELPER_TAG=p%d", p), fmt.Sprintf("PRIVATAE_RESEAL_HELPER_ROUNDS=%d", rounds))
|
||||
outs[p], errs[p] = cmd.CombinedOutput()
|
||||
}(p)
|
||||
}
|
||||
wg.Wait()
|
||||
for p := range errs {
|
||||
if errs[p] != nil {
|
||||
t.Fatalf("writer %d: %v\n%s", p, errs[p], outs[p])
|
||||
}
|
||||
}
|
||||
m, err := resealRead(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m) != procs*rounds {
|
||||
t.Fatalf("%d of %d requests are on file: writes were lost", len(m), procs*rounds)
|
||||
}
|
||||
for _, r := range m {
|
||||
if r.State != "refused" || !strings.HasPrefix(r.Error, "p") {
|
||||
t.Fatalf("an answer was lost: %+v", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A request file that does not parse is set aside whole, the error says
|
||||
// where, and the next request starts a new file.
|
||||
func TestResealUnreadableFileIsSetAsideNotWiped(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
junk := []byte(`{"br-1": {"id": "br-1", "state": "pending"` + "\x00 torn")
|
||||
if err := os.WriteFile(resealPath(dir), junk, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
to := mustRecipient(t)
|
||||
_, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar("a", 0), To: to, Stamp: fixtureStamp})
|
||||
if err == nil || !strings.Contains(err.Error(), "kept as") {
|
||||
t.Fatalf("an unreadable request file was not reported: %v", err)
|
||||
}
|
||||
aside, _ := filepath.Glob(resealPath(dir) + resealAsideSuffix + "*")
|
||||
if len(aside) != 1 {
|
||||
t.Fatalf("the unreadable file was not set aside: %v", aside)
|
||||
}
|
||||
if got, _ := os.ReadFile(aside[0]); string(got) != string(junk) {
|
||||
t.Fatal("the file set aside is not the file that was there")
|
||||
}
|
||||
if !strings.Contains(err.Error(), aside[0]) {
|
||||
t.Errorf("the error does not say where the file is: %v", err)
|
||||
}
|
||||
r, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar("a", 0), To: to, Stamp: fixtureStamp})
|
||||
if err != nil || r.State != "pending" {
|
||||
t.Fatalf("a new file was not started: %v", err)
|
||||
}
|
||||
// "null" is an empty record, not a crash.
|
||||
_ = os.WriteFile(resealPath(dir), []byte("null"), 0o600)
|
||||
if _, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar("a", 1), To: to, Stamp: fixtureStamp}); err != nil {
|
||||
t.Fatalf("a null record: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The bound is on requests waiting for an answer: answering one, either
|
||||
// way, frees its room at once, and an answered request no longer holds the
|
||||
// sidecar it came with.
|
||||
func TestResealAnswerFreesRoom(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
to := mustRecipient(t)
|
||||
var ids []string
|
||||
for i := 0; i < resealMaxRequests; i++ {
|
||||
r, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar("q", i), To: to, Stamp: fixtureStamp})
|
||||
if err != nil {
|
||||
t.Fatalf("request %d: %v", i, err)
|
||||
}
|
||||
ids = append(ids, r.ID)
|
||||
}
|
||||
next := resealReq{Sidecar: junkSidecar("q", 100), To: to, Stamp: fixtureStamp}
|
||||
if _, err := resealFile(dir, fixtureWho, next); err == nil {
|
||||
t.Fatal("a request past the bound was filed")
|
||||
}
|
||||
if _, err := resealRefuse(dir, fixtureWho, ids[0], ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := resealFile(dir, fixtureWho, next); err != nil {
|
||||
t.Fatalf("a deny did not free room: %v", err)
|
||||
}
|
||||
// An approval frees room too, whatever its outcome (this sidecar does not
|
||||
// open, so it fails).
|
||||
withBackupKeysDir(t)
|
||||
newBackupFixture(t, "").installAsBastionKey(t)
|
||||
if _, err := resealDecide(dir, fixtureWho, ids[1], true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar("q", 101), To: to, Stamp: fixtureStamp}); err != nil {
|
||||
t.Fatalf("an approval did not free room: %v", err)
|
||||
}
|
||||
m, _ := resealRead(dir)
|
||||
for _, id := range ids[:2] {
|
||||
if m[id].State == "pending" || m[id].Sidecar != "" {
|
||||
t.Fatalf("an answered request still holds its sidecar: %+v", m[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Answered requests are kept for the host to read, at most resealMaxAnswered
|
||||
// of them, the oldest answered going first.
|
||||
func TestResealAnsweredRequestsAreBounded(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
to := mustRecipient(t)
|
||||
var first string
|
||||
for i := 0; i < resealMaxAnswered+5; i++ {
|
||||
r, err := resealFile(dir, fixtureWho, resealReq{Sidecar: junkSidecar("z", i), To: to, Stamp: fixtureStamp})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if i == 0 {
|
||||
first = r.ID
|
||||
}
|
||||
if _, err := resealRefuse(dir, fixtureWho, r.ID, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
m, _ := resealRead(dir)
|
||||
if len(m) != resealMaxAnswered {
|
||||
t.Fatalf("%d answered requests kept, want %d", len(m), resealMaxAnswered)
|
||||
}
|
||||
if _, ok := m[first]; ok {
|
||||
t.Fatal("the oldest answered request was kept and a newer one dropped")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user