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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user