Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/tools.go
T
jschoubben fb039250f6 The vault adopts the hashes of the bus accounts it holds no value for from the bus node's handing (ADR 0547 clause 9, issue 1945)
take-over waits on a hash for every user of the loaded list, and the vault holds no value for the controller's own
account, the node accounts and the enrolled devices. The bus node's engine now hands every loaded user's hash in the
bus module's have, sealed to the vault's key (mesh-host and mesh-sdk on the same branch). The vault answers its key in
node-key, reads the haves (manifest reads node-engine.have, granted from the manifest, no controller change), watches
the bus module's have, opens the handing with its own key, and writes each user it holds no value for into
bus-accounts marked adopted, with the list and node it came from, saying account-hashed. It never touches a user whose
covered account it holds or whose key is its own hash, writes an adopted key again only for another hash, and refuses
a handing from any node the assignments do not name as holding the bus module.

Tools: account_adopt adopts now from the have as the watch does; secret_own, unnarrowed by module, lists the adopted
accounts and the last adoption.
2026-10-26 11:36:33 +01:00

802 lines
35 KiB
Go

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; beside them secret_handed and
// secret_vault_export over the vault's own store, and ADR 0230's four retirement tools over the custody
// ledger, and vault_status. None returns a value: `get` returns one sealed to the asker's key, the others
// fingerprints and ciphertext only.
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/seal"
"os"
"path/filepath"
"strings"
"sync"
"time"
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
Custody *Custody
// refused is why the operator key in the mesh's export was not taken, or empty.
mu sync.Mutex
refused string
// Broken is why the vault could not be built, said by `get`.
Broken error
// failedPasses is how many re-seal passes in a row failed, and lastFailure why the last one did
// (vault_health, novox/hq ADR 0366 §4).
failedPasses int
lastFailure string
// pairsSaid is what the last pass over the shared credentials said of each, and lastPairFailure why it last
// did not finish (pair.go): each said once, and again when it changes.
pairsSaid map[string]string
lastPairFailure string
}
func (e *Env) keyRefused() string {
e.mu.Lock()
defer e.mu.Unlock()
return e.refused
}
func (e *Env) refuseKey(why string) {
e.mu.Lock()
defer e.mu.Unlock()
e.refused = why
}
// 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: Seat + ".accept",
Description: "Take a value given at the controller's terminal (ADR 0349 clause 8), in two steps from the " +
"controller alone: name the secret (node, module and name for a module's own; provider and provision " +
"for a provision's one credential) and get a call and a key made for it; then send the call and the " +
"value sealed to that key. The vault alone opens it, and never remakes an accepted credential. With " +
"origin issued it keeps a covered bus account the controller minted, replacing it in place.",
Input: intakeInput(map[string]string{"node": "the machine, for a module's own secret",
"module": "the module, for a module's own secret", "name": "the secret's name in the module",
"provider": "the provider's machine, for a provision's credential", "provision": "the provision",
"by": "who gave it at the terminal, as the controller names them",
"origin": "issued for a bus account the controller minted for a core or moved module and hands at " +
"once (ADR 0374, issue 787), kept as that module's own secret and replaced in place on a " +
"re-issue; left out for a value given at the terminal"}),
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
if call := arg(a, "call"); call != "" {
return v.AcceptTake(caller, call, arg(a, "sealed"))
}
secret, err := secretNamed(a)
if err != nil {
return nil, err
}
return v.AcceptOpen(caller, secret, arg(a, "by"), arg(a, "origin"))
}},
{Name: Seat + ".secret-ask",
Description: "Ask the operator for a module's own secret at a desk (ADR 0349 clause 9, ADR 0277 §22–§27), " +
"asked by the controller alone: node, module, name, desk and asked_by. The ask is recorded before any " +
"prompt opens — one open ask per secret and three an hour — and the vault asks the desk's prompt itself, " +
"in its own name, with a key made for the call; what is typed opens in the vault alone. Answers how the " +
"prompt ended (given, dismissed, timed-out, empty, refused or failed) and who asked as recorded; never " +
"the value.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"node": str("the machine the secret is for"), "module": str("the module"),
"name": str("the secret's name in the module"), "desk": str("the machine whose prompt asks"),
"asked_by": str("who asked the controller, as the bus named its caller")}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.AskAtDesk(caller, AskOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
Desk: arg(a, "desk"), AskedBy: arg(a, "asked_by")})
}},
{Name: Seat + ".issued",
Description: "Take the credential a provider's software issued for a provision (ADR 0349 clause 10, ADR 0305), " +
"asked by the controller alone: provision, provider (its machine), module and tool. The vault asks that " +
"tool itself, in its own name, with a key made for the call, and keeps what it sealed as the provision's " +
"one credential, or, when the tool answers a value per holder named in holders, each as that holder's " +
"own, handed to it alone; the same value again writes nothing. Never answers the value.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"provision": str("the provision"), "provider": str("the provider's machine"),
"module": str("the provider's module, whose software issued it"),
"tool": str("the provider's tool that answers it, as its offer names it"),
"holders": map[string]any{"type": "array", "description": "the provision's holders from that " +
"provider, each {node, module}: the tool may answer a value for each, kept as that holder's own " +
"and handed to it alone",
"items": map[string]any{"type": "object", "properties": map[string]any{
"node": str("the holder's machine"), "module": str("the consuming module")}}}}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
holders, err := holdersArg(a)
if err != nil {
return nil, err
}
return v.TakeIssuedFor(caller, arg(a, "provision"), arg(a, "provider"), arg(a, "module"), arg(a, "tool"),
holders)
}},
{Name: Seat + ".rotate",
Description: "Make a secret anew (ADR 0114, ADR 0228): node, module and name for a module's own, or provider " +
"and provision for a provision's credential. Refused for a value a provider's software issued, which " +
"the software rotates, and for an accepted credential, which the vault does not remake. Never answers " +
"the value. From the controller alone, and with a why.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"node": str("the machine, for a module's own secret"), "module": str("the module"),
"name": str("the secret's name in the module"), "provider": str("the provider's machine"),
"provision": str("the provision"),
"consumer": str("a provision's credential of a provider whose consumers keep a login each: only " +
"this consumer's machine's own"),
"consumer_module": str("with it or alone: only this consuming module's own login"),
"by": str("who asked, as the controller names them when it carries the act"),
"why": str("why it is rotated: required")}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
secret, err := secretNamed(a)
if err != nil {
return nil, err
}
one, err := narrowNamed(a)
if err != nil {
return nil, err
}
return v.RotateFor(caller, secret, one, arg(a, "by"), arg(a, "why"))
}},
{Name: Seat + ".move-in",
Description: "Take one value of the controller's store into the vault, as it is (design 57's V3): from the " +
"node-engine of a machine it was sealed to, and no other caller, in two steps — name the row (node, " +
"module and name for a module's own; provider, provision, consumer, consumer_module and local for a " +
"pair credential), its origin and the controller's generation, and get a call and a key made for it; " +
"then send the call, the value sealed to that key and read_back_to, a key of your own. Never replaces " +
"a value the vault holds; answers what it kept, read back from its store sealed to read_back_to.",
Input: intakeInput(map[string]string{"node": "a module's own secret: its machine", "module": "its module",
"name": "its name in the module", "provider": "a pair credential: the provider's machine",
"provision": "the provision", "consumer": "the consumer's machine",
"consumer_module": "the consuming module", "local": "the credential's local name, when it has one",
"origin": "where the value came from in the controller's store: made, accepted or issued",
"generation": "the controller's generation for the value, kept as it is",
"made_at": "when the controller made or took it (RFC 3339)",
"read_back_to": "the second step: a raw X25519 public key in base64 the read-back is sealed to",
"sealing_key": "the first step, optional: this machine's sealing key, a raw X25519 public key in " +
"base64, which the core's credentials are sealed to from then on (ADR 0349 §16)"}),
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
if call := arg(a, "call"); call != "" {
return v.MoveTake(caller, call, arg(a, "sealed"), arg(a, "read_back_to"))
}
m := MoveOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
Provider: arg(a, "provider"), Provision: arg(a, "provision"), Consumer: arg(a, "consumer"),
ConsumerModule: arg(a, "consumer_module"), Local: arg(a, "local"), Origin: arg(a, "origin"),
Generation: arg(a, "generation"), SealingKey: arg(a, "sealing_key")}
if at := arg(a, "made_at"); at != "" {
if m.MadeAt, err = time.Parse(time.RFC3339Nano, at); err != nil {
return nil, fmt.Errorf("made_at %q is not a time (RFC 3339)", at)
}
}
return v.MoveOpen(caller, m)
}},
{Name: Seat + ".node-key",
Description: "State this machine's sealing key to the vault, in its own name (ADR 0372, issue 786): from the " +
"node-engine of the machine alone, at its start and whenever the key changes; the machine is the caller's, " +
"as the bus proved it. A first or changed key is recorded and `secret-changed` is raised for the machine's " +
"core secrets, so the controller asks again; the same key again changes nothing. Answers the vault's own " +
"public key (`vault_key`, SPKI PEM), which the bus node seals the loaded list's hashes to (ADR 0547 clause 9).",
Input: map[string]any{"type": "object", "properties": map[string]any{
"key": str("this machine's sealing key, a raw X25519 public key in base64, as move-in's sealing_key carries it")}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
if err := v.StateNodeKey(caller, arg(a, "key")); err != nil {
return nil, err
}
// And the vault's own public key, which the bus node seals the loaded list's hashes to (ADR 0547
// clause 9, adopt.go): a public half, never the private one.
return map[string]any{"recorded": true, "vault_key": v.Own.PublicKey}, nil
}},
{Name: Seat + ".move-asks",
Description: "Copy rows of the controller's record of asks at a desk into the vault's, as they are and once " +
"(design 57's V3): from the controller alone, at most 500 at a time. No value is in any of them.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"asks": map[string]any{"type": "array", "description": "the controller's rows: id, node, module, name, " +
"desk, asked_by, opened_at, and ended_at and outcome when the ask ended",
"items": map[string]any{"type": "object"}}}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
raw, err := json.Marshal(a["asks"])
if err != nil {
return nil, err
}
var asks []MovedAsk
if err := json.Unmarshal(raw, &asks); err != nil {
return nil, fmt.Errorf("asks is not the controller's rows: %w", err)
}
return v.MoveAsks(caller, asks)
}},
{Name: "secret_asks",
Description: "The record of asks for a module's own secret at a desk, newest first: who asked as the bus named " +
"them, for what, at which desk, when, and how each ended. Never a value.",
Input: map[string]any{"limit": map[string]any{"type": "number", "description": "how many lines (default 50)"}},
run: func(_ string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.Store.Asks(limitOf(a))
}},
{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:<hex>`). 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:<hex> 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
}},
{Name: "secret_handed",
Description: "Who was handed which secret by `get`, and when, and who was refused, newest first, as the " +
"vault's own store records it. Never a value.",
Input: map[string]any{"limit": map[string]any{"type": "number", "description": "how many (default 50)"}},
run: func(_ string, a map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
return env.Vault.Store.Handed(limitOf(a))
}},
{Name: "secret_own",
Description: "The secrets the vault keeps for modules themselves (`own/<node>/<module>/<name>`, e.g. a module's " +
"bus account `broker`): for each its name, generation, the fingerprint of the value it holds now " +
"(sha256:<hex>), when it was made and last changed, by whom, where the value came from, and whether it is a " +
"covered bus account; for a bus account, its user and whether the hash of this generation is written in " +
"`bus-accounts` (hashed, hashedAt, or why not). Narrow by node, module or name prefix. At most 1000 rows; " +
"says when cut. Unnarrowed by module, also `adopted`: every account whose hash the vault adopted from the " +
"bus's loaded list because it holds no value for it (user, adoptedAt, the list's digest, the node), and " +
"`adoption`: the last adoption and why the last look adopted nothing. Never a value or a hash.",
Input: map[string]any{"node": str("only this machine's secrets"), "module": str("only this module's secrets"),
"name": str("only secrets whose name (the last part) starts with this, e.g. broker")},
run: func(_ string, a map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
out, err := ownAnswer(env.Vault, arg(a, "node"), arg(a, "module"), arg(a, "name"))
if err != nil || arg(a, "module") != "" {
return out, err
}
adopted, err := env.Vault.adoptedRows()
if err != nil {
out["adoptedWhy"] = "bus-accounts could not be read: " + err.Error()
}
if adopted == nil {
adopted = []map[string]any{}
}
out["adopted"], out["adoption"] = adopted, env.Vault.adoptionSaid()
return out, nil
}},
{Name: "account_adopt",
Description: "Adopt now the hashes of the bus accounts the vault holds no value for (ADR 0547 clause 9): read " +
"the have of the bus module on the node holding it, open the loaded list's hashes its node-engine handed " +
"sealed to the vault's key, and write each user's into `bus-accounts` marked adopted, saying " +
"`account-hashed`. Never a user whose value the vault holds or whose key is its own hash. As the watch of " +
"that have does, though the handing is the one adopted before. Answers the users adopted and counted, never a hash.",
run: func(caller string, _ map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
return env.Vault.AdoptNow(caller)
}},
{Name: "account_hash",
Description: "Hash one covered bus account again now (ADR 0547 clause 9): the vault writes the bcrypt hash of " +
"the account it holds for the bus user into its state `bus-accounts` and says `account-hashed`, as it does " +
"when the account is handed. Answers the generation and fingerprint hashed, never the hash or the value.",
Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")},
run: func(caller string, a map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
return env.Vault.HashUser(arg(a, "user"), caller)
}},
{Name: "secret_vault_export",
Description: "Every secret the vault itself holds, as its copy sealed to the operator key, in the shape of " +
"`secret export`: saved to a file, `nox secret recover vault mesh-vault <name> --key <operator key file> " +
"--from-export <file> --out <file>` opens one with the operator's private key. Ciphertext only.",
run: func(string, map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
return vaultExport(env.Vault)
}},
{Name: "provisioner_retirement",
Description: "This provider's retirement (novox/hq ADR 0230): the holders it keeps active, a retirement waiting " +
"for a person, one a person rejected, and every holder retired with when and why. Mark-only: a retired " +
"holder keeps what it has until a person deletes it.",
run: func(string, map[string]any) (any, error) {
if env.Custody == nil {
return nil, errors.New("the vault has no custody ledger")
}
return env.Custody.Retirement(), nil
}},
{Name: "provisioner_retire_approve",
Description: "Retire exactly the set waiting for a person (or rejected earlier). Repeat the set in consumers; why is required.",
Input: actInput(true, false),
run: func(caller string, a map[string]any) (any, error) {
if env.Custody == nil {
return nil, errors.New("the vault has no custody ledger")
}
done, err := env.Custody.Approve(stringsOf(a["consumers"]), actOf(caller, a))
if err != nil {
return nil, err
}
return map[string]any{"retired": done}, nil
}},
{Name: "provisioner_retire_reject",
Description: "Keep the set waiting for a person active, though the mesh no longer asks for it. Repeat the set in consumers; why is required.",
Input: actInput(true, false),
run: func(caller string, a map[string]any) (any, error) {
if env.Custody == nil {
return nil, errors.New("the vault has no custody ledger")
}
kept, err := env.Custody.Reject(stringsOf(a["consumers"]), actOf(caller, a))
if err != nil {
return nil, err
}
return map[string]any{"kept": kept}, nil
}},
{Name: "provisioner_delete",
Description: "Delete one RETIRED holder from the custody ledger, its rotation history with it — never an active one. " +
"Repeat its name in confirm; why is required.",
Input: actInput(false, true),
run: func(caller string, a map[string]any) (any, error) {
if env.Custody == nil {
return nil, errors.New("the vault has no custody ledger")
}
consumer, _ := a["consumer"].(string)
confirm, _ := a["confirm"].(string)
if err := env.Custody.Delete(consumer, confirm, actOf(caller, a)); err != nil {
return nil, err
}
return map[string]any{"deleted": consumer, "freed_bytes": -1}, nil
}},
{Name: "vault_health",
Description: "The vault's runtime health, as the node-engine asks it (novox/hq ADR 0366): unhealthy once " +
"re-sealing the operator copies has failed on three passes in a row, saying what failed and why; " +
"healthy again after one pass succeeds.",
run: func(string, map[string]any) (any, error) { return env.health(), nil }},
{Name: "vault_status",
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
"key the mesh's export holds if the vault refused it, and how often the record of hand-outs was rolled.",
run: func(string, map[string]any) (any, error) {
if env.Vault == nil {
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
}
left, err := env.Vault.StillSealedToAnother()
if err != nil {
return nil, err
}
key, _ := env.Vault.Store.OperatorKey()
return map[string]any{"ready": true, "operator_key": key, "copies_on_an_earlier_key": left,
"operator_key_refused": env.keyRefused(), "handed_rolled": env.Vault.Store.Rolled(),
"ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
}},
}
}
// actInput is the arguments of an act on retirement, as the SDK's harness takes them.
func actInput(set, one bool) map[string]any {
text := map[string]any{"type": "string"}
in := map[string]any{"why": text, "by": text, "via": text}
if set {
in["consumers"] = map[string]any{"type": "array", "items": text}
}
if one {
in["consumer"], in["confirm"] = text, text
}
return in
}
// actOf is an act's why, by and via. `by` is the person the controller names; with none, the caller the
// runtime proved. `by` is an argument, so it is not proven: when it names somebody other than the caller the
// runtime proved, the proven caller is kept beside it in via, so the record says both.
func actOf(caller string, a map[string]any) Act {
act := Act{}
act.Why, _ = a["why"].(string)
act.By, _ = a["by"].(string)
act.Via, _ = a["via"].(string)
if act.By == "" {
act.By = caller
}
if act.By != caller {
proven := "called by " + caller
if act.Via == "" {
act.Via = proven
} else {
act.Via += ", " + proven
}
}
return act
}
// ready is the vault, or why there is none.
func (e *Env) ready() (*Vault, error) {
if e.Vault != nil {
return e.Vault, nil
}
if e.Broken != nil {
return nil, e.Broken
}
return nil, errors.New("the vault is not ready")
}
func arg(a map[string]any, key string) string {
s, _ := a[key].(string)
return strings.TrimSpace(s)
}
// secretNamed is the secret a call names: a module's own (node, module, name), or a provision's one credential
// (provider, provision) — one or the other.
func secretNamed(a map[string]any) (string, error) {
own := arg(a, "node") != "" || arg(a, "module") != "" || arg(a, "name") != ""
pair := arg(a, "provider") != "" || arg(a, "provision") != ""
switch {
case own && !pair:
return OwnSecret(arg(a, "node"), arg(a, "module"), arg(a, "name"))
case pair && !own:
return ProvisionSecret(arg(a, "provider"), arg(a, "provision"))
}
return "", errors.New("name a module's own secret (node, module, name) or a provision's credential " +
"(provider, provision), one of the two")
}
// narrowNamed is the one consumer a rotation names, if any (issue 767).
func narrowNamed(a map[string]any) (Narrow, error) {
n := Narrow{Consumer: arg(a, "consumer"), ConsumerModule: arg(a, "consumer_module")}
for what, v := range map[string]string{"the consumer's machine": n.Consumer, "the consuming module": n.ConsumerModule} {
if v != "" && !namePart.MatchString(v) {
return Narrow{}, fmt.Errorf("%s %q is not a name the mesh writes", what, v)
}
}
return n, nil
}
// intakeInput is a two-step verb's arguments: what its first step names, then the call and what was sealed.
func intakeInput(first map[string]string) map[string]any {
props := map[string]any{}
for k, d := range first {
props[k] = str(d)
}
props["call"] = str("the second step: the call the first step answered")
props["sealed"] = str("the second step: the value sealed to the call's key (NaCl anonymous box, base64) — never the value")
return map[string]any{"type": "object", "properties": props}
}
func stringsOf(v any) []string {
list, _ := v.([]any)
out := []string{}
for _, x := range list {
if s, ok := x.(string); ok {
out = append(out, s)
}
}
return out
}
func limitOf(a map[string]any) int {
if v, ok := a["limit"].(float64); ok && v > 0 {
return int(v)
}
return 50
}
// keyFingerprint is a key's fingerprint as the controller's secrets.Fingerprint says it: sha256 of the key,
// its first eight bytes in hex.
func keyFingerprint(key string) string {
if key == "" {
return ""
}
sum := sha256.Sum256([]byte(strings.TrimSpace(key)))
return "sha256:" + hex.EncodeToString(sum[:8])
}
// vaultExport is the vault's operator copies in the shape `secret export` writes and `secret recover
// --from-export` reads: each as node "vault", module "mesh-vault" and the secret's name.
func vaultExport(v *Vault) (map[string]any, error) {
key, err := v.Store.OperatorKey()
if err != nil {
return nil, err
}
all, err := v.Store.List()
if err != nil {
return nil, err
}
kept, earlier := []map[string]any{}, []map[string]any{}
for _, rec := range all {
k := map[string]any{"node": "vault", "module": Seat, "name": rec.Name, "kind": "own", "origin": "made",
"sealed": rec.Operator.Sealed, "key": rec.Operator.Key, "made-at": rec.MadeAt}
if rec.Operator.Key == key {
kept = append(kept, k)
} else {
earlier = append(earlier, k)
}
}
return map[string]any{"export": 1, "operator-key": key, "fingerprint": keyFingerprint(key), "kept": kept,
"sealed-to-earlier-key": earlier}, 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
}
// ownRowsMax bounds what secret_own answers.
const ownRowsMax = 1000
// ownAnswer lists the own secrets the store holds, narrowed by node, module and name prefix: fingerprints only.
func ownAnswer(v *Vault, node, module, prefix string) (map[string]any, error) {
records, err := v.Store.List()
if err != nil {
return nil, err
}
rows := []map[string]any{}
matched := 0
for _, r := range records {
parts := strings.Split(r.Name, "/")
if len(parts) != 4 || parts[0] != "own" {
continue
}
if (node != "" && parts[1] != node) || (module != "" && parts[2] != module) || !strings.HasPrefix(parts[3], prefix) {
continue
}
matched++
if len(rows) >= ownRowsMax {
continue
}
fp := ""
var note any
if value, err := seal.Open(r.AtRest, v.Own.PrivateKey); err != nil {
note = "the vault could not open its copy: " + err.Error()
} else {
fp = Fingerprint(value)
}
row := map[string]any{"name": r.Name, "node": parts[1], "module": parts[2], "secret": parts[3],
"generation": r.Generation, "fingerprint": fp, "made": r.MadeAt, "changed": r.ChangedAt,
"changedBy": r.ChangedBy, "origin": r.Origin, "busAccount": Covered(r.Name)}
if r.ChangedVia != "" {
row["changedVia"] = r.ChangedVia
}
if note != nil {
row["error"] = note
}
if Covered(r.Name) && fp != "" {
hashedRow(v, r, fp, row)
}
rows = append(rows, row)
}
return map[string]any{"secrets": rows, "count": len(rows), "matched": matched, "cut": matched > len(rows),
"note": "fingerprint is sha256 of the value the vault holds; a bus account's hash is in the state bus-accounts, never here"}, nil
}
// hashedRow says of a covered account's row whether the hash of its current generation is written in bus-accounts.
func hashedRow(v *Vault, r Record, fp string, row map[string]any) {
value, err := openAtRest(r, v)
if err != nil {
return
}
user, _, err := accountOf(r.Name, value)
if err != nil {
row["hashed"], row["hashWhy"] = false, err.Error()
return
}
row["user"] = user
if v.Accounts == nil {
row["hashed"], row["hashWhy"] = false, "the vault has no state of bus accounts"
return
}
h, err := v.Accounts.Get(user)
switch {
case err != nil:
row["hashed"], row["hashWhy"] = false, "its key could not be read: "+err.Error()
case h == nil:
row["hashed"], row["hashWhy"] = false, "no key"
case h.Adopted:
row["hashed"], row["hashedAt"], row["hashWhy"] = false, h.At, "the key is adopted from the loaded list, not of this value"
case h.Of != r.Name || h.Generation != r.Generation || h.Fingerprint != fp:
row["hashed"], row["hashedAt"], row["hashWhy"] = false, h.At, fmt.Sprintf("the key is of %s generation %d", h.Of, h.Generation)
default:
row["hashed"], row["hashedAt"] = true, h.At
}
if why, owed := v.hashes.list()[r.Name]; owed {
row["hashOwed"] = why
}
}