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

333 lines
13 KiB
Go

package main
// The tools the TypeScript vault offered, answering as they did: secret_export, secret_holders and
// secret_verify — none returns a value. And the seat's verb `get`, reached the way the runtime calls it.
import (
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seal"
)
func toolNamed(t *testing.T, tools []toolDef, name string) toolDef {
t.Helper()
for _, tl := range tools {
if tl.Name == name {
return tl
}
}
t.Fatalf("no tool %s", name)
return toolDef{}
}
func TestTheManifestNamesWhatTheBundleServes(t *testing.T) {
raw, _ := os.ReadFile("../../module.json")
var m struct {
Tools []string `json:"tools"`
Claims []struct {
Name string `json:"name"`
Serves []string `json:"serves"`
} `json:"claims"`
Emits []string `json:"emits"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
served := map[string]bool{}
for _, tl := range definitions(&Env{}) {
served[tl.Name] = true
}
for _, name := range m.Tools {
if !served[name] {
t.Errorf("the manifest names the tool %s, which the bundle does not serve", name)
}
}
for _, c := range m.Claims {
for _, verb := range c.Serves {
if !served[c.Name+"."+verb] {
t.Errorf("the claim of %s serves %s, which the bundle does not", c.Name, verb)
}
}
}
for name := range served {
if !strings.HasPrefix(name, Seat+".") && !contains(m.Tools, name) {
t.Errorf("the bundle serves %s, which the manifest does not name", name)
}
}
if !contains(m.Emits, ChangedEvent) {
t.Errorf("the manifest does not say the vault emits %s", ChangedEvent)
}
}
func TestSecretExportAnswersTheMeshsExport(t *testing.T) {
root := t.TempDir()
env := &Env{Root: root}
export := toolNamed(t, definitions(env), "secret_export")
got, _ := export.Run(map[string]any{})
if got.(map[string]any)["available"] != false {
t.Errorf("no export said %v", got)
}
doc := `{"export":1,"operator-key":"b3A=","fingerprint":"sha256:f","kept":[{"node":"g14","module":"dunst","name":"token","kind":"own","origin":"made","sealed":"c2VhbGVk","key":"b3A=","made-at":"2026-10-01T00:00:00Z"}],"unrecoverable":[{"node":"ace","module":"x","name":"y"}]}`
_ = os.WriteFile(filepath.Join(root, "export.json"), []byte(doc), 0o600)
got, _ = export.Run(map[string]any{})
a := got.(map[string]any)
if a["available"] != true || a["count"] != 1 || a["operator-key"] != "b3A=" {
t.Errorf("the export said %v", a)
}
got, _ = export.Run(map[string]any{"sealed": false})
kept := got.(map[string]any)["kept"].([]map[string]any)
if _, ok := kept[0]["sealed"]; ok {
t.Error("sealed=false still returned the blobs")
}
}
func TestSecretHoldersAndVerifySayFingerprintsOnly(t *testing.T) {
dir := t.TempDir()
l, _ := NewLedger(filepath.Join(dir, "ledger"))
secret := filepath.Join(dir, "s")
_ = os.WriteFile(secret, []byte("the-value"), 0o600)
receives := filepath.Join(dir, "mesh.json")
doc, _ := json.Marshal(map[string]any{"given": []map[string]string{{"from": "redis", "node": "anchor", "as": "anchor-redis", "secret": secret}}})
_ = os.WriteFile(receives, doc, 0o600)
_, _, _ = l.Record("anchor-redis", "anchor", "the-value", time.Now())
env := &Env{Ledger: l, Receives: receives}
holders, _ := toolNamed(t, definitions(env), "secret_holders").Run(map[string]any{})
verify, _ := toolNamed(t, definitions(env), "secret_verify").Run(map[string]any{"as": "anchor-redis", "fingerprint": Fingerprint("the-value")})
unknown, _ := toolNamed(t, definitions(env), "secret_verify").Run(map[string]any{"as": "nobody"})
for _, answer := range []any{holders, verify, unknown} {
raw, _ := json.Marshal(answer)
if strings.Contains(string(raw), "the-value") {
t.Errorf("a tool returned the value: %s", raw)
}
}
v := verify.(map[string]any)
if v["ok"] != true || v["deliveredMatchesRecorded"] != true || v["givenMatchesRecorded"] != true {
t.Errorf("verify said %v", v)
}
h := holders.(map[string]any)
if h["count"] != 1 {
t.Errorf("holders said %v", h)
}
if unknown.(map[string]any)["known"] != false {
t.Errorf("an unknown login said %v", unknown)
}
}
// The seat's verb, called as the runtime calls it: the caller is the one the runtime proved, never an
// argument.
func TestTheGetVerbTakesItsCallerFromTheRuntime(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
get := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".get")
// No runtime proved a caller here, so the call is unproven whatever the arguments say.
if _, err := get.Run(map[string]any{"name": "pair/postgres/gitea", "public_key": asker.PublicKey, "caller": "g14.node-provisioner"}); err == nil {
t.Error("get answered a caller named in its arguments")
}
answer, err := get.runAs("g14.node-provisioner", map[string]any{"name": "pair/postgres/gitea", "public_key": asker.PublicKey})
if err != nil {
t.Fatal(err)
}
a := answer.(Answer)
if got, err := seal.Open(a.Sealed, asker.PrivateKey); err != nil || got != "x" {
t.Errorf("%q %v", got, err)
}
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
func TestSecretOwnListsFingerprintsOfOwnSecretsOnly(t *testing.T) {
v := newVault(t, AllowList{})
acc := &fakeAccounts{}
v.Accounts = acc
for name, value := range map[string]string{"own/ace/mesh-build-go/broker": "pw-one", "own/ace/mesh-build-go/token": "tok-secret",
"own/g14/dunst/broker": `{"user":"g14.dunst","password":"pw-two"}`, "pair/postgres/gitea": "pair-value"} {
if _, err := v.Put(name, value, "the test"); err != nil {
t.Fatal(err)
}
}
if _, err := v.Put("own/ace/mesh-build-go/broker", "pw-one-b", "the test"); err != nil {
t.Fatal(err)
}
own := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_own")
for _, c := range []struct {
args map[string]any
names []string
}{
{map[string]any{}, []string{"own/ace/mesh-build-go/broker", "own/ace/mesh-build-go/token", "own/g14/dunst/broker"}},
{map[string]any{"node": "ace"}, []string{"own/ace/mesh-build-go/broker", "own/ace/mesh-build-go/token"}},
{map[string]any{"module": "dunst"}, []string{"own/g14/dunst/broker"}},
{map[string]any{"name": "bro"}, []string{"own/ace/mesh-build-go/broker", "own/g14/dunst/broker"}},
{map[string]any{"node": "ace", "name": "broker"}, []string{"own/ace/mesh-build-go/broker"}},
{map[string]any{"node": "nowhere"}, nil},
} {
got, err := own.Run(c.args)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
for _, value := range []string{"pw-one", "pw-two", "tok-secret", "pair-value"} {
if strings.Contains(string(raw), value) {
t.Errorf("%v returned a value: %s", c.args, raw)
}
}
rows := got.(map[string]any)["secrets"].([]map[string]any)
if len(rows) != len(c.names) || got.(map[string]any)["cut"] != false {
t.Fatalf("%v said %s", c.args, raw)
}
for i, n := range c.names {
if rows[i]["name"] != n {
t.Errorf("%v row %d is %v, want %s", c.args, i, rows[i]["name"], n)
}
}
if len(rows) > 0 && rows[0]["name"] == "own/ace/mesh-build-go/broker" {
if rows[0]["generation"] != 2 || rows[0]["fingerprint"] != Fingerprint("pw-one-b") || rows[0]["busAccount"] != true ||
rows[0]["changedBy"] != "the test" || rows[0]["hashed"] != false || rows[0]["hashWhy"] == nil {
t.Errorf("%v: broker row said %v", c.args, rows[0])
}
}
for _, r := range rows {
if r["name"] == "own/g14/dunst/broker" && (r["hashed"] != true || r["user"] != "g14.dunst" || r["hashedAt"] == nil) {
t.Errorf("%v: a hashed account's row said %v", c.args, r)
}
if r["name"] == "own/ace/mesh-build-go/token" && r["hashed"] != nil {
t.Errorf("%v: a secret that is no bus account says hashed: %v", c.args, r)
}
}
}
hash := toolNamed(t, definitions(&Env{Vault: v.Vault}), "account_hash")
got, err := hash.Run(map[string]any{"user": "g14.dunst"})
if h, _ := got.(Hashed); err != nil || !h.Written || h.Generation != 1 || acc.puts != 2 {
t.Errorf("account_hash = %+v, %v (%d writes)", got, err, acc.puts)
}
if _, err := hash.Run(map[string]any{"user": "ace.mesh-build-go"}); err == nil {
t.Error("account_hash hashed a value that is no account")
}
// ADR 0547 clause 9: the hashes of the accounts the vault holds no value for are adopted from the bus node's
// handing, sealed to the vault's key (which node-key answers), never over a user whose value it holds, again only
// when the loaded list carries another hash, and only from the node the assignments name as holding the bus.
defs := definitions(&Env{Vault: v.Vault})
answer, err := toolNamed(t, defs, Seat+".node-key").runAs("node.novox",
map[string]any{"key": base64.StdEncoding.EncodeToString(make([]byte, 32))})
if err != nil || answer.(map[string]any)["vault_key"] != v.Own.PublicKey {
t.Fatalf("node-key answered %v, %v: no vault key", answer, err)
}
h := func(c string) string { return "$2a$11$" + strings.Repeat(c, 53) }
handing := map[string]string{"controller": h("c"), "node.ace": h("n"), "g14.dunst": h("d")}
sealedTo := v.Own.PublicKey
have := func() []byte {
var accounts []delivery.HandedAccount
for _, u := range []string{"controller", "node.ace", "g14.dunst", "bad.user"} {
if hash, ok := handing[u]; ok {
accounts = append(accounts, delivery.HandedAccount{User: u, Hash: hash})
}
}
// The list's digest, as the node says it: another list (another hash or user) is another digest.
list, _ := json.Marshal(accounts)
handed, err := delivery.HandAccounts(accounts, sealedTo, Fingerprint(string(list)), time.Now())
if err != nil {
t.Fatal(err)
}
raw, _ := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
UserList: &delivery.UserListWritten{Source: "k", Users: 3, Handed: &handed}})
return raw
}
v.Assignments = Mirror{Keys: func() ([]string, error) { return []string{"novox.nats", "novox.mesh-vault", "ace.dunst"}, nil }}
v.Haves = func(key string) ([]byte, error) {
if key != "novox.nats" {
return nil, nil
}
return have(), nil
}
dunst := acc.keys["g14.dunst"]
adopt := toolNamed(t, defs, "account_adopt")
for _, step := range []struct {
name string
change func()
adopted []string
same, own int
wantErr, said string
}{
{name: "the users it holds no value for are adopted", adopted: []string{"controller", "node.ace"}, own: 1},
{name: "again: the same hashes are kept", adopted: []string{}, same: 2, own: 1},
{name: "another hash in the loaded list is adopted again", change: func() { handing["node.ace"] = h("m") },
adopted: []string{"node.ace"}, same: 1, own: 1},
{name: "a handing sealed to another key is refused", change: func() {
other, _ := seal.GenerateKeyPair()
sealedTo = other.PublicKey
}, wantErr: "not to this one"},
} {
if step.change != nil {
step.change()
}
before := len(v.bus.events)
got, err := adopt.Run(map[string]any{})
if step.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), step.wantErr) {
t.Fatalf("%s: %v %+v", step.name, err, got)
}
continue
}
a, _ := got.(Adoption)
if err != nil || strings.Join(a.Adopted, ",") != strings.Join(step.adopted, ",") || a.Same != step.same ||
a.Own != step.own || a.Handed != 3 || eventsNamed(v, before, AccountHashedEvent) != len(step.adopted) {
t.Fatalf("%s: %+v, %v", step.name, a, err)
}
raw, _ := json.Marshal(got)
for _, hash := range handing {
if strings.Contains(string(raw), hash) {
t.Fatalf("%s: account_adopt said a hash: %s", step.name, raw)
}
}
}
if k := acc.keys["controller"]; !k.Adopted || k.Hash != h("c") || k.Node != "novox" || acc.keys["node.ace"].Hash != h("m") {
t.Errorf("adopted keys %+v %+v", k, acc.keys["node.ace"])
}
if acc.keys["g14.dunst"] != dunst {
t.Errorf("a user whose value the vault holds was adopted over: %+v", acc.keys["g14.dunst"])
}
// A refused user is named in the last adoption, and the same handing is not opened again at the next change.
sealedTo, handing["bad.user"] = v.Own.PublicKey, "not-bcrypt"
if a, err := v.AdoptFrom("novox", have(), "the watch", false); err != nil || a.Refused["bad.user"] == "" || a.Handed != 4 {
t.Fatalf("a refused user: %+v, %v", a, err)
}
if a, err := v.AdoptFrom("novox", have(), "the watch", false); err != nil || !a.At.IsZero() {
t.Fatalf("the same handing was opened again after a refusal: %+v, %v", a, err)
}
if said := v.adoptionSaid(); said["refused"] == nil {
t.Errorf("the refusal is not said: %v", said)
}
if _, err := v.AdoptFrom("ace", have(), "the test", true); err == nil || !strings.Contains(err.Error(), "does not hold the bus") {
t.Errorf("a handing from a node not holding the bus: %v", err)
}
listed, err := own.Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
rows := listed.(map[string]any)["adopted"].([]map[string]any)
if len(rows) != 2 || rows[0]["user"] != "controller" || rows[1]["user"] != "node.ace" || rows[0]["node"] != "novox" {
t.Errorf("secret_own's adopted rows %v", rows)
}
if narrowed, _ := own.Run(map[string]any{"module": "dunst"}); narrowed.(map[string]any)["adopted"] != nil {
t.Errorf("secret_own narrowed to a module lists adopted accounts: %v", narrowed)
}
}