diff --git a/modules/mesh-vault/README.md b/modules/mesh-vault/README.md index 429b861a..a64d59c1 100644 --- a/modules/mesh-vault/README.md +++ b/modules/mesh-vault/README.md @@ -206,10 +206,26 @@ account (and, until R1, the controller) and no other caller, and only sealed to answers the controller alone today; it must also answer `.mesh-vault` when it moves with the media catalogue, after release 57. +## Taking what runs: `own-take-running` (issue 1126, cause C4) + +`move-in` never replaces a value the vault holds, so where the controller made an own secret anew after the move (a +rotation at start, a re-issue), the vault kept the first value while the new one runs. The node-engine of the machine +the secret is for hands the vault what runs, in an intake's two steps (`owntake.go`): module, name and why, then the +value sealed to the call's key. Its own machine's secrets alone (the machine is the caller's, as the bus proved it); a +value the vault holds, never a person's (`accept`) or a provider's software's (`issued`, but for a covered bus +account); never over a value the vault changed in the last 15 minutes. The same value changes nothing and says so. +The replaced value is kept sealed as the record's `previous` (dropped by any later change), the generation is raised, +`secret-changed` and `own-replaced` are said, and the act is in `secret_handed` (`own-replaced`, `own-same`, +`own-take-refused`). A covered bus account so replaced has its hash written again. `own_drift` lists, from every +node's latest shadow answer (`.node` in `node-engine.have`), each own secret whose node says another value, +with what the vault holds and whether the node's ask would be taken now. + ## Events - `secret-changed` — `name`, `by`, `at`, `generation`: which secret, who changed it, when. Never a value or a sealed copy. +- `own-replaced` — `secret`, `node`, `module`, `name`, `from_generation`, `to_generation`, `by`, `why`, `at`: + an own secret replaced by what runs on its machine. Never a value or a fingerprint. - `provisioned`, `rotated` — the custody ledger's, by fingerprint, as before. - `provisioner.retirement` (ADR 0230 §35) — `waiting`, `settled`, `approved`, `rejected`, `retired`, `reenabled`, `deleted`, naming holders, never a value. diff --git a/modules/mesh-vault/cmd/mesh-vault/intake.go b/modules/mesh-vault/cmd/mesh-vault/intake.go index 33e91639..be8012d3 100644 --- a/modules/mesh-vault/cmd/mesh-vault/intake.go +++ b/modules/mesh-vault/cmd/mesh-vault/intake.go @@ -91,7 +91,9 @@ type callKey struct { // ask is the ask's record for `secret-ask`, which the call's id is. ask *AskRecord // move is what `move-in` takes: the row of the controller's store (move.go). - move *MoveOf + move *MoveOf + // why is what `own-take-running` was asked for, in the caller's words (owntake.go). + why string public [32]byte private [32]byte until time.Time diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index c74e0449..e78a7ff7 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -51,7 +51,8 @@ func build() *Env { // Where it sees a consumer bind a provider (pair.go): the same mirror, every key. env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: readAssignment, Store: store}, Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask, - Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave} + Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, + HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }} return env } diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index d2e41c1b..5e41e511 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/seal" "golang.org/x/crypto/nacl/box" ) @@ -319,3 +321,118 @@ func TestMoveInThroughTheTool(t *testing.T) { t.Fatalf("asks through the tool: %+v, %v", moved, err) } } + +// Issue 1126, cause C4: a machine's node-engine hands the vault what runs of its own machine's own secret, where the +// vault holds another. Refused for another machine's secret, one that is not an own secret or not held, a person's or a +// provider's software's value, an empty value, and one the vault changed in the last 15 minutes; the same value changes +// nothing. A replacement keeps the old value as the previous generation, raises the generation, and says own-replaced +// with both generations and never a value; own_drift lists it from the node's shadow answer. +func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { + v := newVault(t, nil) + tick, later := v.Now, time.Duration(0) + v.Now = func() time.Time { return tick().Add(later) } + old, running := "the-value-moved-in-first", "the-value-the-controller-made-at-start" + if _, err := moveIn(t, v, "node.anchor", superuser(), old); err != nil { + t.Fatal(err) + } + for _, origin := range []string{"accepted", "issued"} { + m := superuser() + m.Name, m.Origin = "from-"+origin, origin + if _, err := moveIn(t, v, "node.anchor", m, "value-"+origin); err != nil { + t.Fatal(err) + } + } + later = time.Hour + take := func(caller string, of OwnTakeOf, value string) (OwnReplaced, error) { + opened, err := v.OwnTakeOpen(caller, of) + if err != nil { + return OwnReplaced{}, err + } + return v.OwnTakeTake(caller, opened.Call, sealToCall(t, opened.SealTo, value)) + } + pg := OwnTakeOf{Module: "postgres", Name: "superuser-password", Why: "the shadow answer says another value"} + for name, row := range map[string]struct { + caller string + of OwnTakeOf + value string + says string + }{ + "another machine's, named": {"node.shanks", OwnTakeOf{Node: "anchor", Module: "postgres", Name: "superuser-password"}, running, "anchor's"}, + "another machine's, whole name": {"node.shanks", OwnTakeOf{Secret: "own/anchor/postgres/superuser-password"}, running, "anchor's"}, + "not a node-engine": {ControllerCaller, pg, running, "not a node-engine"}, + "a module's tools": {"anchor.node-tools", pg, running, "not a node-engine"}, + "not an own secret": {"node.anchor", OwnTakeOf{Secret: "pair/anchor/database/shanks/gitea"}, running, "not a module's own secret"}, + "not held": {"node.anchor", OwnTakeOf{Module: "postgres", Name: "never-made"}, running, "nothing to replace"}, + "a person's value": {"node.anchor", OwnTakeOf{Module: "postgres", Name: "from-accepted"}, running, "by a person"}, + "a provider's software's value": {"node.anchor", OwnTakeOf{Module: "postgres", Name: "from-issued"}, running, "issued by its provider"}, + "an empty value": {"node.anchor", pg, " ", "empty"}, + } { + if _, err := take(row.caller, row.of, row.value); err == nil || !strings.Contains(err.Error(), row.says) { + t.Errorf("%s: %v, want refused saying %q", name, err, row.says) + } + } + if rec, _, _ := v.Store.Read("own/anchor/postgres/superuser-password"); rec.Generation != 1 || rec.Previous != nil { + t.Fatalf("a refusal changed the secret: generation %d, previous %+v", rec.Generation, rec.Previous) + } + // The value the vault holds already changes nothing, and says so. + if same, err := take("node.anchor", pg, old); err != nil || !same.Unchanged || same.To != 1 { + t.Errorf("the same value: %+v %v", same, err) + } + // What runs replaces it: one generation more, the old kept as the previous, origin kept, its fingerprint answered. + got, err := take("node.anchor", pg, running) + if err != nil { + t.Fatal(err) + } + rec, _, _ := v.Store.Read("own/anchor/postgres/superuser-password") + if got.From != 1 || got.To != 2 || got.Unchanged || got.Fingerprint != Fingerprint(running) || rec.Generation != 2 || + rec.Origin != OriginMade || rec.ChangedBy != "node.anchor" || rec.Previous == nil || rec.Previous.Generation != 1 { + t.Fatalf("replaced %+v, the record at generation %d origin %q by %s, previous %+v", got, rec.Generation, rec.Origin, + rec.ChangedBy, rec.Previous) + } + if now, _ := seal.Open(rec.AtRest, v.Own.PrivateKey); now != running { + t.Error("the vault does not hold what runs") + } + if was, _ := seal.Open(rec.Previous.AtRest, v.Own.PrivateKey); was != old || + openOperator(t, v.operatorPublic, v.operatorPrivate, rec.Previous.Operator.Sealed) != old { + t.Error("the previous value is not kept, for the vault and the operator") + } + // Said once, with both generations and why, never a value. + var replaced []map[string]any + for _, e := range v.bus.events { + if e["event"] == OwnReplacedEvent { + replaced = append(replaced, e["body"].(map[string]any)) + } + } + if len(replaced) != 1 || replaced[0]["secret"] != "own/anchor/postgres/superuser-password" || + replaced[0]["from_generation"] != 1 || replaced[0]["to_generation"] != 2 || replaced[0]["node"] != "anchor" || + replaced[0]["why"] != pg.Why { + t.Errorf("own-replaced said %v", replaced) + } + if all := everything(t, v, got); strings.Contains(all, running) || strings.Contains(all, old) { + t.Fatal("a value is in something the vault said, answered or wrote in the clear") + } + // Taken again at once, over the value the vault just took: refused. + if _, err := take("node.anchor", pg, "a-value-older-than-the-vaults"); err == nil || !strings.Contains(err.Error(), "less than") { + t.Errorf("a running value over one the vault just took: %v", err) + } + // own_drift reads it from the node's latest shadow answer, mesh-wide. + answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{ + Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "needs-superuser-password", + Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}} + have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": answer}, + Composes: delivery.ComposesController}) + if err != nil { + t.Fatal(err) + } + v.HaveKeys = func() ([]string, error) { return []string{"anchor.node", "anchor.postgres"}, nil } + v.Haves = func(key string) ([]byte, error) { return have, nil } + drift, err := v.OwnDrift() + if err != nil { + t.Fatal(err) + } + if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" || + drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable || + len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["g"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController { + t.Errorf("own_drift: %+v", drift) + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/owntake.go b/modules/mesh-vault/cmd/mesh-vault/owntake.go new file mode 100644 index 00000000..c05ab440 --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/owntake.go @@ -0,0 +1,326 @@ +package main + +// The vault takes what runs (novox/hq issue 1126, cause C4; ADR 0550 §5: a module's own secrets are the vault's). +// +// `move-in` never replaces a value the vault holds (move.go), so its first import of a machine's own secret is fixed. +// Where the controller later made that secret anew (a rotation at start, a re-issue) without handing it here, the new +// value is what runs and what its consumers hold, and the vault keeps the old one. Only the machine the value is sealed +// to can open what runs, so its node-engine hands it in, with the verb `own-take-running`, in the two steps of every +// intake (intake.go): name the module and the secret and why, and get a call and a key made for it; then send the value +// sealed to that key. +// +// **From the node-engine of the machine the secret is for, about its own machine alone**: the machine is the caller's, +// as the bus proved it (`node.`), never an argument; a machine named that is not the caller's is refused, and so +// is a name that is not an own secret. **Only a value the vault holds is replaced**, and never a person's (accept) or a +// provider's software's (issued) but for a covered bus account, nor one the vault changed less than RecentlyChanged ago: +// a running value is older than a value the vault just took (a re-issue the machine has not applied yet), and is never +// put back over it. The same value again changes nothing and says so; an empty one is refused. +// +// The value replaced is kept as the record's previous generation (Record.Previous), the generation is raised, +// `secret-changed` and `own-replaced` are said (the secret, the machine, both generations and why; never a value), and the +// act is written in the record of hand-outs (secret_handed). A covered bus account so replaced has its hash written again +// (accounts.go), so the bus-accounts key is of the value the bus runs. + +import ( + "errors" + "fmt" + "sort" + "strings" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// OwnTakeRunningVerb is the seat verb a machine's node-engine hands the vault a running own secret with. +const OwnTakeRunningVerb = "own-take-running" + +// OwnReplacedEvent is what the vault says when it replaced an own secret with what runs: never a value. +const OwnReplacedEvent = "own-replaced" + +// RecentlyChanged is how long after the vault changed a secret a running value is not taken over it. +var RecentlyChanged = 15 * time.Minute + +// OwnTakeOf names the secret of one `own-take-running`: Module and Name (or Secret whole), Node only to be checked +// against the caller's machine, and Why in the caller's words. +type OwnTakeOf struct { + Node, Module, Name, Secret, Why string +} + +// OwnReplaced is what one `own-take-running` did: the secret, the generation it replaced and the one it holds now, the +// fingerprint of the value held now (what the node-engine checks against the value it handed), whether the value was +// held already. Never a value. +type OwnReplaced struct { + Secret string `json:"secret"` + Node string `json:"node"` + Module string `json:"module"` + Name string `json:"name"` + From int `json:"from_generation"` + To int `json:"to_generation"` + Unchanged bool `json:"unchanged,omitempty"` + Fingerprint string `json:"fingerprint"` + Said string `json:"said"` +} + +// ownTakeSecret is the secret a node-engine names, and its machine: refused for a caller that is no node-engine, for +// another machine, and for a name that is not an own secret. +func ownTakeSecret(caller string, of OwnTakeOf) (secret, node string, err error) { + node, ok := nodeEngineOf(caller) + if !ok { + if caller == "" || caller == stdio.Unproven { + caller = "an unproven caller" + } + return "", "", fmt.Errorf("%s takes a value from a machine's node-engine alone, about its own machine; %s is "+ + "not a node-engine; nothing is taken", OwnTakeRunningVerb, caller) + } + if of.Secret != "" { + parts := strings.Split(of.Secret, "/") + if len(parts) != 4 || parts[0] != "own" { + return "", "", fmt.Errorf("%s is not a module's own secret (own///): %s replaces "+ + "those alone; nothing is taken", of.Secret, OwnTakeRunningVerb) + } + if of.Node != "" && of.Node != parts[1] { + return "", "", fmt.Errorf("%s is not on %s; nothing is taken", of.Secret, of.Node) + } + of.Node, of.Module, of.Name = parts[1], parts[2], parts[3] + } + if of.Node != "" && of.Node != node { + return "", "", fmt.Errorf("%s may hand the vault its own machine's secrets alone, and %s/%s is %s's: nothing "+ + "is taken", caller, of.Module, of.Name, of.Node) + } + secret, err = OwnSecret(node, of.Module, of.Name) + if err != nil { + return "", "", fmt.Errorf("%w; nothing is taken", err) + } + return secret, node, nil +} + +// ownTakeRefusal says why the held record rec is not replaced by a running value, or "". +func ownTakeRefusal(rec Record, now time.Time) string { + switch { + case rec.Origin == OriginAccepted: + return fmt.Sprintf("%s was given by a person, and a person alone gives it anew (accept); nothing is taken", rec.Name) + case rec.Origin == OriginIssued && !Covered(rec.Name): + return fmt.Sprintf("%s is issued by its provider's software, and `issued` is the only way it enters the vault "+ + "(ADR 0305 §10); nothing is taken", rec.Name) + case now.Sub(rec.ChangedAt) < RecentlyChanged: + return fmt.Sprintf("the vault changed %s at %s (generation %d, by %s), less than %s ago: a running value is "+ + "taken only over an older one, so a value the vault just took is never put back; nothing is taken", + rec.Name, rec.ChangedAt.Format(time.RFC3339), rec.Generation, rec.ChangedBy, RecentlyChanged) + } + return "" +} + +// OwnTakeOpen opens a call to take the running value of one of the caller's machine's own secrets. +func (v *Vault) OwnTakeOpen(caller string, of OwnTakeOf) (Opened, error) { + secret, _, err := ownTakeSecret(caller, of) + if err != nil { + return Opened{}, err + } + why := strings.TrimSpace(of.Why) + if len(why) > 300 || strings.ContainsAny(why, "\r\n\x00") { + return Opened{}, errors.New("why is not one short line; nothing is taken") + } + rec, held, err := v.Store.Read(secret) + if err != nil { + return Opened{}, err + } + refused := "" + if !held { + refused = fmt.Sprintf("the vault holds no secret named %s, so there is nothing to replace; nothing is taken", secret) + } else { + refused = ownTakeRefusal(rec, v.Now()) + } + if refused != "" { + _ = v.recordHanding(Handing{To: caller, Name: secret, Generation: rec.Generation, Outcome: "own-take-refused", Why: refused}) + return Opened{}, errors.New(refused) + } + id, key, err := v.openCall(OwnTakeRunningVerb, secret, caller, nil) + if err != nil { + return Opened{}, err + } + v.callsMu.Lock() + if c := v.pending[id]; c != nil { + c.why = why + } + v.callsMu.Unlock() + return Opened{Call: id, SealTo: key, Secret: secret}, nil +} + +// OwnTakeTake opens the value sealed to the call's key and replaces the secret with it, keeping the value it replaces. +func (v *Vault) OwnTakeTake(caller, call, sealed string) (OwnReplaced, error) { + if _, ok := nodeEngineOf(caller); !ok { + return OwnReplaced{}, fmt.Errorf("%s takes a value from a machine's node-engine alone; %q is not one", + OwnTakeRunningVerb, caller) + } + c, err := v.takeCall(OwnTakeRunningVerb, call) + if err != nil { + return OwnReplaced{}, err + } + if c.by != caller { + return OwnReplaced{}, fmt.Errorf("the %s call %q was opened by another caller; nothing was taken", OwnTakeRunningVerb, call) + } + value, err := c.open(sealed) + if err != nil { + return OwnReplaced{}, err + } + defer zero(value) + parts := strings.Split(c.secret, "/") + out := OwnReplaced{Secret: c.secret, Node: parts[1], Module: parts[2], Name: parts[3]} + if strings.TrimSpace(string(value)) == "" { + why := "the value handed is empty; nothing was taken" + _ = v.recordHanding(Handing{To: caller, Name: c.secret, Outcome: "own-take-refused", Why: why}) + return OwnReplaced{}, errors.New(why) + } + now := v.Now() + changed, err := v.keepWith(c.secret, string(value), caller, "", "", keepOptions{replaceOnly: true, keepPrevious: true, + refuse: func(before Record) string { return ownTakeRefusal(before, now) }}) + if err != nil { + _ = v.recordHanding(Handing{To: caller, Name: c.secret, Outcome: "own-take-refused", Why: err.Error()}) + return OwnReplaced{}, err + } + out.Fingerprint = Fingerprint(string(value)) + if changed.Unchanged { + out.From, out.To, out.Unchanged = changed.Generation, changed.Generation, true + out.Said = fmt.Sprintf("the vault holds this value of %s already (generation %d): nothing changed", c.secret, changed.Generation) + _ = v.recordHanding(Handing{To: caller, Name: c.secret, Generation: changed.Generation, Outcome: "own-same", Why: c.why}) + v.log("%s handed %s's running value, the one the vault holds (generation %d): nothing changed", caller, c.secret, + changed.Generation) + return out, nil + } + out.From, out.To = changed.Generation-1, changed.Generation + out.Said = fmt.Sprintf("%s is now the value that runs on %s, generation %d; generation %d is kept as the previous", + c.secret, out.Node, out.To, out.From) + if err := v.recordHanding(Handing{To: caller, Name: c.secret, Generation: out.To, Outcome: "own-replaced", Why: c.why}); err != nil { + out.Said += "; the record of hand-outs could not be written: " + err.Error() + } + v.log("replaced %s (generation %d → %d) with the value that runs, handed by %s: %s", c.secret, out.From, out.To, + caller, someoneOr(c.why, "no reason given")) + if v.Emit != nil { + body := map[string]any{"secret": c.secret, "node": out.Node, "module": out.Module, "name": out.Name, + "from_generation": out.From, "to_generation": out.To, "by": caller, "why": c.why, + "at": changed.At.Format(time.RFC3339Nano)} + if err := v.Emit(OwnReplacedEvent, body); err != nil { + v.log("%s is replaced at generation %d, but %s could not be said: %v", c.secret, out.To, OwnReplacedEvent, err) + } + } + return out, nil +} + +// ---- own_drift ---------------------------------------------------------------------------------------------- + +// DriftRow is one own secret a node's latest shadow answer says the vault holds another value of than runs there. +type DriftRow struct { + Secret string `json:"secret"` + Node string `json:"node"` + Module string `json:"module"` + Name string `json:"name"` + Groups []string `json:"groups"` + Held bool `json:"held"` + Generation int `json:"generation,omitempty"` + Changed time.Time `json:"changed,omitzero"` + ChangedBy string `json:"changed_by,omitempty"` + Origin string `json:"origin,omitempty"` + Previous int `json:"previous_generation,omitempty"` + // Takeable says own-take-running would take the running value now; Refused why not. + Takeable bool `json:"takeable"` + Refused string `json:"refused,omitempty"` +} + +// DriftNode is one node's answers as read: whose compositions it applies, and per group how many values differ. +type DriftNode struct { + Node string `json:"node"` + Composes string `json:"composes,omitempty"` + ValuesDiffer map[string]int `json:"values_differ,omitempty"` + Unread string `json:"unread,omitempty"` +} + +// Drift is own_drift's answer. +type Drift struct { + Secrets []DriftRow `json:"secrets"` + Nodes []DriftNode `json:"nodes"` + Note string `json:"note"` +} + +// OwnDrift reads every node's own have key (`.node`) and lists, from its latest answers to the placement +// candidates, every own secret whose shown bodies say another value (delivery.OwnValuesDiffer), with what the vault holds +// of it and whether own-take-running would take the running value now. +func (v *Vault) OwnDrift() (Drift, error) { + if v.Haves == nil || v.HaveKeys == nil { + return Drift{}, errors.New("the vault reads no node's have, so it cannot say what drifted") + } + keys, err := v.HaveKeys() + if err != nil { + return Drift{}, fmt.Errorf("the keys of %s could not be listed: %w", HaveRead, err) + } + sort.Strings(keys) + rows := map[string]*DriftRow{} + out := Drift{Secrets: []DriftRow{}, Nodes: []DriftNode{}, + Note: "from each node's latest shadow answers; a value is never shown: own-take-running from that node replaces it"} + for _, key := range keys { + node, isNode := strings.CutSuffix(key, "."+delivery.HaveNodeWord) + if !isNode || !namePart.MatchString(node) { + continue + } + dn := DriftNode{Node: node} + raw, err := v.Haves(key) + if err != nil || raw == nil { + dn.Unread = fmt.Sprintf("its have could not be read: %v", err) + out.Nodes = append(out.Nodes, dn) + continue + } + have, err := delivery.DecodeHaveNode(raw) + if err != nil { + dn.Unread = err.Error() + out.Nodes = append(out.Nodes, dn) + continue + } + dn.Composes = have.Composes + for group, a := range have.Candidates { + if n := delivery.ValuesDiffer(a); n > 0 { + if dn.ValuesDiffer == nil { + dn.ValuesDiffer = map[string]int{} + } + dn.ValuesDiffer[group] = n + } + for _, o := range delivery.OwnValuesDiffer(a) { + secret, err := OwnSecret(node, o.Module, o.Name) + if err != nil { + continue + } + r := rows[secret] + if r == nil { + r = &DriftRow{Secret: secret, Node: node, Module: o.Module, Name: o.Name} + rows[secret] = r + } + r.Groups = append(r.Groups, group) + } + } + out.Nodes = append(out.Nodes, dn) + } + names := make([]string, 0, len(rows)) + for n := range rows { + names = append(names, n) + } + sort.Strings(names) + for _, n := range names { + r := rows[n] + sort.Strings(r.Groups) + rec, held, err := v.Store.Read(n) + switch { + case err != nil: + r.Refused = err.Error() + case !held: + r.Refused = "the vault holds no secret named " + n + default: + r.Held, r.Generation, r.Changed, r.ChangedBy, r.Origin = true, rec.Generation, rec.ChangedAt, rec.ChangedBy, rec.Origin + if rec.Previous != nil { + r.Previous = rec.Previous.Generation + } + r.Refused = ownTakeRefusal(rec, v.Now()) + r.Takeable = r.Refused == "" + } + out.Secrets = append(out.Secrets, *r) + } + return out, nil +} diff --git a/modules/mesh-vault/cmd/mesh-vault/store.go b/modules/mesh-vault/cmd/mesh-vault/store.go index dd98a506..49b4bf8b 100644 --- a/modules/mesh-vault/cmd/mesh-vault/store.go +++ b/modules/mesh-vault/cmd/mesh-vault/store.go @@ -48,6 +48,20 @@ type Record struct { // MovedFrom is, for a value the move took from the controller's store (design 57's V3), the node-engine that // handed it in and the controller's generation for it, kept as it was. MovedFrom *MovedFrom `json:"moved-from,omitempty"` + // Previous is the value this one replaced when a machine's node-engine handed the vault what runs there + // (own-take-running, owntake.go): kept sealed as it was, so the replacement can be read and undone. Any later + // change of the secret drops it. + Previous *PreviousValue `json:"previous,omitempty"` +} + +// PreviousValue is a replaced value as the store kept it: its generation, both its seals, and who changed it when. +type PreviousValue struct { + Generation int `json:"generation"` + AtRest seal.Box `json:"at-rest"` + Operator OperatorCopy `json:"operator"` + ChangedAt time.Time `json:"changed-at"` + ChangedBy string `json:"changed-by"` + Origin string `json:"origin,omitempty"` } // OperatorCopy is a value sealed to the operator key named in Key. diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index 2ef64c8d..f1397ceb 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -195,6 +195,43 @@ func definitions(env *Env) []toolDef { } return v.RotateFor(caller, secret, one, arg(a, "by"), arg(a, "why")) }}, + {Name: Seat + "." + OwnTakeRunningVerb, + Description: "Replace one own secret of the asking machine (own///, the machine as the " + + "bus proved the caller) with the value that runs there (issue 1126, cause C4), from that machine's " + + "node-engine alone, in two steps: name module, name and why, and get a call and a key made for it; then " + + "send the call and the value sealed to that key. Keeps the old value as the previous generation, raises " + + "the generation and says own-replaced, never the value; answers both generations and the fingerprint of " + + "what it holds. Refused: another machine's secret, one that is not an own secret or not held, an empty " + + "value, a person's value, and a value the vault changed in the last 15 minutes. The same value is a no-op.", + Input: intakeInput(map[string]string{"module": "the module", "name": "its own secret's name", + "node": "the machine, when named: the caller's own, or refused", + "secret": "or the secret's whole name, own///", + "why": "why the value is handed, in words"}), + 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.OwnTakeTake(caller, call, arg(a, "sealed")) + } + return v.OwnTakeOpen(caller, OwnTakeOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), + Secret: arg(a, "secret"), Why: arg(a, "why")}) + }}, + {Name: "own_drift", + Description: "Every own secret whose node's latest shadow answer says the vault holds another value than runs " + + "there (the masked word \"another value\", issue 1126), for all nodes in one call: the secret, the " + + "groups that show it, the vault's generation, origin and previous generation, and whether the node's " + + "own-take-running would take the running value now (or why not); and per node whose compositions it " + + "applies and how many values differ per group. Never a value.", + Input: map[string]any{"type": "object", "properties": map[string]any{}}, + run: func(_ string, _ map[string]any) (any, error) { + v, err := env.ready() + if err != nil { + return nil, err + } + return v.OwnDrift() + }}, {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, " + @@ -782,6 +819,9 @@ func ownAnswer(v *Vault, node, module, prefix string) (map[string]any, error) { if r.ChangedVia != "" { row["changedVia"] = r.ChangedVia } + if r.Previous != nil { + row["previousGeneration"] = r.Previous.Generation + } if note != nil { row["error"] = note } diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 6734901d..00a61548 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -49,6 +49,8 @@ type Vault struct { // Haves reads the node-engines' have bucket, where the bus node hands the loaded list's hashes (adopt.go): nil // adopts nothing. Haves HaveReader + // HaveKeys lists the keys of the node-engines' have bucket, for own_drift (owntake.go): nil lists none. + HaveKeys func() ([]string, error) mu sync.Mutex // hashMu keeps one hashing at a time, and hashes what is owed (accounts.go). @@ -106,6 +108,11 @@ type keepOptions struct { neverReplace bool madeAt time.Time movedFrom *MovedFrom + // replaceOnly, keepPrevious and refuse are what own-take-running keeps (owntake.go): only a value held is replaced, + // keeping its origin and the value it replaces, and refuse, under the store's lock, says why a held value is not. + replaceOnly bool + keepPrevious bool + refuse func(before Record) string } // keepWith is keep with the move's options. A covered bus account kept anew has its hash written once it is kept @@ -147,10 +154,18 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions) if err != nil { return Changed{}, err } + if !held && opt.replaceOnly { + return Changed{}, fmt.Errorf("the vault holds no secret named %s, so there is nothing to replace; nothing was taken", name) + } if held { if old, err := seal.Open(before.AtRest, v.Own.PrivateKey); err == nil && old == value { return Changed{Name: name, Generation: before.Generation, At: before.ChangedAt, By: before.ChangedBy, Unchanged: true}, nil } + if opt.refuse != nil { + if why := opt.refuse(before); why != "" { + return Changed{}, errors.New(why) + } + } if opt.neverReplace { return Changed{}, fmt.Errorf("the vault already holds another value for %s (generation %d, changed by %s "+ "at %s), and the move never replaces a value the vault holds: nothing was taken", name, @@ -174,6 +189,13 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions) MadeAt: now, ChangedAt: now, ChangedBy: by, ChangedVia: via, Origin: origin} if held { rec.Generation, rec.MadeAt = before.Generation+1, before.MadeAt + if opt.replaceOnly { + rec.Origin, rec.MovedFrom = before.Origin, before.MovedFrom + } + if opt.keepPrevious { + rec.Previous = &PreviousValue{Generation: before.Generation, AtRest: before.AtRest, Operator: before.Operator, + ChangedAt: before.ChangedAt, ChangedBy: before.ChangedBy, Origin: before.Origin} + } } else if !opt.madeAt.IsZero() { rec.MadeAt = opt.madeAt.UTC() } @@ -343,7 +365,8 @@ func (v *Vault) reseal(key string) (int, error) { n := 0 var failed []error for _, rec := range all { - if rec.Operator.Key == key && rec.Operator.Box == OperatorBox { + if rec.Operator.Key == key && rec.Operator.Box == OperatorBox && + (rec.Previous == nil || rec.Previous.Operator.Key == key && rec.Previous.Operator.Box == OperatorBox) { continue } value, err := seal.Open(rec.AtRest, v.Own.PrivateKey) @@ -352,6 +375,16 @@ func (v *Vault) reseal(key string) (int, error) { continue } sealed, err := operatorSeal(key, []byte(value)) + if err == nil && rec.Previous != nil { + // And the value it replaced (owntake.go): a copy of every value the vault keeps is the operator's. + var old string + if old, err = seal.Open(rec.Previous.AtRest, v.Own.PrivateKey); err == nil { + var prev string + if prev, err = operatorSeal(key, []byte(old)); err == nil { + rec.Previous.Operator = OperatorCopy{Box: OperatorBox, Key: key, Sealed: prev} + } + } + } if err == nil { rec.Operator = OperatorCopy{Box: OperatorBox, Key: key, Sealed: sealed} err = v.Store.Write(rec) diff --git a/modules/mesh-vault/cmd/mesh-vault/vault_test.go b/modules/mesh-vault/cmd/mesh-vault/vault_test.go index fefbff17..1ae1cb08 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault_test.go @@ -167,10 +167,11 @@ func TestNoBucketHoldsASecret(t *testing.T) { code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Put(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Keys(", "", 1)) } - // The haves, in main.go alone: one read and one watch, never a write. + // The haves, in main.go alone: one read, one watch and their keys (own_drift), never a write. if f == "main.go" { code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Get(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Watch(", "", 1)) + code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1)) } if m := bucket.Find(code); m != nil { t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m) diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 304b95df..6872935d 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -15,7 +15,8 @@ "provisioned", "rotated", "secret-changed", - "account-hashed" + "account-hashed", + "own-replaced" ], "reads": [ "mesh-controller.assignments", @@ -163,7 +164,8 @@ "provisioner_retire_reject", "provisioner_delete", "vault_status", - "vault_health" + "vault_health", + "own_drift" ], "claims": [ { @@ -178,7 +180,8 @@ "move-in", "node-key", "move-asks", - "own" + "own", + "own-take-running" ] } ]