diff --git a/modules/mesh-vault/cmd/mesh-vault/intake_test.go b/modules/mesh-vault/cmd/mesh-vault/intake_test.go new file mode 100644 index 00000000..80bb2ddf --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/intake_test.go @@ -0,0 +1,495 @@ +package main + +// ADR 0349's tests for what the vault takes in (design 57's V2, issue 675): the vault's `secret-ask` test, the +// issuing provider's test with a stand-in provider (plex's own tool moves with the media catalogue, after +// release 57), an accepted value opened in the vault alone and never remade, and rotate. +// +// In each, "the controller" is a test function that holds no private key: it only carries what the desk or the +// provider sealed to the key the vault made for the call, as the controller does on the bus. + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/seal" + "golang.org/x/crypto/nacl/box" +) + +// sealToCall seals value to a call's key as the desk's prompt and a provider's tool do: the NaCl anonymous box. +func sealToCall(t *testing.T, sealTo, value string) string { + t.Helper() + raw, err := base64.StdEncoding.DecodeString(sealTo) + if err != nil || len(raw) != 32 { + t.Fatalf("seal_to %q is not a key", sealTo) + } + var key [32]byte + copy(key[:], raw) + sealed, err := box.SealAnonymous(nil, []byte(value), &key, rand.Reader) + if err != nil { + t.Fatal(err) + } + return base64.StdEncoding.EncodeToString(sealed) +} + +// handedTo opens what `get` hands an allowed asker: the value the vault holds. +func handedTo(t *testing.T, v testVault, asker, name string) string { + t.Helper() + key, _ := seal.GenerateKeyPair() + a, err := v.Get(asker, name, key.PublicKey) + if err != nil { + t.Fatal(err) + } + value, err := seal.Open(a.Sealed, key.PrivateKey) + if err != nil { + t.Fatal(err) + } + return value +} + +// everything is all the vault said and wrote during a test, as text. +func everything(t *testing.T, v testVault, more ...any) string { + t.Helper() + var b strings.Builder + for _, said := range v.bus.raw { + b.WriteString(said) + } + for _, content := range filesUnder(t, v.dir) { + b.WriteString(content) + } + for _, m := range more { + raw, _ := json.Marshal(m) + b.Write(raw) + } + return b.String() +} + +const typedAtTheDesk = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g" + +func telegramAsk(by string) AskOf { + return AskOf{Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: by} +} + +// ---- the vault's secret-ask test (ADR 0349 "How it is checked") --------------------------------------------- + +// An ask is recorded in the vault's store before the prompt opens; its prompt names the asker as the bus named +// it; the typed value opens only in the vault; the controller carries ciphertext only. +func TestAnAskIsRecordedBeforeThePromptAndTheTypedValueOpensOnlyInTheVault(t *testing.T) { + v := newVault(t, AllowList{"anchor.node-provisioner": {"own/anchor/telegram/telegram-token"}}) + opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14/claude-code, via the console")) + if err != nil { + t.Fatal(err) + } + // Recorded before any prompt opens: the record holds the ask now, with no end. + asks, _ := v.Store.Asks(10) + if len(asks) != 1 || asks[0].Event != AskOpened || asks[0].ID != opened.Call || asks[0].AskedBy != "g14/claude-code" || + asks[0].Caller != ControllerCaller || asks[0].Desk != "laptop" || asks[0].Secret != "own/anchor/telegram/telegram-token" { + t.Fatalf("the record before the prompt: %+v", asks) + } + // The prompt names the asker as the bus named it, cut to a name: what the vault answers is what it recorded. + if opened.AskedBy != "g14/claude-code" { + t.Errorf("the prompt would name %q", opened.AskedBy) + } + // The desk seals what is typed to the call's key; the controller passes it on. + changed, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, typedAtTheDesk+"\n"), "") + if err != nil { + t.Fatal(err) + } + if changed.Generation != 1 || changed.By != "g14/claude-code" { + t.Errorf("kept as %+v", changed) + } + if got := handedTo(t, v, "anchor.node-provisioner", "own/anchor/telegram/telegram-token"); got != typedAtTheDesk { + t.Errorf("the vault holds %q, not what was typed with its line ending taken off", got) + } + rec, _, _ := v.Store.Read("own/anchor/telegram/telegram-token") + if rec.Origin != OriginTyped || rec.ChangedBy != "g14/claude-code" || rec.ChangedVia != ControllerCaller { + t.Errorf("the record says origin %q, by %q, via %q", rec.Origin, rec.ChangedBy, rec.ChangedVia) + } + asks, _ = v.Store.Asks(10) + if len(asks) != 2 || asks[0].Event != AskEnded || asks[0].Outcome != "given" || asks[0].ID != opened.Call { + t.Errorf("how it ended: %+v", asks) + } + if strings.Contains(everything(t, v, opened, changed, asks), typedAtTheDesk) { + t.Fatal("the typed value is in something the vault said, answered or wrote in the clear") + } +} + +// One open ask per secret, and three an hour (ADR 0277 §24), read from the vault's own record. +func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) { + v := newVault(t, nil) + first, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + if err != nil { + t.Fatal(err) + } + if _, err := v.AskOpen(ControllerCaller, telegramAsk("ace.claude-code")); err == nil || + !strings.Contains(err.Error(), "still open (asked by g14.claude-code)") { + t.Fatalf("a second ask while one is open: %v", err) + } + // Another secret is not held back by it. + other := telegramAsk("ace.claude-code") + other.Name = "webhook-secret" + if _, err := v.AskOpen(ControllerCaller, other); err != nil { + t.Fatalf("another secret's ask was held back: %v", err) + } + if _, err := v.AskEnd(ControllerCaller, first.Call, "", "dismissed"); err != nil { + t.Fatal(err) + } + for i := 0; i < 2; i++ { + o, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + if err != nil { + t.Fatalf("ask %d of the hour: %v", i+2, err) + } + if _, err := v.AskEnd(ControllerCaller, o.Call, "", "timed-out"); err != nil { + t.Fatal(err) + } + } + if _, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err == nil || + !strings.Contains(err.Error(), "3 times in the last hour") { + t.Fatalf("a fourth ask in the hour: %v", err) + } + asks, _ := v.Store.Asks(50) + outcomes := map[string]int{} + for _, a := range asks { + outcomes[a.Outcome]++ + } + if outcomes["dismissed"] != 1 || outcomes["timed-out"] != 2 { + t.Errorf("the record's outcomes: %v", outcomes) + } + // An ask left open counts as open for SecretAskOpenFor only: a prompt that died does not hold the secret. + w := newVault(t, nil) + clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC) + w.Now = func() time.Time { return clock } + if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil { + t.Fatal(err) + } + clock = clock.Add(SecretAskOpenFor + time.Second) + if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil { + t.Errorf("an ask left open past its time still held the secret: %v", err) + } +} + +// Only the controller reaches a desk's prompt, so only it opens and ends an ask; a value not sealed to the +// call's key, or a call that waited too long, keeps nothing. +func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) { + v := newVault(t, nil) + for _, caller := range []string{"g14.node-tools", "unproven", "", "person.jochen"} { + if _, err := v.AskOpen(caller, telegramAsk("x")); err == nil { + t.Errorf("%q opened an ask", caller) + } + } + if asks, _ := v.Store.Asks(10); len(asks) != 0 { + t.Fatalf("a refused caller filled the record: %+v", asks) + } + opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + if err != nil { + t.Fatal(err) + } + if _, err := v.AskEnd("g14.node-tools", opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil { + t.Error("another caller ended the ask") + } + other, _, _ := box.GenerateKey(rand.Reader) + if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(other[:]), "x"), ""); err == nil || + !strings.Contains(err.Error(), "not sealed to this call") { + t.Errorf("a value sealed to another key: %v", err) + } + if _, held, _ := v.Store.Read("own/anchor/telegram/telegram-token"); held { + t.Error("a value not sealed to the call was kept") + } + // The call's key is forgotten once used: the same call cannot be answered twice. + if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil { + t.Error("a call was answered twice") + } + asks, _ := v.Store.Asks(1) + if asks[0].Outcome != "failed" { + t.Errorf("the ask that failed ended %+v", asks[0]) + } + // A call that waited longer than its time is closed. + clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC) + v.Now = func() time.Time { return clock } + late, err := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex") + if err != nil { + t.Fatal(err) + } + clock = clock.Add(CallOpenFor + time.Second) + if _, err := v.IssuedTake(ControllerCaller, late.Call, sealToCall(t, late.SealTo, "x")); err == nil { + t.Error("a call that waited too long was taken") + } + // An empty answer gives nothing and says so. + clock = clock.Add(time.Hour) + empty, _ := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + if _, err := v.AskEnd(ControllerCaller, empty.Call, sealToCall(t, empty.SealTo, " \n"), ""); err == nil { + t.Error("an empty answer was kept") + } + if asks, _ := v.Store.Asks(1); asks[0].Outcome != "empty" { + t.Errorf("an empty answer ended %+v", asks[0]) + } +} + +// ---- the issuing provider's test, with a stand-in provider ------------------------------------------------- + +// standInProvider is a provider's tool for an `issued-by: software` offer, as plex's must answer (ADR 0305 §2 +// with the vault in the controller's place): it refuses every caller but the controller — the one caller the +// bus lets ask it, carrying the vault's call — and answers only the software's value sealed to the call's key. +func standInProvider(software *string) func(caller string, args map[string]any) (map[string]any, error) { + return func(caller string, args map[string]any) (map[string]any, error) { + if caller != ControllerCaller { + return nil, errors.New("this tool answers the controller alone") + } + key, _ := args["seal_to"].(string) + raw, err := base64.StdEncoding.DecodeString(key) + if err != nil || len(raw) != 32 { + return nil, errors.New("seal_to is not a sealing key") + } + var to [32]byte + copy(to[:], raw) + sealed, err := box.SealAnonymous(nil, []byte(*software), &to, rand.Reader) + if err != nil { + return nil, err + } + return map[string]any{"sealed": base64.StdEncoding.EncodeToString(sealed)}, nil + } +} + +// issue is the controller's `secret issued` for one provider, carried as the controller carries it: open the +// vault's call, ask the provider's tool with the call's key, pass on what it sealed. +func issue(t *testing.T, v testVault, provider func(string, map[string]any) (map[string]any, error)) (Changed, error) { + t.Helper() + opened, err := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex") + if err != nil { + return Changed{}, err + } + answer, err := provider(ControllerCaller, map[string]any{"provision": "plex-api", "seal_to": opened.SealTo}) + if err != nil { + return Changed{}, err + } + return v.IssuedTake(ControllerCaller, opened.Call, answer["sealed"].(string)) +} + +func TestAnIssuingProviderAnswersOnlyTheControllersCallSealedToTheVault(t *testing.T) { + v := newVault(t, AllowList{"anchor.node-provisioner": {"provision/anchor/plex-api"}}) + software := "plex-token-from-the-vendor" + provider := standInProvider(&software) + + // The provider's tool refuses every caller but the controller, whatever it asks. + for _, caller := range []string{"anchor.node-tools", "g14.claude-code", "unproven", ""} { + if _, err := provider(caller, map[string]any{"provision": "plex-api", "seal_to": "x"}); err == nil { + t.Errorf("the provider's tool answered %q", caller) + } + } + // The vault takes the steps from the controller alone. + if _, err := v.IssuedOpen("anchor.node-tools", "plex-api", "anchor", "plex"); err == nil { + t.Error("the vault opened an issued call for another caller") + } + + changed, err := issue(t, v, provider) + if err != nil { + t.Fatal(err) + } + if changed.Name != "provision/anchor/plex-api" || changed.Generation != 1 || changed.By != "plex on anchor" { + t.Errorf("kept as %+v", changed) + } + if got := handedTo(t, v, "anchor.node-provisioner", "provision/anchor/plex-api"); got != software { + t.Errorf("the vault holds %q", got) + } + rec, _, _ := v.Store.Read("provision/anchor/plex-api") + if rec.Origin != OriginIssued || rec.ChangedVia != ControllerCaller { + t.Errorf("the record: origin %q via %q", rec.Origin, rec.ChangedVia) + } + + // Nothing rewritten for a value every holder has (ADR 0305 §5): no new generation, no write, nothing said. + path := filepath.Join(v.dir, "store", "secrets", "provision%2Fanchor%2Fplex-api.json") + before, _ := os.ReadFile(path) + said := len(v.bus.raw) + again, err := issue(t, v, provider) + if err != nil { + t.Fatal(err) + } + after, _ := os.ReadFile(path) + if !again.Unchanged || again.Generation != 1 || string(before) != string(after) || len(v.bus.raw) != said { + t.Errorf("the same value again rewrote something: %+v", again) + } + + // The software changes its value: the next ask keeps it as a new generation, and says so without it. + software = "plex-token-renewed" + renewed, err := issue(t, v, provider) + if err != nil || renewed.Generation != 2 { + t.Fatalf("%+v %v", renewed, err) + } + if got := handedTo(t, v, "anchor.node-provisioner", "provision/anchor/plex-api"); got != software { + t.Errorf("after the change the vault holds %q", got) + } + + // A value sealed to any other key is refused, and nothing changes. + opened, _ := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex") + stranger, _, _ := box.GenerateKey(rand.Reader) + if _, err := v.IssuedTake(ControllerCaller, opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(stranger[:]), "forged")); err == nil { + t.Error("a value not sealed to the call was taken") + } + + // The software rotates it, not the mesh (ADR 0305 §9); and no other path puts it into the vault (§10). + if _, err := v.Rotate(ControllerCaller, "provision/anchor/plex-api", "person.jochen"); err == nil || + !strings.Contains(err.Error(), "software") { + t.Errorf("rotate of an issued value: %v", err) + } + if _, err := v.AcceptOpen(ControllerCaller, "provision/anchor/plex-api", "person.jochen"); err == nil { + t.Error("an issued value was taken by accept") + } + if strings.Contains(everything(t, v), "plex-token") { + t.Fatal("the issued value is in something the vault said or wrote in the clear") + } +} + +// ---- accept and rotate ------------------------------------------------------------------------------------- + +// A value given at the controller's terminal is sealed there to the vault's call and opened here alone; an +// accepted provision's credential is never remade; a module's own given value rotates as a made one (ADR 0228). +func TestAnAcceptedValueOpensInTheVaultAloneAndIsNotRemade(t *testing.T) { + v := newVault(t, AllowList{"g14.node-provisioner": {"provision/novox/postgres", "own/g14/dunst/token"}}) + if _, err := v.AcceptOpen("g14.claude-code", "provision/novox/postgres", ""); err == nil { + t.Error("accept was opened for a caller that is not the controller") + } + for name, value := range map[string]string{"provision/novox/postgres": "pw-of-last-year", "own/g14/dunst/token": "tok"} { + opened, err := v.AcceptOpen(ControllerCaller, name, "jochen") + if err != nil { + t.Fatal(err) + } + changed, err := v.AcceptTake(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, value)) + if err != nil { + t.Fatal(err) + } + if changed.By != "jochen" { + t.Errorf("%s accepted by %q", name, changed.By) + } + if got := handedTo(t, v, "g14.node-provisioner", name); got != value { + t.Errorf("%s holds %q", name, got) + } + rec, _, _ := v.Store.Read(name) + if rec.Origin != OriginAccepted || rec.ChangedVia != ControllerCaller { + t.Errorf("%s: origin %q via %q", name, rec.Origin, rec.ChangedVia) + } + } + if _, err := v.Rotate(ControllerCaller, "provision/novox/postgres", "jochen"); err == nil || + !strings.Contains(err.Error(), "not remade") { + t.Errorf("rotate of an accepted credential: %v", err) + } + rotated, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen") + if err != nil || rotated.Generation != 2 { + t.Fatalf("rotate of a module's own given value: %+v %v", rotated, err) + } + if got := handedTo(t, v, "g14.node-provisioner", "own/g14/dunst/token"); got == "tok" || len(got) != 40 { + t.Errorf("rotated to %q", got) + } + if strings.Contains(everything(t, v), "pw-of-last-year") { + t.Fatal("an accepted value is in something the vault said or wrote in the clear") + } +} + +// Rotate: refused for a secret not held and for an unproven caller; who asked is the proven caller, or the +// person the controller names when it carries the act — both kept. +func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) { + v := newVault(t, nil) + if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen"); err == nil { + t.Error("a secret not held was rotated") + } + if _, err := v.Mint("own/g14/dunst/token", "the test"); err != nil { + t.Fatal(err) + } + if _, err := v.Rotate("unproven", "own/g14/dunst/token", ""); err == nil { + t.Error("an unproven caller rotated") + } + if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen"); err != nil { + t.Fatal(err) + } + rec, _, _ := v.Store.Read("own/g14/dunst/token") + if rec.ChangedBy != "jochen" || rec.ChangedVia != ControllerCaller || rec.Generation != 2 { + t.Errorf("by %q via %q generation %d", rec.ChangedBy, rec.ChangedVia, rec.Generation) + } + // A `by` from any caller but the controller names nobody: the proven caller is who asked. + if _, err := v.Rotate("g14.claude-code", "own/g14/dunst/token", "jochen"); err != nil { + t.Fatal(err) + } + rec, _, _ = v.Store.Read("own/g14/dunst/token") + if rec.ChangedBy != "g14.claude-code" || rec.ChangedVia != "" { + t.Errorf("by %q via %q", rec.ChangedBy, rec.ChangedVia) + } +} + +// The names the vault keeps them under are names, never paths. +func TestTheNamesOfWhatIsTakenIn(t *testing.T) { + if n, err := OwnSecret("g14", "dunst", "token"); err != nil || n != "own/g14/dunst/token" { + t.Errorf("%q %v", n, err) + } + if n, err := ProvisionSecret("novox", "postgres"); err != nil || n != "provision/novox/postgres" { + t.Errorf("%q %v", n, err) + } + for _, bad := range [][3]string{{"..", "x", "y"}, {"g14", "a/b", "y"}, {"g14", "x", ""}, {"G14", "x", "y"}} { + if _, err := OwnSecret(bad[0], bad[1], bad[2]); err == nil { + t.Errorf("%v was taken", bad) + } + } +} + +// ---- the verbs as the runtime calls them ------------------------------------------------------------------ + +// Each verb takes its caller from the runtime, never from its arguments, and names its secret one way only. +func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) { + v := newVault(t, nil) + env := &Env{Vault: v.Vault} + defs := definitions(env) + ask := toolNamed(t, defs, Seat+".secret-ask") + args := map[string]any{"node": "anchor", "module": "telegram", "name": "telegram-token", "desk": "laptop", + "asked_by": "g14.claude-code", "caller": ControllerCaller} + if _, err := ask.Run(args); err == nil { + t.Error("an ask opened for a caller named in the arguments") + } + answer, err := ask.runAs(ControllerCaller, args) + if err != nil { + t.Fatal(err) + } + opened := answer.(Opened) + if _, err := ask.runAs(ControllerCaller, map[string]any{"call": opened.Call, "outcome": "dismissed"}); err != nil { + t.Fatal(err) + } + listed, err := toolNamed(t, defs, "secret_asks").Run(map[string]any{}) + if err != nil || len(listed.([]AskRecord)) != 2 { + t.Fatalf("secret_asks: %v %v", listed, err) + } + + accept := toolNamed(t, defs, Seat+".accept") + if _, err := accept.runAs(ControllerCaller, map[string]any{"node": "g14", "provision": "postgres"}); err == nil { + t.Error("accept took a name both ways") + } + answer, err = accept.runAs(ControllerCaller, map[string]any{"provider": "novox", "provision": "postgres", "by": "jochen"}) + if err != nil { + t.Fatal(err) + } + o := answer.(Opened) + if _, err := accept.runAs(ControllerCaller, map[string]any{"call": o.Call, "sealed": sealToCall(t, o.SealTo, "pw")}); err != nil { + t.Fatal(err) + } + rotate := toolNamed(t, defs, Seat+".rotate") + if _, err := rotate.runAs("g14.claude-code", map[string]any{"node": "g14", "module": "dunst", "name": "token"}); err == nil { + t.Error("a secret not held was rotated through the verb") + } + + issued := toolNamed(t, defs, Seat+".issued") + answer, err = issued.runAs(ControllerCaller, map[string]any{"provision": "plex-api", "provider": "anchor", "module": "plex"}) + if err != nil { + t.Fatal(err) + } + o = answer.(Opened) + if _, err := issued.runAs(ControllerCaller, map[string]any{"call": o.Call, "sealed": sealToCall(t, o.SealTo, "tok")}); err != nil { + t.Fatal(err) + } + broken := &Env{Broken: errors.New("no store")} + if _, err := toolNamed(t, definitions(broken), Seat+".rotate").runAs(ControllerCaller, map[string]any{}); err == nil || + err.Error() != "no store" { + t.Errorf("a broken vault said %v", err) + } +}