//go:build linux package provisioner import ( "bytes" "context" "database/sql" "io" "os" "os/exec" "strconv" "testing" "time" "amadan.net/oficina/home/internal/operator" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" "golang.org/x/sys/unix" ) func TestPrivateRequestDescriptorsSubprocess(t *testing.T) { if mode := os.Getenv("OFICINA_DESCRIPTOR_CHILD"); mode != "" { var output *requestOutput var e error if mode == "input-alias" { output, e = privateRequestOutput(3, 4) } else { output, e = privateRequestOutput(3) } if mode != "valid" { if output != nil { output.Close() } if e != ErrRefused { os.Exit(2) } os.Exit(0) } if e != nil { os.Exit(3) } if e = writePrivateRequest(output.file, []byte("synthetic-private-output")); e != nil { os.Exit(4) } output.Close() os.Exit(0) } for _, mode := range []string{"valid", "stdout-alias", "stderr-alias", "stdin-alias", "input-alias", "read-only", "regular", "socket", "named-pipe"} { t.Run(mode, func(t *testing.T) { reader, writer, e := os.Pipe() if e != nil { t.Fatal(e) } defer reader.Close() defer writer.Close() inherited := writer var extra *os.File switch mode { case "read-only": inherited = reader case "regular": extra, e = os.CreateTemp(t.TempDir(), "private") if e != nil { t.Fatal(e) } defer extra.Close() inherited = extra case "socket": pair, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) if err != nil { t.Fatal(err) } extra = os.NewFile(uintptr(pair[0]), "socket") defer extra.Close() defer unix.Close(pair[1]) inherited = extra case "named-pipe": path := t.TempDir() + "/pipe" if e = unix.Mkfifo(path, 0600); e != nil { t.Fatal(e) } holder, err := unix.Open(path, unix.O_RDWR|unix.O_NONBLOCK, 0) if err != nil { t.Fatal(err) } defer unix.Close(holder) fd, err := unix.Open(path, unix.O_WRONLY|unix.O_NONBLOCK, 0) if err != nil { t.Fatal(err) } extra = os.NewFile(uintptr(fd), path) defer extra.Close() inherited = extra } cmd := exec.Command(os.Args[0], "-test.run=^TestPrivateRequestDescriptorsSubprocess$") cmd.Env = append(os.Environ(), "OFICINA_DESCRIPTOR_CHILD="+mode) cmd.ExtraFiles = []*os.File{inherited} if mode == "input-alias" { cmd.ExtraFiles = append(cmd.ExtraFiles, reader) } var public bytes.Buffer cmd.Stdout = &public cmd.Stderr = &public if mode == "stdout-alias" { cmd.Stdout = writer } if mode == "stderr-alias" { cmd.Stderr = writer } if mode == "stdin-alias" { cmd.Stdin = reader } if e = cmd.Start(); e != nil { t.Fatal(e) } writer.Close() private, e := io.ReadAll(reader) if e != nil { t.Fatal(e) } if e = cmd.Wait(); e != nil { t.Fatalf("descriptor %s: %v %s", mode, e, public.Bytes()) } if public.Len() != 0 { t.Fatal("private output reached standard streams") } if mode == "valid" && !bytes.Equal(private, []byte("synthetic-private-output")) { t.Fatal("private pipe output") } if mode != "valid" && len(private) != 0 { t.Fatal("refused output wrote bytes") } }) } } func ownerOperationPlan() OperationPlan { input := operator.OwnerInput{InputBase: operator.InputBase{Version: 1, Operation: "credential.owner-seed", OperationID: testToken(23)}, Directory: planFixture().Directory.Origin, TeamID: 1, TeamSQID: "Uk", Address: "native-prepare-private-83@example.invalid", ExistingOnly: true} raw, _ := json.Marshal(input, json.Deterministic(true)) return OperationPlan{1, testToken(20), "credential", testToken(23), "credential.owner-seed", raw} } func TestNativePreparationPrivateReplayAndFailedOutput(t *testing.T) { if os.Getenv("OFICINA_PREPARE_CHILD") == "1" { args := []string{"provision", "prepare", "--state", os.Getenv("OFICINA_STATE_PATH"), "--floor-dir", os.Getenv("OFICINA_FLOOR_PATH"), "--password-fd", "3", "--plan-fd", "4", "--request-fd", "5"} if e := Run(args, os.Stdout); e != nil { os.Exit(3) } os.Exit(0) } state, floor := persistentDirectory(t), persistentDirectory(t) password := []byte("synthetic-prepare-private-password") if _, e := Initialize(state, floor, password); e != nil { t.Fatal(e) } enrollment, _ := canonicalJSON(planFixture()) if _, e := EnrollPlan(state, floor, password, enrollment); e != nil { t.Fatal(e) } p := ownerOperationPlan() plan, _ := json.Marshal(p, json.Deterministic(true)) run := func(raw []byte, broken bool) ([]byte, []byte, error) { pr, pw, e := os.Pipe() if e != nil { t.Fatal(e) } defer pr.Close() ir, iw, e := os.Pipe() if e != nil { t.Fatal(e) } defer ir.Close() rr, rw, e := os.Pipe() if e != nil { t.Fatal(e) } defer rr.Close() if broken { rr.Close() } cmd := exec.Command(os.Args[0], "-test.run=^TestNativePreparationPrivateReplayAndFailedOutput$") cmd.Env = append(os.Environ(), "OFICINA_PREPARE_CHILD=1", "OFICINA_STATE_PATH="+state, "OFICINA_FLOOR_PATH="+floor) cmd.ExtraFiles = []*os.File{pr, ir, rw} var public, stderr bytes.Buffer cmd.Stdout = &public cmd.Stderr = &stderr if e = cmd.Start(); e != nil { t.Fatal(e) } pr.Close() ir.Close() rw.Close() pw.Write(password) pw.Close() iw.Write(raw) iw.Close() var request []byte if !broken { request, e = io.ReadAll(rr) if e != nil { t.Fatal(e) } } e = cmd.Wait() for _, stream := range [][]byte{public.Bytes(), stderr.Bytes()} { if bytes.Contains(stream, []byte("native-prepare-private-83@example.invalid")) || bytes.Contains(stream, password) || bytes.Contains(stream, []byte("signed_authorization")) { t.Fatal("private request leaked") } } return request, public.Bytes(), e } if _, public, e := run(plan, true); e == nil || len(public) != 0 { t.Fatal("failed private output reported success", e) } j, e := existingJournal(state, floor, password) if e != nil { t.Fatal(e) } before := snapshot(t, j).installations[testToken(20)] saved := append([]byte(nil), before.Operations[testToken(23)].Raw...) if before.Operations[testToken(23)].Status != "prepared" || before.Heads["credential"].Sequence != 0 { t.Fatal("output failure advanced state") } closeExisting(j) request, public, e := run(plan, false) if e != nil || !bytes.Equal(request, saved) { t.Fatal("exact failed-output resume", e) } var info PreparationInfo if json.Unmarshal(public, &info, json.RejectUnknownMembers(true)) != nil || info.State != "local-request" || info.Sequence != 1 { t.Fatal("public preparation status") } again, _, e := run(plan, false) if e != nil || !bytes.Equal(again, request) { t.Fatal("cross-process replay changed bytes", e) } padded := append(append([]byte(nil), plan...), bytes.Repeat([]byte(" "), operator.BodyLimit-len(plan))...) if len(padded) != operator.BodyLimit { t.Fatal("test boundary") } exact, _, e := run(padded, false) if e != nil || !bytes.Equal(exact, request) { t.Fatal("64KiB local plan boundary", e) } if _, public, e = run(append(padded, ' '), false); e == nil || len(public) != 0 { t.Fatal("oversized plan accepted", e) } wrong := p wrong.Role = "directory" raw, _ := json.Marshal(wrong) if _, public, e = run(raw, false); e == nil || len(public) != 0 { t.Fatal("foreign role accepted") } wrong = p wrong.Installation = testToken(99) raw, _ = json.Marshal(wrong) if _, public, e = run(raw, false); e == nil || len(public) != 0 { t.Fatal("foreign installation accepted") } j, e = existingJournal(state, floor, password) if e != nil { t.Fatal(e) } defer closeExisting(j) after := snapshot(t, j).installations[testToken(20)] if len(after.Operations) != 1 || after.Heads["credential"].Sequence != 0 || after.Operations[testToken(23)].Status != "prepared" { t.Fatal("local request implied execution") } } type shortPrivateWriter struct{} func (shortPrivateWriter) Write(p []byte) (int, error) { return len(p) - 1, nil } func TestPrivatePreparationBoundsAndCollectorRefusal(t *testing.T) { for _, size := range []int{0, 1, operator.BodyLimit, operator.BodyLimit + 1} { raw := bytes.Repeat([]byte("x"), size) var out bytes.Buffer e := writePrivateRequest(&out, raw) if size == 0 || size > operator.BodyLimit { if e != ErrRefused || out.Len() != 0 { t.Fatal(size, e) } } else if e != nil || !bytes.Equal(raw, out.Bytes()) { t.Fatal(size, e) } } if e := writePrivateRequest(shortPrivateWriter{}, []byte("private")); e != ErrUnavailable { t.Fatal("short output") } for _, action := range []string{"directory.verify-consumer", "directory.activate-consumer"} { p := ownerOperationPlan() p.Role = "directory" p.Action = action p.Input = jsontext.Value(`{}`) raw, _ := json.Marshal(p) if _, _, e := decodeOperationPlan(raw); e != ErrRefused { t.Fatal("missing collector signed", action) } } for _, fd := range []int{-1, 0, 1, 2, 99999} { if out, e := privateRequestOutput(fd); out != nil || e != ErrRefused { t.Fatal("unsafe descriptor", strconv.Itoa(fd), e) } } p := ownerOperationPlan() input := new(operator.OwnerInput) if json.Unmarshal(p.Input, input) != nil { t.Fatal("fixture") } input.ExistingOnly = false if preparationScope(&installationState{Plan: planFixture()}, input) { t.Fatal("unbacked person creation preparation") } } func TestPreparationClosedPlanRefusesUnknownAndDuplicateFields(t *testing.T) { p := ownerOperationPlan() raw, _ := json.Marshal(p, json.Deterministic(true)) for _, bad := range [][]byte{ append([]byte{}, append(raw, raw...)...), bytes.Replace(raw, []byte(`"version":1`), []byte(`"version":1,"version":1`), 1), bytes.Replace(raw, []byte(`"version":1`), []byte(`"version":1,"unknown":"private@example.invalid"`), 1), } { if _, _, e := decodeOperationPlan(bad); e != ErrRefused { t.Fatal("non-closed plan", e) } } } func TestFullPrivatePipeCancellationKeepsExactRequest(t *testing.T) { j, p := stateFixture(t) op := testToken(23) raw, e := j.prepare(context.Background(), p.Installation, op, "directory.initialize", initialization(op)) if e != nil { t.Fatal(e) } fds := make([]int, 2) if e = unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK); e != nil { t.Fatal(e) } defer unix.Close(fds[0]) output, e := privateRequestOutput(fds[1]) if e != nil { unix.Close(fds[1]) t.Fatal(e) } defer output.Close() fill := bytes.Repeat([]byte("x"), 4096) filled := 0 for { n, err := unix.Write(fds[1], fill) filled += n if err == unix.EAGAIN { break } if err != nil { t.Fatal(err) } } if filled <= 0 { t.Fatal("pipe was not filled") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { done <- output.write(ctx, raw) }() select { case e := <-done: t.Fatal("full pipe did not block", e) case <-time.After(50 * time.Millisecond): } cancel() select { case e := <-done: if e != ErrUnavailable { t.Fatal("cancellation error", e) } case <-time.After(2 * time.Second): t.Fatal("cancellation did not interrupt pipe") } j.close() reopened, e := openJournal(j.dir, j.floor, j.key, false) if e != nil { t.Fatal("process lock retained", e) } defer reopened.close() repeated, e := reopened.prepare(context.Background(), p.Installation, op, "directory.initialize", initialization(op)) if e != nil || !bytes.Equal(raw, repeated) { t.Fatal("interrupted output lost exact request", e) } s := snapshot(t, reopened).installations[p.Installation] if s.Operations[op].Status != "prepared" || s.Heads["directory"].Sequence != 0 { t.Fatal("cancellation implied execution") } } func TestPrepareRequestCancelsFullPipeAndReleasesRegistry(t *testing.T) { state, floor := persistentDirectory(t), persistentDirectory(t) password := []byte("synthetic-full-pipe-password") if _, e := Initialize(state, floor, password); e != nil { t.Fatal(e) } enrollment, _ := canonicalJSON(planFixture()) if _, e := EnrollPlan(state, floor, password, enrollment); e != nil { t.Fatal(e) } p := ownerOperationPlan() plan, _ := json.Marshal(p, json.Deterministic(true)) fds := make([]int, 2) if e := unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK); e != nil { t.Fatal(e) } defer unix.Close(fds[0]) output, e := privateRequestOutput(fds[1]) if e != nil { unix.Close(fds[1]) t.Fatal(e) } defer output.Close() for { _, e = unix.Write(fds[1], bytes.Repeat([]byte("x"), 4096)) if e == unix.EAGAIN { break } if e != nil { t.Fatal(e) } } ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { _, e := prepareRequest(ctx, state, floor, password, plan, output); done <- e }() // Observe the immutable fixture record without joining its writer lock. observer, e := sql.Open("sqlite", "file:"+state+"/registry.db?mode=ro") if e != nil { t.Fatal(e) } defer observer.Close() deadline := time.Now().Add(30 * time.Second) for { var n int e = observer.QueryRow("SELECT count(*) FROM records WHERE operation=? AND state='finalized'", p.Operation).Scan(&n) if e == nil && n == 1 { break } select { case e := <-done: t.Fatal("preparation stopped before output", e) default: } if time.Now().After(deadline) { t.Fatal("fixture request was not persisted") } time.Sleep(10 * time.Millisecond) } select { case e := <-done: t.Fatal("full output did not wait", e) case <-time.After(50 * time.Millisecond): } cancel() select { case e := <-done: if e != ErrUnavailable { t.Fatal("canceled preparation", e) } case <-time.After(2 * time.Second): t.Fatal("prepareRequest ignored cancellation") } observer.Close() j, e := existingJournal(state, floor, password) if e != nil { t.Fatal("registry lock not released", e) } defer closeExisting(j) s := snapshot(t, j).installations[p.Installation] saved := s.Operations[p.Operation] if saved == nil || saved.Status != "prepared" || s.Heads[p.Role].Sequence != 0 { t.Fatal("canceled output changed authority") } _, input, e := decodeOperationPlan(plan) if e != nil { t.Fatal(e) } replay, e := j.prepare(context.Background(), p.Installation, p.Operation, p.Action, input) if e != nil || !bytes.Equal(replay, saved.Raw) { t.Fatal("canceled prepare lost exact retry", e) } }