From da04ad62ae781d65a00206ee6758dad374e07699 Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 00:55:34 +0000 Subject: [PATCH] mesh-vault: the tools the TypeScript vault offered, in Go (issue 674) Co-Authored-By: Claude Opus 5.5 --- modules/mesh-vault/cmd/mesh-vault/ledger.go | 259 ++++++++++++++++++++ modules/mesh-vault/cmd/mesh-vault/tools.go | 211 ++++++++++++++++ 2 files changed, 470 insertions(+) create mode 100644 modules/mesh-vault/cmd/mesh-vault/ledger.go create mode 100644 modules/mesh-vault/cmd/mesh-vault/tools.go diff --git a/modules/mesh-vault/cmd/mesh-vault/ledger.go b/modules/mesh-vault/cmd/mesh-vault/ledger.go new file mode 100644 index 00000000..61988608 --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/ledger.go @@ -0,0 +1,259 @@ +package main + +// The custody ledger (novox/hq ADR 0085, design 24), carried over from the TypeScript vault as it was: +// for each pair credential the controller delivers to this vault as the `secret` provider, who holds it, +// since when, its fingerprint and every rotation — and never the value. The controller still mints and +// seals those until the secrets move to the vault (design 57's V3, V4); this pass records what it delivered, +// so `secret_holders` and `secret_verify` answer as before. Its files are the same JSON, read as they are. + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" +) + +// isoMillis is JavaScript's toISOString, the form the ledger's times were first written in. +const isoMillis = "2006-01-02T15:04:05.000Z" + +// Held is one holder of a secret this vault provides: everything the vault knows of it, and no value. +type Held struct { + As string `json:"as"` + Consumer string `json:"consumer"` + Fingerprint string `json:"fingerprint"` + Length int `json:"length"` + Since string `json:"since"` + Changed string `json:"changed"` + Rotations int `json:"rotations"` + History []Earlier `json:"history"` +} + +// Earlier is a fingerprint a rotation replaced, and when. +type Earlier struct { + Fingerprint string `json:"fingerprint"` + Until string `json:"until"` +} + +// Outcome is what recording a delivery found. +type Outcome string + +const ( + Granted Outcome = "granted" + Rotated Outcome = "rotated" + Unchanged Outcome = "unchanged" +) + +// Fingerprint is sha256 of a value, as `sha256:`: the one thing about a secret that may be said. +func Fingerprint(value string) string { + sum := sha256.Sum256([]byte(value)) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// Ledger is the directory of holders. +type Ledger struct{ dir string } + +// NewLedger opens (and makes) the ledger at dir. +func NewLedger(dir string) (*Ledger, error) { + if dir == "" { + return nil, errors.New("MESH_VAULT_LEDGER is not set: the vault has nowhere to keep its ledger") + } + if err := os.MkdirAll(dir, 0o700); err != nil { + return nil, err + } + return &Ledger{dir: dir}, nil +} + +var login = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]*$`) + +func (l *Ledger) pathOf(as string) (string, error) { + if !login.MatchString(as) || strings.Contains(as, "..") { + return "", fmt.Errorf("a login is a name, not a path: %q", as) + } + return filepath.Join(l.dir, as+".json"), nil +} + +// Record says the mesh delivered value for as: the same value again changes nothing, another is a rotation. +// The value is fingerprinted here and goes no further. +func (l *Ledger) Record(as, consumer, value string, now time.Time) (Held, Outcome, error) { + path, err := l.pathOf(as) + if err != nil { + return Held{}, "", err + } + fp, at := Fingerprint(value), now.UTC().Format(isoMillis) + before, ok := l.Get(as) + if !ok { + h := Held{As: as, Consumer: consumer, Fingerprint: fp, Length: len(value), Since: at, Changed: at, History: []Earlier{}} + return h, Granted, l.write(path, h) + } + if before.Fingerprint == fp && before.Length == len(value) { + return before, Unchanged, nil + } + h := before + h.Consumer, h.Fingerprint, h.Length, h.Changed = consumer, fp, len(value), at + h.Rotations++ + h.History = append(append([]Earlier{}, before.History...), Earlier{Fingerprint: before.Fingerprint, Until: at}) + return h, Rotated, l.write(path, h) +} + +// Withdraw forgets a holder, saying whether there was one. +func (l *Ledger) Withdraw(as string) bool { + path, err := l.pathOf(as) + if err != nil { + return false + } + return os.Remove(path) == nil +} + +// Get is one holder. +func (l *Ledger) Get(as string) (Held, bool) { + path, err := l.pathOf(as) + if err != nil { + return Held{}, false + } + raw, err := os.ReadFile(path) + if err != nil { + return Held{}, false + } + var h Held + if json.Unmarshal(raw, &h) != nil { + return Held{}, false + } + return h, true +} + +// List is every holder, by login. +func (l *Ledger) List() []Held { + entries, err := os.ReadDir(l.dir) + if err != nil { + return nil + } + var out []Held + for _, e := range entries { + if strings.HasSuffix(e.Name(), ".json") { + if h, ok := l.Get(strings.TrimSuffix(e.Name(), ".json")); ok { + out = append(out, h) + } + } + } + sort.Slice(out, func(i, j int) bool { return out[i].As < out[j].As }) + return out +} + +func (l *Ledger) write(path string, h Held) error { + raw, err := json.MarshalIndent(h, "", " ") + if err != nil { + return err + } + return writeWhole(path, append(raw, '\n')) +} + +// Contribution is one entry of the contributions file the mesh delivers to the vault. +type Contribution struct { + From string `json:"from,omitempty"` + Node string `json:"node,omitempty"` + As string `json:"as"` + Secret string `json:"secret"` +} + +// Contributions is what the mesh asks this vault to serve: the entries with a login and a secret file. +func Contributions(receives string) ([]Contribution, error) { + raw, err := os.ReadFile(receives) + if err != nil { + return nil, err + } + var doc struct { + Given []Contribution `json:"given"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + return nil, err + } + var out []Contribution + for _, g := range doc.Given { + if g.As != "" && g.Secret != "" { + out = append(out, g) + } + } + return out, nil +} + +// delivered is the value the mesh delivered for c, without the newline a file may end with. +func delivered(c Contribution) (string, error) { + raw, err := os.ReadFile(c.Secret) + if err != nil { + return "", fmt.Errorf("the delivered secret is not readable: %v", err) + } + return strings.TrimSuffix(string(raw), "\n"), nil +} + +// DeliveredFingerprint is the fingerprint and length of what the mesh currently delivers for c. +func DeliveredFingerprint(c Contribution) (string, int, error) { + v, err := delivered(c) + if err != nil { + return "", 0, err + } + return Fingerprint(v), len(v), nil +} + +// Custody is the pass over the contributions file that keeps the ledger. +type Custody struct { + Ledger *Ledger + Receives string + Emit func(event string, body map[string]any) error + Now func() time.Time + Log func(format string, args ...any) +} + +// Pass records every delivered value and withdraws every holder no longer asked for — but only from a +// contributions file it could read whole: an unreadable one withdraws nobody. +func (c Custody) Pass() { + asked, err := Contributions(c.Receives) + if err != nil { + c.Log("the contributions file is not readable (%s): %v — nothing withdrawn", c.Receives, err) + return + } + keep := map[string]bool{} + for _, a := range asked { + keep[a.As] = true + value, err := delivered(a) + if err != nil { + c.Log("%s: %v", a.As, err) + continue + } + held, outcome, err := c.Ledger.Record(a.As, a.Node, value, c.Now()) + if err != nil { + c.Log("%s: %v", a.As, err) + continue + } + if outcome == Unchanged { + continue + } + event := "provisioned" + if outcome == Rotated { + event = "rotated" + } + c.Log("%s: %s (%s…, rotations %d)", held.As, outcome, held.Fingerprint[:19], held.Rotations) + c.say(event, map[string]any{"consumer": held.Consumer, "as": held.As, "fingerprint": held.Fingerprint, "rotations": held.Rotations}) + } + for _, h := range c.Ledger.List() { + if !keep[h.As] && c.Ledger.Withdraw(h.As) { + c.Log("%s: withdrawn", h.As) + c.say("deprovisioned", map[string]any{"as": h.As}) + } + } +} + +func (c Custody) say(event string, body map[string]any) { + if c.Emit == nil { + return + } + if err := c.Emit(event, body); err != nil { + c.Log("%s could not be said: %v", event, err) + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go new file mode 100644 index 00000000..07cd9711 --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -0,0 +1,211 @@ +package main + +// What the vault serves: the seat's verb `get`, and the three tools the TypeScript vault offered — +// secret_export, secret_holders and secret_verify — answering as they did. None returns a value: `get` +// returns one sealed to the asker's key, the others fingerprints and ciphertext only. + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// Env is what the tools work on. A part the module's words did not give is nil, and a tool that needs it +// says so. +type Env struct { + Vault *Vault + Ledger *Ledger + Receives string + Root string + // Broken is why the vault could not be built, said by `get`. + Broken error +} + +// toolDef is a tool with its caller given apart from its arguments, so a test can be the runtime. +type toolDef struct { + Name string + Description string + Input map[string]any + run func(caller string, a map[string]any) (any, error) +} + +// Run answers a call the runtime made: the caller is the one it proved. +func (t toolDef) Run(a map[string]any) (any, error) { return t.run(stdio.CallerOf(a), a) } + +func (t toolDef) runAs(caller string, a map[string]any) (any, error) { return t.run(caller, a) } + +func str(description string) map[string]any { + return map[string]any{"type": "string", "description": description} +} + +func definitions(env *Env) []toolDef { + return []toolDef{ + {Name: Seat + ".get", + Description: "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its " + + "generation; answered only to an asker the vault allows, as the runtime proved it (ADR 0349 §11). " + + "Never the value in the clear, and never to a private key.", + Input: map[string]any{"name": str("the secret's name"), + "public_key": str("the asker's X25519 public key as SPKI PEM — never a private key")}, + run: func(caller string, a map[string]any) (any, error) { + if env.Vault == nil { + if env.Broken != nil { + return nil, env.Broken + } + return nil, errors.New("the vault is not ready") + } + name, _ := a["name"].(string) + key, _ := a["public_key"].(string) + return env.Vault.Get(caller, strings.TrimSpace(name), key) + }}, + {Name: "secret_export", + Description: "The mesh's root secrets as this vault keeps them: every secret a module holds for itself, " + + "sealed to the operator's key (novox/hq ADR 0085, amended). Ciphertext — nothing here can open a line " + + "of it; the operator, holding the private key off the mesh, recovers one with " + + "`nox secret recover --from-export` on the control-node. Also lists what is NOT recoverable: secrets " + + "made before the mesh had an operator key.", + Input: map[string]any{"sealed": map[string]any{"type": "boolean", + "description": "include the sealed blobs (default true); false lists holders and the key only"}}, + run: func(_ string, a map[string]any) (any, error) { + return exportAnswer(env.Root, a["sealed"] != false), nil + }}, + {Name: "secret_holders", + Description: "Who holds a secret from this vault: each consumer's login, node and module, when it was " + + "granted, how many times it has been rotated and when, and the fingerprint of the current value. " + + "Fingerprints only — the value is never returned.", + run: func(string, map[string]any) (any, error) { + if env.Ledger == nil { + return map[string]any{"holders": []any{}, "count": 0, "error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}, nil + } + var asked []Contribution + if env.Receives != "" { + asked, _ = Contributions(env.Receives) + } + holders := []map[string]any{} + for _, h := range env.Ledger.List() { + row := map[string]any{} + raw, _ := json.Marshal(h) + _ = json.Unmarshal(raw, &row) + row["module"], row["asked"] = nil, false + for _, c := range asked { + if c.As == h.As { + row["module"], row["asked"] = c.From, true + } + } + holders = append(holders, row) + } + return map[string]any{"holders": holders, "count": len(holders)}, nil + }}, + {Name: "secret_verify", + Description: "Check one holder's secret without seeing it: the fingerprint the vault recorded against the " + + "fingerprint of the value the mesh currently delivers here, and optionally against a fingerprint " + + "computed on the holder's own machine (sha256 of the file, as `sha256:`). Two ends agreeing " + + "proves they agree, not that either works — the login itself is the test.", + Input: map[string]any{"as": str("the holder's login, e.g. anchor-redis"), + "fingerprint": str("optional: sha256: of the value as the holder reads it, computed there — never the value")}, + run: func(_ string, a map[string]any) (any, error) { + as, _ := a["as"].(string) + given, _ := a["fingerprint"].(string) + return verifyAnswer(env, as, given), nil + }}, + } +} + +func exportAnswer(root string, withBlobs bool) map[string]any { + if root == "" { + return map[string]any{"available": false, "error": "this vault keeps no root secrets (MESH_VAULT_ROOT is not set)"} + } + raw, err := os.ReadFile(filepath.Join(root, "export.json")) + if err != nil { + return map[string]any{"available": false, "error": "the mesh has not written an export here yet: " + err.Error()} + } + var doc struct { + Export int `json:"export"` + OperatorKey string `json:"operator-key"` + Fingerprint string `json:"fingerprint"` + Kept []map[string]any `json:"kept"` + Unrecoverable []map[string]any `json:"unrecoverable"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + return map[string]any{"available": false, "error": "the export is not readable: " + err.Error()} + } + kept := []map[string]any{} + for _, k := range doc.Kept { + if !withBlobs { + k = map[string]any{"node": k["node"], "module": k["module"], "name": k["name"], "origin": k["origin"], "made-at": k["made-at"]} + } + kept = append(kept, k) + } + if doc.Unrecoverable == nil { + doc.Unrecoverable = []map[string]any{} + } + return map[string]any{"available": true, "export": doc.Export, "operator-key": doc.OperatorKey, + "fingerprint": doc.Fingerprint, "count": len(kept), "kept": kept, "unrecoverable": doc.Unrecoverable} +} + +// exportedOperatorKey is the operator key in the export the mesh writes to the vault's root, or "". +func exportedOperatorKey(root string) string { + raw, err := os.ReadFile(filepath.Join(root, "export.json")) + if err != nil { + return "" + } + var doc struct { + OperatorKey string `json:"operator-key"` + } + _ = json.Unmarshal(raw, &doc) + return doc.OperatorKey +} + +func verifyAnswer(env *Env, as, given string) map[string]any { + if env.Ledger == nil { + return map[string]any{"as": as, "known": false, "error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"} + } + recorded, ok := env.Ledger.Get(as) + if !ok { + return map[string]any{"as": as, "known": false, "error": "this vault holds nothing for " + as} + } + var delivered, deliveredErr any + var deliveredMatches any + var asked *Contribution + if env.Receives != "" { + all, _ := Contributions(env.Receives) + for i := range all { + if all[i].As == as { + asked = &all[i] + } + } + } + if asked == nil { + deliveredErr = "the mesh does not currently ask this vault to serve that login" + } else if fp, _, err := DeliveredFingerprint(*asked); err != nil { + deliveredErr = err.Error() + } else { + delivered, deliveredMatches = fp, fp == recorded.Fingerprint + } + var givenAny, givenMatches, earlier any + if given != "" { + givenAny, givenMatches = given, given == recorded.Fingerprint + was := false + for _, h := range recorded.History { + was = was || h.Fingerprint == given + } + earlier = was + } + return map[string]any{"as": recorded.As, "known": true, "recorded": recorded.Fingerprint, + "rotations": recorded.Rotations, "changed": recorded.Changed, + "delivered": delivered, "deliveredError": deliveredErr, "deliveredMatchesRecorded": deliveredMatches, + "given": givenAny, "givenMatchesRecorded": givenMatches, "givenIsAnEarlierValue": earlier, + "ok": deliveredMatches != false && givenMatches != false} +} + +// tools are the definitions as the SDK serves them. +func tools(env *Env) []stdio.Tool { + var out []stdio.Tool + for _, d := range definitions(env) { + out = append(out, stdio.Tool{Name: d.Name, Description: d.Description, Input: d.Input, Run: d.Run}) + } + return out +}