Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/perholder_test.go
T

292 lines
13 KiB
Go

package main
// A value a provider's software issued for each holder apart (design 57's V2c, issue 749; ADR 0349 clauses 10 and
// 11; ADR 0365 clause 4, where the forge mints one read token per agent account): the vault asks the provider's tool
// naming the holders, keeps each value the tool answers as that holder's own, and hands it, sealed, to that holder
// alone. A provider that answers one value keeps the provision's one credential, as before.
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/seal"
"golang.org/x/crypto/nacl/box"
)
// The two agents' tokens the stand-in forge mints: one per holder, each different.
var agentTokens = map[string]string{
"anchor/claude-code": "gta_anchor-agent-read-token-0001",
"laptop/claude-code": "gta_laptop-agent-read-token-0002",
}
// perHolderProvider is a provider's tool that issues one value per holder (the forge's read token per agent
// account): it answers the vault's account alone, each asked holder's value sealed to the call's key.
func perHolderProvider(tokens map[string]string, asked *[]map[string]any) func(string, map[string]any) (map[string]any, error) {
return func(caller string, args map[string]any) (map[string]any, error) {
if asked != nil {
*asked = append(*asked, args)
}
if !strings.HasSuffix(caller, ".mesh-vault") {
return nil, errors.New("this tool answers the vault 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)
list, _ := args["holders"].([]any)
var out []map[string]any
for _, h := range list {
m, _ := h.(map[string]any)
node, _ := m["node"].(string)
module, _ := m["module"].(string)
value, ok := tokens[node+"/"+module]
if !ok {
continue
}
sealed, err := box.SealAnonymous(nil, []byte(value), &to, rand.Reader)
if err != nil {
return nil, err
}
out = append(out, map[string]any{"node": node, "module": module,
"sealed": base64.StdEncoding.EncodeToString(sealed)})
}
return map[string]any{"holders": out}, nil
}
}
var twoAgents = []Holder{{Node: "anchor", Module: "claude-code"}, {Node: "laptop", Module: "claude-code"}}
// The holders' secrets' names in the vault's store.
const (
anchorsToken = "provision/novox/gitea-read/anchor/claude-code"
laptopsToken = "provision/novox/gitea-read/laptop/claude-code"
)
// bothMayAsk is a stand-in rule that would let both machines' clients ask for both values: the vault's own rule
// for a holder's value is what keeps each to its own.
var bothMayAsk = AllowList{
"provisioner.anchor": {anchorsToken, laptopsToken},
"provisioner.laptop": {anchorsToken, laptopsToken},
"laptop.claude-code": {anchorsToken, laptopsToken},
}
func forgeMesh(provider func(string, map[string]any) (map[string]any, error)) *aMesh {
return &aMesh{tools: map[string]func(string, map[string]any) (map[string]any, error){
"gitea.gitea_agent_token@novox": provider}}
}
// Two holders on two machines: the vault asks the tool naming both, keeps a value for each, and hands each only to
// that holder (its machine's provisioner client, or the module under an account of its own), sealed to its key.
func TestAProviderIssuesAValueForEachHolderAndEachHolderGetsOnlyItsOwn(t *testing.T) {
v := newVault(t, bothMayAsk)
var asked []map[string]any
mesh := forgeMesh(perHolderProvider(agentTokens, &asked))
v.Ask = mesh.ask
got, err := v.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", twoAgents)
if err != nil {
t.Fatal(err)
}
if len(asked) != 1 || fmt.Sprint(asked[0]["holders"]) != "[map[module:claude-code node:anchor] map[module:claude-code node:laptop]]" {
t.Fatalf("the tool was asked %v", asked)
}
if len(got.Holders) != 2 || got.Holders[0].Secret != anchorsToken || got.Holders[1].Secret != laptopsToken ||
got.Holders[0].Generation != 1 || got.Unchanged {
t.Fatalf("answered %+v", got)
}
if _, held, _ := v.Store.Read("provision/novox/gitea-read"); held {
t.Error("a value per holder was also kept as the provision's one credential")
}
// Each holder gets its own, by its machine's client and by itself under an account of its own.
if got := handedTo(t, v, "provisioner.anchor", anchorsToken); got != agentTokens["anchor/claude-code"] {
t.Errorf("anchor got %q", got)
}
if got := handedTo(t, v, "provisioner.laptop", laptopsToken); got != agentTokens["laptop/claude-code"] {
t.Errorf("laptop got %q", got)
}
if got := handedTo(t, v, "laptop.claude-code", laptopsToken); got != agentTokens["laptop/claude-code"] {
t.Errorf("laptop's agent got %q", got)
}
// Neither gets the other's, though the stand-in rule would allow it.
for caller, name := range map[string]string{"provisioner.laptop": anchorsToken, "provisioner.anchor": laptopsToken,
"laptop.claude-code": anchorsToken} {
if _, err := v.Get(caller, name, mustKey(t)); err == nil || !strings.Contains(err.Error(), "alone") {
t.Errorf("%s was handed %s: %v", caller, name, err)
}
}
handed, _ := v.Store.Handed(20)
refused := 0
for _, h := range handed {
if h.Outcome == "refused" {
refused++
}
}
if refused != 3 {
t.Errorf("%d refusals recorded, want 3: %+v", refused, handed)
}
for _, token := range agentTokens {
if strings.Contains(mesh.said(), token) || strings.Contains(everything(t, v, got, handed), token) {
t.Fatal("an issued value crossed the bus, or is in something the vault said or wrote, in the clear")
}
}
rec, _, _ := v.Store.Read(anchorsToken)
if rec.Origin != OriginIssued || rec.ChangedVia != ControllerCaller || rec.Operator.Sealed == "" {
t.Errorf("the record: %+v", rec)
}
// The same values again write nothing; one holder's new value changes that holder alone (ADR 0305 §5).
again, err := v.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", twoAgents)
if err != nil || !again.Unchanged || again.Holders[0].Generation != 1 || again.Holders[1].Generation != 1 {
t.Errorf("again: %+v %v", again, err)
}
renewed := map[string]string{"anchor/claude-code": agentTokens["anchor/claude-code"],
"laptop/claude-code": "gta_laptop-agent-read-token-0003"}
v.Ask = forgeMesh(perHolderProvider(renewed, nil)).ask
moved, err := v.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", twoAgents)
if err != nil || moved.Unchanged || !moved.Holders[0].Unchanged || moved.Holders[1].Unchanged ||
moved.Holders[1].Generation != 2 {
t.Errorf("renewed: %+v %v", moved, err)
}
// A holder's value is issued by the software: never rotated, never accepted.
if _, err := v.Rotate(ControllerCaller, laptopsToken, "jochen", "test"); err == nil {
t.Error("a holder's issued value was rotated")
}
if _, err := v.AcceptOpen(ControllerCaller, laptopsToken, "jochen", ""); err == nil {
t.Error("a holder's issued value was opened for accept")
}
}
// A holder the tool was not asked about, a holder twice, a value not sealed to the call, or an answer of both
// shapes: nothing is kept, for any holder. A holder the tool did not answer is said, not kept.
func TestAPerHolderAnswerIsTakenWholeOrNotAtAll(t *testing.T) {
sealedTo := func(args map[string]any, value string) string {
raw, _ := base64.StdEncoding.DecodeString(args["seal_to"].(string))
var to [32]byte
copy(to[:], raw)
s, _ := box.SealAnonymous(nil, []byte(value), &to, rand.Reader)
return base64.StdEncoding.EncodeToString(s)
}
var stranger [32]byte
_, _ = rand.Read(stranger[:])
notToTheCall, _ := box.SealAnonymous(nil, []byte("forged"), &stranger, rand.Reader)
answers := map[string]func(args map[string]any) map[string]any{
"a holder not asked about": func(args map[string]any) map[string]any {
return map[string]any{"holders": []any{
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, "a")},
map[string]any{"node": "ace", "module": "claude-code", "sealed": sealedTo(args, "b")}}}
},
"a holder twice": func(args map[string]any) map[string]any {
return map[string]any{"holders": []any{
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, "a")},
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, "b")}}}
},
"a value not sealed to the call": func(args map[string]any) map[string]any {
return map[string]any{"holders": []any{
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, "a")},
map[string]any{"node": "laptop", "module": "claude-code",
"sealed": base64.StdEncoding.EncodeToString(notToTheCall)}}}
},
"both shapes": func(args map[string]any) map[string]any {
return map[string]any{"sealed": sealedTo(args, "one"), "holders": []any{
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, "a")}}}
},
"an empty value": func(args map[string]any) map[string]any {
return map[string]any{"holders": []any{
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, " ")}}}
},
}
for what, answer := range answers {
v := newVault(t, nil)
answer := answer
v.Ask = forgeMesh(func(_ string, args map[string]any) (map[string]any, error) { return answer(args), nil }).ask
if _, err := v.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", twoAgents); err == nil {
t.Errorf("%s was taken", what)
}
if all, _ := v.Store.List(); len(all) != 0 {
t.Errorf("%s kept %d values", what, len(all))
}
if len(v.pending) != 0 {
t.Errorf("%s left a call's key kept", what)
}
}
// Values per holder when the controller named no holder: refused, nothing kept.
v := newVault(t, nil)
v.Ask = forgeMesh(func(_ string, args map[string]any) (map[string]any, error) {
return map[string]any{"holders": []any{
map[string]any{"node": "anchor", "module": "claude-code", "sealed": sealedTo(args, "a")}}}, nil
}).ask
if _, err := v.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", nil); err == nil {
t.Error("values per holder were taken for no holder named")
}
// A holder the tool did not answer is said and keeps nothing; the others are kept.
w := newVault(t, nil)
w.Ask = forgeMesh(perHolderProvider(map[string]string{"anchor/claude-code": "only-anchors"}, nil)).ask
got, err := w.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", twoAgents)
if err != nil || len(got.Holders) != 1 || len(got.NotAnswered) != 1 || got.NotAnswered[0] != twoAgents[1] {
t.Errorf("a holder not answered: %+v %v", got, err)
}
if _, held, _ := w.Store.Read(laptopsToken); held {
t.Error("a holder the tool did not answer holds a value")
}
// The holders the controller names are names the mesh writes, each once.
for _, bad := range [][]Holder{{{Node: "anchor", Module: ""}}, {{Node: "../x", Module: "claude-code"}},
{twoAgents[0], twoAgents[0]}} {
if _, err := w.TakeIssuedFor(ControllerCaller, "gitea-read", "novox", "gitea", "gitea_agent_token", bad); err == nil {
t.Errorf("holders %v were asked for", bad)
}
}
}
// A provider that answers one value keeps the provision's one credential, though the controller named holders:
// the per-provision path stays as it is.
func TestAProviderAnsweringOneValueKeepsTheProvisionsOneCredential(t *testing.T) {
v := newVault(t, AllowList{"provisioner.anchor": {"provision/anchor/plex-api"}})
software := "plex-token-from-the-vendor"
v.Ask = (&aMesh{tools: map[string]func(string, map[string]any) (map[string]any, error){
"plex.plex_issued_credential@anchor": standInProvider(&software)}}).ask
got, err := v.TakeIssuedFor(ControllerCaller, "plex-api", "anchor", "plex", "plex_issued_credential",
[]Holder{{Node: "anchor", Module: "kometa"}, {Node: "anchor", Module: "tautulli"}})
if err != nil || got.Secret != "provision/anchor/plex-api" || got.Generation != 1 || len(got.Holders) != 0 {
t.Fatalf("%+v %v", got, err)
}
if handedTo(t, v, "provisioner.anchor", "provision/anchor/plex-api") != software {
t.Error("the provision's one credential is not what the software issued")
}
}
// The seat's verb takes the holders as the controller sends them, and refuses a list it cannot read.
func TestTheIssuedVerbTakesTheHolders(t *testing.T) {
v := newVault(t, nil)
v.Ask = forgeMesh(perHolderProvider(agentTokens, nil)).ask
defs := definitions(&Env{Vault: v.Vault})
issued := toolNamed(t, defs, Seat+".issued")
answer, err := issued.runAs(ControllerCaller, map[string]any{"provision": "gitea-read", "provider": "novox",
"module": "gitea", "tool": "gitea_agent_token", "holders": []any{
map[string]any{"node": "anchor", "module": "claude-code"},
map[string]any{"node": "laptop", "module": "claude-code"}}})
if err != nil || len(answer.(Issued).Holders) != 2 {
t.Fatalf("%+v %v", answer, err)
}
for _, bad := range []any{"anchor", []any{"anchor"}, []any{map[string]any{"node": 1}}} {
if _, err := issued.runAs(ControllerCaller, map[string]any{"provision": "gitea-read", "provider": "novox",
"module": "gitea", "tool": "gitea_agent_token", "holders": bad}); err == nil {
t.Errorf("holders %v were read", bad)
}
}
}
func mustKey(t *testing.T) string {
t.Helper()
key, err := seal.GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
return key.PublicKey
}