664 lines
29 KiB
Go
664 lines
29 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"
|
|
"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.",
|
|
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
|
|
}
|
|
return map[string]any{"recorded": true}, 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_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
|
|
}
|