The vault takes the value that runs on a machine over the one it was first handed, from that machine alone (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, the
new value runs while the vault keeps the first. The verb own-take-running takes it from the node-engine of the machine
it is for, keeps the replaced value as the previous generation, raises the generation and says own-replaced without a
value; own_drift lists every own secret a node's shadow answer says another value of, mesh-wide. The module needs the
mesh-sdk head of fix/1126-seed-same-own-resync (delivery.OwnValuesDiffer, the seat's verb).
This commit is contained in:
2026-10-28 09:37:54 +01:00
parent 3fb7951524
commit 47ed384d97
10 changed files with 560 additions and 7 deletions
+16
View File
@@ -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 `<node>.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>.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.
+3 -1
View File
@@ -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
+2 -1
View File
@@ -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
}
@@ -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)
}
}
@@ -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.<machine>`), 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/<machine>/<module>/<name>): %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>.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
}
@@ -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.
@@ -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/<machine>/<module>/<name>, 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/<machine>/<module>/<name>",
"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
}
+34 -1
View File
@@ -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)
@@ -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)
+6 -3
View File
@@ -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"
]
}
]